From dee4949ae1cee02aab404b74f6809948778953c4 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 8 Nov 2024 21:44:49 -0800 Subject: [PATCH 1/5] clipmenud: Add support for receiving INCR selections --- src/clipmenud.c | 213 +++++++++++++++++++++++++++++++++++++++++------- src/x.c | 26 ++++++ src/x.h | 20 +++++ 3 files changed, 228 insertions(+), 31 deletions(-) diff --git a/src/clipmenud.c b/src/clipmenud.c index 81de0c9..33b7d84 100644 --- a/src/clipmenud.c +++ b/src/clipmenud.c @@ -29,6 +29,9 @@ static Window win; static int enabled = 1; static int sig_fd; +static Atom incr_atom; +static struct incr_transfer *it_list; + static struct cm_selections sels[CM_SEL_MAX]; /** @@ -69,11 +72,20 @@ static char *get_clipboard_text(Atom clip_atom) { int actual_format; unsigned long nitems, bytes_after; - int res = - XGetWindowProperty(dpy, DefaultRootWindow(dpy), clip_atom, 0L, (~0L), - False, AnyPropertyType, &actual_type, &actual_format, - &nitems, &bytes_after, &cur_text); - return res == Success ? (char *)cur_text : NULL; + int res = XGetWindowProperty(dpy, win, clip_atom, 0L, (~0L), False, + AnyPropertyType, &actual_type, &actual_format, + &nitems, &bytes_after, &cur_text); + if (res != Success) { + return NULL; + } + + if (actual_type == incr_atom) { + dbg("Unexpected INCR transfer detected\n"); + XFree(cur_text); + return NULL; + } + + return (char *)cur_text; } /** @@ -231,45 +243,170 @@ static uint64_t store_clip(char *text) { } /** - * Something changed in our clip storage atoms. Work out whether we want to - * store the new content as a clipboard entry. + * Process the final data collected during an INCR transfer. */ -static int handle_property_notify(const XPropertyEvent *pe) { - bool found = false; - for (size_t i = 0; i < CM_SEL_MAX; ++i) { - if (sels[i].storage == pe->atom) { - found = true; - break; - } - } - if (!found || pe->state != PropertyNewValue) { - return -EINVAL; - } +static void incr_receive_finish(struct incr_transfer *it) { + it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size); + _drop_(free) char *text = malloc(it->data_size + 1); + expect(text); + memcpy(text, it->data, it->data_size); + text[it->data_size] = '\0'; - dbg("Received notification that selection conversion is ready\n"); - char *text = get_clipboard_text(pe->atom); char line[CS_SNIP_LINE_SIZE]; first_line(text, line); - dbg("First line: %s\n", line); + it_dbg(it, "First line: %s\n", line); if (is_salient_text(text)) { uint64_t hash = store_clip(text); maybe_trim(); - /* We only own CLIPBOARD because otherwise the behaviour is wonky: - * - * 1. When you select in a browser and press ^V, it repastes what you - * have selected instead of the previous content - * 2. urxvt and some other terminal emulators will unhilight on PRIMARY - * ownership being taken away from them - */ enum selection_type sel = - storage_atom_to_selection_type(pe->atom, sels); + storage_atom_to_selection_type(it->property, sels); if (cfg.owned_selections[sel].active && cfg.own_clipboard) { run_clipserve(hash); } } else { - dbg("Clipboard text is whitespace only, ignoring\n"); - XFree(text); + it_dbg(it, "Clipboard text is whitespace only, ignoring\n"); + } + + free(it->data); + it_remove(&it_list, it); + free(it); +} + +#define INCR_DATA_START_BYTES 1024 * 1024 + +/** + * Acknowledge and start an INCR transfer. + */ +static void incr_receive_start(const XPropertyEvent *pe) { + struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); + expect(it); + *it = (struct incr_transfer){ + .property = pe->atom, + .requestor = pe->window, + .data_size = 0, + .data_capacity = INCR_DATA_START_BYTES, + .data = malloc(INCR_DATA_START_BYTES), + }; + expect(it->data); + + it_dbg(it, "Starting transfer\n"); + it_add(&it_list, it); + + // Signal readiness for chunks + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Continue receiving data during an INCR transfer. + */ +static void incr_receive_data(const XPropertyEvent *pe, + struct incr_transfer *it) { + if (pe->state != PropertyNewValue) { + return; + } + + it_dbg(it, "Receiving chunk (bytes buffered: %zu)\n", it->data_size); + + _drop_(XFree) unsigned char *chunk = NULL; + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + XGetWindowProperty(dpy, win, pe->atom, 0, LONG_MAX, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &chunk); + + size_t chunk_size = nitems * (actual_format / 8); + + if (chunk_size == 0) { + it_dbg(it, "Transfer complete\n"); + incr_receive_finish(it); + return; + } + + if (it->data_size + chunk_size > it->data_capacity) { + it->data_capacity = (it->data_size + chunk_size) * 2; + it->data = realloc(it->data, it->data_capacity); + expect(it->data); + it_dbg(it, "Expanded data buffer to %zu bytes\n", it->data_capacity); + } + + memcpy(it->data + it->data_size, chunk, chunk_size); + it->data_size += chunk_size; + + // Signal readiness for next chunk + XDeleteProperty(dpy, win, pe->atom); +} + +/** + * Something changed in our clip storage atoms. Work out whether we want to + * store the new content as a clipboard entry. + */ +static int handle_property_notify(const XPropertyEvent *pe) { + if (pe->state != PropertyNewValue && pe->state != PropertyDelete) { + return -EINVAL; + } + + // Check if this property corresponds to an INCR transfer in progress + struct incr_transfer *it = it_list; + while (it) { + if (it->property == pe->atom && it->requestor == pe->window) { + break; + } + it = it->next; + } + + if (it) { + incr_receive_data(pe, it); + return 0; + } + + // Not an INCR transfer in progress. Check if this is an INCR transfer + // starting + Atom actual_type; + int actual_format; + unsigned long nitems, bytes_after; + _drop_(XFree) unsigned char *prop = NULL; + + XGetWindowProperty(dpy, win, pe->atom, 0, 0, False, AnyPropertyType, + &actual_type, &actual_format, &nitems, &bytes_after, + &prop); + + if (actual_type == incr_atom) { + incr_receive_start(pe); + } else { + dbg("Received non-INCR PropertyNotify\n"); + + // store_clip will take care of freeing this later when it's gone from + // last_text. + char *text = get_clipboard_text(pe->atom); + if (!text) { + dbg("Failed to get clipboard text\n"); + return -EINVAL; + } + char line[CS_SNIP_LINE_SIZE]; + first_line(text, line); + dbg("First line: %s\n", line); + + if (is_salient_text(text)) { + uint64_t hash = store_clip(text); + maybe_trim(); + /* We only own CLIPBOARD because otherwise the behaviour is wonky: + * + * 1. When you select in a browser and press ^V, it repastes what + * you have selected instead of the previous content + * 2. urxvt and some other terminal emulators will unhilight on + * PRIMARY ownership being taken away from them + */ + enum selection_type sel = + storage_atom_to_selection_type(pe->atom, sels); + if (cfg.owned_selections[sel].active && cfg.own_clipboard) { + run_clipserve(hash); + } + } else { + dbg("Clipboard text is whitespace only, ignoring\n"); + XFree(text); + } } return 0; @@ -323,6 +460,18 @@ static int handle_x11_event(int evt_base) { /** * Continuously wait for and process X11 or signal events until we fully * process success or failure for a clip. + * + * The usual sequence is: + * + * 1. Get an XFixesSelectionNotify that we have a new selection. + * 2. Call XConvertSelection() on it to get a string in our prop. + * 3. Wait for a PropertyNotify that says that's ready. + * 4. When it's ready, store it, and return from the function. + * + * Another possible outcome, especially when trying to get the initial state at + * startup, is that we get a SelectionNotify even with owner == None, which + * means the selection is unowned. At that point we also return, since it's + * clear that an explicit request has been nacked. */ static int get_one_clip(int evt_base) { while (1) { @@ -407,6 +556,8 @@ int main(int argc, char *argv[]) { win = DefaultRootWindow(dpy); setup_selections(dpy, sels); + incr_atom = XInternAtom(dpy, "INCR", False); + sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGUSR1); diff --git a/src/x.c b/src/x.c index cd4aa03..f453950 100644 --- a/src/x.c +++ b/src/x.c @@ -52,3 +52,29 @@ int xerror_handler(Display *dpy _unused_, XErrorEvent *ee) { die("X error with request code=%d, error code=%d\n", ee->request_code, ee->error_code); } + +/** + * Add a new INCR transfer to the active list. + */ +void it_add(struct incr_transfer **it_list, struct incr_transfer *it) { + if (*it_list) { + (*it_list)->prev = it; + } + it->next = *it_list; + it->prev = NULL; + *it_list = it; +} +/** + * Remove an INCR transfer from the active list. + */ +void it_remove(struct incr_transfer **it_list, struct incr_transfer *it) { + if (it->prev) { + it->prev->next = it->next; + } + if (it->next) { + it->next->prev = it->prev; + } + if (*it_list == it) { + *it_list = it->next; + } +} diff --git a/src/x.h b/src/x.h index 8995005..089d5e9 100644 --- a/src/x.h +++ b/src/x.h @@ -10,4 +10,24 @@ DEFINE_DROP_FUNC_VOID(XFree) char _nonnull_ *get_window_title(Display *dpy, Window owner); int xerror_handler(Display *dpy _unused_, XErrorEvent *ee); +struct incr_transfer { + struct incr_transfer *next; + struct incr_transfer *prev; + Window requestor; + Atom property; + Atom target; + int format; + char *data; + size_t data_size; + size_t data_capacity; + size_t offset; + size_t chunk_size; +}; + +#define it_dbg(it, fmt, ...) \ + dbg("[incr 0x%lx] " fmt, (unsigned long)(it)->requestor, ##__VA_ARGS__) +void _nonnull_ it_add(struct incr_transfer **it_list, struct incr_transfer *it); +void _nonnull_ it_remove(struct incr_transfer **it_list, + struct incr_transfer *it); + #endif From c92173d9f84f34b5caf3c69c95f8a024cc4dd288 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 8 Nov 2024 21:45:42 -0800 Subject: [PATCH 2/5] clipserve: Add support for sending INCR selections --- src/clipserve.c | 99 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/src/clipserve.c b/src/clipserve.c index ded04cc..3a7ca66 100644 --- a/src/clipserve.c +++ b/src/clipserve.c @@ -4,13 +4,93 @@ #include #include #include +#include +#include #include "config.h" #include "store.h" #include "util.h" #include "x.h" +#define INCR_CHUNK_BYTES 4096 + +static struct incr_transfer *it_list = NULL; static Display *dpy; +static Atom incr_atom; + +/** + * Start an INCR transfer. + */ +static void incr_send_start(XSelectionRequestEvent *req, + struct cs_content *content) { + long incr_size = content->size; + XChangeProperty(dpy, req->requestor, req->property, incr_atom, 32, + PropModeReplace, (unsigned char *)&incr_size, 1); + + struct incr_transfer *it = malloc(sizeof(struct incr_transfer)); + expect(it); + *it = (struct incr_transfer){ + .requestor = req->requestor, + .property = req->property, + .target = req->target, + .format = 8, + .data = (char *)content->data, + .data_size = content->size, + .offset = 0, + .chunk_size = INCR_CHUNK_BYTES, + }; + + it_dbg(it, "Starting transfer\n"); + it_add(&it_list, it); + + // Listen for PropertyNotify events on the requestor window + XSelectInput(dpy, it->requestor, PropertyChangeMask); +} + +/** + * Finish an INCR transfer. + */ +static void incr_send_finish(struct incr_transfer *it) { + XChangeProperty(dpy, it->requestor, it->property, it->target, it->format, + PropModeReplace, NULL, 0); + it_dbg(it, "Transfer complete\n"); + it_remove(&it_list, it); + free(it); +} + +/** + * Continue sending data during an INCR transfer. + */ +static void incr_send_chunk(const XPropertyEvent *pe) { + if (pe->state != PropertyDelete) { + return; + } + + struct incr_transfer *it = it_list; + while (it) { + if (it->requestor == pe->window && it->property == pe->atom) { + size_t remaining = it->data_size - it->offset; + size_t chunk_size = + (remaining > it->chunk_size) ? it->chunk_size : remaining; + + it_dbg(it, + "Sending chunk (bytes sent: %zu, bytes remaining: %zu)\n", + it->offset, remaining); + + if (chunk_size > 0) { + XChangeProperty(dpy, it->requestor, it->property, it->target, + it->format, PropModeReplace, + (unsigned char *)(it->data + it->offset), + chunk_size); + it->offset += chunk_size; + } else { + incr_send_finish(it); + } + break; + } + it = it->next; + } +} /** * Serve clipboard content for all X11 selection requests until all selections @@ -31,6 +111,7 @@ static void _nonnull_ serve_clipboard(uint64_t hash, XStoreName(dpy, win, "clipserve"); targets = XInternAtom(dpy, "TARGETS", False); utf8_string = XInternAtom(dpy, "UTF8_STRING", False); + incr_atom = XInternAtom(dpy, "INCR", False); selections[1] = XInternAtom(dpy, "CLIPBOARD", False); for (size_t i = 0; i < arrlen(selections); i++) { @@ -66,10 +147,16 @@ static void _nonnull_ serve_clipboard(uint64_t hash, arrlen(available_targets)); } else if (req->target == utf8_string || req->target == XA_STRING) { - XChangeProperty(dpy, req->requestor, req->property, - req->target, 8, PropModeReplace, - (unsigned char *)content->data, - (int)content->size); + if (content->size <= INCR_CHUNK_BYTES) { + // Data size is small enough, send directly + XChangeProperty(dpy, req->requestor, req->property, + req->target, 8, PropModeReplace, + (unsigned char *)content->data, + (int)content->size); + } else { + // Initiate INCR transfer + incr_send_start(req, content); + } } else { sev.property = None; } @@ -88,6 +175,10 @@ static void _nonnull_ serve_clipboard(uint64_t hash, } break; } + case PropertyNotify: { + incr_send_chunk(&evt.xproperty); + break; + } } } From e00123d6682dfbc65d88796ec246e3346ef1e87d Mon Sep 17 00:00:00 2001 From: Chris Down Date: Fri, 8 Nov 2024 21:46:15 -0800 Subject: [PATCH 3/5] tests: Add test for INCR support --- tests/x_integration_tests | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/x_integration_tests b/tests/x_integration_tests index 00a084e..7b211b0 100755 --- a/tests/x_integration_tests +++ b/tests/x_integration_tests @@ -52,6 +52,10 @@ settle() { sleep 0.2 } +long_settle() { + sleep 2 +} + check_nr_clips() { if [[ $SELECT ]]; then clipmenu @@ -166,6 +170,21 @@ clipctl toggle clipctl toggle [[ "$(clipctl status)" == enabled ]] +# Test INCR support +set +x +printf '%.0sa' {1..9999999} | xsel -p +set -x +long_settle # This is a big one, give it some time +len_before=$(xsel -po | wc -c) +check_nr_clips 5 +SELECT='[5] aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' \ + clipmenu +long_settle +len_after=$(xsel -po | wc -c) + +# Make sure we got the whole thing +(( len_before == len_after )) + if (( _UNSHARED )); then umount -l /tmp fi From 5ecb9523b85ddaed1270fba8d3cd27994a011dd2 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Sat, 9 Nov 2024 01:01:59 -0800 Subject: [PATCH 4/5] x: Dynamically configure INCR chunk size --- src/clipserve.c | 19 ++++++++++--------- src/x.c | 18 ++++++++++++++++++ src/x.h | 2 +- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/clipserve.c b/src/clipserve.c index 3a7ca66..ba69a78 100644 --- a/src/clipserve.c +++ b/src/clipserve.c @@ -12,12 +12,12 @@ #include "util.h" #include "x.h" -#define INCR_CHUNK_BYTES 4096 - static struct incr_transfer *it_list = NULL; static Display *dpy; static Atom incr_atom; +static size_t chunk_size; + /** * Start an INCR transfer. */ @@ -37,7 +37,6 @@ static void incr_send_start(XSelectionRequestEvent *req, .data = (char *)content->data, .data_size = content->size, .offset = 0, - .chunk_size = INCR_CHUNK_BYTES, }; it_dbg(it, "Starting transfer\n"); @@ -70,19 +69,19 @@ static void incr_send_chunk(const XPropertyEvent *pe) { while (it) { if (it->requestor == pe->window && it->property == pe->atom) { size_t remaining = it->data_size - it->offset; - size_t chunk_size = - (remaining > it->chunk_size) ? it->chunk_size : remaining; + size_t this_chunk_size = + (remaining > chunk_size) ? chunk_size : remaining; it_dbg(it, "Sending chunk (bytes sent: %zu, bytes remaining: %zu)\n", it->offset, remaining); - if (chunk_size > 0) { + if (this_chunk_size > 0) { XChangeProperty(dpy, it->requestor, it->property, it->target, it->format, PropModeReplace, (unsigned char *)(it->data + it->offset), - chunk_size); - it->offset += chunk_size; + this_chunk_size); + it->offset += this_chunk_size; } else { incr_send_finish(it); } @@ -107,6 +106,8 @@ static void _nonnull_ serve_clipboard(uint64_t hash, dpy = XOpenDisplay(NULL); expect(dpy); + chunk_size = get_chunk_size(dpy); + win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 0, 0, 0); XStoreName(dpy, win, "clipserve"); targets = XInternAtom(dpy, "TARGETS", False); @@ -147,7 +148,7 @@ static void _nonnull_ serve_clipboard(uint64_t hash, arrlen(available_targets)); } else if (req->target == utf8_string || req->target == XA_STRING) { - if (content->size <= INCR_CHUNK_BYTES) { + if (content->size < (off_t)chunk_size) { // Data size is small enough, send directly XChangeProperty(dpy, req->requestor, req->property, req->target, 8, PropModeReplace, diff --git a/src/x.c b/src/x.c index f453950..618c7c4 100644 --- a/src/x.c +++ b/src/x.c @@ -53,6 +53,24 @@ int xerror_handler(Display *dpy _unused_, XErrorEvent *ee) { ee->error_code); } +#define FALLBACK_CHUNK_BYTES 4 * 1024 + +/** + * Calculate and cache an appropriate INCR chunk size. + * + * We consider selections larger than a quarter of the maximum request size to + * be "large". That's what others (like xclip) do, so it's clearly ok in + * practice. + */ +size_t get_chunk_size(Display *dpy) { + // Units are 4-byte words, so this is 1/4 in bytes + size_t chunk_size = XExtendedMaxRequestSize(dpy); + if (chunk_size == 0) { + chunk_size = XMaxRequestSize(dpy); + } + return chunk_size ? chunk_size / 4 : FALLBACK_CHUNK_BYTES; +} + /** * Add a new INCR transfer to the active list. */ diff --git a/src/x.h b/src/x.h index 089d5e9..063e269 100644 --- a/src/x.h +++ b/src/x.h @@ -7,6 +7,7 @@ DEFINE_DROP_FUNC_VOID(XFree) +size_t _nonnull_ get_chunk_size(Display *dpy); char _nonnull_ *get_window_title(Display *dpy, Window owner); int xerror_handler(Display *dpy _unused_, XErrorEvent *ee); @@ -21,7 +22,6 @@ struct incr_transfer { size_t data_size; size_t data_capacity; size_t offset; - size_t chunk_size; }; #define it_dbg(it, fmt, ...) \ From 74077bdd19f4cdc517ec34974839abad27137639 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Sat, 9 Nov 2024 12:54:56 -0800 Subject: [PATCH 5/5] sels: Return CM_SEL_INVALID on invalid sel die() is too much here -- in the early code who we could get notifications from was tightly contained, but now not so much. --- src/clipmenud.c | 30 ++++++++++++++++++++++++------ src/config.c | 14 ++++++++++++-- src/config.h | 3 ++- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/clipmenud.c b/src/clipmenud.c index 33b7d84..6cf3bfc 100644 --- a/src/clipmenud.c +++ b/src/clipmenud.c @@ -162,14 +162,19 @@ static void handle_signalfd_event(void) { * desired property type. */ static void handle_xfixes_selection_notify(XFixesSelectionNotifyEvent *se) { + enum selection_type sel = + selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received XFixesSelectionNotify for unknown sel\n"); + return; + } + _drop_(XFree) char *win_title = get_window_title(dpy, se->owner); if (is_clipserve(win_title) || is_ignored_window(win_title)) { dbg("Ignoring clip from window titled '%s'\n", win_title); return; } - enum selection_type sel = - selection_atom_to_selection_type(se->selection, sels); dbg("Notified about selection update. Selection: %s, Owner: '%s' (0x%lx)\n", cfg.selections[sel].name, strnull(win_title), (unsigned long)se->owner); XConvertSelection(dpy, se->selection, @@ -189,6 +194,10 @@ static int handle_selection_notify(const XSelectionEvent *se) { if (se->property == None) { enum selection_type sel = selection_atom_to_selection_type(se->selection, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received no owner notification for unknown sel\n"); + return 0; + } dbg("X reports that %s has no current owner\n", cfg.selections[sel].name); return -ENOENT; @@ -246,6 +255,13 @@ static uint64_t store_clip(char *text) { * Process the final data collected during an INCR transfer. */ static void incr_receive_finish(struct incr_transfer *it) { + enum selection_type sel = + storage_atom_to_selection_type(it->property, sels); + if (sel == CM_SEL_INVALID) { + it_dbg(it, "Received INCR finish for unknown sel\n"); + return; + } + it_dbg(it, "Finished (bytes buffered: %zu)\n", it->data_size); _drop_(free) char *text = malloc(it->data_size + 1); expect(text); @@ -259,8 +275,6 @@ static void incr_receive_finish(struct incr_transfer *it) { if (is_salient_text(text)) { uint64_t hash = store_clip(text); maybe_trim(); - enum selection_type sel = - storage_atom_to_selection_type(it->property, sels); if (cfg.owned_selections[sel].active && cfg.own_clipboard) { run_clipserve(hash); } @@ -347,6 +361,12 @@ static int handle_property_notify(const XPropertyEvent *pe) { return -EINVAL; } + enum selection_type sel = storage_atom_to_selection_type(pe->atom, sels); + if (sel == CM_SEL_INVALID) { + dbg("Received PropertyNotify for unknown sel\n"); + return -EINVAL; + } + // Check if this property corresponds to an INCR transfer in progress struct incr_transfer *it = it_list; while (it) { @@ -398,8 +418,6 @@ static int handle_property_notify(const XPropertyEvent *pe) { * 2. urxvt and some other terminal emulators will unhilight on * PRIMARY ownership being taken away from them */ - enum selection_type sel = - storage_atom_to_selection_type(pe->atom, sels); if (cfg.owned_selections[sel].active && cfg.own_clipboard) { run_clipserve(hash); } diff --git a/src/config.c b/src/config.c index c55fd60..4a31791 100644 --- a/src/config.c +++ b/src/config.c @@ -357,6 +357,11 @@ void setup_selections(Display *dpy, struct cm_selections *sels) { XInternAtom(dpy, "CLIPMENUD_CUR_SECONDARY", False); } +/** + * Maps an Atom to a selection_type based on the selection atoms in the + * provided cm_selections struct. Returns CM_SEL_INVALID on error if the + * selection type is not found. + */ enum selection_type selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { for (size_t i = 0; i < CM_SEL_MAX; ++i) { @@ -364,9 +369,14 @@ selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { return i; } } - die("Unreachable\n"); + return CM_SEL_INVALID; } +/** + * Maps an Atom to a selection_type based on the storage atoms in the provided + * cm_selections struct. Returns CM_SEL_INVALID on error if the storage type is + * not found. + */ enum selection_type storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { for (size_t i = 0; i < CM_SEL_MAX; ++i) { @@ -374,5 +384,5 @@ storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { return i; } } - die("Unreachable\n"); + return CM_SEL_INVALID; } diff --git a/src/config.h b/src/config.h index a1d994f..bfec09b 100644 --- a/src/config.h +++ b/src/config.h @@ -18,7 +18,8 @@ enum selection_type { CM_SEL_CLIPBOARD, CM_SEL_PRIMARY, CM_SEL_SECONDARY, - CM_SEL_MAX + CM_SEL_MAX, + CM_SEL_INVALID }; struct cm_selections { Atom selection;