diff --git a/CHANGELOG.md b/CHANGELOG.md index cc9e0e95..23f10509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,12 @@ ## Unreleased ### Added + +* `[mouse-bindings].selection-override-modifiers` option, specifying + which modifiers to hold to override mouse grabs by client + applications and force selection instead. + + ### Changed ### Deprecated ### Removed diff --git a/config.c b/config.c index 0616e87a..92a0fb53 100644 --- a/config.c +++ b/config.c @@ -1333,6 +1333,10 @@ parse_section_cursor(struct context *ctx) return true; } +static bool +parse_modifiers(struct context *ctx, const char *text, size_t len, + struct config_key_modifiers *modifiers); + static bool parse_section_mouse(struct context *ctx) { @@ -1474,6 +1478,11 @@ parse_modifiers(struct context *ctx, const char *text, size_t len, bool ret = false; *modifiers = (struct config_key_modifiers){0}; + + /* Handle "none" separately because e.g. none+shift is nonsense */ + if (strncmp(text, "none", len) == 0) + return true; + char *copy = xstrndup(text, len); for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); @@ -1947,6 +1956,31 @@ parse_section_url_bindings(struct context *ctx) &ctx->conf->bindings.url); } +static const struct { + const char *name; + int code; +} button_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}, +}; + +static const char* +mouse_event_code_get_name(int code) +{ + for (size_t i = 0; i < ALEN(button_map); i++) { + if (code == button_map[i].code) + return button_map[i].name; + } + + return NULL; +} + static bool value_to_mouse_combos(struct context *ctx, struct key_combo_list *key_combos) { @@ -1971,10 +2005,6 @@ value_to_mouse_combos(struct context *ctx, struct key_combo_list *key_combos) *key = '\0'; if (!parse_modifiers(ctx, combo, key - combo, &modifiers)) goto err; - if (modifiers.shift) { - LOG_CONTEXTUAL_ERR("Shift cannot be used in mouse bindings"); - goto err; - } key++; /* Skip past the '+' */ } @@ -1999,24 +2029,10 @@ value_to_mouse_combos(struct context *ctx, struct key_combo_list *key_combos) } } - 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; + for (size_t i = 0; i < ALEN(button_map); i++) { + if (strcmp(key, button_map[i].name) == 0) { + button = button_map[i].code; break; } } @@ -2054,6 +2070,98 @@ err: return false; } +static bool +modifiers_equal(const struct config_key_modifiers *mods1, + const struct config_key_modifiers *mods2) +{ + bool shift = mods1->shift == mods2->shift; + bool alt = mods1->alt == mods2->alt; + bool ctrl = mods1->ctrl == mods2->ctrl; + bool meta = mods1->meta == mods2->meta; + return shift && alt && ctrl && meta; +} + +static bool +modifiers_disjoint(const struct config_key_modifiers *mods1, + const struct config_key_modifiers *mods2) +{ + bool shift = mods1->shift && mods2->shift; + bool alt = mods1->alt && mods2->alt; + bool ctrl = mods1->ctrl && mods2->ctrl; + bool meta = mods1->meta && mods2->meta; + return !(shift || alt || ctrl || meta); +} + +static char * +modifiers_to_str(const struct config_key_modifiers *mods) +{ + char *ret = xasprintf("%s%s%s%s", + mods->ctrl ? "Control+" : "", + mods->alt ? "Alt+": "", + mods->meta ? "Meta+": "", + mods->shift ? "Shift+": ""); + ret[strlen(ret) - 1] = '\0'; + return ret; +} + +static char * +mouse_combo_to_str(const struct key_combo *combo) +{ + char *combo_modifiers_str = modifiers_to_str(&combo->modifiers); + const char *combo_button_str = mouse_event_code_get_name(combo->m.button); + xassert(combo_button_str != NULL); + + char *ret; + if (combo->m.count == 1) + ret = xasprintf("%s+%s", combo_modifiers_str, combo_button_str); + else + ret = xasprintf("%s+%s-%d", + combo_modifiers_str, + combo_button_str, + combo->m.count); + + free (combo_modifiers_str); + return ret; +} + +static bool +selection_override_interferes_with_mouse_binding(struct context *ctx, + int action, + const struct key_combo_list *key_combos, + bool blame_modifiers) +{ + struct config *conf = ctx->conf; + + if (action == BIND_ACTION_NONE) + return false; + + const struct config_key_modifiers *override_mods = + &conf->mouse.selection_override_modifiers; + for (size_t i = 0; i < key_combos->count; i++) { + const struct key_combo *combo = &key_combos->combos[i]; + + if (!modifiers_disjoint(&combo->modifiers, override_mods)) { + char *modifiers_str = modifiers_to_str(override_mods); + char *combo_str = mouse_combo_to_str(combo); + if (blame_modifiers) { + LOG_CONTEXTUAL_ERR( + "modifiers conflict with existing binding %s=%s", + binding_action_map[action], + combo_str); + } else { + LOG_CONTEXTUAL_ERR( + "binding conflicts with selection override modifiers (%s)", + modifiers_str); + } + free (modifiers_str); + free (combo_str); + return false; + } + } + + return false; +} + static bool has_mouse_binding_collisions(struct context *ctx, const struct key_combo_list *key_combos) @@ -2071,14 +2179,10 @@ has_mouse_binding_collisions(struct context *ctx, const struct config_key_modifiers *mods1 = &combo1->modifiers; const struct config_key_modifiers *mods2 = &combo2->modifiers; - bool shift = mods1->shift == mods2->shift; - bool alt = mods1->alt == mods2->alt; - bool ctrl = mods1->ctrl == mods2->ctrl; - bool meta = mods1->meta == mods2->meta; bool button = combo1->button == combo2->m.button; bool count = combo1->count == combo2->m.count; - if (shift && alt && ctrl && meta && button && count) { + if (modifiers_equal(mods1, mods2) && button && count) { bool has_pipe = combo1->pipe.argv.args != NULL; LOG_CONTEXTUAL_ERR("%s already mapped to '%s%s%s%s'", combo2->text, @@ -2102,6 +2206,37 @@ parse_section_mouse_bindings(struct context *ctx) const char *key = ctx->key; const char *value = ctx->value; + if (strcmp(ctx->key, "selection-override-modifiers") == 0) { + if (!parse_modifiers(ctx, ctx->value, strlen(ctx->value), + &conf->mouse.selection_override_modifiers)) { + LOG_CONTEXTUAL_ERR("%s: invalid modifiers '%s'", key, ctx->value); + return false; + } + + /* Ensure no existing bindings use these modifiers */ + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_mouse_binding *binding = &conf->bindings.mouse.arr[i]; + struct key_combo combo = { + .modifiers = binding->modifiers, + .m = { + .button = binding->button, + .count = binding->count, + }, + }; + + struct key_combo_list key_combos = { + .count = 1, + .combos = &combo, + }; + + if (selection_override_interferes_with_mouse_binding(ctx, binding->action, &key_combos, true)) { + return false; + } + } + + return true; + } + struct argv pipe_argv; ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &pipe_argv); @@ -2136,7 +2271,8 @@ parse_section_mouse_bindings(struct context *ctx) struct key_combo_list key_combos = {0}; if (!value_to_mouse_combos(ctx, &key_combos) || - has_mouse_binding_collisions(ctx, &key_combos)) + has_mouse_binding_collisions(ctx, &key_combos) || + selection_override_interferes_with_mouse_binding(ctx, action, &key_combos, false)) { free_argv(&pipe_argv); free_key_combo_list(&key_combos); @@ -2780,6 +2916,12 @@ config_load(struct config *conf, const char *conf_path, .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, + .selection_override_modifiers = { + .shift = true, + .alt = false, + .ctrl = false, + .meta = false, + }, }, .csd = { .preferred = CONF_CSD_PREFER_SERVER, diff --git a/config.h b/config.h index afe9cf74..2ae3a2d5 100644 --- a/config.h +++ b/config.h @@ -193,6 +193,7 @@ struct config { struct { bool hide_when_typing; bool alternate_scroll_mode; + struct config_key_modifiers selection_override_modifiers; } mouse; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 1b6c4d8c..e1922634 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -865,10 +865,6 @@ of the modifiers *must* be valid XKB key names, and the button name *must* be a valid libinput name. You can find the button names using *libinput debug-events*. -Note that *Shift* cannot be used as a modifier in mouse bindings since -it is used to enable selection when the client application is grabbing -the mouse. - The trailing *COUNT* is optional and specifies the click count required to trigger the binding. The default if *COUNT* is omitted is _1_. @@ -879,7 +875,19 @@ say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since need to unmap the default binding. This can be done by setting _action=none_; e.g. *primary-paste=none*. -All actions listed under *key-bindings* can be used here as well. +*selection-override-modifiers* + The modifiers set in this set (which may be set to any combination + of modifiers, e.g. _mod1+mod2+mod3_, as well as _none_) are used + to enable selecting text with the mouse irrespective of whether a + client application currently has the mouse grabbed. + These modifiers cannot be used as modifiers in mouse bindings. + Because the order of bindings is significant, it is best to set + this prior to any other mouse bindings that might use modifiers in + the default set. + Default: _Shift_ + +The actions to which mouse combos can be bound are listed below. All +actions listed under *key-bindings* can be used here as well. *select-begin* Begin an interactive selection. The selection is finalized, and diff --git a/foot.ini b/foot.ini index 09049b68..399becaa 100644 --- a/foot.ini +++ b/foot.ini @@ -169,6 +169,7 @@ # toggle-url-visible=t [mouse-bindings] +# selection-override-modifiers=Shift # primary-paste=BTN_MIDDLE # select-begin=BTN_LEFT # select-begin-block=Control+BTN_LEFT diff --git a/input.c b/input.c index 055c0fc8..36991567 100644 --- a/input.c +++ b/input.c @@ -701,6 +701,11 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); } + /* Set selection-override modmask from configured mods and seat's mod indices */ + const struct config_key_modifiers* override_mods = + &wayl->conf->mouse.selection_override_modifiers; + seat->kbd.selection_override_modmask = conf_modifiers_to_mask(seat, override_mods); + munmap(map_str, size); close(fd); @@ -983,7 +988,7 @@ UNITTEST xassert(strcmp(info->seq, "\033[27;3;13~") == 0); } -static void +void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, uint32_t key) @@ -2372,11 +2377,8 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, get_current_modifiers(seat, &mods, NULL, 0); mods &= seat->kbd.bind_significant; - /* Ignore Shift when matching modifiers, since it is - * used to enable selection in mouse grabbing client - * applications */ - if (seat->kbd.mod_shift != XKB_MOD_INVALID) - mods &= ~(1 << seat->kbd.mod_shift); + /* Ignore selection override modifiers when matching modifiers */ + mods &= ~seat->kbd.selection_override_modmask; const struct mouse_binding *match = NULL; diff --git a/input.h b/input.h index da887ef2..8430eef9 100644 --- a/input.h +++ b/input.h @@ -27,4 +27,9 @@ extern const struct wl_pointer_listener pointer_listener; void input_repeat(struct seat *seat, uint32_t key); +void get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, + uint32_t key); + const char *xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/terminal.c b/terminal.c index 25310d36..1d27693a 100644 --- a/terminal.c +++ b/terminal.c @@ -2826,10 +2826,15 @@ term_mouse_grabbed(const struct terminal *term, struct seat *seat) /* * Mouse is grabbed by us, regardless of whether mouse tracking has been enabled or not. */ + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0); + + const xkb_mod_mask_t override_modmask = seat->kbd.selection_override_modmask; + bool override_mods_pressed = (mods & override_modmask) == override_modmask; + return term->mouse_tracking == MOUSE_NONE || - (seat->kbd_focus == term && - seat->kbd.shift && - !seat->kbd.alt && /*!seat->kbd.ctrl &&*/ !seat->kbd.super); + (seat->kbd_focus == term && override_mods_pressed); } void diff --git a/wayland.h b/wayland.h index d415588b..e84ce237 100644 --- a/wayland.h +++ b/wayland.h @@ -201,6 +201,8 @@ struct seat { xkb_mod_mask_t bind_significant; xkb_mod_mask_t kitty_significant; + xkb_mod_mask_t selection_override_modmask; + xkb_keycode_t key_arrow_up; xkb_keycode_t key_arrow_down;