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 diff --git a/config.c b/config.c index 0ed8b1a9..4aae6c32 100644 --- a/config.c +++ b/config.c @@ -652,66 +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.key); + tll_foreach(*key_combos, it) + free(it->item.text); + tll_free(*key_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; - } +static bool +parse_modifiers(struct config *conf, const char *text, size_t len, + struct config_key_modifiers *modifiers, const char *path, unsigned lineno) +{ + 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); - } - - /* Check scrollback search bindings */ - tll_foreach(conf->bindings.search, it) { - char *copy = xstrdup(it->item.key); - - 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; - } + /* 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; } - 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; + 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 @@ -777,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.key); - 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); } } @@ -791,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) || @@ -805,29 +904,33 @@ parse_section_key_bindings( argv_compare(it->item.pipe.argv, pipe_argv) == 0))) { - free(it->item.key); - free(it->item.pipe.cmd); - free(it->item.pipe.argv); - - it->item.key = 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, - .key = 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; } @@ -851,39 +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.key); + 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.key); - - it->item.key = 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, - .key = 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; } @@ -892,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, @@ -904,66 +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) { + if (it->item.action == action) tll_remove(conf->bindings.mouse, it); - break; - } } return true; } - 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 mouse_binding binding = { - .action = action, - .button = i, - .count = count, - }; - tll_push_back(conf->bindings.mouse, binding); - } - return true; + 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; } - LOG_AND_NOTIFY_ERR("%s:%d: invalid mouse button: %s", path, lineno, value); - return false; + /* Remove existing bindings for this action */ + tll_foreach(conf->bindings.mouse, it) { + if (it->item.action == action) { + tll_remove(conf->bindings.mouse, it); + } + } + /* Emit mouse bindings */ + tll_foreach(key_combos, it) { + struct config_mouse_binding binding = { + .action = action, + .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; } LOG_AND_NOTIFY_ERR("%s:%u: [mouse-bindings]: %s: invalid key", path, lineno, key); @@ -1232,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) { @@ -1320,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 mouse_binding primary_paste = {BIND_ACTION_PRIMARY_PASTE, BTN_MIDDLE, 1}; - 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) { @@ -1428,12 +1646,11 @@ config_free(struct config conf) free(conf.server_socket_path); tll_foreach(conf.bindings.key, it) { - free(it->item.key); - 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.search, it) - free(it->item.key); tll_free(conf.bindings.key); tll_free(conf.bindings.mouse); diff --git a/config.h b/config.h index 7963aee7..fac5ea89 100644 --- a/config.h +++ b/config.h @@ -15,18 +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 *key; + struct config_key_modifiers modifiers; + xkb_keysym_t sym; struct { char *cmd; char **argv; + bool master_copy; } pipe; }; struct config_key_binding_search { enum bind_action_search action; - char *key; + 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 { @@ -91,7 +108,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/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 diff --git a/input.c b/input.c index 5372d044..b7c5031a 100644 --- a/input.c +++ b/input.c @@ -260,97 +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_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; +} - xkb_keysym_t sym = XKB_KEY_NoSymbol; +static xkb_keycode_list_t +key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) +{ + xkb_keycode_list_t key_codes = tll_init(); - char *copy = xstrdup(combos); + /* + * 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 (char *save1 = NULL, *combo = strtok_r(copy, " ", &save1); - combo != NULL; - combo = strtok_r(NULL, " ", &save1)) + for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); + code <= xkb_keymap_max_keycode(keymap); + code++) { - 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); - break; - } - - /* - * 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); + 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 @@ -402,12 +415,13 @@ 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, 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"); @@ -424,35 +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.key, &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.key, &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); - } + convert_key_bindings(wayl->conf, seat); + convert_search_bindings(wayl->conf, seat); + convert_mouse_bindings(wayl->conf, seat); } static void @@ -1119,7 +1107,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) { @@ -1444,9 +1437,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) { @@ -1470,15 +1466,16 @@ 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); } } - else { - tll_foreach(wayl->conf->bindings.mouse, it) { + 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; if (binding->button != button) { @@ -1486,6 +1483,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; @@ -1496,6 +1498,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 c1c14f7c..1ecc9e28 100644 --- a/input.h +++ b/input.h @@ -9,6 +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); diff --git a/wayland.c b/wayland.c index a10ed2b3..385e2098 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, @@ -913,22 +912,27 @@ 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); tll_remove(wayl->seats, it); + return; } LOG_WARN("unknown global removed: 0x%08x", name); 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 */