From ee39966dedf9e8fbee5528c324c807b92b9a6c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 30 Jan 2021 12:43:59 +0100 Subject: [PATCH 01/55] config: add infrastructure to handle URL mode specific key bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add “show-urls” action to regular key bindings * Add url-bindings section to foot.ini * Add “cancel” action to URL mode key bindings --- config.c | 120 +++++++++++++++++++++++++++++++++++++++++++++ config.h | 9 ++++ doc/foot.ini.5.scd | 16 ++++++ foot.ini | 3 ++ input.c | 28 +++++++++++ wayland.h | 15 +++++- 6 files changed, 190 insertions(+), 1 deletion(-) 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; From b255aea3edc4be75226e6ae6807d2a31ca42aeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 30 Jan 2021 15:42:51 +0100 Subject: [PATCH 02/55] config: free URL bindings --- config.c | 1 + wayland.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/config.c b/config.c index edbf6d58..5fccbea4 100644 --- a/config.c +++ b/config.c @@ -2406,6 +2406,7 @@ config_free(struct config conf) tll_free(conf.bindings.key); tll_free(conf.bindings.mouse); tll_free(conf.bindings.search); + tll_free(conf.bindings.url); user_notifications_free(&conf.notifications); } diff --git a/wayland.c b/wayland.c index d3c560b7..f30e1b6f 100644 --- a/wayland.c +++ b/wayland.c @@ -148,6 +148,10 @@ seat_destroy(struct seat *seat) tll_free(it->item.bind.key_codes); tll_free(seat->kbd.bindings.search); + tll_foreach(seat->kbd.bindings.url, it) + tll_free(it->item.bind.key_codes); + tll_free(seat->kbd.bindings.url); + tll_free(seat->mouse.bindings); if (seat->kbd.xkb_compose_state != NULL) From 2cc84db97978388475504592c88911046bcd2eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:12:07 +0100 Subject: [PATCH 03/55] urls: initial support for detecting URLs and rendering jump-labels The jump labels work, but is currently hardcoded to use xdg-open --- input.c | 13 +- meson.build | 1 + pgo/pgo.c | 1 + render.c | 123 +++++++++++++++---- render.h | 1 + shm.h | 3 + terminal.c | 3 + terminal.h | 16 +++ url-mode.c | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++++ url-mode.h | 18 +++ wayland.c | 8 ++ wayland.h | 7 ++ 12 files changed, 503 insertions(+), 28 deletions(-) create mode 100644 url-mode.c create mode 100644 url-mode.h diff --git a/input.c b/input.c index 892b7f32..9dbdaad1 100644 --- a/input.c +++ b/input.c @@ -34,6 +34,7 @@ #include "spawn.h" #include "terminal.h" #include "tokenize.h" +#include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" @@ -272,8 +273,11 @@ execute_binding(struct seat *seat, struct terminal *term, } case BIND_ACTION_SHOW_URLS: - assert(false); - break; + xassert(!urls_mode_is_active(term)); + + urls_collect(term); + render_refresh_urls(term); + return true; case BIND_ACTION_SELECT_BEGIN: selection_start( @@ -854,6 +858,11 @@ key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, start_repeater(seat, key); search_input(seat, term, key, sym, effective_mods, serial); return; + } else if (urls_mode_is_active(term)) { + if (should_repeat) + start_repeater(seat, key); + urls_input(seat, term, key, sym, effective_mods, serial); + return; } #if 0 diff --git a/meson.build b/meson.build index b9168206..f2e6da2b 100644 --- a/meson.build +++ b/meson.build @@ -180,6 +180,7 @@ executable( 'slave.c', 'slave.h', 'spawn.c', 'spawn.h', 'tokenize.c', 'tokenize.h', + 'url-mode.c', 'url-mode.h', 'user-notification.h', 'wayland.c', 'wayland.h', wl_proto_src + wl_proto_headers, version, diff --git a/pgo/pgo.c b/pgo/pgo.c index fa95a0ec..4bb67d35 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -138,6 +138,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} +void urls_reset(struct terminal *term) {} int main(int argc, const char *const *argv) diff --git a/render.c b/render.c index aa415a18..e19d187c 100644 --- a/render.c +++ b/render.c @@ -28,13 +28,14 @@ #include "config.h" #include "grid.h" #include "hsl.h" +#include "ime.h" #include "quirks.h" #include "selection.h" -#include "sixel.h" #include "shm.h" +#include "sixel.h" +#include "url-mode.h" #include "util.h" #include "xmalloc.h" -#include "ime.h" #define TIME_SCROLL_DAMAGE 0 @@ -1706,10 +1707,10 @@ static void render_osd(struct terminal *term, struct wl_surface *surf, struct wl_subsurface *sub_surf, struct buffer *buf, - const wchar_t *text, uint32_t _fg, uint32_t _bg, + const wchar_t *text, uint32_t _fg, uint32_t _bg, int alpha, unsigned width, unsigned height, unsigned x, unsigned y) { - pixman_color_t bg = color_hex_to_pixman(_bg); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, width, height}); @@ -1896,7 +1897,7 @@ render_scrollback_position(struct terminal *term) term, win->scrollback_indicator_surface, win->scrollback_indicator_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 4], + term->colors.table[0], term->colors.table[8 + 4], 0xffff, width, height, width - margin - wcslen(text) * term->cell_width, margin); } @@ -1928,7 +1929,7 @@ render_render_timer(struct terminal *term, struct timeval render_time) term, win->render_timer_surface, win->render_timer_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 1], + term->colors.table[0], term->colors.table[8 + 1], 0xffff, width, height, margin, margin); } @@ -2517,6 +2518,61 @@ render_search_box(struct terminal *term) #undef WINDOW_Y } +static void +render_urls(struct terminal *term) +{ + struct wl_window *win = term->window; + xassert(tll_length(win->urls) > 0); + + tll_foreach(win->urls, it) { + const struct url *url = it->item.url; + const wchar_t *text = url->text; + const wchar_t *key = url->key; + + struct wl_surface *surf = it->item.surf; + struct wl_subsurface *sub_surf = it->item.sub_surf; + + if (surf == NULL || sub_surf == NULL) + continue; + + size_t text_len = wcslen(text); + size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); + + const size_t max_chars = 50; + chars = max(chars, max_chars); + + wchar_t label[chars + 2]; + if (text_len == 0) + wcscpy(label, key); + else { + int count = swprintf(label, chars, L"%s - %s", key, text); + if (count >= max_chars) { + label[max_chars] = L'…'; + label[max_chars + 1] = L'\0'; + } + } + + size_t len = wcslen(label); + int cols = wcswidth(label, len); + + const int margin = 3 * term->scale; + int width = 2 * margin + cols * term->cell_width; + int height = 2 * margin + term->cell_height; + + struct buffer *buf = shm_get_buffer( + term->wl->shm, width, height, shm_cookie_url(url), false, 1); + + const struct coord *pos = &url->start; + wl_subsurface_set_position( + sub_surf, + (term->margins.left + pos->col * term->cell_width - term->cell_width) / term->scale, + (term->margins.top + pos->row * term->cell_height - term->cell_height / 2) / term->scale); + render_osd(term, surf, sub_surf, buf, label, + term->colors.table[0], term->colors.table[3], 0xf000, + width, height, margin, margin); + } +} + static void render_update_title(struct terminal *term) { @@ -2545,13 +2601,15 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; - bool search = term->render.pending.search; + bool search = term->is_searching && term->render.pending.search; bool title = term->render.pending.title; + bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; term->render.pending.grid = false; term->render.pending.csd = false; term->render.pending.search = false; term->render.pending.title = false; + term->render.pending.urls = false; if (csd && term->window->use_csd == CSD_YES) { quirk_weston_csd_on(term); @@ -2562,15 +2620,19 @@ frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_da if (title) render_update_title(term); - if (search && term->is_searching) + if (search) render_search_box(term); - tll_foreach(term->wl->seats, it) + if (urls) + render_urls(term); + + if (grid && (!term->delayed_render_timer.is_armed | csd | search | urls)) + grid_render(term); + + tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_update_cursor_rect(&it->item, term); - - if (grid && (!term->delayed_render_timer.is_armed || csd || search)) - grid_render(term); + } } static void @@ -2755,6 +2817,9 @@ maybe_resize(struct terminal *term, int width, int height, bool force) /* Cancel an application initiated "Synchronized Update" */ term_disable_app_sync_updates(term); + /* Drop out of URL mode */ + urls_reset(term); + term->width = width; term->height = height; term->scale = scale; @@ -2981,27 +3046,21 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; - bool search = term->render.refresh.search; + bool search = term->is_searching && term->render.refresh.search; bool title = term->render.refresh.title; + bool urls = urls_mode_is_active(term) && term->render.refresh.urls; - if (!term->is_searching) - search = false; - - if (!(grid | csd | search | title)) + if (!(grid | csd | search | title | urls)) continue; - if (term->render.app_sync_updates.enabled && !(csd | search | title)) + if (term->render.app_sync_updates.enabled && !(csd | search | title | urls)) continue; - if (csd | search) { - /* Force update of parent surface */ - grid = true; - } - term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; term->render.refresh.title = false; + term->render.refresh.urls = false; if (term->window->frame_callback == NULL) { if (csd && term->window->use_csd == CSD_YES) { @@ -3013,17 +3072,22 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) render_update_title(term); if (search) render_search_box(term); - tll_foreach(term->wl->seats, it) + if (urls) + render_urls(term); + if (grid | csd | search | urls) + grid_render(term); + + tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_update_cursor_rect(&it->item, term); - if (grid) - grid_render(term); + } } else { /* Tells the frame callback to render again */ term->render.pending.grid |= grid; term->render.pending.csd |= csd; term->render.pending.search |= search; term->render.pending.title |= title; + term->render.pending.urls |= urls; } } @@ -3065,6 +3129,13 @@ render_refresh_search(struct terminal *term) term->render.refresh.search = true; } +void +render_refresh_urls(struct terminal *term) +{ + if (urls_mode_is_active(term)) + term->render.refresh.urls = true; +} + bool render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor) { diff --git a/render.h b/render.h index 05c79322..1179a5dc 100644 --- a/render.h +++ b/render.h @@ -16,6 +16,7 @@ void render_refresh(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); +void render_refresh_urls(struct terminal *term); bool render_xcursor_set(struct seat *seat, struct terminal *term, const char *xcursor); struct render_worker_context { diff --git a/shm.h b/shm.h index fc48308e..2e819369 100644 --- a/shm.h +++ b/shm.h @@ -52,3 +52,6 @@ static inline unsigned long shm_cookie_search(const struct terminal *term) { ret static inline unsigned long shm_cookie_scrollback_indicator(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 2; } static inline unsigned long shm_cookie_render_timer(const struct terminal *term) { return (unsigned long)(uintptr_t)term + 3; } static inline unsigned long shm_cookie_csd(const struct terminal *term, int n) { return (unsigned long)((uintptr_t)term + 4 + (n)); } + +struct url; +static inline unsigned long shm_cookie_url(const struct url *url) { return (unsigned long)(uintptr_t)url; } diff --git a/terminal.c b/terminal.c index 701313cc..1eed0966 100644 --- a/terminal.c +++ b/terminal.c @@ -36,6 +36,7 @@ #include "sixel.h" #include "slave.h" #include "spawn.h" +#include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" @@ -1485,6 +1486,7 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); + urls_reset(term); term_ime_reset(term); free(term->foot_exe); @@ -2963,3 +2965,4 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, } #endif } + diff --git a/terminal.h b/terminal.h index 17873c40..24183c7b 100644 --- a/terminal.h +++ b/terminal.h @@ -223,6 +223,14 @@ enum term_surface { typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; +struct url { + wchar_t *url; + wchar_t *text; + wchar_t key[4]; + struct coord start; + struct coord end; +}; + struct terminal { struct fdm *fdm; struct reaper *reaper; @@ -421,6 +429,7 @@ struct terminal { bool csd; bool search; bool title; + bool urls; } refresh; /* Scheduled for rendering, in the next frame callback */ @@ -429,6 +438,7 @@ struct terminal { bool csd; bool search; bool title; + bool urls; } pending; bool margins; /* Someone explicitly requested a refresh of the margins */ @@ -499,6 +509,9 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; + tll(struct url) urls; + wchar_t url_keys[5]; + #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct { bool enabled; @@ -646,3 +659,6 @@ void term_ime_disable(struct terminal *term); void term_ime_reset(struct terminal *term); void term_ime_set_cursor_rect( struct terminal *term, int x, int y, int width, int height); + +void term_urls_reset(struct terminal *term); +void term_collect_urls(struct terminal *term); diff --git a/url-mode.c b/url-mode.c new file mode 100644 index 00000000..aabb9032 --- /dev/null +++ b/url-mode.c @@ -0,0 +1,337 @@ +#include "url-mode.h" + +#include +#include + +#define LOG_MODULE "url-mode" +#define LOG_ENABLE_DBG 1 +#include "log.h" +#include "grid.h" +#include "spawn.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" + +static bool +execute_binding(struct seat *seat, struct terminal *term, + enum bind_action_url action, uint32_t serial) +{ + switch (action) { + case BIND_ACTION_URL_NONE: + return false; + + case BIND_ACTION_URL_CANCEL: + urls_reset(term); + return true; + + case BIND_ACTION_URL_COUNT: + return false; + + } + return true; +} + +void +urls_input(struct seat *seat, struct terminal *term, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial) +{ + /* Key bindings */ + tll_foreach(seat->kbd.bindings.url, it) { + if (it->item.bind.mods != mods) + continue; + + /* Match symbol */ + if (it->item.bind.sym == sym) { + execute_binding(seat, term, it->item.action, serial); + return; + } + + /* Match raw key code */ + tll_foreach(it->item.bind.key_codes, code) { + if (code->item == key) { + execute_binding(seat, term, it->item.action, serial); + return; + } + } + } + + wchar_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + + /* + * Determine if this is a “valid” key. I.e. if there is an URL + * label with a key combo where this key is the next in + * sequence. + */ + + size_t seq_len = wcslen(term->url_keys); + + bool is_valid = false; + const struct url *match = NULL; + + tll_foreach(term->urls, it) { + const struct url *url = &it->item; + const size_t key_len = wcslen(it->item.key); + + if (key_len >= seq_len + 1 && + wcsncmp(url->key, term->url_keys, seq_len) == 0 && + url->key[seq_len] == wc) + { + is_valid = true; + if (key_len == seq_len + 1) { + match = url; + break; + } + } + } + + if (match) { + size_t chars = wcstombs(NULL, match->url, 0); + + char url_utf8[chars + 1]; + wcstombs(url_utf8, match->url, chars); + + spawn(term->reaper, term->cwd, (char *const[]){"xdg-open", url_utf8, NULL}, -1, -1, -1); + urls_reset(term); + } else if (is_valid) { + xassert(seq_len + 1 <= ALEN(term->url_keys)); + term->url_keys[seq_len] = wc; + } +} + +IGNORE_WARNING("-Wpedantic") + +static void +auto_detected(struct terminal *term) +{ + static const wchar_t *const prots[] = { + L"http://", + L"https://", + }; + + size_t max_prot_len = 0; + for (size_t i = 0; i < ALEN(prots); i++) { + size_t len = wcslen(prots[i]); + if (len > max_prot_len) + max_prot_len = len; + } + + wchar_t proto_chars[max_prot_len]; + struct coord proto_start[max_prot_len]; + size_t proto_char_count = 0; + + enum { + STATE_PROTOCOL, + STATE_URL, + } state = STATE_PROTOCOL; + + struct coord start = {-1, -1}; + wchar_t url[term->cols * term->rows + 1]; + size_t len = 0; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + for (int c = 0; c < term->cols; c++) { + const struct cell *cell = &row->cells[c]; + wchar_t wc = towlower(cell->wc); + + switch (state) { + case STATE_PROTOCOL: + for (size_t i = 0; i < max_prot_len - 1; i++) { + proto_chars[i] = proto_chars[i + 1]; + proto_start[i] = proto_start[i + 1]; + } + + if (proto_char_count == max_prot_len) + proto_char_count--; + + proto_chars[proto_char_count] = wc; + proto_start[proto_char_count] = (struct coord){c, r}; + proto_char_count++; + + for (size_t i = 0; i < ALEN(prots); i++) { + size_t prot_len = wcslen(prots[i]); + + if (proto_char_count < prot_len) + continue; + + const wchar_t *proto = &proto_chars[max_prot_len - prot_len]; + + if (wcsncmp(prots[i], proto, prot_len) == 0) { + state = STATE_URL; + start = proto_start[max_prot_len - prot_len]; + + wcsncpy(url, proto, prot_len); + len = prot_len; + break; + } + } + break; + + case STATE_URL: { + // static const wchar_t allowed[] = + // L"abcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;="; + // static const wchar_t unwise[] = L"{}|\\^[]`"; + // static const wchar_t reserved[] = L";/?:@&=+$,"; + + switch (wc) { + case L'a'...L'z': + case L'0'...L'9': + case L'-': case L'.': case L'_': case L'~': case L':': + case L'/': case L'?': case L'#': case L'[': case L']': + case L'@': case L'!': case L'$': case L'&': case L'\'': + case L'(': case L')': case L'*': case L'+': case L',': + case L';': case L'=': case L'"': + url[len++] = wc; + break; + + default: { + /* Heuristic to remove trailing characters that + * are valid URL characters, but typically not at + * the end of the URL */ + bool done = false; + struct coord end = {c, r}; + + do { + switch (url[len - 1]) { + case L'.': case L',': case L':': case L';': case L'?': + case L'!': case L'"': case L'\'': + len--; + end.col--; + if (end.col < 0) { + end.row--; + end.col = term->cols - 1; + } + break; + + default: + done = true; + break; + } + } while (!done); + + url[len] = L'\0'; + + tll_push_back( + term->urls, + ((struct url){ + .url = xwcsdup(url), + .text = xwcsdup(L""), + .start = start, + .end = end})); + + state = STATE_PROTOCOL; + len = 0; + break; + } + } + break; + } + } + } + } +} + +UNIGNORE_WARNINGS + +void +urls_collect(struct terminal *term) +{ + xassert(tll_length(term->urls) == 0); + auto_detected(term); + + size_t count = tll_length(term->urls); + + /* Assign key combos */ + + static const wchar_t *const single[] = { + L"f", L"j", L"d", L"k", L"e", L"i", L"c", L"m", L"r", + L"u", L"s", L"l", L"w", L"o", L"x", L"a", L"q", L"p", + }; + + if (count < ALEN(single)) { + size_t idx = 0; + tll_foreach(term->urls, it) { + xassert(wcslen(single[idx]) < ALEN(it->item.key) - 1); + wcscpy(it->item.key, single[idx++]); + } + } else { + LOG_ERR("unimplemented: more URLs than %zu", ALEN(single)); + assert(false); + } + +#if defined(_DEBUG) && LOG_ENABLE_DBG + tll_foreach(term->urls, it) { + char url[1024]; + wcstombs(url, it->item.url, sizeof(url) - 1); + + char key[32]; + wcstombs(key, it->item.key, sizeof(key) - 1); + + LOG_DBG("URL: %s (%s)", url, key); + } +#endif + + struct wl_window *win = term->window; + struct wayland *wayl = term->wl; + + xassert(tll_length(win->urls) == 0); + tll_foreach(win->term->urls, it) { + struct wl_surface *surf = wl_compositor_create_surface(wayl->compositor); + wl_surface_set_user_data(surf, win); + + struct wl_subsurface *sub_surf = NULL; + + if (surf != NULL) { + sub_surf = wl_subcompositor_get_subsurface( + wayl->sub_compositor, surf, win->surface); + + if (sub_surf != NULL) + wl_subsurface_set_sync(sub_surf); + } + + if (surf == NULL || sub_surf == NULL) { + LOG_WARN("failed to create URL (sub)-surface"); + + if (surf != NULL) { + wl_surface_destroy(surf); + surf = NULL; + } + + if (sub_surf != NULL) { + wl_subsurface_destroy(sub_surf); + sub_surf = NULL; + } + } + + struct wl_url url = { + .url = &it->item, + .surf = surf, + .sub_surf = sub_surf, + }; + + tll_push_back(win->urls, url); + } +} + +void +urls_reset(struct terminal *term) +{ + if (term->window != NULL) { + tll_foreach(term->window->urls, it) { + if (it->item.sub_surf != NULL) + wl_subsurface_destroy(it->item.sub_surf); + if (it->item.surf != NULL) + wl_surface_destroy(it->item.surf); + } + tll_free(term->window->urls); + } + + tll_foreach(term->urls, it) { + free(it->item.url); + free(it->item.text); + } + tll_free(term->urls); + + memset(term->url_keys, 0, sizeof(term->url_keys)); +} diff --git a/url-mode.h b/url-mode.h new file mode 100644 index 00000000..150a0222 --- /dev/null +++ b/url-mode.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +#include "terminal.h" + +static inline bool urls_mode_is_active(const struct terminal *term) +{ + return tll_length(term->urls) > 0; +} + +void urls_collect(struct terminal *term); +void urls_reset(struct terminal *term); + +void urls_input(struct seat *seat, struct terminal *term, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial); diff --git a/wayland.c b/wayland.c index f30e1b6f..2a79bb10 100644 --- a/wayland.c +++ b/wayland.c @@ -1426,6 +1426,14 @@ wayl_win_destroy(struct wl_window *win) tll_free(win->on_outputs); + tll_foreach(win->urls, it) { + if (it->item.sub_surf != NULL) + wl_subsurface_destroy(it->item.sub_surf); + if (it->item.surf != NULL) + wl_surface_destroy(it->item.surf); + } + tll_free(win->urls); + csd_destroy(win); if (win->render_timer_sub_surface != NULL) wl_subsurface_destroy(win->render_timer_sub_surface); diff --git a/wayland.h b/wayland.h index fdfe905c..2e5997e4 100644 --- a/wayland.h +++ b/wayland.h @@ -362,6 +362,12 @@ struct monitor { bool use_output_release; }; +struct wl_url { + const struct url *url; + struct wl_surface *surf; + struct wl_subsurface *sub_surf; +}; + struct wayland; struct wl_window { struct terminal *term; @@ -393,6 +399,7 @@ struct wl_window { struct wl_callback *frame_callback; tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ + tll(struct wl_url) urls; bool is_configured; bool is_fullscreen; From d75688b0bd804b7c5f84178bbc139982a2a0ab39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:38:56 +0100 Subject: [PATCH 04/55] =?UTF-8?q?url-mode:=20fix=20=E2=80=98n=E2=80=99=20p?= =?UTF-8?q?arameter=20to=20wcstombs()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/url-mode.c b/url-mode.c index aabb9032..0f23845c 100644 --- a/url-mode.c +++ b/url-mode.c @@ -87,12 +87,16 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, if (match) { size_t chars = wcstombs(NULL, match->url, 0); - char url_utf8[chars + 1]; - wcstombs(url_utf8, match->url, chars); + if (chars != (size_t)-1) { + char url_utf8[chars + 1]; + wcstombs(url_utf8, match->url, chars + 1); + spawn(term->reaper, term->cwd, (char *const[]){"xdg-open", url_utf8, NULL}, -1, -1, -1); + } - spawn(term->reaper, term->cwd, (char *const[]){"xdg-open", url_utf8, NULL}, -1, -1, -1); urls_reset(term); - } else if (is_valid) { + } + + else if (is_valid) { xassert(seq_len + 1 <= ALEN(term->url_keys)); term->url_keys[seq_len] = wc; } From 2315aba458b662dcbb2ce4b8ceb3d9bfd14f4970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:39:23 +0100 Subject: [PATCH 05/55] =?UTF-8?q?url-mode:=20urls=5Freset()=20do=20an=20ea?= =?UTF-8?q?rly=20return=20if=20we=20don=E2=80=99t=20have=20any=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/url-mode.c b/url-mode.c index 0f23845c..170be4c9 100644 --- a/url-mode.c +++ b/url-mode.c @@ -321,6 +321,9 @@ urls_collect(struct terminal *term) void urls_reset(struct terminal *term) { + if (likely(tll_length(term->urls) == 0)) + return; + if (term->window != NULL) { tll_foreach(term->window->urls, it) { if (it->item.sub_surf != NULL) From 44b7758416d488911f9722ecccd5cc6bf8efddf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:52:44 +0100 Subject: [PATCH 06/55] render: urls: positioning: place a bit further away from the starting position --- render.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index e19d187c..40e6b698 100644 --- a/render.c +++ b/render.c @@ -2565,8 +2565,8 @@ render_urls(struct terminal *term) const struct coord *pos = &url->start; wl_subsurface_set_position( sub_surf, - (term->margins.left + pos->col * term->cell_width - term->cell_width) / term->scale, - (term->margins.top + pos->row * term->cell_height - term->cell_height / 2) / term->scale); + (term->margins.left + pos->col * term->cell_width - 2 * term->cell_width) / term->scale, + (term->margins.top + pos->row * term->cell_height - term->cell_height) / term->scale); render_osd(term, surf, sub_surf, buf, label, term->colors.table[0], term->colors.table[3], 0xf000, width, height, margin, margin); From 0cbdf657a78d2932ef9435f53645c7bb9a4cd28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 11:53:12 +0100 Subject: [PATCH 07/55] term: destroy: set term->window = NULL after destroying it This fixes a crash in urls_reset() on destroy, where we tried to access an already free:d window pointer in order to destroy the jump label surfaces. --- terminal.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 1eed0966..624b7a21 100644 --- a/terminal.c +++ b/terminal.c @@ -1405,8 +1405,10 @@ term_destroy(struct terminal *term) fdm_del(term->fdm, term->flash.fd); fdm_del(term->fdm, term->ptmx); - if (term->window != NULL) + if (term->window != NULL) { wayl_win_destroy(term->window); + term->window = NULL; + } mtx_lock(&term->render.workers.lock); xassert(tll_length(term->render.workers.queue) == 0); From 4233c806c32fef8f92e51361b2f00fd50799f9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 14:27:13 +0100 Subject: [PATCH 08/55] =?UTF-8?q?config:=20add=20=E2=80=98url-launch?= =?UTF-8?q?=E2=80=99=20option,=20defaulting=20to=20=E2=80=9Cxdg-open=20${u?= =?UTF-8?q?rl}=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 66 ++++++++++++++++++++++++++++++++++------------ config.h | 11 +++++--- doc/foot.ini.5.scd | 4 +++ foot.ini | 4 ++- 4 files changed, 63 insertions(+), 22 deletions(-) diff --git a/config.c b/config.c index 5fccbea4..18503541 100644 --- a/config.c +++ b/config.c @@ -453,6 +453,33 @@ str_to_pt_or_px(const char *s, struct pt_or_px *res, struct config *conf, return true; } +static bool +str_to_spawn_template(struct config *conf, + const char *s, struct config_spawn_template *template, + const char *path, int lineno, const char *section, + const char *key) +{ + free(template->raw_cmd); + free(template->argv); + + template->raw_cmd = NULL; + template->argv = NULL; + + char *raw_cmd = xstrdup(s); + char **argv = NULL; + + if (!tokenize_cmdline(raw_cmd, &argv)) { + LOG_AND_NOTIFY_ERR( + "%s:%d: [%s]: %s: syntax error in command line", + path, lineno, section, key); + return false; + } + + template->raw_cmd = raw_cmd; + template->argv = argv; + return true; +} + static bool parse_section_main(const char *key, const char *value, struct config *conf, const char *path, unsigned lineno) @@ -675,24 +702,19 @@ parse_section_main(const char *key, const char *value, struct config *conf, } else if (strcmp(key, "notify") == 0) { - free(conf->notify.raw_cmd); - free(conf->notify.argv); - - conf->notify.raw_cmd = NULL; - conf->notify.argv = NULL; - - char *raw_cmd = xstrdup(value); - char **argv = NULL; - - if (!tokenize_cmdline(raw_cmd, &argv)) { - LOG_AND_NOTIFY_ERR( - "%s:%d: [default]: notify: syntax error in command line", - path, lineno); + if (!str_to_spawn_template(conf, value, &conf->notify, path, lineno, + "default", "notify")) + { return false; } + } - conf->notify.raw_cmd = raw_cmd; - conf->notify.argv = argv; + else if (strcmp(key, "url-launch") == 0) { + if (!str_to_spawn_template(conf, value, &conf->url_launch, path, lineno, + "default", "url-launch")) + { + return false; + } } else if (strcmp(key, "selection-target") == 0) { @@ -2308,6 +2330,9 @@ config_load(struct config *conf, const char *conf_path, "notify-send -a foot -i foot ${title} ${body}"); tokenize_cmdline(conf->notify.raw_cmd, &conf->notify.argv); + conf->url_launch.raw_cmd = xstrdup("xdg-open ${url}"); + tokenize_cmdline(conf->url_launch.raw_cmd, &conf->url_launch.argv); + tll_foreach(*initial_user_notifications, it) tll_push_back(conf->notifications, it->item); tll_free(*initial_user_notifications); @@ -2372,6 +2397,13 @@ out: return ret; } +static void +free_spawn_template(struct config_spawn_template *template) +{ + free(template->raw_cmd); + free(template->argv); +} + void config_free(struct config conf) { @@ -2381,8 +2413,8 @@ config_free(struct config conf) free(conf.app_id); free(conf.word_delimiters); free(conf.scrollback.indicator.text); - free(conf.notify.raw_cmd); - free(conf.notify.argv); + free_spawn_template(&conf.notify); + free_spawn_template(&conf.url_launch); for (size_t i = 0; i < ALEN(conf.fonts); i++) { tll_foreach(conf.fonts[i], it) config_font_destroy(&it->item); diff --git a/config.h b/config.h index f46fa35f..d6ad2b24 100644 --- a/config.h +++ b/config.h @@ -66,6 +66,11 @@ struct pt_or_px { float pt; }; +struct config_spawn_template { + char *raw_cmd; + char **argv; +}; + struct config { char *term; char *shell; @@ -198,10 +203,8 @@ struct config { SELECTION_TARGET_BOTH } selection_target; - struct { - char *raw_cmd; - char **argv; - } notify; + struct config_spawn_template notify; + struct config_spawn_template url_launch; struct { enum fcft_scaling_filter fcft_filter; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 9982acb8..71fbc313 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -225,6 +225,10 @@ in this order: Default: _notify-send -a foot -i foot ${title} ${body}_. +*url-launch* + Command to execute when opening URLs. _${url}_ will be replaced + with the actual URL. Default: _xdg-open ${url}_. + *selection-target* Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. diff --git a/foot.ini b/foot.ini index 2ea574a2..c69f9716 100644 --- a/foot.ini +++ b/foot.ini @@ -20,10 +20,12 @@ # pad=2x2 # optionally append 'center' # resize-delay-ms=100 +# notify=notify-send -a foot -i foot ${title} ${body} +# url-launch=xdg-open ${url} + # bold-text-in-bright=no # bell=none # word-delimiters=,│`|:"'()[]{}<> -# notify=notify-send -a foot -i foot ${title} ${body} # selection-target=primary # workers= From 06aba59430bdb1ad5058ef513eeb91416057d1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 14:40:27 +0100 Subject: [PATCH 09/55] notify: break out command template expansion to spawn_expand_template() --- notify.c | 71 ++++++++----------------------------------------- spawn.c | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ spawn.h | 6 +++++ 3 files changed, 97 insertions(+), 60 deletions(-) diff --git a/notify.c b/notify.c index b8480d68..3f36e904 100644 --- a/notify.c +++ b/notify.c @@ -30,69 +30,20 @@ notify_notify(const struct terminal *term, const char *title, const char *body) if (term->conf->notify.argv == NULL) return; - size_t argv_size = 0; - for (; term->conf->notify.argv[argv_size] != NULL; argv_size++) - ; + char **argv = NULL; + size_t argc = 0; -#define append(s, n) \ - do { \ - expanded = xrealloc(expanded, len + (n) + 1); \ - memcpy(&expanded[len], s, n); \ - len += n; \ - expanded[len] = '\0'; \ - } while (0) - - char **argv = malloc((argv_size + 1) * sizeof(argv[0])); - - /* Expand ${title} and ${body} */ - for (size_t i = 0; i < argv_size; i++) { - size_t len = 0; - char *expanded = NULL; - - char *start = NULL; - char *last_end = term->conf->notify.argv[i]; - - while ((start = strstr(last_end, "${")) != NULL) { - /* Append everything from the last template's end to this - * one's beginning */ - append(last_end, start - last_end); - - /* Find end of template */ - start += 2; - char *end = strstr(start, "}"); - - if (end == NULL) { - /* Ensure final append() copies the unclosed '${' */ - last_end = start - 2; - LOG_WARN("notify: unclosed template: %s", last_end); - break; - } - - /* Expand template */ - if (strncmp(start, "title", end - start) == 0) - append(title, strlen(title)); - else if (strncmp(start, "body", end - start) == 0) - append(body, strlen(body)); - else { - /* Unrecognized template - append it as-is */ - start -= 2; - append(start, end + 1 - start); - LOG_WARN("notify: unrecognized template: %.*s", - (int)(end + 1 - start), start); - } - - last_end = end + 1;; - } - - append(last_end, term->conf->notify.argv[i] + strlen(term->conf->notify.argv[i]) - last_end); - argv[i] = expanded; + if (!spawn_expand_template( + &term->conf->notify, 2, + (const char *[]){"title", "body"}, + (const char *[]){title, body}, + &argc, &argv)) + { + return; } - argv[argv_size] = NULL; - -#undef append LOG_DBG("notify command:"); - for (size_t i = 0; i < argv_size; i++) + for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); /* Redirect stdin to /dev/null, but ignore failure to open */ @@ -102,7 +53,7 @@ notify_notify(const struct terminal *term, const char *title, const char *body) if (devnull >= 0) close(devnull); - for (size_t i = 0; i < argv_size; i++) + for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); } diff --git a/spawn.c b/spawn.c index fbcf0952..e9fb2f96 100644 --- a/spawn.c +++ b/spawn.c @@ -1,5 +1,6 @@ #include "spawn.h" +#include #include #include @@ -11,6 +12,7 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" +#include "xmalloc.h" bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], @@ -74,3 +76,81 @@ err: close(pipe_fds[1]); return false; } + +bool +spawn_expand_template(const struct config_spawn_template *template, + size_t key_count, + const char *key_names[static key_count], + const char *key_values[static key_count], + size_t *argc, char ***argv) +{ + *argc = 0; + *argv = NULL; + + for (; template->argv[*argc] != NULL; (*argc)++) + ; + +#define append(s, n) \ + do { \ + expanded = xrealloc(expanded, len + (n) + 1); \ + memcpy(&expanded[len], s, n); \ + len += n; \ + expanded[len] = '\0'; \ + } while (0) + + *argv = malloc((*argc + 1) * sizeof((*argv)[0])); + + /* Expand the provided keys */ + for (size_t i = 0; i < *argc; i++) { + size_t len = 0; + char *expanded = NULL; + + char *start = NULL; + char *last_end = template->argv[i]; + + while ((start = strstr(last_end, "${")) != NULL) { + /* Append everything from the last template's end to this + * one's beginning */ + append(last_end, start - last_end); + + /* Find end of template */ + start += 2; + char *end = strstr(start, "}"); + + if (end == NULL) { + /* Ensure final append() copies the unclosed '${' */ + last_end = start - 2; + LOG_WARN("notify: unclosed template: %s", last_end); + break; + } + + /* Expand template */ + bool valid_key = false; + for (size_t j = 0; j < key_count; j++) { + if (strncmp(start, key_names[j], end - start) != 0) + continue; + + append(key_values[j], strlen(key_values[j])); + valid_key = true; + break; + } + + if (!valid_key) { + /* Unrecognized template - append it as-is */ + start -= 2; + append(start, end + 1 - start); + LOG_WARN("notify: unrecognized template: %.*s", + (int)(end + 1 - start), start); + } + + last_end = end + 1; + } + + append(last_end, template->argv[i] + strlen(template->argv[i]) - last_end); + (*argv)[i] = expanded; + } + (*argv)[*argc] = NULL; + +#undef append + return true; +} diff --git a/spawn.h b/spawn.h index 2ab645a8..c6f9582e 100644 --- a/spawn.h +++ b/spawn.h @@ -1,7 +1,13 @@ #pragma once #include +#include "config.h" #include "reaper.h" bool spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd); + +bool spawn_expand_template( + const struct config_spawn_template *template, + size_t key_count, const char *key_names[static key_count], + const char *key_values[static key_count], size_t *argc, char ***argv); From 9d8ec857cec426b4f0a3277a63abeb1a420e6107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 31 Jan 2021 14:43:26 +0100 Subject: [PATCH 10/55] =?UTF-8?q?url-mode:=20use=20=E2=80=98url-launch?= =?UTF-8?q?=E2=80=99=20from=20config=20to=20open=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 170be4c9..a4fea23f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -90,7 +90,22 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, if (chars != (size_t)-1) { char url_utf8[chars + 1]; wcstombs(url_utf8, match->url, chars + 1); - spawn(term->reaper, term->cwd, (char *const[]){"xdg-open", url_utf8, NULL}, -1, -1, -1); + + size_t argc; + char **argv; + + if (spawn_expand_template( + &term->conf->url_launch, 1, + (const char *[]){"url"}, + (const char *[]){url_utf8}, + &argc, &argv)) + { + spawn(term->reaper, term->cwd, argv, -1, -1, -1); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } } urls_reset(term); From f61f7c131fc896af23437880778556e506e73018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 1 Feb 2021 09:55:18 +0100 Subject: [PATCH 11/55] url-mode: auto-detect: heuristics for parenthesis and brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While parenthesis and brackets _are_ valid URL characters, there are many times when we do *not* want them to be part of the URL. For example, in markdown we write “[text](url)”, or even “[![alt-text](url-1)](url-2)”. Here, the URLs are clearly *not* “url)” or “url-1)](url2)”. --- url-mode.c | 47 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/url-mode.c b/url-mode.c index a4fea23f..fecbaa6b 100644 --- a/url-mode.c +++ b/url-mode.c @@ -147,6 +147,9 @@ auto_detected(struct terminal *term) wchar_t url[term->cols * term->rows + 1]; size_t len = 0; + ssize_t parenthesis = 0; + ssize_t brackets = 0; + for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); @@ -182,6 +185,8 @@ auto_detected(struct terminal *term) wcsncpy(url, proto, prot_len); len = prot_len; + + parenthesis = brackets = 0; break; } } @@ -193,18 +198,47 @@ auto_detected(struct terminal *term) // static const wchar_t unwise[] = L"{}|\\^[]`"; // static const wchar_t reserved[] = L";/?:@&=+$,"; + bool emit_url = false; switch (wc) { case L'a'...L'z': case L'0'...L'9': case L'-': case L'.': case L'_': case L'~': case L':': - case L'/': case L'?': case L'#': case L'[': case L']': - case L'@': case L'!': case L'$': case L'&': case L'\'': - case L'(': case L')': case L'*': case L'+': case L',': - case L';': case L'=': case L'"': + case L'/': case L'?': case L'#': case L'@': case L'!': + case L'$': case L'&': case L'\'': case L'*': case L'+': + case L',': case L';': case L'=': case L'"': url[len++] = wc; break; - default: { + case L'(': + parenthesis++; + url[len++] = wc; + break; + + case L'[': + brackets++; + url[len++] = wc; + break; + + case L')': + if (--parenthesis < 0) + emit_url = true; + else + url[len++] = wc; + break; + + case L']': + if (--brackets < 0) + emit_url = true; + else + url[len++] = wc; + break; + + default: + emit_url = true; + break; + } + + if (emit_url) { /* Heuristic to remove trailing characters that * are valid URL characters, but typically not at * the end of the URL */ @@ -241,8 +275,7 @@ auto_detected(struct terminal *term) state = STATE_PROTOCOL; len = 0; - break; - } + parenthesis = brackets = 0; } break; } From eb0b244c891f3cb6d5ba08fb3ea8af4bc421398e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 1 Feb 2021 10:04:25 +0100 Subject: [PATCH 12/55] =?UTF-8?q?render:=20url=20labels:=20don=E2=80=99t?= =?UTF-8?q?=20position=20outside=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index 40e6b698..00b6290d 100644 --- a/render.c +++ b/render.c @@ -2563,10 +2563,19 @@ render_urls(struct terminal *term) term->wl->shm, width, height, shm_cookie_url(url), false, 1); const struct coord *pos = &url->start; + int x = pos->col * term->cell_width - 2 * term->cell_width; + int y = pos->row * term->cell_height - term->cell_height; + + if (x < 0) + x += 4 * term->cell_width; + if (y < 0) + y += 2 * term->cell_height; + wl_subsurface_set_position( sub_surf, - (term->margins.left + pos->col * term->cell_width - 2 * term->cell_width) / term->scale, - (term->margins.top + pos->row * term->cell_height - term->cell_height) / term->scale); + (term->margins.left + x) / term->scale, + (term->margins.top + y) / term->scale); + render_osd(term, surf, sub_surf, buf, label, term->colors.table[0], term->colors.table[3], 0xf000, width, height, margin, margin); From 65caa33084426cb00df96eda1089768b4251b811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 1 Feb 2021 10:40:15 +0100 Subject: [PATCH 13/55] =?UTF-8?q?url-mode:=20auto-detect:=20don=E2=80=99t?= =?UTF-8?q?=20line-wrap=20URL=20is=20row=20isn=E2=80=99t=20line-wrapped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/url-mode.c b/url-mode.c index fecbaa6b..78291685 100644 --- a/url-mode.c +++ b/url-mode.c @@ -238,6 +238,9 @@ auto_detected(struct terminal *term) break; } + if (c >= term->cols - 1 && row->linebreak) + emit_url = true; + if (emit_url) { /* Heuristic to remove trailing characters that * are valid URL characters, but typically not at From 82e2541760e05e989a6fadf834e088b8128ac74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 19:40:24 +0100 Subject: [PATCH 14/55] config: add ctrl+d as (yet another) default binding url-bindings.cancel --- config.c | 1 + doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index 18503541..2cf62f45 100644 --- a/config.c +++ b/config.c @@ -2181,6 +2181,7 @@ add_default_url_bindings(struct config *conf) const struct config_key_modifiers ctrl = {.ctrl = true}; add_binding(BIND_ACTION_URL_CANCEL, ctrl, XKB_KEY_g); + add_binding(BIND_ACTION_URL_CANCEL, ctrl, XKB_KEY_d); add_binding(BIND_ACTION_URL_CANCEL, none, XKB_KEY_Escape); #undef add_binding diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 71fbc313..0eb4cc11 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -586,7 +586,7 @@ mode. The syntax is exactly the same as the regular **key-bindings**. *cancel* Exits URL mode without opening an URL. Default: _Control+g - Escape_. + Control+d Escape_. # SECTION: mouse-bindings diff --git a/foot.ini b/foot.ini index c69f9716..b8998c18 100644 --- a/foot.ini +++ b/foot.ini @@ -119,7 +119,7 @@ # primary-paste=Shift+Insert [url-bindings] -# cancel=Control+g Escape +# cancel=Control+g Control+d Escape [mouse-bindings] # primary-paste=BTN_MIDDLE From d69497efe4025b5650ede2a5d24e38da4b23f2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 19:43:32 +0100 Subject: [PATCH 15/55] render: reduce URL jump label margins --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 00b6290d..710ef4a0 100644 --- a/render.c +++ b/render.c @@ -2555,7 +2555,7 @@ render_urls(struct terminal *term) size_t len = wcslen(label); int cols = wcswidth(label, len); - const int margin = 3 * term->scale; + const int margin = 2 * term->scale; int width = 2 * margin + cols * term->cell_width; int height = 2 * margin + term->cell_height; From 013b26d21221795d1107a74e28f5008a16864049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 19:43:47 +0100 Subject: [PATCH 16/55] render: urls: tweak positioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don’t offset the labels too much vertically, _or_ horizontally. --- render.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/render.c b/render.c index 710ef4a0..451b0ff3 100644 --- a/render.c +++ b/render.c @@ -2563,13 +2563,15 @@ render_urls(struct terminal *term) term->wl->shm, width, height, shm_cookie_url(url), false, 1); const struct coord *pos = &url->start; - int x = pos->col * term->cell_width - 2 * term->cell_width; - int y = pos->row * term->cell_height - term->cell_height; + int x = pos->col * term->cell_width - 15 * term->cell_width / 10; + int y = pos->row * term->cell_height - 5 * term->cell_height / 10; if (x < 0) - x += 4 * term->cell_width; + x = 0; +#if 0 if (y < 0) - y += 2 * term->cell_height; + y += 15 * term->cell_height / 10; +#endif wl_subsurface_set_position( sub_surf, From 0a1c5e44c4459390e927440a59acafafc8372da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 20:39:13 +0100 Subject: [PATCH 17/55] =?UTF-8?q?config:=20rename=20=E2=80=98show-urls?= =?UTF-8?q?=E2=80=99=20to=20=E2=80=98show-urls-launch=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 4 ++-- doc/foot.ini.5.scd | 2 +- foot.ini | 1 + input.c | 2 +- wayland.h | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/config.c b/config.c index 2cf62f45..1c320da8 100644 --- a/config.c +++ b/config.c @@ -75,7 +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", + [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", /* Mouse-specific actions */ [BIND_ACTION_SELECT_BEGIN] = "select-begin", @@ -2114,7 +2114,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); + add_binding(BIND_ACTION_SHOW_URLS_LAUNCH, ctrl_shift, XKB_KEY_F); #undef add_binding } diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 0eb4cc11..4d552a67 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -495,7 +495,7 @@ e.g. *search-start=none*. Default: _not bound_ -*show-urls* +*show-urls-launch* 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_. diff --git a/foot.ini b/foot.ini index b8998c18..41dec6c6 100644 --- a/foot.ini +++ b/foot.ini @@ -97,6 +97,7 @@ # pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox"] none # pipe-scrollback=[sh -c "xurls | bemenu | xargs -r firefox"] none # pipe-selected=[xargs -r firefox] none +# show-urls=Control+Shift+F [search-bindings] # cancel=Control+g Escape diff --git a/input.c b/input.c index 9dbdaad1..dfbf4fa3 100644 --- a/input.c +++ b/input.c @@ -272,7 +272,7 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } - case BIND_ACTION_SHOW_URLS: + case BIND_ACTION_SHOW_URLS_LAUNCH: xassert(!urls_mode_is_active(term)); urls_collect(term); diff --git a/wayland.h b/wayland.h index 2e5997e4..3ad7b862 100644 --- a/wayland.h +++ b/wayland.h @@ -49,7 +49,7 @@ enum bind_action_normal { BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_SELECTED, - BIND_ACTION_SHOW_URLS, + BIND_ACTION_SHOW_URLS_LAUNCH, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SELECT_BEGIN, @@ -60,7 +60,7 @@ enum bind_action_normal { BIND_ACTION_SELECT_WORD_WS, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_SHOW_URLS + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_SHOW_URLS_LAUNCH + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; From b17a392b8cd1a16a74c4190d4d95cdb32c168445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 20:54:17 +0100 Subject: [PATCH 18/55] config: show-urls-launch: change default key binding to ctrl+shift+u --- config.c | 2 +- doc/foot.ini.5.scd | 2 +- foot.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index 1c320da8..0ea45b02 100644 --- a/config.c +++ b/config.c @@ -2114,7 +2114,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_LAUNCH, ctrl_shift, XKB_KEY_F); + add_binding(BIND_ACTION_SHOW_URLS_LAUNCH, ctrl_shift, XKB_KEY_U); #undef add_binding } diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 4d552a67..fe158279 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -498,7 +498,7 @@ e.g. *search-start=none*. *show-urls-launch* 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_. + _Control+Shift+U_. # SECTION: search-bindings diff --git a/foot.ini b/foot.ini index 41dec6c6..692eac4f 100644 --- a/foot.ini +++ b/foot.ini @@ -97,7 +97,7 @@ # pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox"] none # pipe-scrollback=[sh -c "xurls | bemenu | xargs -r firefox"] none # pipe-selected=[xargs -r firefox] none -# show-urls=Control+Shift+F +# show-urls-launch=Control+Shift+U [search-bindings] # cancel=Control+g Escape From 93181649b32249f572bc4c6892c99155186fc714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 20:55:08 +0100 Subject: [PATCH 19/55] config: add show-urls-copy action This works just like show-urls-launch, except that instead of opening the URL (typically using xdg-open), it is placed in the clipboard when activated. --- config.c | 1 + doc/foot.ini.5.scd | 5 +++++ foot.ini | 1 + input.c | 10 ++++++++-- terminal.h | 2 ++ url-mode.c | 50 ++++++++++++++++++++++++++++++---------------- url-mode.h | 2 +- wayland.h | 1 + 8 files changed, 52 insertions(+), 20 deletions(-) diff --git a/config.c b/config.c index 0ea45b02..74cff1f3 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_COPY] = "show-urls-copy", [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", /* Mouse-specific actions */ diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index fe158279..727b81f1 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -500,6 +500,11 @@ e.g. *search-start=none*. a jump label with a key sequence that will open the URL. Default: _Control+Shift+U_. +*show-urls-copy* + Enters URL mode, where all currently visible URLs are tagged with + a jump label with a key sequence that will place the URL in the + clipboard. Default: _none_. + # SECTION: search-bindings diff --git a/foot.ini b/foot.ini index 692eac4f..4ed3d64b 100644 --- a/foot.ini +++ b/foot.ini @@ -98,6 +98,7 @@ # pipe-scrollback=[sh -c "xurls | bemenu | xargs -r firefox"] none # pipe-selected=[xargs -r firefox] none # show-urls-launch=Control+Shift+U +# show-urls-copy=none [search-bindings] # cancel=Control+g Escape diff --git a/input.c b/input.c index dfbf4fa3..65670aa3 100644 --- a/input.c +++ b/input.c @@ -272,12 +272,18 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } - case BIND_ACTION_SHOW_URLS_LAUNCH: + case BIND_ACTION_SHOW_URLS_COPY: + case BIND_ACTION_SHOW_URLS_LAUNCH: { xassert(!urls_mode_is_active(term)); - urls_collect(term); + enum url_action url_action = action == BIND_ACTION_SHOW_URLS_COPY + ? URL_ACTION_COPY + : URL_ACTION_LAUNCH; + + urls_collect(term, url_action); render_refresh_urls(term); return true; + } case BIND_ACTION_SELECT_BEGIN: selection_start( diff --git a/terminal.h b/terminal.h index 24183c7b..b58cd137 100644 --- a/terminal.h +++ b/terminal.h @@ -223,12 +223,14 @@ enum term_surface { typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; +enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH }; struct url { wchar_t *url; wchar_t *text; wchar_t key[4]; struct coord start; struct coord end; + enum url_action action; }; struct terminal { diff --git a/url-mode.c b/url-mode.c index 78291685..79c6c17c 100644 --- a/url-mode.c +++ b/url-mode.c @@ -7,6 +7,7 @@ #define LOG_ENABLE_DBG 1 #include "log.h" #include "grid.h" +#include "selection.h" #include "spawn.h" #include "terminal.h" #include "util.h" @@ -88,24 +89,38 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, size_t chars = wcstombs(NULL, match->url, 0); if (chars != (size_t)-1) { - char url_utf8[chars + 1]; + char *url_utf8 = malloc(chars + 1); wcstombs(url_utf8, match->url, chars + 1); - size_t argc; - char **argv; + switch (match->action) { + case URL_ACTION_COPY: + if (text_to_clipboard(seat, term, url_utf8, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_utf8 = NULL; + } + break; - if (spawn_expand_template( - &term->conf->url_launch, 1, - (const char *[]){"url"}, - (const char *[]){url_utf8}, - &argc, &argv)) - { - spawn(term->reaper, term->cwd, argv, -1, -1, -1); + case URL_ACTION_LAUNCH: { + size_t argc; + char **argv; - for (size_t i = 0; i < argc; i++) - free(argv[i]); - free(argv); + if (spawn_expand_template( + &term->conf->url_launch, 1, + (const char *[]){"url"}, + (const char *[]){url_utf8}, + &argc, &argv)) + { + spawn(term->reaper, term->cwd, argv, -1, -1, -1); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + break; } + } + + free(url_utf8); } urls_reset(term); @@ -120,7 +135,7 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, IGNORE_WARNING("-Wpedantic") static void -auto_detected(struct terminal *term) +auto_detected(struct terminal *term, enum url_action action) { static const wchar_t *const prots[] = { L"http://", @@ -274,7 +289,8 @@ auto_detected(struct terminal *term) .url = xwcsdup(url), .text = xwcsdup(L""), .start = start, - .end = end})); + .end = end, + .action = action})); state = STATE_PROTOCOL; len = 0; @@ -290,10 +306,10 @@ auto_detected(struct terminal *term) UNIGNORE_WARNINGS void -urls_collect(struct terminal *term) +urls_collect(struct terminal *term, enum url_action action) { xassert(tll_length(term->urls) == 0); - auto_detected(term); + auto_detected(term, action); size_t count = tll_length(term->urls); diff --git a/url-mode.h b/url-mode.h index 150a0222..94c1e675 100644 --- a/url-mode.h +++ b/url-mode.h @@ -11,7 +11,7 @@ static inline bool urls_mode_is_active(const struct terminal *term) return tll_length(term->urls) > 0; } -void urls_collect(struct terminal *term); +void urls_collect(struct terminal *term, enum url_action action); void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, uint32_t key, diff --git a/wayland.h b/wayland.h index 3ad7b862..3c160b7c 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_COPY, BIND_ACTION_SHOW_URLS_LAUNCH, /* Mouse specific actions - i.e. they require a mouse coordinate */ From 6b7003bcc385bda1bcf39aca441a02d122f8ab06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 20:57:19 +0100 Subject: [PATCH 20/55] =?UTF-8?q?url-mode:=20auto-detect:=20don=E2=80=99t?= =?UTF-8?q?=20store=20the=20lower-cased=20URL;=20use=20original=20casing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/url-mode.c b/url-mode.c index 79c6c17c..8d31908c 100644 --- a/url-mode.c +++ b/url-mode.c @@ -221,31 +221,31 @@ auto_detected(struct terminal *term, enum url_action action) case L'/': case L'?': case L'#': case L'@': case L'!': case L'$': case L'&': case L'\'': case L'*': case L'+': case L',': case L';': case L'=': case L'"': - url[len++] = wc; + url[len++] = cell->wc; break; case L'(': parenthesis++; - url[len++] = wc; + url[len++] = cell->wc; break; case L'[': brackets++; - url[len++] = wc; + url[len++] = cell->wc; break; case L')': if (--parenthesis < 0) emit_url = true; else - url[len++] = wc; + url[len++] = cell->wc; break; case L']': if (--brackets < 0) emit_url = true; else - url[len++] = wc; + url[len++] = cell->wc; break; default: From 607ee63b77138acd3c366e1a746b6b97e03e0da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 21:08:49 +0100 Subject: [PATCH 21/55] url-mode: auto-detect: use wcsncasecmp() instead of towlower() When matching the URI scheme, use wcsncasecmp() when comparing the strings, instead of calling towlower() on each cell. --- url-mode.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/url-mode.c b/url-mode.c index 8d31908c..1b6dc9d8 100644 --- a/url-mode.c +++ b/url-mode.c @@ -170,7 +170,7 @@ auto_detected(struct terminal *term, enum url_action action) for (int c = 0; c < term->cols; c++) { const struct cell *cell = &row->cells[c]; - wchar_t wc = towlower(cell->wc); + wchar_t wc = cell->wc; switch (state) { case STATE_PROTOCOL: @@ -194,7 +194,7 @@ auto_detected(struct terminal *term, enum url_action action) const wchar_t *proto = &proto_chars[max_prot_len - prot_len]; - if (wcsncmp(prots[i], proto, prot_len) == 0) { + if (wcsncasecmp(prots[i], proto, prot_len) == 0) { state = STATE_URL; start = proto_start[max_prot_len - prot_len]; @@ -216,36 +216,37 @@ auto_detected(struct terminal *term, enum url_action action) bool emit_url = false; switch (wc) { case L'a'...L'z': + case L'A'...L'Z': case L'0'...L'9': case L'-': case L'.': case L'_': case L'~': case L':': case L'/': case L'?': case L'#': case L'@': case L'!': case L'$': case L'&': case L'\'': case L'*': case L'+': case L',': case L';': case L'=': case L'"': - url[len++] = cell->wc; + url[len++] = wc; break; case L'(': parenthesis++; - url[len++] = cell->wc; + url[len++] = wc; break; case L'[': brackets++; - url[len++] = cell->wc; + url[len++] = wc; break; case L')': if (--parenthesis < 0) emit_url = true; else - url[len++] = cell->wc; + url[len++] = wc; break; case L']': if (--brackets < 0) emit_url = true; else - url[len++] = cell->wc; + url[len++] = wc; break; default: From ef3ce530baf48bdf52bf6f1a9a25c59c0fb46bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 4 Feb 2021 21:19:30 +0100 Subject: [PATCH 22/55] url-mode: refactor: break out URL activation to a separate function --- url-mode.c | 79 +++++++++++++++++++++++++++++------------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/url-mode.c b/url-mode.c index 1b6dc9d8..f38026b4 100644 --- a/url-mode.c +++ b/url-mode.c @@ -32,6 +32,47 @@ execute_binding(struct seat *seat, struct terminal *term, return true; } +static void +activate_url(struct seat *seat, struct terminal *term, const struct url *url) +{ + size_t chars = wcstombs(NULL, url->url, 0); + + if (chars != (size_t)-1) { + char *url_utf8 = malloc(chars + 1); + wcstombs(url_utf8, url->url, chars + 1); + + switch (url->action) { + case URL_ACTION_COPY: + if (text_to_clipboard(seat, term, url_utf8, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_utf8 = NULL; + } + break; + + case URL_ACTION_LAUNCH: { + size_t argc; + char **argv; + + if (spawn_expand_template( + &term->conf->url_launch, 1, + (const char *[]){"url"}, + (const char *[]){url_utf8}, + &argc, &argv)) + { + spawn(term->reaper, term->cwd, argv, -1, -1, -1); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + break; + } + } + + free(url_utf8); + } +} + void urls_input(struct seat *seat, struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial) @@ -86,43 +127,7 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, } if (match) { - size_t chars = wcstombs(NULL, match->url, 0); - - if (chars != (size_t)-1) { - char *url_utf8 = malloc(chars + 1); - wcstombs(url_utf8, match->url, chars + 1); - - switch (match->action) { - case URL_ACTION_COPY: - if (text_to_clipboard(seat, term, url_utf8, seat->kbd.serial)) { - /* Now owned by our clipboard “manager” */ - url_utf8 = NULL; - } - break; - - case URL_ACTION_LAUNCH: { - size_t argc; - char **argv; - - if (spawn_expand_template( - &term->conf->url_launch, 1, - (const char *[]){"url"}, - (const char *[]){url_utf8}, - &argc, &argv)) - { - spawn(term->reaper, term->cwd, argv, -1, -1, -1); - - for (size_t i = 0; i < argc; i++) - free(argv[i]); - free(argv); - } - break; - } - } - - free(url_utf8); - } - + activate_url(seat, term, match); urls_reset(term); } From 0d17fd6a5d51133ae54e25a8b1b4f0a1f8c3a255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 10:35:00 +0100 Subject: [PATCH 23/55] foot.ini: fix default values for csd.button-*-color --- foot.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/foot.ini b/foot.ini index 4ed3d64b..d90c0a06 100644 --- a/foot.ini +++ b/foot.ini @@ -72,9 +72,9 @@ # size=26 # color= # button-width=26 -# button-minimize-color=ff0000ff -# button-maximize-color=ff00ff00 -# button-close-color=ffff0000 +# button-minimize-color= +# button-maximize-color= +# button-close-color= [key-bindings] # scrollback-up-page=Shift+Page_Up From fcbb5a0bf7c6f77eb067fa223b37a792c575df56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 10:35:48 +0100 Subject: [PATCH 24/55] =?UTF-8?q?config:=20use=20a=20packed=20bitfield=20f?= =?UTF-8?q?or=20=E2=80=9Cuse=20custom=20color=E2=80=9D=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 6 ++++-- config.h | 5 ++++- render.c | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config.c b/config.c index 74cff1f3..285bc889 100644 --- a/config.c +++ b/config.c @@ -2281,7 +2281,9 @@ config_load(struct config *conf, const char *conf_path, .alpha = 0xffff, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ - .selection_uses_custom_colors = false, + .use_custom = { + .selection = false, + }, }, .cursor = { @@ -2378,7 +2380,7 @@ config_load(struct config *conf, const char *conf_path, ret = parse_config_file(f, conf, conf_file.path, errors_are_fatal); fclose(f); - conf->colors.selection_uses_custom_colors = + conf->colors.use_custom.selection = conf->colors.selection_fg >> 24 == 0 && conf->colors.selection_bg >> 24 == 0; diff --git a/config.h b/config.h index d6ad2b24..f8ebb572 100644 --- a/config.h +++ b/config.h @@ -139,7 +139,10 @@ struct config { uint16_t alpha; uint32_t selection_fg; uint32_t selection_bg; - bool selection_uses_custom_colors; + + struct { + bool selection:1; + } use_custom; } colors; struct { diff --git a/render.c b/render.c index 451b0ff3..8415ac81 100644 --- a/render.c +++ b/render.c @@ -412,7 +412,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, uint32_t _fg = 0; uint32_t _bg = 0; - if (is_selected && term->conf->colors.selection_uses_custom_colors) { + if (is_selected && term->conf->colors.use_custom.selection) { _fg = term->conf->colors.selection_fg; _bg = term->conf->colors.selection_bg; } else { From e9c3d03837bb50ec39b85ee92f2906e2bb40971f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:10:40 +0100 Subject: [PATCH 25/55] config: add colors.jump_labels and colors.urls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * colors.jump_labels configures the foreground and background colors used when rendering URL jump labels. Defaults to “regular0 regular3” (i.e. black on yellow). * colors.urls configures the color to use when highlighting URLs in URL mode. Note that we aren’t currently doing any highlighting... Defaults to regular3 (i.e. yellow). --- config.c | 68 +++++++++++++++++++++++++++++++++++++--------- config.h | 8 ++++++ doc/foot.ini.5.scd | 9 ++++++ foot.ini | 2 ++ render.c | 12 ++++++-- 5 files changed, 84 insertions(+), 15 deletions(-) diff --git a/config.c b/config.c index 285bc889..f7452994 100644 --- a/config.c +++ b/config.c @@ -420,6 +420,30 @@ str_to_color(const char *s, uint32_t *color, bool allow_alpha, return true; } +static bool +str_to_two_colors(const char *s, uint32_t *first, uint32_t *second, + bool allow_alpha, struct config *conf, const char *path, + int lineno, const char *section, const char *key) +{ + /* TODO: do this without strdup() */ + char *value_copy = xstrdup(s); + const char *first_as_str = strtok(value_copy, " "); + const char *second_as_str = strtok(NULL, " "); + + if (first_as_str == NULL || second_as_str == NULL || + !str_to_color(first_as_str, first, allow_alpha, conf, path, lineno, section, key) || + !str_to_color(second_as_str, second, allow_alpha, conf, path, lineno, section, key)) + { + LOG_AND_NOTIFY_ERR("%s:%d: [%s]: %s: invalid colors: %s", + path, lineno, section, key, s); + free(value_copy); + return false; + } + + free(value_copy); + return true; +} + static bool str_to_pt_or_px(const char *s, struct pt_or_px *res, struct config *conf, const char *path, int lineno, const char *section, const char *key) @@ -845,6 +869,30 @@ parse_section_colors(const char *key, const char *value, struct config *conf, else if (strcmp(key, "bright7") == 0) color = &conf->colors.bright[7]; else if (strcmp(key, "selection-foreground") == 0) color = &conf->colors.selection_fg; else if (strcmp(key, "selection-background") == 0) color = &conf->colors.selection_bg; + + else if (strcmp(key, "jump-labels") == 0) { + if (!str_to_two_colors( + value, &conf->colors.jump_label.fg, &conf->colors.jump_label.bg, + false, conf, path, lineno, "colors", "jump-labels")) + { + return false; + } + + conf->colors.use_custom.jump_label = true; + return true; + } + + else if (strcmp(key, "urls") == 0) { + if (!str_to_color(value, &conf->colors.url, false, + conf, path, lineno, "colors", "urls")) + { + return false; + } + + conf->colors.use_custom.url = true; + return true; + } + else if (strcmp(key, "alpha") == 0) { double alpha; if (!str_to_double(value, &alpha) || alpha < 0. || alpha > 1.) { @@ -892,23 +940,15 @@ parse_section_cursor(const char *key, const char *value, struct config *conf, conf->cursor.blink = str_to_bool(value); else if (strcmp(key, "color") == 0) { - char *value_copy = xstrdup(value); - const char *text = strtok(value_copy, " "); - const char *cursor = strtok(NULL, " "); - - uint32_t text_color, cursor_color; - if (text == NULL || cursor == NULL || - !str_to_color(text, &text_color, false, conf, path, lineno, "cursor", "color") || - !str_to_color(cursor, &cursor_color, false, conf, path, lineno, "cursor", "color")) + if (!str_to_two_colors( + value, &conf->cursor.color.text, &conf->cursor.color.cursor, + false, conf, path, lineno, "cursor", "color")) { - LOG_AND_NOTIFY_ERR("%s:%d: invalid cursor colors: %s", path, lineno, value); - free(value_copy); return false; } - conf->cursor.color.text = 1u << 31 | text_color; - conf->cursor.color.cursor = 1u << 31 | cursor_color; - free(value_copy); + conf->cursor.color.text |= 1u << 31; + conf->cursor.color.cursor |= 1u << 31; } else { @@ -2283,6 +2323,8 @@ config_load(struct config *conf, const char *conf_path, .selection_bg = 0x80000000, /* Use default fg */ .use_custom = { .selection = false, + .jump_label = false, + .url = false, }, }, diff --git a/config.h b/config.h index f8ebb572..94998f28 100644 --- a/config.h +++ b/config.h @@ -139,9 +139,17 @@ struct config { uint16_t alpha; uint32_t selection_fg; uint32_t selection_bg; + uint32_t url; + + struct { + uint32_t fg; + uint32_t bg; + } jump_label; struct { bool selection:1; + bool jump_label:1; + bool url:1; } use_custom; } colors; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 727b81f1..3b913eda 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -351,6 +351,15 @@ _alpha_ option. text. Note that *both* options must be set, or the default will be used. Default: _inverse foreground/background_. +*jump-labels* + To RRGGBB values specifying the foreground (text) and background + colors to use when rendering jump labels in URL mode. Default: + _regular0 regular3_. + +*urls* + Color to use for the underline used to highlight URLs in URL + mode. Default: _regular3_. + # SECTION: csd diff --git a/foot.ini b/foot.ini index d90c0a06..8896eef0 100644 --- a/foot.ini +++ b/foot.ini @@ -66,6 +66,8 @@ # bright7=ffffff # bright white # selection-foreground= # selection-background= +# jump-labels= +# urls= [csd] # preferred=server diff --git a/render.c b/render.c index 8415ac81..040fa34a 100644 --- a/render.c +++ b/render.c @@ -2578,9 +2578,17 @@ render_urls(struct terminal *term) (term->margins.left + x) / term->scale, (term->margins.top + y) / term->scale); + uint32_t fg = term->conf->colors.use_custom.jump_label + ? term->conf->colors.jump_label.fg + : term->colors.table[0]; + uint32_t bg = term->conf->colors.use_custom.jump_label + ? term->conf->colors.jump_label.bg + : term->colors.table[3]; + + uint16_t alpha = 0xffff; + render_osd(term, surf, sub_surf, buf, label, - term->colors.table[0], term->colors.table[3], 0xf000, - width, height, margin, margin); + fg, bg, alpha, width, height, margin, margin); } } From ee1d179b8f147e6314e4823e4c66a35030ab2a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:12:22 +0100 Subject: [PATCH 26/55] config: use a bitfield for flags tracking whether to use custom colors for CSD or not --- config.h | 8 ++++---- render.c | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config.h b/config.h index 94998f28..5ae70ae3 100644 --- a/config.h +++ b/config.h @@ -192,10 +192,10 @@ struct config { int button_width; struct { - bool title_set; - bool minimize_set; - bool maximize_set; - bool close_set; + bool title_set:1; + bool minimize_set:1; + bool maximize_set:1; + bool close_set:1; uint32_t title; uint32_t minimize; uint32_t maximize; diff --git a/render.c b/render.c index 040fa34a..d04fd5d4 100644 --- a/render.c +++ b/render.c @@ -1605,27 +1605,27 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx) uint32_t _color; uint16_t alpha = 0xffff; bool is_active = false; - const bool *is_set = NULL; + bool is_set = false; const uint32_t *conf_color = NULL; switch (surf_idx) { case CSD_SURF_MINIMIZE: _color = term->colors.default_table[4]; /* blue */ - is_set = &term->conf->csd.color.minimize_set; + is_set = term->conf->csd.color.minimize_set; conf_color = &term->conf->csd.color.minimize; is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE; break; case CSD_SURF_MAXIMIZE: _color = term->colors.default_table[2]; /* green */ - is_set = &term->conf->csd.color.maximize_set; + is_set = term->conf->csd.color.maximize_set; conf_color = &term->conf->csd.color.maximize; is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE; break; case CSD_SURF_CLOSE: _color = term->colors.default_table[1]; /* red */ - is_set = &term->conf->csd.color.close_set; + is_set = term->conf->csd.color.close_set; conf_color = &term->conf->csd.color.close; is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE; break; @@ -1636,7 +1636,7 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx) } if (is_active) { - if (*is_set) { + if (is_set) { _color = *conf_color; alpha = _color >> 24 | (_color >> 24 << 8); } From d63bc1a88026b5481fec9559681cdf8244a9f905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:18:35 +0100 Subject: [PATCH 27/55] readme: add ctrl+shift+u, with description, to the list of default key bindings --- README.md | 4 ++++ doc/foot.ini.5.scd | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e55d3e82..07d82b1e 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,10 @@ These are the default shortcuts. See `man foot.ini` and the example sequence](https://codeberg.org/dnkl/foot/wiki#user-content-how-to-configure-my-shell-to-emit-the-osc-7-escape-sequence), the new terminal will start in the current working directory. +ctrl+shift+u +: Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will open the URL. + #### Scrollback search diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 3b913eda..5bf9bbf5 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -505,13 +505,13 @@ e.g. *search-start=none*. Default: _not bound_ *show-urls-launch* - Enters URL mode, where all currently visible URLs are tagged with - a jump label with a key sequence that will open the URL. Default: + Enter 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+U_. *show-urls-copy* - Enters URL mode, where all currently visible URLs are tagged with - a jump label with a key sequence that will place the URL in the + Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will place the URL in the clipboard. Default: _none_. From 69847a19d61812c5132331c4c5a5b4da68f339d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:20:42 +0100 Subject: [PATCH 28/55] readme: add URL mode to the feature list --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 07d82b1e..12622c90 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,13 @@ The fast, lightweight and minimalistic Wayland terminal emulator. * Lightweight, in dependencies, on-disk and in-memory * Wayland native * DE agnostic +* Server/daemon mode * User configurable font fallback * On-the-fly font resize * On-the-fly DPI font size adjustment * Scrollback search +* Keyboard driven URL detection * Color emoji support -* Server/daemon mode * IME (via `text-input-v3`) * Multi-seat * [Synchronized Updates](https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2) support From 69706546c8ca4de832fcd81e8feae4122670e4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:30:40 +0100 Subject: [PATCH 29/55] term: surface-kind: add TERM_SURF_JUMP_LABEL --- terminal.c | 7 ++++++- terminal.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 624b7a21..c67dda58 100644 --- a/terminal.c +++ b/terminal.c @@ -2827,8 +2827,13 @@ term_surface_kind(const struct terminal *term, const struct wl_surface *surface) return TERM_SURF_BUTTON_MAXIMIZE; else if (surface == term->window->csd.surface[CSD_SURF_CLOSE]) return TERM_SURF_BUTTON_CLOSE; - else + else { + tll_foreach(term->window->urls, it) { + if (surface == it->item.surf) + return TERM_SURF_JUMP_LABEL; + } return TERM_SURF_NONE; + } } static bool diff --git a/terminal.h b/terminal.h index b58cd137..c6a618bf 100644 --- a/terminal.h +++ b/terminal.h @@ -211,6 +211,7 @@ enum term_surface { TERM_SURF_SEARCH, TERM_SURF_SCROLLBACK_INDICATOR, TERM_SURF_RENDER_TIMER, + TERM_SURF_JUMP_LABEL, TERM_SURF_TITLE, TERM_SURF_BORDER_LEFT, TERM_SURF_BORDER_RIGHT, From d6ea2a4bdc30f033a72c1fc05172f414a0998248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:31:22 +0100 Subject: [PATCH 30/55] input: mouse events on jump label surfaces do nothing --- input.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/input.c b/input.c index 65670aa3..10507aa8 100644 --- a/input.c +++ b/input.c @@ -1237,6 +1237,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: + case TERM_SURF_JUMP_LABEL: case TERM_SURF_TITLE: render_xcursor_set(seat, term, XCURSOR_LEFT_PTR); break; @@ -1325,6 +1326,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: + case TERM_SURF_JUMP_LABEL: case TERM_SURF_TITLE: case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: @@ -1373,6 +1375,7 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: + case TERM_SURF_JUMP_LABEL: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: @@ -1717,6 +1720,7 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer, case TERM_SURF_SEARCH: case TERM_SURF_SCROLLBACK_INDICATOR: case TERM_SURF_RENDER_TIMER: + case TERM_SURF_JUMP_LABEL: break; case TERM_SURF_GRID: { From a578faf494981fdd042dd49e298eeeafdc646499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:45:58 +0100 Subject: [PATCH 31/55] url-mode: make the end coordinate *inclusive* --- url-mode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/url-mode.c b/url-mode.c index f38026b4..4feea2df 100644 --- a/url-mode.c +++ b/url-mode.c @@ -269,6 +269,11 @@ auto_detected(struct terminal *term, enum url_action action) bool done = false; struct coord end = {c, r}; + if (--end.col < 0) { + end.row--; + end.col = term->cols - 1; + } + do { switch (url[len - 1]) { case L'.': case L',': case L':': case L';': case L'?': From 6726494f4c2836a5ed1c4611c181e4a57a4adbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:47:59 +0100 Subject: [PATCH 32/55] url-mode: store absolute row numbers in start/end coordinates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to update the jump label positions when the viewport changes. This in turn allows us to stay in URL mode while the user is using the mouse to scroll in the scrollback history. Scrolling with the keyboard is currently not possible, since input handling in URL mode does not recognize “regular” key bindings. We _could_ add scrollback up/down bindings to URL mode too, but lets not, for the time being. (Note: an alternative to this patch is to disallow mouse scrolling too. Then we could have kept the URL start/end as viewport local coordinates). --- commands.c | 2 ++ render.c | 32 +++++++++++++++++++++++++++++--- url-mode.c | 3 +++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/commands.c b/commands.c index ac91ab19..01ad4e1e 100644 --- a/commands.c +++ b/commands.c @@ -83,6 +83,7 @@ cmd_scrollback_up(struct terminal *term, int rows) } else term_damage_view(term); + render_refresh_urls(term); render_refresh(term); } @@ -157,5 +158,6 @@ cmd_scrollback_down(struct terminal *term, int rows) } else term_damage_view(term); + render_refresh_urls(term); render_refresh(term); } diff --git a/render.c b/render.c index d04fd5d4..fff280fc 100644 --- a/render.c +++ b/render.c @@ -2524,6 +2524,15 @@ render_urls(struct terminal *term) struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); + /* Calculate view start, counted from the *current* scrollback start */ + const int scrollback_end + = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + const int view_start + = (term->grid->view + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + const int view_end = view_start + term->rows - 1; + tll_foreach(win->urls, it) { const struct url *url = it->item.url; const wchar_t *text = url->text; @@ -2535,6 +2544,18 @@ render_urls(struct terminal *term) if (surf == NULL || sub_surf == NULL) continue; + const struct coord *pos = &url->start; + const int _row + = (pos->row + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + + if (_row < view_start || _row > view_end) { + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + size_t text_len = wcslen(text); size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); @@ -2562,9 +2583,14 @@ render_urls(struct terminal *term) struct buffer *buf = shm_get_buffer( term->wl->shm, width, height, shm_cookie_url(url), false, 1); - const struct coord *pos = &url->start; - int x = pos->col * term->cell_width - 15 * term->cell_width / 10; - int y = pos->row * term->cell_height - 5 * term->cell_height / 10; + int col = pos->col; + int row = pos->row - term->grid->view; + while (row < 0) + row += term->grid->num_rows; + row &= (term->grid->num_rows - 1); + + int x = col * term->cell_width - 15 * term->cell_width / 10; + int y = row * term->cell_height - 5 * term->cell_height / 10; if (x < 0) x = 0; diff --git a/url-mode.c b/url-mode.c index 4feea2df..8f3b867e 100644 --- a/url-mode.c +++ b/url-mode.c @@ -294,6 +294,9 @@ auto_detected(struct terminal *term, enum url_action action) url[len] = L'\0'; + start.row += term->grid->view; + end.row += term->grid->view; + tll_push_back( term->urls, ((struct url){ From 2c10a147ea0e471f12facfa11913272d42163f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 11:51:58 +0100 Subject: [PATCH 33/55] url-mode: underline URLs using the color from colors.urls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is implemented by allocating one of the (few!) remaining bits in the cells’ attribute struct to indicate the cell should be “URL highlighted”. render_cell() looks at this bit and draws an underline using the color from colors.urls (defaults to regular3 - i.e. yellow). A new function, url_tag_cells(), iterates the currently detected URLs and sets the new ‘url’ attribute flag on the affected cells. Note: this is done in a separate function to keep urls_collect() free from as many dependencies as possible. urls_reset() is updated to *clear* the ‘url’ flag (and thus implicitly also results in a grid refresh, _if_ there were any URLs). We now exit URL mode on *any* client application input. This needs to be so since we can’t know if the URLs we previously detected are still valid. --- input.c | 1 + render.c | 9 +++++++++ terminal.c | 2 ++ terminal.h | 3 ++- url-mode.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ url-mode.h | 1 + 6 files changed, 61 insertions(+), 1 deletion(-) diff --git a/input.c b/input.c index 10507aa8..7ba8e8e0 100644 --- a/input.c +++ b/input.c @@ -281,6 +281,7 @@ execute_binding(struct seat *seat, struct terminal *term, : URL_ACTION_LAUNCH; urls_collect(term, url_action); + urls_tag_cells(term); render_refresh_urls(term); return true; } diff --git a/render.c b/render.c index fff280fc..4e41f48a 100644 --- a/render.c +++ b/render.c @@ -616,6 +616,15 @@ render_cell(struct terminal *term, pixman_image_t *pix, if (cell->attrs.strikethrough) draw_strikeout(term, pix, font, &fg, x, y, cell_cols); + if (unlikely(cell->attrs.url)) { + pixman_color_t url_color = color_hex_to_pixman( + term->conf->colors.use_custom.url + ? term->conf->colors.url + : term->colors.table[3] + ); + draw_underline(term, pix, font, &url_color, x, y, cell_cols); + } + draw_cursor: if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); diff --git a/terminal.c b/terminal.c index c67dda58..91a442ec 100644 --- a/terminal.c +++ b/terminal.c @@ -228,6 +228,8 @@ fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) cursor_blink_rearm_timer(term); } + urls_reset(term); + uint8_t buf[24 * 1024]; ssize_t count = sizeof(buf); diff --git a/terminal.h b/terminal.h index c6a618bf..bb98efc2 100644 --- a/terminal.h +++ b/terminal.h @@ -40,7 +40,8 @@ struct attributes { uint32_t have_fg:1; uint32_t have_bg:1; uint32_t selected:2; - uint32_t reserved:3; + uint32_t url:1; + uint32_t reserved:2; uint32_t bg:24; }; static_assert(sizeof(struct attributes) == 8, "bad size"); diff --git a/url-mode.c b/url-mode.c index 8f3b867e..e418eb7e 100644 --- a/url-mode.c +++ b/url-mode.c @@ -7,6 +7,7 @@ #define LOG_ENABLE_DBG 1 #include "log.h" #include "grid.h" +#include "render.h" #include "selection.h" #include "spawn.h" #include "terminal.h" @@ -399,6 +400,49 @@ urls_collect(struct terminal *term, enum url_action action) } } +static void +tag_cells_for_url(struct terminal *term, const struct url *url, bool value) +{ + const struct coord *start = &url->start; + const struct coord *end = &url->end; + + size_t end_r = end->row & (term->grid->num_rows - 1); + + size_t r = start->row & (term->grid->num_rows - 1); + size_t c = start->col; + + struct row *row = term->grid->rows[r]; + row->dirty = true; + + while (true) { + struct cell *cell = &row->cells[c]; + cell->attrs.url = value; + cell->attrs.clean = 0; + + if (r == end_r && c == end->col) + break; + + if (++c >= term->cols) { + r = (r + 1) & (term->grid->num_rows - 1); + c = 0; + + row = term->grid->rows[r]; + row->dirty = true; + } + } +} + +void +urls_tag_cells(struct terminal *term) +{ + if (unlikely(tll_length(term->urls)) == 0) + return; + + tll_foreach(term->urls, it) + tag_cells_for_url(term, &it->item, true); + render_refresh(term); +} + void urls_reset(struct terminal *term) { @@ -416,10 +460,12 @@ urls_reset(struct terminal *term) } tll_foreach(term->urls, it) { + tag_cells_for_url(term, &it->item, false); free(it->item.url); free(it->item.text); } tll_free(term->urls); memset(term->url_keys, 0, sizeof(term->url_keys)); + render_refresh(term); } diff --git a/url-mode.h b/url-mode.h index 94c1e675..6651e119 100644 --- a/url-mode.h +++ b/url-mode.h @@ -12,6 +12,7 @@ static inline bool urls_mode_is_active(const struct terminal *term) } void urls_collect(struct terminal *term, enum url_action action); +void urls_tag_cells(struct terminal *term); void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, uint32_t key, From a988138492ba39d507f644bbb0b6d452a864ca79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:01:52 +0100 Subject: [PATCH 34/55] url-mode: urls_collect(): URL list pointer as an argument --- input.c | 6 ++-- terminal.h | 3 +- url-mode.c | 100 +++++++++++++++++++++++++++-------------------------- url-mode.h | 7 ++-- 4 files changed, 61 insertions(+), 55 deletions(-) diff --git a/input.c b/input.c index 7ba8e8e0..69fce248 100644 --- a/input.c +++ b/input.c @@ -280,9 +280,9 @@ execute_binding(struct seat *seat, struct terminal *term, ? URL_ACTION_COPY : URL_ACTION_LAUNCH; - urls_collect(term, url_action); - urls_tag_cells(term); - render_refresh_urls(term); + urls_collect(term, url_action, &term->urls); + urls_assign_key_combos(&term->urls); + urls_render(term); return true; } diff --git a/terminal.h b/terminal.h index bb98efc2..b0084f63 100644 --- a/terminal.h +++ b/terminal.h @@ -234,6 +234,7 @@ struct url { struct coord end; enum url_action action; }; +typedef tll(struct url) url_list_t; struct terminal { struct fdm *fdm; @@ -513,7 +514,7 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; - tll(struct url) urls; + url_list_t urls; wchar_t url_keys[5]; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED diff --git a/url-mode.c b/url-mode.c index e418eb7e..e770aa9b 100644 --- a/url-mode.c +++ b/url-mode.c @@ -141,7 +141,7 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, IGNORE_WARNING("-Wpedantic") static void -auto_detected(struct terminal *term, enum url_action action) +auto_detected(const struct terminal *term, enum url_action action, url_list_t *urls) { static const wchar_t *const prots[] = { L"http://", @@ -299,7 +299,7 @@ auto_detected(struct terminal *term, enum url_action action) end.row += term->grid->view; tll_push_back( - term->urls, + *urls, ((struct url){ .url = xwcsdup(url), .text = xwcsdup(L""), @@ -321,12 +321,16 @@ auto_detected(struct terminal *term, enum url_action action) UNIGNORE_WARNINGS void -urls_collect(struct terminal *term, enum url_action action) +urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) { xassert(tll_length(term->urls) == 0); - auto_detected(term, action); + auto_detected(term, action, urls); +} - size_t count = tll_length(term->urls); +void +urls_assign_key_combos(url_list_t *urls) +{ + size_t count = tll_length(*urls); /* Assign key combos */ @@ -337,7 +341,7 @@ urls_collect(struct terminal *term, enum url_action action) if (count < ALEN(single)) { size_t idx = 0; - tll_foreach(term->urls, it) { + tll_foreach(*urls, it) { xassert(wcslen(single[idx]) < ALEN(it->item.key) - 1); wcscpy(it->item.key, single[idx++]); } @@ -347,7 +351,7 @@ urls_collect(struct terminal *term, enum url_action action) } #if defined(_DEBUG) && LOG_ENABLE_DBG - tll_foreach(term->urls, it) { + tll_foreach(*urls, it) { char url[1024]; wcstombs(url, it->item.url, sizeof(url) - 1); @@ -358,6 +362,43 @@ urls_collect(struct terminal *term, enum url_action action) } #endif +} + +static void +tag_cells_for_url(struct terminal *term, const struct url *url, bool value) +{ + const struct coord *start = &url->start; + const struct coord *end = &url->end; + + size_t end_r = end->row & (term->grid->num_rows - 1); + + size_t r = start->row & (term->grid->num_rows - 1); + size_t c = start->col; + + struct row *row = term->grid->rows[r]; + row->dirty = true; + + while (true) { + struct cell *cell = &row->cells[c]; + cell->attrs.url = value; + cell->attrs.clean = 0; + + if (r == end_r && c == end->col) + break; + + if (++c >= term->cols) { + r = (r + 1) & (term->grid->num_rows - 1); + c = 0; + + row = term->grid->rows[r]; + row->dirty = true; + } + } +} + +void +urls_render(struct terminal *term) +{ struct wl_window *win = term->window; struct wayland *wayl = term->wl; @@ -397,49 +438,10 @@ urls_collect(struct terminal *term, enum url_action action) }; tll_push_back(win->urls, url); - } -} - -static void -tag_cells_for_url(struct terminal *term, const struct url *url, bool value) -{ - const struct coord *start = &url->start; - const struct coord *end = &url->end; - - size_t end_r = end->row & (term->grid->num_rows - 1); - - size_t r = start->row & (term->grid->num_rows - 1); - size_t c = start->col; - - struct row *row = term->grid->rows[r]; - row->dirty = true; - - while (true) { - struct cell *cell = &row->cells[c]; - cell->attrs.url = value; - cell->attrs.clean = 0; - - if (r == end_r && c == end->col) - break; - - if (++c >= term->cols) { - r = (r + 1) & (term->grid->num_rows - 1); - c = 0; - - row = term->grid->rows[r]; - row->dirty = true; - } - } -} - -void -urls_tag_cells(struct terminal *term) -{ - if (unlikely(tll_length(term->urls)) == 0) - return; - - tll_foreach(term->urls, it) tag_cells_for_url(term, &it->item, true); + } + + render_refresh_urls(term); render_refresh(term); } diff --git a/url-mode.h b/url-mode.h index 6651e119..12d20192 100644 --- a/url-mode.h +++ b/url-mode.h @@ -11,8 +11,11 @@ static inline bool urls_mode_is_active(const struct terminal *term) return tll_length(term->urls) > 0; } -void urls_collect(struct terminal *term, enum url_action action); -void urls_tag_cells(struct terminal *term); +void urls_collect( + const struct terminal *term, enum url_action action, url_list_t *urls); +void urls_assign_key_combos(url_list_t *urls); + +void urls_render(struct terminal *term); void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, uint32_t key, From e6612927bea46164b823e679b21790e8f1c5b396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:11:31 +0100 Subject: [PATCH 35/55] url-mode: add ftp://, ftps://, file://, gemini:// and gopher:// --- url-mode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/url-mode.c b/url-mode.c index e770aa9b..c631c323 100644 --- a/url-mode.c +++ b/url-mode.c @@ -146,6 +146,11 @@ auto_detected(const struct terminal *term, enum url_action action, url_list_t *u static const wchar_t *const prots[] = { L"http://", L"https://", + L"ftp://", + L"ftps://", + L"file://", + L"gemini://", + L"gopher://", }; size_t max_prot_len = 0; From ab1224ba913c3c76113c9126b2cf2a688e9de0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:26:01 +0100 Subject: [PATCH 36/55] url-mode: urls_assign_key_combos(): remove URLs when all key combos have been used up --- url-mode.c | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/url-mode.c b/url-mode.c index c631c323..038ea2e8 100644 --- a/url-mode.c +++ b/url-mode.c @@ -332,27 +332,30 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur auto_detected(term, action, urls); } +static void url_destroy(struct url *url); + void urls_assign_key_combos(url_list_t *urls) { - size_t count = tll_length(*urls); - - /* Assign key combos */ - - static const wchar_t *const single[] = { + static const wchar_t *const combos[] = { L"f", L"j", L"d", L"k", L"e", L"i", L"c", L"m", L"r", L"u", L"s", L"l", L"w", L"o", L"x", L"a", L"q", L"p", }; - if (count < ALEN(single)) { - size_t idx = 0; - tll_foreach(*urls, it) { - xassert(wcslen(single[idx]) < ALEN(it->item.key) - 1); - wcscpy(it->item.key, single[idx++]); + size_t idx = 0; + tll_foreach(*urls, it) { + if (idx < ALEN(combos)) { + xassert(wcslen(combos[idx]) < ALEN(it->item.key) - 1); + wcscpy(it->item.key, combos[idx]); + } else { + url_destroy(&it->item); + tll_remove(*urls, it); + } + + if (++idx == ALEN(combos)) { + LOG_WARN("not enough key combos (%zu) for %zu URLs", + ALEN(combos), tll_length(*urls)); } - } else { - LOG_ERR("unimplemented: more URLs than %zu", ALEN(single)); - assert(false); } #if defined(_DEBUG) && LOG_ENABLE_DBG @@ -450,6 +453,13 @@ urls_render(struct terminal *term) render_refresh(term); } +static void +url_destroy(struct url *url) +{ + free(url->url); + free(url->text); +} + void urls_reset(struct terminal *term) { @@ -468,8 +478,7 @@ urls_reset(struct terminal *term) tll_foreach(term->urls, it) { tag_cells_for_url(term, &it->item, false); - free(it->item.url); - free(it->item.text); + url_destroy(&it->item); } tll_free(term->urls); From 5df2e990e33a027a4517f59aefad4c6be20d53bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:52:04 +0100 Subject: [PATCH 37/55] =?UTF-8?q?render:=20urls:=20don=E2=80=99t=20display?= =?UTF-8?q?=20URLs=20whose=20key=20sequence=20doesn=E2=80=99t=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index 4e41f48a..60b41dee 100644 --- a/render.c +++ b/render.c @@ -2546,6 +2546,7 @@ render_urls(struct terminal *term) const struct url *url = it->item.url; const wchar_t *text = url->text; const wchar_t *key = url->key; + const size_t entered_key_len = wcslen(term->url_keys); struct wl_surface *surf = it->item.surf; struct wl_subsurface *sub_surf = it->item.sub_surf; @@ -2553,13 +2554,21 @@ render_urls(struct terminal *term) if (surf == NULL || sub_surf == NULL) continue; + bool hide = false; const struct coord *pos = &url->start; const int _row = (pos->row - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); - if (_row < view_start || _row > view_end) { + if (_row < view_start || _row > view_end) + hide = true; + if (wcslen(key) <= entered_key_len) + hide = true; + if (wcsncmp(term->url_keys, key, entered_key_len) != 0) + hide = true; + + if (hide) { wl_surface_attach(surf, NULL, 0, 0); wl_surface_commit(surf); continue; From 01c0535b5ab2e4c78b04eccbfc46dd359315ab6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:52:48 +0100 Subject: [PATCH 38/55] render: urls: add TODO: highlight already entered keys --- render.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/render.c b/render.c index 60b41dee..5d2f4263 100644 --- a/render.c +++ b/render.c @@ -2633,6 +2633,12 @@ render_urls(struct terminal *term) render_osd(term, surf, sub_surf, buf, label, fg, bg, alpha, width, height, margin, margin); + +#if 0 + /* TODO: somehow highlight the key(s) entered so far */ + pixman_color_t color = color_hex_to_pixman(fg); + draw_strikeout(term, buf->pix[0], term->fonts[0], &color, margin, margin, entered_key_len); +#endif } } From e9ff8bac1c56cee56e8c8dca8dfdca926ed14343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 20:53:06 +0100 Subject: [PATCH 39/55] url-mode: refresh rendered URLs after accepting a key --- url-mode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/url-mode.c b/url-mode.c index 038ea2e8..21b11b8d 100644 --- a/url-mode.c +++ b/url-mode.c @@ -135,6 +135,7 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, else if (is_valid) { xassert(seq_len + 1 <= ALEN(term->url_keys)); term->url_keys[seq_len] = wc; + render_refresh_urls(term); } } From 29c86612df087f71c523909988715a96cb0f98b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 21:37:26 +0100 Subject: [PATCH 40/55] =?UTF-8?q?url-mode:=20generate=20key=20combinations?= =?UTF-8?q?=20using=20vimium=E2=80=99s=20algorithm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- url-mode.c | 63 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/url-mode.c b/url-mode.c index 21b11b8d..1bb38943 100644 --- a/url-mode.c +++ b/url-mode.c @@ -335,28 +335,57 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur static void url_destroy(struct url *url); +static void +generate_key_combos(size_t count, wchar_t *combos[static count]) +{ + /* vimium default */ + static const wchar_t alphabet[] = L"sadfjklewcmpgh"; + + tll(wchar_t *) hints = tll_init(); + tll_push_back(hints, xwcsdup(L"")); + + size_t offset = 0; + while (tll_length(hints) - offset < count || tll_length(hints) == 1) { + wchar_t *hint = tll_back(hints); + offset++; + + for (size_t i = 0; i < ALEN(alphabet); i++) { + wchar_t wc = alphabet[i]; + wchar_t *next_hint = malloc((wcslen(hint) + 1 + 1) * sizeof(wchar_t)); + next_hint[0] = wc; + wcscpy(&next_hint[1], hint); + tll_push_back(hints, next_hint); + } + } + + /* Slice the list */ + for (size_t i = 0; i < offset; i++) + free(tll_pop_front(hints)); + + xassert(tll_length(hints) >= count); + + /* Fill in the callers array */ + size_t idx = 0; + tll_foreach(hints, it) { + if (idx >= count) + free(it->item); + else + combos[idx] = it->item; + idx++; + } + tll_free(hints); +} + void urls_assign_key_combos(url_list_t *urls) { - static const wchar_t *const combos[] = { - L"f", L"j", L"d", L"k", L"e", L"i", L"c", L"m", L"r", - L"u", L"s", L"l", L"w", L"o", L"x", L"a", L"q", L"p", - }; + wchar_t *combos[tll_length(*urls)]; + generate_key_combos(tll_length(*urls), combos); size_t idx = 0; tll_foreach(*urls, it) { - if (idx < ALEN(combos)) { - xassert(wcslen(combos[idx]) < ALEN(it->item.key) - 1); - wcscpy(it->item.key, combos[idx]); - } else { - url_destroy(&it->item); - tll_remove(*urls, it); - } - - if (++idx == ALEN(combos)) { - LOG_WARN("not enough key combos (%zu) for %zu URLs", - ALEN(combos), tll_length(*urls)); - } + xassert(wcslen(combos[idx]) < ALEN(it->item.key) - 1); + wcscpy(it->item.key, combos[idx++]); } #if defined(_DEBUG) && LOG_ENABLE_DBG @@ -371,6 +400,8 @@ urls_assign_key_combos(url_list_t *urls) } #endif + for (size_t i = 0; i < tll_length(*urls); i++) + free(combos[i]); } static void From 19e23254fbce1d4233133754617e01aa24494de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 23:03:05 +0100 Subject: [PATCH 41/55] render: urls: uppercase the activation key sequence --- render.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/render.c b/render.c index 5d2f4263..57af555c 100644 --- a/render.c +++ b/render.c @@ -1,6 +1,7 @@ #include "render.h" #include +#include #include #include @@ -2591,6 +2592,9 @@ render_urls(struct terminal *term) } } + for (size_t i = 0; i < wcslen(key); i++) + label[i] = towupper(label[i]); + size_t len = wcslen(label); int cols = wcswidth(label, len); From 672c414b91eaa6a1a46c1ea8de14cae7f63faca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 6 Feb 2021 23:04:46 +0100 Subject: [PATCH 42/55] render: urls: reduce vertical margin --- render.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/render.c b/render.c index 57af555c..06065436 100644 --- a/render.c +++ b/render.c @@ -2598,9 +2598,10 @@ render_urls(struct terminal *term) size_t len = wcslen(label); int cols = wcswidth(label, len); - const int margin = 2 * term->scale; - int width = 2 * margin + cols * term->cell_width; - int height = 2 * margin + term->cell_height; + const int x_margin = 2 * term->scale; + const int y_margin = 1 * term->scale; + int width = 2 * x_margin + cols * term->cell_width; + int height = 2 * y_margin + term->cell_height; struct buffer *buf = shm_get_buffer( term->wl->shm, width, height, shm_cookie_url(url), false, 1); @@ -2636,12 +2637,12 @@ render_urls(struct terminal *term) uint16_t alpha = 0xffff; render_osd(term, surf, sub_surf, buf, label, - fg, bg, alpha, width, height, margin, margin); + fg, bg, alpha, width, height, x_margin, y_margin); #if 0 /* TODO: somehow highlight the key(s) entered so far */ pixman_color_t color = color_hex_to_pixman(fg); - draw_strikeout(term, buf->pix[0], term->fonts[0], &color, margin, margin, entered_key_len); + draw_strikeout(term, buf->pix[0], term->fonts[0], &color, x_margin, y_margin, entered_key_len); #endif } } From ddd6f1d944100ab1eb9d01a73d19981aaf2e4332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 00:01:29 +0100 Subject: [PATCH 43/55] url-mode: fix key sequence generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * We were using the ‘back’ element of the list as prefix for the next iteration of sequences, instead of the element at index ‘offset’ * ALEN() on a wchar_t[] includes the NULL terminator. We don’t want that. --- terminal.h | 2 +- url-mode.c | 92 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/terminal.h b/terminal.h index b0084f63..43dd8e03 100644 --- a/terminal.h +++ b/terminal.h @@ -229,7 +229,7 @@ enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH }; struct url { wchar_t *url; wchar_t *text; - wchar_t key[4]; + wchar_t *key; struct coord start; struct coord end; enum url_action action; diff --git a/url-mode.c b/url-mode.c index 1bb38943..b0203caa 100644 --- a/url-mode.c +++ b/url-mode.c @@ -335,58 +335,80 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur static void url_destroy(struct url *url); +static int +wcscmp_qsort_wrapper(const void *_a, const void *_b) +{ + const wchar_t *a = *(const wchar_t **)_a; + const wchar_t *b = *(const wchar_t **)_b; + return wcscmp(a, b); +} + static void generate_key_combos(size_t count, wchar_t *combos[static count]) { /* vimium default */ static const wchar_t alphabet[] = L"sadfjklewcmpgh"; + static const size_t alphabet_len = ALEN(alphabet) - 1; - tll(wchar_t *) hints = tll_init(); - tll_push_back(hints, xwcsdup(L"")); + size_t hints_count = 1; + wchar_t **hints = malloc(hints_count * sizeof(hints[0])); + + hints[0] = xwcsdup(L""); size_t offset = 0; - while (tll_length(hints) - offset < count || tll_length(hints) == 1) { - wchar_t *hint = tll_back(hints); - offset++; + while (hints_count - offset < count || hints_count == 1) { + const wchar_t *prefix = hints[offset++]; - for (size_t i = 0; i < ALEN(alphabet); i++) { + hints = realloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); + + for (size_t i = 0; i < alphabet_len; i++) { wchar_t wc = alphabet[i]; - wchar_t *next_hint = malloc((wcslen(hint) + 1 + 1) * sizeof(wchar_t)); - next_hint[0] = wc; - wcscpy(&next_hint[1], hint); - tll_push_back(hints, next_hint); + wchar_t *hint = malloc((wcslen(prefix) + 1 + 1) * sizeof(wchar_t)); + + /* Will be reversed later */ + hint[0] = wc; + wcscpy(&hint[1], prefix); + hints[hints_count + i] = hint; + } + hints_count += alphabet_len; + } + + xassert(hints_count - offset >= count); + + /* Copy slice of ‘hints’ array to the caller provided array */ + for (size_t i = 0; i < hints_count; i++) { + if (i >= offset && i < offset + count) + combos[i - offset] = hints[i]; + else + free(hints[i]); + } + free(hints); + + /* Sorting is a kind of shuffle, since we’re sorting on the + * *reversed* strings */ + qsort(combos, count, sizeof(wchar_t *), &wcscmp_qsort_wrapper); + + /* Reverse all strings */ + for (size_t i = 0; i < count; i++) { + const size_t len = wcslen(combos[i]); + for (size_t j = 0; j < len / 2; j++) { + wchar_t tmp = combos[i][j]; + combos[i][j] = combos[i][len - j - 1]; + combos[i][len - j - 1] = tmp; } } - - /* Slice the list */ - for (size_t i = 0; i < offset; i++) - free(tll_pop_front(hints)); - - xassert(tll_length(hints) >= count); - - /* Fill in the callers array */ - size_t idx = 0; - tll_foreach(hints, it) { - if (idx >= count) - free(it->item); - else - combos[idx] = it->item; - idx++; - } - tll_free(hints); } void urls_assign_key_combos(url_list_t *urls) { - wchar_t *combos[tll_length(*urls)]; - generate_key_combos(tll_length(*urls), combos); + const size_t count = tll_length(*urls); + wchar_t *combos[count]; + generate_key_combos(count, combos); size_t idx = 0; - tll_foreach(*urls, it) { - xassert(wcslen(combos[idx]) < ALEN(it->item.key) - 1); - wcscpy(it->item.key, combos[idx++]); - } + tll_foreach(*urls, it) + it->item.key = combos[idx++]; #if defined(_DEBUG) && LOG_ENABLE_DBG tll_foreach(*urls, it) { @@ -399,9 +421,6 @@ urls_assign_key_combos(url_list_t *urls) LOG_DBG("URL: %s (%s)", url, key); } #endif - - for (size_t i = 0; i < tll_length(*urls); i++) - free(combos[i]); } static void @@ -490,6 +509,7 @@ url_destroy(struct url *url) { free(url->url); free(url->text); + free(url->key); } void From 1aec534b375a617d1855d4302d886f11fab02172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 10:32:56 +0100 Subject: [PATCH 44/55] url-mode: input: backspace reverts the last key --- url-mode.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/url-mode.c b/url-mode.c index b0203caa..9d6b83fe 100644 --- a/url-mode.c +++ b/url-mode.c @@ -98,6 +98,17 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, } } + size_t seq_len = wcslen(term->url_keys); + + if (sym == XKB_KEY_BackSpace) { + if (seq_len > 0) { + term->url_keys[seq_len - 1] = L'\0'; + render_refresh_urls(term); + } + + return; + } + wchar_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); /* @@ -106,8 +117,6 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, * sequence. */ - size_t seq_len = wcslen(term->url_keys); - bool is_valid = false; const struct url *match = NULL; From 0f57e6c422885d1c2abc13711ad40ec176c177d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 10:34:36 +0100 Subject: [PATCH 45/55] =?UTF-8?q?render:=20render=5Fosd():=20remove=20?= =?UTF-8?q?=E2=80=98alpha=E2=80=99=20parameter=20-=20it=E2=80=99s=20always?= =?UTF-8?q?=200xffff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/render.c b/render.c index 06065436..985f97de 100644 --- a/render.c +++ b/render.c @@ -1717,10 +1717,10 @@ static void render_osd(struct terminal *term, struct wl_surface *surf, struct wl_subsurface *sub_surf, struct buffer *buf, - const wchar_t *text, uint32_t _fg, uint32_t _bg, int alpha, + const wchar_t *text, uint32_t _fg, uint32_t _bg, unsigned width, unsigned height, unsigned x, unsigned y) { - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman(_bg); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, width, height}); @@ -1907,7 +1907,7 @@ render_scrollback_position(struct terminal *term) term, win->scrollback_indicator_surface, win->scrollback_indicator_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 4], 0xffff, + term->colors.table[0], term->colors.table[8 + 4], width, height, width - margin - wcslen(text) * term->cell_width, margin); } @@ -1939,7 +1939,7 @@ render_render_timer(struct terminal *term, struct timeval render_time) term, win->render_timer_surface, win->render_timer_sub_surface, buf, text, - term->colors.table[0], term->colors.table[8 + 1], 0xffff, + term->colors.table[0], term->colors.table[8 + 1], width, height, margin, margin); } @@ -2634,12 +2634,10 @@ render_urls(struct terminal *term) ? term->conf->colors.jump_label.bg : term->colors.table[3]; - uint16_t alpha = 0xffff; - render_osd(term, surf, sub_surf, buf, label, - fg, bg, alpha, width, height, x_margin, y_margin); + fg, bg, width, height, x_margin, y_margin); -#if 0 +#if 1 /* TODO: somehow highlight the key(s) entered so far */ pixman_color_t color = color_hex_to_pixman(fg); draw_strikeout(term, buf->pix[0], term->fonts[0], &color, x_margin, y_margin, entered_key_len); From b3043e92f6e12d809ba101baa6bad68e24eaa0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 10:50:31 +0100 Subject: [PATCH 46/55] =?UTF-8?q?render:=20urls:=20don=E2=80=99t=20use/all?= =?UTF-8?q?ocate=20maximum=20allowed=20characters=20unless=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 985f97de..8e0e8f9c 100644 --- a/render.c +++ b/render.c @@ -2579,7 +2579,7 @@ render_urls(struct terminal *term) size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); const size_t max_chars = 50; - chars = max(chars, max_chars); + chars = min(chars, max_chars); wchar_t label[chars + 2]; if (text_len == 0) From 85f7503aec7fe1f525d47ebfd670e1545b44f357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 10:51:09 +0100 Subject: [PATCH 47/55] render: urls: fix string formatter in swprintf() --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 8e0e8f9c..7ed27f44 100644 --- a/render.c +++ b/render.c @@ -2585,7 +2585,7 @@ render_urls(struct terminal *term) if (text_len == 0) wcscpy(label, key); else { - int count = swprintf(label, chars, L"%s - %s", key, text); + int count = swprintf(label, chars + 1, L"%ls - %ls", key, text); if (count >= max_chars) { label[max_chars] = L'…'; label[max_chars + 1] = L'\0'; From 5af481cd89ba83c7228b0f40c709b6da36a7a34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 10:51:28 +0100 Subject: [PATCH 48/55] render: urls: blank out keys already pressed --- render.c | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/render.c b/render.c index 7ed27f44..c64692b8 100644 --- a/render.c +++ b/render.c @@ -2595,6 +2595,9 @@ render_urls(struct terminal *term) for (size_t i = 0; i < wcslen(key); i++) label[i] = towupper(label[i]); + for (size_t i = 0; i < entered_key_len; i++) + label[i] = L' '; + size_t len = wcslen(label); int cols = wcswidth(label, len); @@ -2636,12 +2639,6 @@ render_urls(struct terminal *term) render_osd(term, surf, sub_surf, buf, label, fg, bg, width, height, x_margin, y_margin); - -#if 1 - /* TODO: somehow highlight the key(s) entered so far */ - pixman_color_t color = color_hex_to_pixman(fg); - draw_strikeout(term, buf->pix[0], term->fonts[0], &color, x_margin, y_margin, entered_key_len); -#endif } } From 4b67394a5f84c1c8b0bdd803079f11a03981c693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 11:38:52 +0100 Subject: [PATCH 49/55] readme: document the new URL mode --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 12622c90..428d4958 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator. 1. [Scrollback search](#scrollback-search) 1. [Mouse](#mouse) 1. [Server (daemon) mode](#server-daemon-mode) +1. [URLs](#urls) 1. [Alt/meta](#alt-meta) 1. [Backspace](#backspace) 1. [Keypad](#keypad) @@ -252,6 +253,32 @@ desktop), and then run `footclient` instead of `foot` whenever you want to launch a new terminal. +## URLs + +Foot supports URL detection. But, unlike many other terminal +emulators, where URLs are highlighted when they are hovered and opened +by clicking on them, foot uses a keyboard driven approach. + +Pressing ctrl+shift+u enters _“URL +mode”_, where all currently visible URLs are underlined, and is +associated with a _“jump-label”_. The jump-label indicates the _key +sequence_ (e.g. **”AF”**) to use to activate the URL. + +The key binding can, of course, be customized, like all other key +bindings in foot. See `show-urls-launch` and `show-urls-copy` in the +`foot.ini` man page. + +`show-urls-launch` by default opens the URL with `xdg-open`. This can +be changed with the `url-launch` option. + +`show-urls-copy` is an alternative to `show-urls-launch`, that changes +what activating an URL _does_; instead of opening it, it copies it to +the clipboard. It is unbound by default. + +Both the jump label colors, and the URL underline color can be +configured, independently. + + ## Alt/meta By default, foot prefixes _Meta characters_ with ESC. This corresponds From 0c847cfe7b5b18fa99078554123a2b2c9e9b0fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 11:39:07 +0100 Subject: [PATCH 50/55] doc: foot.1: document the new URL mode --- doc/foot.1.scd | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc/foot.1.scd b/doc/foot.1.scd index 9099adc2..4965ca56 100644 --- a/doc/foot.1.scd +++ b/doc/foot.1.scd @@ -236,6 +236,31 @@ _Examples_: - Dina:weight=bold:slant=italic - Courier New:size=12 +# URLs + +Foot supports URL detection. But, unlike many other terminal +emulators, where URLs are highlighted when they are hovered and opened +by clicking on them, foot uses a keyboard driven approach. + +Pressing *ctrl*+*shift*+*u* enters _“URL mode”_, where all currently +visible URLs are underlined, and is associated with a +_“jump-label”_. The jump-label indicates the _key sequence_ +(e.g. *”AF”*) to use to activate the URL. + +The key binding can, of course, be customized, like all other key +bindings in foot. See *show-urls-launch* and *show-urls-copy* in +*foot.ini*(5). + +*show-urls-launch* by default opens the URL with *xdg-open*. This can +be changed with the *url-launch* option. + +*show-urls-copy* is an alternative to *show-urls-launch*, that changes +what activating an URL _does_; instead of opening it, it copies it to +the clipboard. It is unbound by default. + +Both the jump label colors, and the URL underline color can be +configured, independently. + # ALT/META CHARACTERS By default, foot prefixes meta characters with *ESC*. This corresponds From fec19f1503a27f1e9b99941f3e9d325ba51bed48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 11:42:41 +0100 Subject: [PATCH 51/55] changelog: URL mode --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bed2a4f..6fc679df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,10 @@ copied to. The default is `primary`, which corresponds to the behavior in older foot releases (https://codeberg.org/dnkl/foot/issues/288). +* URL detection. URLs are highlighted and activated using the keyboard + (**no** mouse support). See **foot**(1)::URLs, or + [README.md](README.md#urls) for details + (https://codeberg.org/dnkl/foot/issues/14). ### Changed From 9066ba87df574a59c4b572bbbb2f1c7962cab1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 14:29:34 +0100 Subject: [PATCH 52/55] url-mode: early exit when assigning key combos to empty list --- url-mode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/url-mode.c b/url-mode.c index 9d6b83fe..fc710fe6 100644 --- a/url-mode.c +++ b/url-mode.c @@ -412,6 +412,9 @@ void urls_assign_key_combos(url_list_t *urls) { const size_t count = tll_length(*urls); + if (count == 0) + return; + wchar_t *combos[count]; generate_key_combos(count, combos); From c84e37976763487832b8c80321f0df0fbd793f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 15:10:48 +0100 Subject: [PATCH 53/55] url-mode: be consistent; use xmalloc() + xrealloc() --- url-mode.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/url-mode.c b/url-mode.c index fc710fe6..4d52dab2 100644 --- a/url-mode.c +++ b/url-mode.c @@ -39,7 +39,7 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url) size_t chars = wcstombs(NULL, url->url, 0); if (chars != (size_t)-1) { - char *url_utf8 = malloc(chars + 1); + char *url_utf8 = xmalloc(chars + 1); wcstombs(url_utf8, url->url, chars + 1); switch (url->action) { @@ -360,7 +360,7 @@ generate_key_combos(size_t count, wchar_t *combos[static count]) static const size_t alphabet_len = ALEN(alphabet) - 1; size_t hints_count = 1; - wchar_t **hints = malloc(hints_count * sizeof(hints[0])); + wchar_t **hints = xmalloc(hints_count * sizeof(hints[0])); hints[0] = xwcsdup(L""); @@ -368,11 +368,11 @@ generate_key_combos(size_t count, wchar_t *combos[static count]) while (hints_count - offset < count || hints_count == 1) { const wchar_t *prefix = hints[offset++]; - hints = realloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); + hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); for (size_t i = 0; i < alphabet_len; i++) { wchar_t wc = alphabet[i]; - wchar_t *hint = malloc((wcslen(prefix) + 1 + 1) * sizeof(wchar_t)); + wchar_t *hint = xmalloc((wcslen(prefix) + 1 + 1) * sizeof(wchar_t)); /* Will be reversed later */ hint[0] = wc; From 9ee392dc8ce678113ecc7827dedb23e0bdae3b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 16:03:11 +0100 Subject: [PATCH 54/55] url-mode: generate-key-combos: minor efficiency tweaks * Use a do..while loop; this lets us drop the second half of the loop condition. * Call wcslen(prefix) once, *before* iterating the alphabet characters. * Step through the alphabet characters using a pointer, as this avoids an indexed load (with possibly an imul instruction in e.g. -Os builds). --- url-mode.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/url-mode.c b/url-mode.c index 4d52dab2..2a3ee41d 100644 --- a/url-mode.c +++ b/url-mode.c @@ -365,22 +365,23 @@ generate_key_combos(size_t count, wchar_t *combos[static count]) hints[0] = xwcsdup(L""); size_t offset = 0; - while (hints_count - offset < count || hints_count == 1) { + do { const wchar_t *prefix = hints[offset++]; + const size_t prefix_len = wcslen(prefix); hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); - for (size_t i = 0; i < alphabet_len; i++) { - wchar_t wc = alphabet[i]; - wchar_t *hint = xmalloc((wcslen(prefix) + 1 + 1) * sizeof(wchar_t)); + const wchar_t *wc = &alphabet[0]; + for (size_t i = 0; i < alphabet_len; i++, wc++) { + wchar_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(wchar_t)); + hints[hints_count + i] = hint; /* Will be reversed later */ - hint[0] = wc; + hint[0] = *wc; wcscpy(&hint[1], prefix); - hints[hints_count + i] = hint; } hints_count += alphabet_len; - } + } while (hints_count - offset < count); xassert(hints_count - offset >= count); From 24263412dc121325ca980bd34f2b7ed097c7145b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 7 Feb 2021 16:05:02 +0100 Subject: [PATCH 55/55] url-mode: urls_render(): early exit when URL list is empty --- url-mode.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/url-mode.c b/url-mode.c index 2a3ee41d..40f3372f 100644 --- a/url-mode.c +++ b/url-mode.c @@ -474,6 +474,9 @@ urls_render(struct terminal *term) struct wl_window *win = term->window; struct wayland *wayl = term->wl; + if (tll_length(win->term->urls) == 0) + return; + xassert(tll_length(win->urls) == 0); tll_foreach(win->term->urls, it) { struct wl_surface *surf = wl_compositor_create_surface(wayl->compositor);