From 544c1b1976544085a2f27497c5c516bf1d9edfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 9 Aug 2020 22:40:53 +0200 Subject: [PATCH 1/7] config: wip: add support for modifiers in mouse bindings Mouse bindings can now include modifiers. They are specified in pretty much the same way as in keyboard bindings: Control+BTN_LEFT --- config.c | 102 +++++++++++++++++++++++++++++++++++++++--------- config.h | 11 ++++-- input.c | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++--- input.h | 3 ++ wayland.c | 5 +-- wayland.h | 4 ++ 6 files changed, 207 insertions(+), 31 deletions(-) diff --git a/config.c b/config.c index 0ed8b1a9..2fa3dcfa 100644 --- a/config.c +++ b/config.c @@ -657,7 +657,7 @@ verify_key_combo(struct config *conf, const char *combo, const char *path, unsig { /* Check regular key bindings */ tll_foreach(conf->bindings.key, it) { - char *copy = xstrdup(it->item.key); + char *copy = xstrdup(it->item.combos); for (char *save = NULL, *collision = strtok_r(copy, " ", &save); collision != NULL; @@ -680,7 +680,7 @@ verify_key_combo(struct config *conf, const char *combo, const char *path, unsig /* Check scrollback search bindings */ tll_foreach(conf->bindings.search, it) { - char *copy = xstrdup(it->item.key); + char *copy = xstrdup(it->item.combos); for (char *save = NULL, *collision = strtok_r(copy, " ", &save); collision != NULL; @@ -714,6 +714,46 @@ verify_key_combo(struct config *conf, const char *combo, const char *path, unsig return true; } +static bool +verify_mouse_combo(struct config *conf, + const char *combo, enum bind_action_normal action, + const char *path, unsigned lineno) +{ + tll_foreach(conf->bindings.mouse, it) { + char *copy = xstrdup(it->item.combos); + for (char *save = NULL, *collision = strtok_r(copy, " ", &save); + collision != NULL; + collision = strtok_r(NULL, " ", &save)) + { + if (strcmp(combo, collision) == 0) { + LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", path, lineno, combo, + binding_action_map[it->item.action]); + free(copy); + return false; + } + } + + free(copy); + } + + struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + struct xkb_keymap *keymap = xkb_keymap_new_from_names( + ctx, &(struct xkb_rule_names){}, XKB_KEYMAP_COMPILE_NO_FLAGS); + + bool valid_combo = input_parse_mouse_binding(keymap, combo, action, NULL); + + xkb_keymap_unref(keymap); + xkb_context_unref(ctx); + + if (!valid_combo) { + LOG_AND_NOTIFY_ERR("%s:%d: invalid mouse binding: %s", + path, lineno, combo); + return false; + } + + return true; +} + static int argv_compare(char *const *argv1, char *const *argv2) { @@ -780,7 +820,7 @@ parse_section_key_bindings( if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.key, it) { if (it->item.action == action) { - free(it->item.key); + free(it->item.combos); free(it->item.pipe.cmd); free(it->item.pipe.argv); tll_remove(conf->bindings.key, it); @@ -805,11 +845,11 @@ parse_section_key_bindings( argv_compare(it->item.pipe.argv, pipe_argv) == 0))) { - free(it->item.key); + free(it->item.combos); free(it->item.pipe.cmd); free(it->item.pipe.argv); - it->item.key = xstrdup(value); + it->item.combos = xstrdup(value); it->item.pipe.cmd = pipe_cmd; it->item.pipe.argv = pipe_argv; already_added = true; @@ -820,7 +860,7 @@ parse_section_key_bindings( if (!already_added) { struct config_key_binding_normal binding = { .action = action, - .key = xstrdup(value), + .combos = xstrdup(value), .pipe = { .cmd = pipe_cmd, .argv = pipe_argv, @@ -854,24 +894,21 @@ parse_section_search_bindings( if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.search, it) { if (it->item.action == action) { - free(it->item.key); + free(it->item.combos); tll_remove(conf->bindings.search, it); } } return true; } - if (!verify_key_combo(conf, value, path, lineno)) { + if (!verify_key_combo(conf, value, path, lineno)) return false; - } bool already_added = false; tll_foreach(conf->bindings.search, it) { if (it->item.action == action) { - - free(it->item.key); - - it->item.key = xstrdup(value); + free(it->item.combos); + it->item.combos = xstrdup(value); already_added = true; break; } @@ -880,7 +917,7 @@ parse_section_search_bindings( if (!already_added) { struct config_key_binding_search binding = { .action = action, - .key = xstrdup(value), + .combos = xstrdup(value), }; tll_push_back(conf->bindings.search, binding); } @@ -907,6 +944,7 @@ parse_section_mouse_bindings( if (strcmp(value, "NONE") == 0) { tll_foreach(conf->bindings.mouse, it) { if (it->item.action == action) { + free(it->item.combos); tll_remove(conf->bindings.mouse, it); break; } @@ -914,6 +952,29 @@ parse_section_mouse_bindings( return true; } + if (!verify_mouse_combo(conf, value, action, path, lineno)) + return false; + + bool already_added = false; + tll_foreach(conf->bindings.mouse, it) { + if (it->item.action == action) { + free(it->item.combos); + it->item.combos = xstrdup(value); + already_added = true; + break; + } + } + + if (!already_added) { + struct config_mouse_binding binding = { + .action = action, + .combos = xstrdup(value), + }; + tll_push_back(conf->bindings.mouse, binding); + } + + return true; +#if 0 const char *map[] = { [BTN_LEFT] = "BTN_LEFT", [BTN_RIGHT] = "BTN_RIGHT", @@ -951,8 +1012,9 @@ parse_section_mouse_bindings( } if (!already_added) { - struct mouse_binding binding = { + struct config_mouse_binding binding = { .action = action, + .mods = xstrdup(""), .button = i, .count = count, }; @@ -963,7 +1025,7 @@ parse_section_mouse_bindings( LOG_AND_NOTIFY_ERR("%s:%d: invalid mouse button: %s", path, lineno, value); return false; - +#endif } LOG_AND_NOTIFY_ERR("%s:%u: [mouse-bindings]: %s: invalid key", path, lineno, key); @@ -1340,7 +1402,7 @@ config_load(struct config *conf, const char *conf_path, bool errors_are_fatal) tll_push_back(conf->bindings.key, font_size_reset); tll_push_back(conf->bindings.key, spawn_terminal); - struct mouse_binding primary_paste = {BIND_ACTION_PRIMARY_PASTE, BTN_MIDDLE, 1}; + struct config_mouse_binding primary_paste = {BIND_ACTION_PRIMARY_PASTE, xstrdup("BTN_MIDDLE")}; tll_push_back(conf->bindings.mouse, primary_paste); struct config_key_binding_search search_cancel = {BIND_ACTION_SEARCH_CANCEL, xstrdup("Control+g Escape")}; @@ -1428,12 +1490,14 @@ config_free(struct config conf) free(conf.server_socket_path); tll_foreach(conf.bindings.key, it) { - free(it->item.key); + free(it->item.combos); free(it->item.pipe.cmd); free(it->item.pipe.argv); } + tll_foreach(conf.bindings.mouse, it) + free(it->item.combos); tll_foreach(conf.bindings.search, it) - free(it->item.key); + free(it->item.combos); tll_free(conf.bindings.key); tll_free(conf.bindings.mouse); diff --git a/config.h b/config.h index 7963aee7..e8a06483 100644 --- a/config.h +++ b/config.h @@ -17,16 +17,21 @@ struct config_font { struct config_key_binding_normal { enum bind_action_normal action; - char *key; + char *combos; struct { char *cmd; char **argv; } pipe; }; +struct config_mouse_binding { + enum bind_action_normal action; + char *combos; +}; + struct config_key_binding_search { enum bind_action_search action; - char *key; + char *combos; }; struct config { @@ -91,7 +96,7 @@ struct config { struct { /* Bindings for "normal" mode */ tll(struct config_key_binding_normal) key; - tll(struct mouse_binding) mouse; + tll(struct config_mouse_binding) mouse; /* * Special modes diff --git a/input.c b/input.c index 55090ff7..afd9155a 100644 --- a/input.c +++ b/input.c @@ -303,7 +303,8 @@ input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, if (sym == XKB_KEY_NoSymbol) { LOG_ERR("%s: key binding is not a valid XKB symbol name", key); - break; + free(copy); + return false; } /* @@ -353,6 +354,90 @@ input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, return true; } +bool +input_parse_mouse_binding(struct xkb_keymap *keymap, const char *combos, + enum bind_action_normal action, + mouse_binding_list_t *bindings) +{ + if (combos == NULL) + return true; + + char *copy = xstrdup(combos); + for (char *save1 = NULL, *combo = strtok_r(copy, " ", &save1); + combo != NULL; + combo = strtok_r(NULL, " ", &save1)) + { + xkb_mod_mask_t mod_mask = 0; + int button = 0; + + for (char *save2 = NULL, *key = strtok_r(combo, "+", &save2), + *next_key = strtok_r(NULL, "+", &save2); + key != NULL; + key = next_key, next_key = strtok_r(NULL, "+", &save2)) + { + if (next_key != NULL) { + /* Modifier */ + xkb_mod_index_t mod = xkb_keymap_mod_get_index(keymap, key); + + if (mod == -1) { + LOG_ERR("%s: not a valid modifier name", key); + free(copy); + return false; + } + + mod_mask |= 1 << mod; + } + + else { + /* Button */ + static const struct { + const char *name; + int code; + } map[] = { + {"BTN_LEFT", BTN_LEFT}, + {"BTN_RIGHT", BTN_RIGHT}, + {"BTN_MIDDLE", BTN_MIDDLE}, + {"BTN_SIDE", BTN_SIDE}, + {"BTN_EXTRA", BTN_EXTRA}, + {"BTN_FORWARD", BTN_FORWARD}, + {"BTN_BACK", BTN_BACK}, + {"BTN_TASK", BTN_TASK}, + }; + + for (size_t i = 0; i < ALEN(map); i++) { + if (strcmp(key, map[i].name) == 0) { + button = map[i].code; + break; + } + } + + if (button == 0) { + LOG_ERR("%s: invalid mouse button name", key); + free(copy); + return false; + } + } + } + + if (button == 0) + continue; + + if (bindings != NULL) { + const struct mouse_binding binding = { + .action = action, + .mods = mod_mask, + .button = button, + .count = 1, + }; + + tll_push_back(*bindings, binding); + } + } + + free(copy); + return true; +} + static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size) @@ -402,6 +487,8 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, tll_free(it->item.bind.key_codes); tll_free(seat->kbd.bindings.search); + tll_free(seat->mouse.bindings); + seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, @@ -427,7 +514,7 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, tll_foreach(wayl->conf->bindings.key, it) { key_binding_list_t bindings = tll_init(); input_parse_key_binding( - seat->kbd.xkb_keymap, it->item.key, &bindings); + seat->kbd.xkb_keymap, it->item.combos, &bindings); tll_foreach(bindings, it2) { tll_push_back( @@ -443,7 +530,7 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, tll_foreach(wayl->conf->bindings.search, it) { key_binding_list_t bindings = tll_init(); input_parse_key_binding( - seat->kbd.xkb_keymap, it->item.key, &bindings); + seat->kbd.xkb_keymap, it->item.combos, &bindings); tll_foreach(bindings, it2) { tll_push_back( @@ -453,6 +540,12 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, } tll_free(bindings); } + + tll_foreach(wayl->conf->bindings.mouse, it) { + input_parse_mouse_binding( + seat->kbd.xkb_keymap, it->item.combos, it->item.action, + &seat->mouse.bindings); + } } static void @@ -1440,9 +1533,12 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + xkb_mod_mask_t mods = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_DEPRESSED); + switch (state) { case WL_POINTER_BUTTON_STATE_PRESSED: { - if (button == BTN_LEFT && seat->mouse.count <= 3) { + if (button == BTN_LEFT && seat->mouse.count <= 3 && mods == 0) { selection_cancel(term); if (selection_enabled(term, seat) && cursor_is_on_grid) { @@ -1466,7 +1562,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } } - else if (button == BTN_RIGHT && seat->mouse.count == 1) { + else if (button == BTN_RIGHT && seat->mouse.count == 1 && mods == 0) { if (selection_enabled(term, seat) && cursor_is_on_grid) { selection_extend( seat, term, seat->mouse.col, seat->mouse.row, serial); @@ -1474,7 +1570,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } else { - tll_foreach(wayl->conf->bindings.mouse, it) { + tll_foreach(seat->mouse.bindings, it) { const struct mouse_binding *binding = &it->item; if (binding->button != button) { @@ -1482,6 +1578,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, continue; } + if (binding->mods != mods) { + /* Modifier mismatch */ + continue; + } + if (binding->count != seat->mouse.count) { /* Not correct click count */ continue; diff --git a/input.h b/input.h index c1c14f7c..37ad8452 100644 --- a/input.h +++ b/input.h @@ -12,3 +12,6 @@ void input_repeat(struct seat *seat, uint32_t key); bool input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, key_binding_list_t *bindings); +bool input_parse_mouse_binding(struct xkb_keymap *keymap, const char *combos, + enum bind_action_normal action, + mouse_binding_list_t *bindings); diff --git a/wayland.c b/wayland.c index a10ed2b3..30271dd2 100644 --- a/wayland.c +++ b/wayland.c @@ -125,6 +125,8 @@ seat_destroy(struct seat *seat) tll_free(it->item.bind.key_codes); tll_free(seat->kbd.bindings.search); + tll_free(seat->mouse.bindings); + if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_unref(seat->kbd.xkb_compose_state); if (seat->kbd.xkb_compose_table != NULL) @@ -762,9 +764,6 @@ handle_global(void *data, struct wl_registry *registry, struct wl_seat *wl_seat = wl_registry_bind( wayl->registry, name, &wl_seat_interface, required); - /* Clipboard */ - /* Primary selection */ - tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, .wl_seat = wl_seat, diff --git a/wayland.h b/wayland.h index dc07fb7a..b08c25fd 100644 --- a/wayland.h +++ b/wayland.h @@ -53,9 +53,11 @@ struct key_binding_normal { struct mouse_binding { enum bind_action_normal action; + xkb_mod_mask_t mods; uint32_t button; int count; }; +typedef tll(struct mouse_binding) mouse_binding_list_t; enum bind_action_search { BIND_ACTION_SEARCH_NONE, @@ -173,6 +175,8 @@ struct seat { /* We used a discrete axis event in the current pointer frame */ double axis_aggregated; bool have_discrete; + + mouse_binding_list_t bindings; } mouse; /* Clipboard */ From 6b18bd998a08eee404fbdb73a1a71627377b874b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 18:57:17 +0200 Subject: [PATCH 2/7] wayland: global-remove: fix call to pointer_listener.leave() Don't call it when we have keyboard focus, call it when we have mouse focus. --- wayland.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/wayland.c b/wayland.c index 30271dd2..ec6c7896 100644 --- a/wayland.c +++ b/wayland.c @@ -912,18 +912,22 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) if (seat->kbd_focus != NULL) { LOG_WARN("compositor destroyed seat '%s' " - "without sending keyboard/pointer leave events", + "without sending a keyboard leave event", seat->name); - struct terminal *term = seat->kbd_focus; - if (seat->wl_keyboard != NULL) keyboard_listener.leave( - seat, seat->wl_keyboard, -1, term->window->surface); + seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface); + } + + if (seat->mouse_focus != NULL) { + LOG_WARN("compositor destroyed seat '%s' " + "without sending a pointer leave event", + seat->name); if (seat->wl_pointer != NULL) pointer_listener.leave( - seat, seat->wl_pointer, -1, term->window->surface); + seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface); } seat_destroy(seat); From 49eea3893e91b6c3df90e53c0403a976bf1cda52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 18:58:39 +0200 Subject: [PATCH 3/7] wayland: global-remove: return after finding a matching seat --- wayland.c | 1 + 1 file changed, 1 insertion(+) diff --git a/wayland.c b/wayland.c index ec6c7896..385e2098 100644 --- a/wayland.c +++ b/wayland.c @@ -932,6 +932,7 @@ handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) seat_destroy(seat); tll_remove(wayl->seats, it); + return; } LOG_WARN("unknown global removed: 0x%08x", name); From 375dcf0810d8fdeff4a7272b22375632bd96ae4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 18:59:03 +0200 Subject: [PATCH 4/7] input: pointer-leave: don't reset mouse properties with memset() Using memset() also clears the mouse bindings. This is both incorrect, and leaked memory. --- input.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index afd9155a..f3a70841 100644 --- a/input.c +++ b/input.c @@ -1211,7 +1211,12 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, } /* Reset mouse state */ - memset(&seat->mouse, 0, sizeof(seat->mouse)); + seat->mouse.x = seat->mouse.y = 0; + seat->mouse.col = seat->mouse.row = 0; + seat->mouse.button = seat->mouse.last_button = seat->mouse.count = 0; + memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); + seat->mouse.axis_aggregated = 0.0; + seat->mouse.have_discrete = false; seat->mouse_focus = NULL; if (old_moused == NULL) { From b97ef5819f806fe401a4b655bd06479a0394ea9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 19:00:03 +0200 Subject: [PATCH 5/7] config: key/mouse bindings: refactor: less parsing in keyboard_enter() This simplifies the handling of mouse and keyboard bindings. Before, the bindings where parsed *both* when loading the configuration, and then on every keyboard enter event. This was done since keys require a keymap to be decoded. Something we don't have at configuration time. The idea was that at config time, we used a default keymap just to verify the key combo strings were valid. The following has changed: * The bindings in the config struct is now *one* key combo per entry. Previously, it was one *action* per entry, and each entry had one or more key combos. Doing it this way makes it easier when converting the binding in the keyboard enter event (which previously had to expand the combos anyway). * The bindings in the config struct no longer contains any unparsed strings. A key binding contains a decoded 'modifier' struct (which specifies whether e.g. ctrl, or shift, or ctrl+shift must be pressed for the binding to be used). It also contains a decoded XKB keysym. * A mouse binding in the config struct is similar to a key binding, except it contains the button, and click count instead of the XKB key sym. * The modifiers in the user-specified key combo is decoded at config time, by using the pre-defined XKB constants XKB_MOD_NAME_. The result is stored in a 'modifiers' struct, which is just a collection of booleans; one for each supported modifier. The supported modifiers are: shift, ctrl, alt and meta/super. * The key sym is decoded at config time using xkb_keysym_from_name(). This call does *not* depend on a keymap. * The mouse button is decoded at config time using a hardcoded mapping table (just like before). * The click count is currently hard-coded to 1. * In the keyboard enter event, all we need to do is pre-compute the xkb_mod_mask_t variable for each key/mouse binding, and find all the *key codes* that map to the (already decoded) symbol. For mouse bindings, the modifiers are the *only* reason we convert the mouse bindings at all. In fact, on button events, we check if the seat has a keyboard. If not, we use the mouse bindings from the configuration directly, and simply filter out those with a non-empty set of modifiers. --- config.c | 651 ++++++++++++++++++++++++++++++++++--------------------- config.h | 26 ++- input.c | 332 +++++++++++----------------- input.h | 6 - 4 files changed, 548 insertions(+), 467 deletions(-) diff --git a/config.c b/config.c index 2fa3dcfa..4aae6c32 100644 --- a/config.c +++ b/config.c @@ -652,106 +652,159 @@ parse_section_csd(const char *key, const char *value, struct config *conf, return true; } -static bool -verify_key_combo(struct config *conf, const char *combo, const char *path, unsigned lineno) +/* Struct that holds temporary key/mouse binding parsed data */ +struct key_combo { + char *text; /* Raw text, e.g. "Control+Shift+V" */ + struct config_key_modifiers modifiers; + union { + xkb_keysym_t sym; /* Key converted to an XKB symbol, e.g. XKB_KEY_V */ + struct { + int button; + int count; + } m; + }; +}; +typedef tll(struct key_combo) key_combo_list_t; + +static void +free_key_combo_list(key_combo_list_t *key_combos) { - /* Check regular key bindings */ - tll_foreach(conf->bindings.key, it) { - char *copy = xstrdup(it->item.combos); - - for (char *save = NULL, *collision = strtok_r(copy, " ", &save); - collision != NULL; - collision = strtok_r(NULL, " ", &save)) - { - if (strcmp(combo, collision) == 0) { - bool has_pipe = it->item.pipe.cmd != NULL; - LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s%s%s%s'", path, lineno, combo, - binding_action_map[it->item.action], - has_pipe ? " [" : "", - has_pipe ? it->item.pipe.cmd : "", - has_pipe ? "]" : ""); - free(copy); - return false; - } - } - - free(copy); - } - - /* Check scrollback search bindings */ - tll_foreach(conf->bindings.search, it) { - char *copy = xstrdup(it->item.combos); - - for (char *save = NULL, *collision = strtok_r(copy, " ", &save); - collision != NULL; - collision = strtok_r(NULL, " ", &save)) - { - if (strcmp(combo, collision) == 0) { - LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", path, lineno, combo, - search_binding_action_map[it->item.action]); - free(copy); - return false; - } - } - - free(copy); - } - - struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - struct xkb_keymap *keymap = xkb_keymap_new_from_names( - ctx, &(struct xkb_rule_names){}, XKB_KEYMAP_COMPILE_NO_FLAGS); - - bool valid_combo = input_parse_key_binding(keymap, combo, NULL); - - xkb_keymap_unref(keymap); - xkb_context_unref(ctx); - - if (!valid_combo) { - LOG_AND_NOTIFY_ERR("%s:%d: invalid key combination: %s", path, lineno, combo); - return false; - } - - return true; + tll_foreach(*key_combos, it) + free(it->item.text); + tll_free(*key_combos); } static bool -verify_mouse_combo(struct config *conf, - const char *combo, enum bind_action_normal action, - const char *path, unsigned lineno) +parse_modifiers(struct config *conf, const char *text, size_t len, + struct config_key_modifiers *modifiers, const char *path, unsigned lineno) { - tll_foreach(conf->bindings.mouse, it) { - char *copy = xstrdup(it->item.combos); - for (char *save = NULL, *collision = strtok_r(copy, " ", &save); - collision != NULL; - collision = strtok_r(NULL, " ", &save)) - { - if (strcmp(combo, collision) == 0) { - LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", path, lineno, combo, - binding_action_map[it->item.action]); - free(copy); - return false; - } + bool ret = false; + + *modifiers = (struct config_key_modifiers){}; + char *copy = xstrndup(text, len); + + for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); + key != NULL; + key = strtok_r(NULL, "+", &tok_ctx)) + { + if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) + modifiers->shift = true; + else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) + modifiers->ctrl = true; + else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) + modifiers->alt = true; + else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) + modifiers->meta = true; + else { + LOG_AND_NOTIFY_ERR("%s:%d: %s: not a valid modifier name", + path, lineno, key); + goto out; + } + } + + ret = true; + +out: + free(copy); + return ret; +} + +static bool +parse_key_combos(struct config *conf, const char *combos, key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + assert(tll_length(*key_combos) == 0); + + char *copy = xstrdup(combos); + + for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); + combo != NULL; + combo = strtok_r(NULL, " ", &tok_ctx)) + { + struct config_key_modifiers modifiers = {}; + const char *key = strrchr(combo, '+'); + + if (key == NULL) { + /* No modifiers */ + key = combo; + } else { + if (!parse_modifiers(conf, combo, key - combo, &modifiers, path, lineno)) + goto err; + key++; /* Skip past the '+' */ } - free(copy); - } - - struct xkb_context *ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - struct xkb_keymap *keymap = xkb_keymap_new_from_names( - ctx, &(struct xkb_rule_names){}, XKB_KEYMAP_COMPILE_NO_FLAGS); - - bool valid_combo = input_parse_mouse_binding(keymap, combo, action, NULL); - - xkb_keymap_unref(keymap); - xkb_context_unref(ctx); - - if (!valid_combo) { - LOG_AND_NOTIFY_ERR("%s:%d: invalid mouse binding: %s", - path, lineno, combo); - return false; + /* Translate key name to symbol */ + xkb_keysym_t sym = xkb_keysym_from_name(key, 0); + if (sym == XKB_KEY_NoSymbol) { + LOG_AND_NOTIFY_ERR("%s:%d: %s: key is not a valid XKB key name", + path, lineno, key); + goto err; + } + + tll_push_back( + *key_combos, + ((struct key_combo){.text = xstrdup(combo), .modifiers = modifiers, .sym = sym})); } + free(copy); return true; + +err: + tll_foreach(*key_combos, it) + free(it->item.text); + tll_free(*key_combos); + free(copy); + return false; +} + +static bool +has_key_binding_collisions(struct config *conf, const key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + tll_foreach(conf->bindings.key, it) { + tll_foreach(*key_combos, it2) { + const struct config_key_modifiers *mods1 = &it->item.modifiers; + const struct config_key_modifiers *mods2 = &it2->item.modifiers; + + if (memcmp(mods1, mods2, sizeof(*mods1)) == 0 && + it->item.sym == it2->item.sym) + { + bool has_pipe = it->item.pipe.cmd != NULL; + LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s%s%s%s'", + path, lineno, it2->item.text, + binding_action_map[it->item.action], + has_pipe ? " [" : "", + has_pipe ? it->item.pipe.cmd : "", + has_pipe ? "]" : ""); + return true; + } + } + } + + return false; +} + +static bool +has_search_binding_collisions(struct config *conf, const key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + tll_foreach(conf->bindings.search, it) { + tll_foreach(*key_combos, it2) { + const struct config_key_modifiers *mods1 = &it->item.modifiers; + const struct config_key_modifiers *mods2 = &it2->item.modifiers; + + if (memcmp(mods1, mods2, sizeof(*mods1)) == 0 && + it->item.sym == it2->item.sym) + { + LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", + path, lineno, it2->item.text, + search_binding_action_map[it->item.action]); + return true; + } + } + } + + return false; } static int @@ -817,12 +870,14 @@ parse_section_key_bindings( if (strcmp(key, binding_action_map[action]) != 0) continue; + /* Unset binding */ if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.key, it) { if (it->item.action == action) { - free(it->item.combos); - free(it->item.pipe.cmd); - free(it->item.pipe.argv); + if (it->item.pipe.master_copy) { + free(it->item.pipe.cmd); + free(it->item.pipe.argv); + } tll_remove(conf->bindings.key, it); } } @@ -831,13 +886,17 @@ parse_section_key_bindings( return true; } - if (!verify_key_combo(conf, value, path, lineno)) { + key_combo_list_t key_combos = tll_init(); + if (!parse_key_combos(conf, value, &key_combos, path, lineno) || + has_key_binding_collisions(conf, &key_combos, path, lineno)) + { free(pipe_argv); free(pipe_cmd); + free_key_combo_list(&key_combos); return false; } - bool already_added = false; + /* Remove existing bindings for this action+pipe */ tll_foreach(conf->bindings.key, it) { if (it->item.action == action && ((it->item.pipe.argv == NULL && pipe_argv == NULL) || @@ -845,29 +904,33 @@ parse_section_key_bindings( argv_compare(it->item.pipe.argv, pipe_argv) == 0))) { - free(it->item.combos); - free(it->item.pipe.cmd); - free(it->item.pipe.argv); - - it->item.combos = xstrdup(value); - it->item.pipe.cmd = pipe_cmd; - it->item.pipe.argv = pipe_argv; - already_added = true; - break; + if (it->item.pipe.master_copy) { + free(it->item.pipe.cmd); + free(it->item.pipe.argv); + } + tll_remove(conf->bindings.key, it); } } - if (!already_added) { + /* Emit key bindings */ + bool first = true; + tll_foreach(key_combos, it) { struct config_key_binding_normal binding = { .action = action, - .combos = xstrdup(value), + .modifiers = it->item.modifiers, + .sym = it->item.sym, .pipe = { .cmd = pipe_cmd, .argv = pipe_argv, + .master_copy = first, }, }; + tll_push_back(conf->bindings.key, binding); + first = false; } + + free_key_combo_list(&key_combos); return true; } @@ -891,36 +954,40 @@ parse_section_search_bindings( if (strcmp(key, search_binding_action_map[action]) != 0) continue; + /* Unset binding */ if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.search, it) { - if (it->item.action == action) { - free(it->item.combos); + if (it->item.action == action) tll_remove(conf->bindings.search, it); - } } return true; } - if (!verify_key_combo(conf, value, path, lineno)) + key_combo_list_t key_combos = tll_init(); + if (!parse_key_combos(conf, value, &key_combos, path, lineno) || + has_search_binding_collisions(conf, &key_combos, path, lineno)) + { + free_key_combo_list(&key_combos); return false; + } - bool already_added = false; + /* Remove existing bindings for this action */ tll_foreach(conf->bindings.search, it) { - if (it->item.action == action) { - free(it->item.combos); - it->item.combos = xstrdup(value); - already_added = true; - break; - } + if (it->item.action == action) + tll_remove(conf->bindings.search, it); } - if (!already_added) { - struct config_key_binding_search binding = { + /* Emit key bindings */ + tll_foreach(key_combos, it) { + struct config_key_binding_normal binding = { .action = action, - .combos = xstrdup(value), + .modifiers = it->item.modifiers, + .sym = it->item.sym, }; - tll_push_back(conf->bindings.search, binding); + tll_push_back(conf->bindings.key, binding); } + + free_key_combo_list(&key_combos); return true; } @@ -929,6 +996,104 @@ parse_section_search_bindings( } +static bool +parse_mouse_combos(struct config *conf, const char *combos, key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + assert(tll_length(*key_combos) == 0); + + char *copy = xstrdup(combos); + + for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); + combo != NULL; + combo = strtok_r(NULL, " ", &tok_ctx)) + { + struct config_key_modifiers modifiers = {}; + const char *key = strrchr(combo, '+'); + + if (key == NULL) { + /* No modifiers */ + key = combo; + } else { + if (!parse_modifiers(conf, combo, key - combo, &modifiers, path, lineno)) + goto err; + key++; /* Skip past the '+' */ + } + + static const struct { + const char *name; + int code; + } map[] = { + {"BTN_LEFT", BTN_LEFT}, + {"BTN_RIGHT", BTN_RIGHT}, + {"BTN_MIDDLE", BTN_MIDDLE}, + {"BTN_SIDE", BTN_SIDE}, + {"BTN_EXTRA", BTN_EXTRA}, + {"BTN_FORWARD", BTN_FORWARD}, + {"BTN_BACK", BTN_BACK}, + {"BTN_TASK", BTN_TASK}, + }; + + int button = 0; + for (size_t i = 0; i < ALEN(map); i++) { + if (strcmp(key, map[i].name) == 0) { + button = map[i].code; + break; + } + } + + if (button == 0) { + LOG_AND_NOTIFY_ERR("%s:%d: %s: invalid mouse button name", path, lineno, key); + goto err; + } + + struct key_combo new = { + .text = xstrdup(combo), + .modifiers = modifiers, + .m = { + .button = button, + .count = 1, + }, + }; + tll_push_back(*key_combos, new); + } + + free(copy); + return true; + +err: + tll_foreach(*key_combos, it) + free(it->item.text); + tll_free(*key_combos); + free(copy); + return false; +} + +static bool +has_mouse_binding_collisions(struct config *conf, const key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + tll_foreach(conf->bindings.mouse, it) { + tll_foreach(*key_combos, it2) { + const struct config_key_modifiers *mods1 = &it->item.modifiers; + const struct config_key_modifiers *mods2 = &it2->item.modifiers; + + if (memcmp(mods1, mods2, sizeof(*mods1)) == 0 && + it->item.button == it2->item.m.button && + it->item.count == it2->item.m.count) + { + LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", + path, lineno, it2->item.text, + binding_action_map[it->item.action]); + return true; + } + } + } + + return false; +} + + static bool parse_section_mouse_bindings( const char *key, const char *value, struct config *conf, @@ -941,91 +1106,43 @@ parse_section_mouse_bindings( if (strcmp(key, binding_action_map[action]) != 0) continue; - if (strcmp(value, "NONE") == 0) { + /* Unset binding */ + if (strcasecmp(value, "none") == 0) { tll_foreach(conf->bindings.mouse, it) { - if (it->item.action == action) { - free(it->item.combos); + if (it->item.action == action) tll_remove(conf->bindings.mouse, it); - break; - } } return true; } - if (!verify_mouse_combo(conf, value, action, path, lineno)) + key_combo_list_t key_combos = tll_init(); + if (!parse_mouse_combos(conf, value, &key_combos, path, lineno) || + has_mouse_binding_collisions(conf, &key_combos, path, lineno)) + { + free_key_combo_list(&key_combos); return false; + } - bool already_added = false; + /* Remove existing bindings for this action */ tll_foreach(conf->bindings.mouse, it) { if (it->item.action == action) { - free(it->item.combos); - it->item.combos = xstrdup(value); - already_added = true; - break; + tll_remove(conf->bindings.mouse, it); } } - if (!already_added) { + /* Emit mouse bindings */ + tll_foreach(key_combos, it) { struct config_mouse_binding binding = { .action = action, - .combos = xstrdup(value), + .modifiers = it->item.modifiers, + .button = it->item.m.button, + .count = it->item.m.count, }; tll_push_back(conf->bindings.mouse, binding); } + free_key_combo_list(&key_combos); return true; -#if 0 - const char *map[] = { - [BTN_LEFT] = "BTN_LEFT", - [BTN_RIGHT] = "BTN_RIGHT", - [BTN_MIDDLE] = "BTN_MIDDLE", - [BTN_SIDE] = "BTN_SIDE", - [BTN_EXTRA] = "BTN_EXTRA", - [BTN_FORWARD] = "BTN_FORWARD", - [BTN_BACK] = "BTN_BACK", - [BTN_TASK] = "BTN_TASK", - }; - - for (size_t i = 0; i < ALEN(map); i++) { - if (map[i] == NULL || strcmp(map[i], value) != 0) - continue; - - const int count = 1; - - /* Make sure button isn't already mapped to another action */ - tll_foreach(conf->bindings.mouse, it) { - if (it->item.button == i && it->item.count == count) { - LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to %s", path, lineno, - value, binding_action_map[it->item.action]); - return false; - } - } - - bool already_added = false; - tll_foreach(conf->bindings.mouse, it) { - if (it->item.action == action) { - it->item.button = i; - it->item.count = count; - already_added = true; - break; - } - } - - if (!already_added) { - struct config_mouse_binding binding = { - .action = action, - .mods = xstrdup(""), - .button = i, - .count = count, - }; - tll_push_back(conf->bindings.mouse, binding); - } - return true; - } - - LOG_AND_NOTIFY_ERR("%s:%d: invalid mouse button: %s", path, lineno, value); - return false; -#endif } LOG_AND_NOTIFY_ERR("%s:%u: [mouse-bindings]: %s: invalid key", path, lineno, key); @@ -1294,6 +1411,98 @@ get_server_socket_path(void) return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } +static void +add_default_key_bindings(struct config *conf) +{ +#define add_binding(action, mods, sym) \ + do { \ + tll_push_back( \ + conf->bindings.key, \ + ((struct config_key_binding_normal){action, mods, sym})); \ + } while (0) + + const struct config_key_modifiers shift = {.shift = true}; + const struct config_key_modifiers ctrl = {.ctrl = true}; + const struct config_key_modifiers ctrl_shift = {.ctrl = true, .shift = true}; + + add_binding(BIND_ACTION_SCROLLBACK_UP, shift, XKB_KEY_Page_Up); + add_binding(BIND_ACTION_SCROLLBACK_DOWN, shift, XKB_KEY_Page_Down); + add_binding(BIND_ACTION_CLIPBOARD_COPY, ctrl_shift, XKB_KEY_C); + add_binding(BIND_ACTION_CLIPBOARD_PASTE, ctrl_shift, XKB_KEY_V); + add_binding(BIND_ACTION_SEARCH_START, ctrl_shift, XKB_KEY_R); + add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_plus); + add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_equal); + add_binding(BIND_ACTION_FONT_SIZE_UP, ctrl, XKB_KEY_KP_Add); + add_binding(BIND_ACTION_FONT_SIZE_DOWN, ctrl, XKB_KEY_minus); + add_binding(BIND_ACTION_FONT_SIZE_DOWN, ctrl, XKB_KEY_KP_Subtract); + add_binding(BIND_ACTION_FONT_SIZE_RESET, ctrl, XKB_KEY_0); + add_binding(BIND_ACTION_FONT_SIZE_RESET, ctrl, XKB_KEY_KP_0); + add_binding(BIND_ACTION_SPAWN_TERMINAL, ctrl_shift, XKB_KEY_N); + +#undef add_binding +} + +static void +add_default_search_bindings(struct config *conf) +{ +#define add_binding(action, mods, sym) \ + do { \ + tll_push_back( \ + conf->bindings.search, \ + ((struct config_key_binding_search){action, mods, sym})); \ +} while (0) + + const struct config_key_modifiers none = {}; + const struct config_key_modifiers alt = {.alt = true}; + const struct config_key_modifiers ctrl = {.ctrl = true}; + const struct config_key_modifiers ctrl_shift = {.ctrl = true, .shift = true}; + + add_binding(BIND_ACTION_SEARCH_CANCEL, ctrl, XKB_KEY_g); + add_binding(BIND_ACTION_SEARCH_CANCEL, none, XKB_KEY_Escape); + add_binding(BIND_ACTION_SEARCH_COMMIT, none, XKB_KEY_Return); + add_binding(BIND_ACTION_SEARCH_FIND_PREV, ctrl, XKB_KEY_r); + add_binding(BIND_ACTION_SEARCH_FIND_NEXT, ctrl, XKB_KEY_s); + add_binding(BIND_ACTION_SEARCH_EDIT_LEFT, none, XKB_KEY_Left); + add_binding(BIND_ACTION_SEARCH_EDIT_LEFT, ctrl, XKB_KEY_b); + add_binding(BIND_ACTION_SEARCH_EDIT_LEFT_WORD, ctrl, XKB_KEY_Left); + add_binding(BIND_ACTION_SEARCH_EDIT_LEFT_WORD, alt, XKB_KEY_b); + add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT, none, XKB_KEY_Right); + add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT, ctrl, XKB_KEY_f); + add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, ctrl, XKB_KEY_Right); + add_binding(BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, alt, XKB_KEY_f); + add_binding(BIND_ACTION_SEARCH_EDIT_HOME, none, XKB_KEY_Home); + add_binding(BIND_ACTION_SEARCH_EDIT_HOME, ctrl, XKB_KEY_a); + add_binding(BIND_ACTION_SEARCH_EDIT_END, none, XKB_KEY_End); + add_binding(BIND_ACTION_SEARCH_EDIT_END, ctrl, XKB_KEY_e); + add_binding(BIND_ACTION_SEARCH_DELETE_PREV, none, XKB_KEY_BackSpace); + add_binding(BIND_ACTION_SEARCH_DELETE_PREV_WORD, ctrl, XKB_KEY_BackSpace); + add_binding(BIND_ACTION_SEARCH_DELETE_PREV_WORD, alt, XKB_KEY_BackSpace); + add_binding(BIND_ACTION_SEARCH_DELETE_NEXT, none, XKB_KEY_Delete); + add_binding(BIND_ACTION_SEARCH_DELETE_NEXT_WORD, ctrl, XKB_KEY_Delete); + add_binding(BIND_ACTION_SEARCH_DELETE_NEXT_WORD, alt, XKB_KEY_d); + add_binding(BIND_ACTION_SEARCH_EXTEND_WORD, ctrl, XKB_KEY_w); + add_binding(BIND_ACTION_SEARCH_EXTEND_WORD_WS, ctrl_shift, XKB_KEY_W); + +#undef add_binding +} + +static void +add_default_mouse_bindings(struct config *conf) +{ +#define add_binding(action, mods, btn, count) \ + do { \ + tll_push_back( \ + conf->bindings.mouse, \ + ((struct config_mouse_binding){action, mods, btn, count})); \ +} while (0) + + const struct config_key_modifiers none = {}; + + add_binding(BIND_ACTION_PRIMARY_PASTE, none, BTN_MIDDLE, 1); + +#undef add_binding +} + bool config_load(struct config *conf, const char *conf_path, bool errors_are_fatal) { @@ -1382,62 +1591,9 @@ config_load(struct config *conf, const char *conf_path, bool errors_are_fatal) .notifications = tll_init(), }; - struct config_key_binding_normal scrollback_up = {BIND_ACTION_SCROLLBACK_UP, xstrdup("Shift+Page_Up")}; - struct config_key_binding_normal scrollback_down = {BIND_ACTION_SCROLLBACK_DOWN, xstrdup("Shift+Page_Down")}; - struct config_key_binding_normal clipboard_copy = {BIND_ACTION_CLIPBOARD_COPY, xstrdup("Control+Shift+C")}; - struct config_key_binding_normal clipboard_paste = {BIND_ACTION_CLIPBOARD_PASTE, xstrdup("Control+Shift+V")}; - struct config_key_binding_normal search_start = {BIND_ACTION_SEARCH_START, xstrdup("Control+Shift+R")}; - struct config_key_binding_normal font_size_up = {BIND_ACTION_FONT_SIZE_UP, xstrdup("Control+plus Control+equal Control+KP_Add")}; - struct config_key_binding_normal font_size_down = {BIND_ACTION_FONT_SIZE_DOWN, xstrdup("Control+minus Control+KP_Subtract")}; - struct config_key_binding_normal font_size_reset = {BIND_ACTION_FONT_SIZE_RESET, xstrdup("Control+0 Control+KP_0")}; - struct config_key_binding_normal spawn_terminal = {BIND_ACTION_SPAWN_TERMINAL, xstrdup("Control+Shift+N")}; - - tll_push_back(conf->bindings.key, scrollback_up); - tll_push_back(conf->bindings.key, scrollback_down); - tll_push_back(conf->bindings.key, clipboard_copy); - tll_push_back(conf->bindings.key, clipboard_paste); - tll_push_back(conf->bindings.key, search_start); - tll_push_back(conf->bindings.key, font_size_up); - tll_push_back(conf->bindings.key, font_size_down); - tll_push_back(conf->bindings.key, font_size_reset); - tll_push_back(conf->bindings.key, spawn_terminal); - - struct config_mouse_binding primary_paste = {BIND_ACTION_PRIMARY_PASTE, xstrdup("BTN_MIDDLE")}; - tll_push_back(conf->bindings.mouse, primary_paste); - - struct config_key_binding_search search_cancel = {BIND_ACTION_SEARCH_CANCEL, xstrdup("Control+g Escape")}; - struct config_key_binding_search search_commit = {BIND_ACTION_SEARCH_COMMIT, xstrdup("Return")}; - struct config_key_binding_search search_find_prev = {BIND_ACTION_SEARCH_FIND_PREV, xstrdup("Control+r")}; - struct config_key_binding_search search_find_next = {BIND_ACTION_SEARCH_FIND_NEXT, xstrdup("Control+s")}; - struct config_key_binding_search search_edit_left = {BIND_ACTION_SEARCH_EDIT_LEFT, xstrdup("Left Control+b")}; - struct config_key_binding_search search_edit_left_word = {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, xstrdup("Control+Left Mod1+b")}; - struct config_key_binding_search search_edit_right = {BIND_ACTION_SEARCH_EDIT_RIGHT, xstrdup("Right Control+f")}; - struct config_key_binding_search search_edit_right_word = {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, xstrdup("Control+Right Mod1+f")}; - struct config_key_binding_search search_edit_home = {BIND_ACTION_SEARCH_EDIT_HOME, xstrdup("Home Control+a")}; - struct config_key_binding_search search_edit_end = {BIND_ACTION_SEARCH_EDIT_END, xstrdup("End Control+e")}; - struct config_key_binding_search search_del_prev = {BIND_ACTION_SEARCH_DELETE_PREV, xstrdup("BackSpace")}; - struct config_key_binding_search search_del_prev_word = {BIND_ACTION_SEARCH_DELETE_PREV_WORD, xstrdup("Mod1+BackSpace Control+BackSpace")}; - struct config_key_binding_search search_del_next = {BIND_ACTION_SEARCH_DELETE_NEXT, xstrdup("Delete")}; - struct config_key_binding_search search_del_next_word = {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, xstrdup("Mod1+d Control+Delete")}; - struct config_key_binding_search search_ext_word = {BIND_ACTION_SEARCH_EXTEND_WORD, xstrdup("Control+w")}; - struct config_key_binding_search search_ext_word_ws = {BIND_ACTION_SEARCH_EXTEND_WORD_WS, xstrdup("Control+Shift+W")}; - - tll_push_back(conf->bindings.search, search_cancel); - tll_push_back(conf->bindings.search, search_commit); - tll_push_back(conf->bindings.search, search_find_prev); - tll_push_back(conf->bindings.search, search_find_next); - tll_push_back(conf->bindings.search, search_edit_left); - tll_push_back(conf->bindings.search, search_edit_left_word); - tll_push_back(conf->bindings.search, search_edit_right); - tll_push_back(conf->bindings.search, search_edit_right_word); - tll_push_back(conf->bindings.search, search_edit_home); - tll_push_back(conf->bindings.search, search_edit_end); - tll_push_back(conf->bindings.search, search_del_prev); - tll_push_back(conf->bindings.search, search_del_prev_word); - tll_push_back(conf->bindings.search, search_del_next); - tll_push_back(conf->bindings.search, search_del_next_word); - tll_push_back(conf->bindings.search, search_ext_word); - tll_push_back(conf->bindings.search, search_ext_word_ws); + add_default_key_bindings(conf); + add_default_search_bindings(conf); + add_default_mouse_bindings(conf); char *default_path = NULL; if (conf_path == NULL) { @@ -1490,14 +1646,11 @@ config_free(struct config conf) free(conf.server_socket_path); tll_foreach(conf.bindings.key, it) { - free(it->item.combos); - free(it->item.pipe.cmd); - free(it->item.pipe.argv); + if (it->item.pipe.master_copy) { + free(it->item.pipe.cmd); + free(it->item.pipe.argv); + } } - tll_foreach(conf.bindings.mouse, it) - free(it->item.combos); - tll_foreach(conf.bindings.search, it) - free(it->item.combos); tll_free(conf.bindings.key); tll_free(conf.bindings.mouse); diff --git a/config.h b/config.h index e8a06483..fac5ea89 100644 --- a/config.h +++ b/config.h @@ -15,23 +15,35 @@ struct config_font { int px_size; }; +struct config_key_modifiers { + bool shift; + bool alt; + bool ctrl; + bool meta; +}; + struct config_key_binding_normal { enum bind_action_normal action; - char *combos; + struct config_key_modifiers modifiers; + xkb_keysym_t sym; struct { char *cmd; char **argv; + bool master_copy; } pipe; }; -struct config_mouse_binding { - enum bind_action_normal action; - char *combos; -}; - struct config_key_binding_search { enum bind_action_search action; - char *combos; + struct config_key_modifiers modifiers; + xkb_keysym_t sym; +}; + +struct config_mouse_binding { + enum bind_action_normal action; + struct config_key_modifiers modifiers; + int button; + int count; }; struct config { diff --git a/input.c b/input.c index f3a70841..b494f659 100644 --- a/input.c +++ b/input.c @@ -260,182 +260,110 @@ execute_binding(struct seat *seat, struct terminal *term, } } -bool -input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, - key_binding_list_t *bindings) +static xkb_mod_mask_t +conf_modifiers_to_mask(const struct seat *seat, + const struct config_key_modifiers *modifiers) { - if (combos == NULL) - return true; - - xkb_keysym_t sym = XKB_KEY_NoSymbol; - - char *copy = xstrdup(combos); - - for (char *save1 = NULL, *combo = strtok_r(copy, " ", &save1); - combo != NULL; - combo = strtok_r(NULL, " ", &save1)) - { - xkb_mod_mask_t mod_mask = 0; - xkb_keycode_list_t key_codes = tll_init(); - - LOG_DBG("%s", combo); - for (char *save2 = NULL, *key = strtok_r(combo, "+", &save2), - *next_key = strtok_r(NULL, "+", &save2); - key != NULL; - key = next_key, next_key = strtok_r(NULL, "+", &save2)) - { - if (next_key != NULL) { - /* Modifier */ - xkb_mod_index_t mod = xkb_keymap_mod_get_index(keymap, key); - - if (mod == -1) { - LOG_ERR("%s: not a valid modifier name", key); - free(copy); - return false; - } - - mod_mask |= 1 << mod; - LOG_DBG("MOD: %d - %s", mod, key); - } else { - /* Symbol */ - sym = xkb_keysym_from_name(key, 0); - - if (sym == XKB_KEY_NoSymbol) { - LOG_ERR("%s: key binding is not a valid XKB symbol name", - key); - free(copy); - return false; - } - - /* - * Find all key codes that map to the lower case - * version of the symbol. - * - * This allows us to match bindings in other layouts - * too. - */ - xkb_keysym_t lower_sym = xkb_keysym_to_lower(sym); - struct xkb_state *state = xkb_state_new(keymap); - - for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); - code <= xkb_keymap_max_keycode(keymap); - code++) - { - if (xkb_state_key_get_one_sym(state, code) == lower_sym) - tll_push_back(key_codes, code); - } - - xkb_state_unref(state); - } - } - - LOG_DBG("mods=0x%08x, sym=%d", mod_mask, sym); - - if (sym == XKB_KEY_NoSymbol) { - assert(tll_length(key_codes) == 0); - tll_free(key_codes); - continue; - } - - assert(sym != 0); - if (bindings != NULL) { - const struct key_binding binding = { - .mods = mod_mask, - .sym = sym, - .key_codes = key_codes, - }; - - tll_push_back(*bindings, binding); - } else - tll_free(key_codes); - } - - free(copy); - return true; + xkb_mod_mask_t mods = 0; + mods |= modifiers->shift << seat->kbd.mod_shift; + mods |= modifiers->ctrl << seat->kbd.mod_ctrl; + mods |= modifiers->alt << seat->kbd.mod_alt; + mods |= modifiers->meta << seat->kbd.mod_meta; + return mods; } -bool -input_parse_mouse_binding(struct xkb_keymap *keymap, const char *combos, - enum bind_action_normal action, - mouse_binding_list_t *bindings) +static xkb_keycode_list_t +key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) { - if (combos == NULL) - return true; + xkb_keycode_list_t key_codes = tll_init(); - char *copy = xstrdup(combos); - for (char *save1 = NULL, *combo = strtok_r(copy, " ", &save1); - combo != NULL; - combo = strtok_r(NULL, " ", &save1)) + /* + * Find all key codes that map to the lower case + * version of the symbol. + * + * This allows us to match bindings in other layouts + * too. + */ + xkb_keysym_t lower_sym = xkb_keysym_to_lower(sym); + struct xkb_state *state = xkb_state_new(keymap); + + for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); + code <= xkb_keymap_max_keycode(keymap); + code++) { - xkb_mod_mask_t mod_mask = 0; - int button = 0; - - for (char *save2 = NULL, *key = strtok_r(combo, "+", &save2), - *next_key = strtok_r(NULL, "+", &save2); - key != NULL; - key = next_key, next_key = strtok_r(NULL, "+", &save2)) - { - if (next_key != NULL) { - /* Modifier */ - xkb_mod_index_t mod = xkb_keymap_mod_get_index(keymap, key); - - if (mod == -1) { - LOG_ERR("%s: not a valid modifier name", key); - free(copy); - return false; - } - - mod_mask |= 1 << mod; - } - - else { - /* Button */ - static const struct { - const char *name; - int code; - } map[] = { - {"BTN_LEFT", BTN_LEFT}, - {"BTN_RIGHT", BTN_RIGHT}, - {"BTN_MIDDLE", BTN_MIDDLE}, - {"BTN_SIDE", BTN_SIDE}, - {"BTN_EXTRA", BTN_EXTRA}, - {"BTN_FORWARD", BTN_FORWARD}, - {"BTN_BACK", BTN_BACK}, - {"BTN_TASK", BTN_TASK}, - }; - - for (size_t i = 0; i < ALEN(map); i++) { - if (strcmp(key, map[i].name) == 0) { - button = map[i].code; - break; - } - } - - if (button == 0) { - LOG_ERR("%s: invalid mouse button name", key); - free(copy); - return false; - } - } - } - - if (button == 0) - continue; - - if (bindings != NULL) { - const struct mouse_binding binding = { - .action = action, - .mods = mod_mask, - .button = button, - .count = 1, - }; - - tll_push_back(*bindings, binding); - } + if (xkb_state_key_get_one_sym(state, code) == lower_sym) + tll_push_back(key_codes, code); } - free(copy); - return true; + xkb_state_unref(state); + return key_codes; +} + +static void +convert_key_binding(struct seat *seat, + const struct config_key_binding_normal *conf_binding) +{ + struct key_binding_normal binding = { + .action = conf_binding->action, + .bind = { + .mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers), + .sym = conf_binding->sym, + .key_codes = key_codes_for_xkb_sym( + seat->kbd.xkb_keymap, conf_binding->sym), + }, + .pipe_argv = conf_binding->pipe.argv, + }; + tll_push_back(seat->kbd.bindings.key, binding); +} + +static void +convert_key_bindings(const struct config *conf, struct seat *seat) +{ + tll_foreach(conf->bindings.key, it) + convert_key_binding(seat, &it->item); +} + +static void +convert_search_binding(struct seat *seat, + const struct config_key_binding_search *conf_binding) +{ + struct key_binding_search binding = { + .action = conf_binding->action, + .bind = { + .mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers), + .sym = conf_binding->sym, + .key_codes = key_codes_for_xkb_sym( + seat->kbd.xkb_keymap, conf_binding->sym), + }, + }; + tll_push_back(seat->kbd.bindings.search, binding); +} + +static void +convert_search_bindings(const struct config *conf, struct seat *seat) +{ + tll_foreach(conf->bindings.search, it) + convert_search_binding(seat, &it->item); +} + +static void +convert_mouse_binding(struct seat *seat, + const struct config_mouse_binding *conf_binding) +{ + struct mouse_binding binding = { + .action = conf_binding->action, + .mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers), + .button = conf_binding->button, + .count = conf_binding->count, + }; + tll_push_back(seat->mouse.bindings, binding); +} + +static void +convert_mouse_bindings(const struct config *conf, struct seat *seat) +{ + tll_foreach(conf->bindings.mouse, it) + convert_mouse_binding(seat, &it->item); } static void @@ -494,7 +422,6 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); - /* TODO: initialize in enter? */ seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); seat->kbd.mod_shift = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, "Shift"); @@ -511,41 +438,9 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, munmap(map_str, size); close(fd); - tll_foreach(wayl->conf->bindings.key, it) { - key_binding_list_t bindings = tll_init(); - input_parse_key_binding( - seat->kbd.xkb_keymap, it->item.combos, &bindings); - - tll_foreach(bindings, it2) { - tll_push_back( - seat->kbd.bindings.key, - ((struct key_binding_normal){ - .bind = it2->item, - .action = it->item.action, - .pipe_argv = it->item.pipe.argv})); - } - tll_free(bindings); - } - - tll_foreach(wayl->conf->bindings.search, it) { - key_binding_list_t bindings = tll_init(); - input_parse_key_binding( - seat->kbd.xkb_keymap, it->item.combos, &bindings); - - tll_foreach(bindings, it2) { - tll_push_back( - seat->kbd.bindings.search, - ((struct key_binding_search){ - .bind = it2->item, .action = it->item.action})); - } - tll_free(bindings); - } - - tll_foreach(wayl->conf->bindings.mouse, it) { - input_parse_mouse_binding( - seat->kbd.xkb_keymap, it->item.combos, it->item.action, - &seat->mouse.bindings); - } + convert_key_bindings(wayl->conf, seat); + convert_search_bindings(wayl->conf, seat); + convert_mouse_bindings(wayl->conf, seat); } static void @@ -1574,7 +1469,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } } - else { + else if (seat->wl_keyboard != NULL) { + /* Seat has keyboard - use mouse bindings *with* modifiers */ tll_foreach(seat->mouse.bindings, it) { const struct mouse_binding *binding = &it->item; @@ -1598,6 +1494,32 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, } } + else { + /* Seat does NOT have a keyboard - use mouse bindings *without* modifiers */ + tll_foreach(seat->wayl->conf->bindings.mouse, it) { + const struct config_mouse_binding *binding = &it->item; + + if (binding->button != button) { + /* Wrong button */ + continue; + } + + if (binding->count != seat->mouse.count) { + /* Incorrect click count */ + continue; + } + + const struct config_key_modifiers no_mods = {}; + if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { + /* Binding has modifiers */ + continue; + } + + execute_binding(seat, term, binding->action, NULL, serial); + break; + } + } + if (!term_mouse_grabbed(term, seat) && cursor_is_on_grid) { term_mouse_down( term, button, seat->mouse.row, seat->mouse.col, diff --git a/input.h b/input.h index 37ad8452..1ecc9e28 100644 --- a/input.h +++ b/input.h @@ -9,9 +9,3 @@ extern const struct wl_keyboard_listener keyboard_listener; extern const struct wl_pointer_listener pointer_listener; void input_repeat(struct seat *seat, uint32_t key); - -bool input_parse_key_binding(struct xkb_keymap *keymap, const char *combos, - key_binding_list_t *bindings); -bool input_parse_mouse_binding(struct xkb_keymap *keymap, const char *combos, - enum bind_action_normal action, - mouse_binding_list_t *bindings); From 704d182c88b95f2db7242c92427e5b8a0275a6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 19:16:26 +0200 Subject: [PATCH 6/7] changelog: modifier support in mouse bindings --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f479ec..e89d9d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ * **colors.selection-foreground** and **colors.selection-background** options to `footrc`. * **tweak.render-timer** option to `footrc`. +* Modifier support in mouse bindings + (https://codeberg.org/dnkl/foot/issues/77). ### Deprecated From b783e4c6ead96bd1c53f528e21d263e56412c30b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 10 Aug 2020 19:41:27 +0200 Subject: [PATCH 7/7] doc: footrc: document modifiers in mouse bindings --- doc/footrc.5.scd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/footrc.5.scd b/doc/footrc.5.scd index 853b0583..12fb2ded 100644 --- a/doc/footrc.5.scd +++ b/doc/footrc.5.scd @@ -362,9 +362,11 @@ scrollback search mode. The syntax is exactly the same as the regular This section lets you override the default mouse bindings. -The general format is _action=BTN\__, where _BTN\__ is -the name of the event code (e.g. *BTN\_LEFT*, *BTN\_RIGHT*). You can -find the event names using *libinput debug-events*. +The general format is _action=combo1...comboN_. That is, each action +may have one or more key combinations, space separated. Each +combination is on the form _mod1+mod2+BTN\__. The names of the +modifiers and the key *must* be valid XKB key names. You can find the +button names using *libinput debug-events*. A button can only be mapped to *one* action. Lets say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since *BTN\_MIDDLE* is the default