diff --git a/config.c b/config.c index e2d9e2cb..edbf6d58 100644 --- a/config.c +++ b/config.c @@ -75,6 +75,7 @@ static const char *const binding_action_map[] = { [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", + [BIND_ACTION_SHOW_URLS] = "show-urls", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -114,6 +115,14 @@ static const char *const search_binding_action_map[] = { static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, "search binding action map size mismatch"); +static const char *const url_binding_action_map[] = { + [BIND_ACTION_URL_NONE] = NULL, + [BIND_ACTION_URL_CANCEL] = "cancel", +}; + +static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, + "URL binding action map size mismatch"); + #define LOG_AND_NOTIFY_ERR(...) \ do { \ LOG_ERR(__VA_ARGS__); \ @@ -1168,6 +1177,37 @@ has_search_binding_collisions(struct config *conf, enum bind_action_search actio return false; } +static bool +has_url_binding_collisions(struct config *conf, enum bind_action_url action, + const key_combo_list_t *key_combos, + const char *path, unsigned lineno) +{ + tll_foreach(conf->bindings.url, it) { + if (it->item.action == action) + continue; + + tll_foreach(*key_combos, it2) { + const struct config_key_modifiers *mods1 = &it->item.modifiers; + const struct config_key_modifiers *mods2 = &it2->item.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 sym = it->item.sym == it2->item.sym; + + if (shift && alt && ctrl && meta && sym) { + LOG_AND_NOTIFY_ERR("%s:%d: %s already mapped to '%s'", + path, lineno, it2->item.text, + url_binding_action_map[it->item.action]); + return true; + } + } + } + + return false; +} + static int argv_compare(char *const *argv1, char *const *argv2) { @@ -1403,6 +1443,63 @@ parse_section_search_bindings( } +static bool +parse_section_url_bindings( + const char *key, const char *value, struct config *conf, + const char *path, unsigned lineno) +{ + for (enum bind_action_url action = 0; + action < BIND_ACTION_URL_COUNT; + action++) + { + if (url_binding_action_map[action] == NULL) + continue; + + if (strcmp(key, url_binding_action_map[action]) != 0) + continue; + + /* Unset binding */ + if (strcasecmp(value, "none") == 0) { + tll_foreach(conf->bindings.url, it) { + if (it->item.action == action) + tll_remove(conf->bindings.url, it); + } + return true; + } + + key_combo_list_t key_combos = tll_init(); + if (!parse_key_combos(conf, value, &key_combos, path, lineno) || + has_url_binding_collisions(conf, action, &key_combos, path, lineno)) + { + free_key_combo_list(&key_combos); + return false; + } + + /* Remove existing bindings for this action */ + tll_foreach(conf->bindings.url, it) { + if (it->item.action == action) + tll_remove(conf->bindings.url, it); + } + + /* Emit key bindings */ + tll_foreach(key_combos, it) { + struct config_key_binding_url binding = { + .action = action, + .modifiers = it->item.modifiers, + .sym = it->item.sym, + }; + tll_push_back(conf->bindings.url, binding); + } + + free_key_combo_list(&key_combos); + return true; + } + + LOG_AND_NOTIFY_ERR("%s:%u: [url-bindings]: %s: invalid key", path, lineno, key); + return false; + +} + static bool parse_mouse_combos(struct config *conf, const char *combos, key_combo_list_t *key_combos, const char *path, unsigned lineno) @@ -1778,6 +1875,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar SECTION_CSD, SECTION_KEY_BINDINGS, SECTION_SEARCH_BINDINGS, + SECTION_URL_BINDINGS, SECTION_MOUSE_BINDINGS, SECTION_TWEAK, SECTION_COUNT, @@ -1800,6 +1898,7 @@ parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_ar [SECTION_CSD] = {&parse_section_csd, "csd"}, [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"}, + [SECTION_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"}, [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, }; @@ -1993,6 +2092,7 @@ add_default_key_bindings(struct config *conf) 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); + add_binding(BIND_ACTION_SHOW_URLS, ctrl_shift, XKB_KEY_F); #undef add_binding } @@ -2045,6 +2145,25 @@ add_default_search_bindings(struct config *conf) #undef add_binding } +static void +add_default_url_bindings(struct config *conf) +{ +#define add_binding(action, mods, sym) \ + do { \ + tll_push_back( \ + conf->bindings.url, \ + ((struct config_key_binding_url){action, mods, sym})); \ +} while (0) + + const struct config_key_modifiers none = {0}; + const struct config_key_modifiers ctrl = {.ctrl = true}; + + add_binding(BIND_ACTION_URL_CANCEL, ctrl, XKB_KEY_g); + add_binding(BIND_ACTION_URL_CANCEL, none, XKB_KEY_Escape); + +#undef add_binding +} + static void add_default_mouse_bindings(struct config *conf) { @@ -2195,6 +2314,7 @@ config_load(struct config *conf, const char *conf_path, add_default_key_bindings(conf); add_default_search_bindings(conf); + add_default_url_bindings(conf); add_default_mouse_bindings(conf); struct config_file conf_file = {.path = NULL, .fd = -1}; diff --git a/config.h b/config.h index 56273053..f46fa35f 100644 --- a/config.h +++ b/config.h @@ -42,6 +42,12 @@ struct config_key_binding_search { xkb_keysym_t sym; }; +struct config_key_binding_url { + enum bind_action_url action; + struct config_key_modifiers modifiers; + xkb_keysym_t sym; +}; + struct config_mouse_binding { enum bind_action_normal action; struct config_key_modifiers modifiers; @@ -157,6 +163,9 @@ struct config { /* While searching (not - action to *start* a search is in the * 'key' bindings above */ tll(struct config_key_binding_search) search; + + /* While showing URL jump labels */ + tll(struct config_key_binding_url) url; } bindings; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 8684850b..9982acb8 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -491,6 +491,11 @@ e.g. *search-start=none*. Default: _not bound_ +*show-urls* + Enters URL mode, where all currently visible URLs are tagged with + a jump label with a key sequence that will open the URL. Default: + _Control+Shift+F_. + # SECTION: search-bindings @@ -569,6 +574,17 @@ scrollback search mode. The syntax is exactly the same as the regular Paste from the _primary selection_ into the search buffer. Default: _Shift+Insert_. + +# SECTION: url-bindings + +This section lets you override the default key bindings used in URL +mode. The syntax is exactly the same as the regular **key-bindings**. + +*cancel* + Exits URL mode without opening an URL. Default: _Control+g + Escape_. + + # SECTION: mouse-bindings This section lets you override the default mouse bindings. diff --git a/foot.ini b/foot.ini index a9c0108f..2ea574a2 100644 --- a/foot.ini +++ b/foot.ini @@ -116,6 +116,9 @@ # clipboard-paste=Control+v Control+y # primary-paste=Shift+Insert +[url-bindings] +# cancel=Control+g Escape + [mouse-bindings] # primary-paste=BTN_MIDDLE # select-begin=BTN_LEFT diff --git a/input.c b/input.c index 8052e265..892b7f32 100644 --- a/input.c +++ b/input.c @@ -271,6 +271,10 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } + case BIND_ACTION_SHOW_URLS: + assert(false); + break; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); @@ -403,6 +407,29 @@ convert_search_bindings(const struct config *conf, struct seat *seat) convert_search_binding(seat, &it->item); } +static void +convert_url_binding(struct seat *seat, + const struct config_key_binding_url *conf_binding) +{ + struct key_binding_url 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.url, binding); +} + +static void +convert_url_bindings(const struct config *conf, struct seat *seat) +{ + tll_foreach(conf->bindings.url, it) + convert_url_binding(seat, &it->item); +} + static void convert_mouse_binding(struct seat *seat, const struct config_mouse_binding *conf_binding) @@ -528,6 +555,7 @@ keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, convert_key_bindings(wayl->conf, seat); convert_search_bindings(wayl->conf, seat); + convert_url_bindings(wayl->conf, seat); convert_mouse_bindings(wayl->conf, seat); } diff --git a/wayland.h b/wayland.h index bcd8dcab..fdfe905c 100644 --- a/wayland.h +++ b/wayland.h @@ -49,6 +49,7 @@ enum bind_action_normal { BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_SELECTED, + BIND_ACTION_SHOW_URLS, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, @@ -59,7 +60,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_WORD_WS, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_PIPE_SELECTED + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_SHOW_URLS + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; @@ -106,6 +107,17 @@ struct key_binding_search { enum bind_action_search action; }; +enum bind_action_url { + BIND_ACTION_URL_NONE, + BIND_ACTION_URL_CANCEL, + BIND_ACTION_URL_COUNT, +}; + +struct key_binding_url { + struct key_binding bind; + enum bind_action_url action; +}; + /* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ enum data_offer_mime_type { DATA_OFFER_MIME_UNSET, @@ -192,6 +204,7 @@ struct seat { struct { tll(struct key_binding_normal) key; tll(struct key_binding_search) search; + tll(struct key_binding_url) url; } bindings; } kbd;