From 3339915d204d1e6ccfe948e9df050e6b16a5a84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:28:53 +0100 Subject: [PATCH 01/36] url-mode: store URL in UTF-8, not UTF-32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only time the URL is actually in UTF-32 is when we’re collecting it (auto-detecting it) from the grid, since cells store their character(s) in UTF-32. Everything *after* that prefers the URL in UTF-8. So, do the conversion while collecting the URL. This patch also changes the URL activation code to strip the ‘file://user@host/’ prefix from file URIs that refer to files on the *local* computer. --- terminal.h | 2 +- url-mode.c | 103 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/terminal.h b/terminal.h index 94433178..39d02859 100644 --- a/terminal.h +++ b/terminal.h @@ -227,7 +227,7 @@ typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH }; struct url { - wchar_t *url; + char *url; wchar_t *text; wchar_t *key; struct coord start; diff --git a/url-mode.c b/url-mode.c index ac379c8e..b6025b57 100644 --- a/url-mode.c +++ b/url-mode.c @@ -11,6 +11,7 @@ #include "selection.h" #include "spawn.h" #include "terminal.h" +#include "uri.h" #include "util.h" #include "xmalloc.h" @@ -36,42 +37,55 @@ execute_binding(struct seat *seat, struct terminal *term, static void activate_url(struct seat *seat, struct terminal *term, const struct url *url) { - size_t chars = wcstombs(NULL, url->url, 0); + char *url_string = NULL; - if (chars != (size_t)-1) { - char *url_utf8 = xmalloc(chars + 1); - wcstombs(url_utf8, url->url, chars + 1); + char *scheme, *host, *path; + if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, + &host, NULL, &path, NULL, NULL) && + strcmp(scheme, "file") == 0 && + hostname_is_localhost(host)) + { + /* + * This is a file in *this* computer. Pass only the + * filename to the URL-launcher. + * + * I.e. strip the ‘file://user@host/’ prefix. + */ + url_string = path; + free(scheme); + free(host); + } else + url_string = xstrdup(url->url); - 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; - } + switch (url->action) { + case URL_ACTION_COPY: + if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_string = NULL; } + break; - free(url_utf8); + case URL_ACTION_LAUNCH: { + size_t argc; + char **argv; + + if (spawn_expand_template( + &term->conf->url_launch, 1, + (const char *[]){"url"}, + (const char *[]){url_string}, + &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_string); } void @@ -313,14 +327,20 @@ auto_detected(const struct terminal *term, enum url_action action, url_list_t *u start.row += term->grid->view; end.row += term->grid->view; - tll_push_back( - *urls, - ((struct url){ - .url = xwcsdup(url), - .text = xwcsdup(L""), - .start = start, - .end = end, - .action = action})); + size_t chars = wcstombs(NULL, url, 0); + if (chars != (size_t)-1) { + char *url_utf8 = xmalloc((chars + 1) * sizeof(wchar_t)); + wcstombs(url_utf8, url, chars + 1); + + tll_push_back( + *urls, + ((struct url){ + .url = url_utf8, + .text = xwcsdup(L""), + .start = start, + .end = end, + .action = action})); + } state = STATE_PROTOCOL; len = 0; @@ -425,13 +445,10 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) #if defined(_DEBUG) && LOG_ENABLE_DBG tll_foreach(*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); + LOG_DBG("URL: %s (%s)", it->item.url, key); } #endif } From fd87bca102321e5804fef74dac8a6d267ae46732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:31:55 +0100 Subject: [PATCH 02/36] =?UTF-8?q?grid:=20enable=20rows=20to=20have=20?= =?UTF-8?q?=E2=80=98extra=E2=80=99=20data=20associated=20with=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds an ‘extra’ member to the row struct. It is a pointer to a struct containing extra data to be associated with this row. Initially, this struct contains a list of URL ranges. These define (OSC-8) URLs on this row. The ‘extra’ data is allocated on-demand. I.e. the pointer is NULL by default; it is *not* allocated by grid_row_alloc(). --- grid.c | 9 +++++++++ terminal.c | 28 ++++++++++++++++++++++++---- terminal.h | 11 +++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/grid.c b/grid.c index 6d6b6b3c..2f0033ec 100644 --- a/grid.c +++ b/grid.c @@ -33,6 +33,7 @@ grid_row_alloc(int cols, bool initialize) struct row *row = xmalloc(sizeof(*row)); row->dirty = false; row->linebreak = false; + row->extra = NULL; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -50,6 +51,14 @@ grid_row_free(struct row *row) if (row == NULL) return; + if (row->extra != NULL) { + tll_foreach(row->extra->uri_ranges, it) { + free(it->item.uri); + tll_remove(row->extra->uri_ranges, it); + } + } + + free(row->extra); free(row->cells); free(row); } diff --git a/terminal.c b/terminal.c index 0051ca6d..17b985a8 100644 --- a/terminal.c +++ b/terminal.c @@ -2154,8 +2154,18 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows grid_swap_row(term->grid, i - rows, i); /* Erase scrolled in lines */ - for (int r = region.end - rows; r < region.end; r++) - erase_line(term, grid_row_and_alloc(term->grid, r)); + for (int r = region.end - rows; r < region.end; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + if (unlikely(row->extra != NULL)) { + tll_foreach(row->extra->uri_ranges, it) { + free(it->item.uri); + tll_remove(row->extra->uri_ranges, it); + } + free(row->extra); + row->extra = NULL; + } + erase_line(term, row); + } term_damage_scroll(term, DAMAGE_SCROLL, region, rows); term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); @@ -2222,8 +2232,18 @@ term_scroll_reverse_partial(struct terminal *term, grid_swap_row(term->grid, i, i - rows); /* Erase scrolled in lines */ - for (int r = region.start; r < region.start + rows; r++) - erase_line(term, grid_row_and_alloc(term->grid, r)); + for (int r = region.start; r < region.start + rows; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + if (unlikely(row->extra != NULL)) { + tll_foreach(row->extra->uri_ranges, it) { + free(it->item.uri); + tll_remove(row->extra->uri_ranges, it); + } + free(row->extra); + row->extra = NULL; + } + erase_line(term, row); + } term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); diff --git a/terminal.h b/terminal.h index 39d02859..7df92efc 100644 --- a/terminal.h +++ b/terminal.h @@ -86,10 +86,21 @@ struct composed { uint8_t count; }; +struct row_uri_range { + int start; + int end; + char *uri; +}; + +struct row_data { + tll(struct row_uri_range) uri_ranges; +}; + struct row { struct cell *cells; bool dirty; bool linebreak; + struct row_data *extra; }; struct sixel { From 841e5f0e50a65bc4787ba06a642355cf4fb514a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:34:11 +0100 Subject: [PATCH 03/36] terminal: add OSC-8 state tracking to the VT sub-struct --- terminal.c | 6 ++++++ terminal.h | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/terminal.c b/terminal.c index 17b985a8..28c061ca 100644 --- a/terminal.c +++ b/terminal.c @@ -1102,6 +1102,9 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .blink = {.fd = -1}, .vt = { .state = 0, /* STATE_GROUND */ + .osc8 = { + .begin = {-1, -1}, + }, }, .colors = { .fg = conf->colors.fg, @@ -1645,6 +1648,9 @@ term_reset(struct terminal *term, bool hard) free(term->vt.osc.data); memset(&term->vt, 0, sizeof(term->vt)); term->vt.state = 0; /* GROUND */ + term->vt.osc8.begin = (struct coord){-1, -1}; + free(term->vt.osc8.uri); + term->vt.osc8.uri = NULL; if (term->grid == &term->alt) { term->grid = &term->normal; diff --git a/terminal.h b/terminal.h index 7df92efc..3f58180b 100644 --- a/terminal.h +++ b/terminal.h @@ -155,12 +155,24 @@ struct vt { struct vt_param v[16]; uint8_t idx; } params; + uint32_t private; /* LSB=priv0, MSB=priv3 */ + + struct attributes attrs; + struct attributes saved_attrs; + struct { uint8_t *data; size_t size; size_t idx; } osc; + + /* Start coordinate for current OSC-8 URI */ + struct { + struct coord begin; + char *uri; + } osc8; + struct { uint8_t *data; size_t size; @@ -168,8 +180,6 @@ struct vt { void (*put_handler)(struct terminal *term, uint8_t c); void (*unhook_handler)(struct terminal *term); } dcs; - struct attributes attrs; - struct attributes saved_attrs; }; enum cursor_origin { ORIGIN_ABSOLUTE, ORIGIN_RELATIVE }; From 682494d45a50e81aec0618bdd2d282ad17fe27d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:34:48 +0100 Subject: [PATCH 04/36] terminal: add term_osc8_{open,close} functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These functions update the OSC-8 URI state in the terminal. term_osc8_open() tracks the beginning of an URL, by storing the start coordinate (i.e. the current cursor location), along with the URL itself. Note that term_osc8_open() may not be called with an empty URL. This is important to notice, since the way OSC-8 works, applications close an URL by “opening” a new, empty one: \E]8;;https://foo.bar\e\\this is an OSC-8 URL\E]8;;\e\\ It is up to the caller to check for this, and call term_osc8_close() instead of term_osc8_open() when the URL is empty. However, it is *also* valid to switch directly from one URL to another: \E]8;;http://123\e\\First URL\E]8;;http//456\e\\Second URL\E]8;;\e\\ This use-case *is* handled by term_osc8_open(). term_osc8_close() uses the information from term_osc8_open() to add per-row URL data (using the new ‘extra’ row data). --- terminal.c | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ terminal.h | 3 +++ 2 files changed, 57 insertions(+) diff --git a/terminal.c b/terminal.c index 28c061ca..c6afbef1 100644 --- a/terminal.c +++ b/terminal.c @@ -3015,3 +3015,57 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, #endif } +void +term_osc8_open(struct terminal *term, const char *uri) +{ + if (unlikely(term->vt.osc8.begin.row < 0)) { + /* It’s valid to switch from one URI to another without + * closing the first one */ + term_osc8_close(term); + } + + term->vt.osc8.begin = (struct coord){ + .col = term->grid->cursor.point.col, + .row = grid_row_absolute(term->grid, term->grid->cursor.point.row), + }; + term->vt.osc8.uri = xstrdup(uri); +} + +void +term_osc8_close(struct terminal *term) +{ + if (term->vt.osc8.begin.row < 0) + return; + + if (term->vt.osc8.uri[0] == '\0') + goto done; + + struct coord start = term->vt.osc8.begin; + struct coord end = (struct coord){ + .col = term->grid->cursor.point.col, + .row = grid_row_absolute(term->grid, term->grid->cursor.point.row), + }; + + int r = start.row; + int start_col = start.col; + do { + int end_col = r == end.row ? end.col : term->cols - 1; + + struct row *row = term->grid->rows[r]; + if (row->extra == NULL) + row->extra = xcalloc(1, sizeof(*row->extra)); + + struct row_uri_range range = { + .start = start_col, + .end = end_col, + .uri = xstrdup(term->vt.osc8.uri), + }; + tll_push_back(row->extra->uri_ranges, range); + start_col = 0; + } while (r++ != end.row); + +done: + free(term->vt.osc8.uri); + term->vt.osc8.uri = NULL; + term->vt.osc8.begin = (struct coord){-1, -1}; +} diff --git a/terminal.h b/terminal.h index 3f58180b..6dc211d9 100644 --- a/terminal.h +++ b/terminal.h @@ -689,3 +689,6 @@ void term_ime_set_cursor_rect( void term_urls_reset(struct terminal *term); void term_collect_urls(struct terminal *term); + +void term_osc8_open(struct terminal *term, const char *uri); +void term_osc8_close(struct terminal *term); From b55f6925d360dcb8109b90b266c0e9c2fc625b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:41:57 +0100 Subject: [PATCH 05/36] osc: parse OSC-8, by calling term_osc8_{open,close} as appropriate --- osc.c | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osc.c b/osc.c index 801b04e9..2f1cfe2b 100644 --- a/osc.c +++ b/osc.c @@ -376,6 +376,45 @@ osc_set_pwd(struct terminal *term, char *string) free(host); } +static void +osc_uri(struct terminal *term, char *string) +{ + /* + * \E]8;;URI\e\\ + * + * Params are key=value pairs, separated by ‘:’. + * + * The only defined key (as of 2020-05-31) is ‘id’, which is used + * to group split-up URIs: + * + * ╔═ file1 ════╗ + * ║ ╔═ file2 ═══╗ + * ║http://exa║Lorem ipsum║ + * ║le.com ║ dolor sit ║ + * ║ ║amet, conse║ + * ╚══════════║ctetur adip║ + * ╚═══════════╝ + * + * This lets a terminal emulator highlight both parts at the same + * time (e.g. when hovering over one of the parts with the mouse). + */ + + const char *params = string; + char *params_end = strchr(params, ';'); + if (params_end == NULL) + return; + + *params_end = '\0'; + const char *uri = params_end + 1; + + LOG_DBG("params=%s, URI=%s", params, uri); + + if (uri[0] == '\0') + term_osc8_close(term); + else + term_osc8_open(term, uri); +} + static void osc_notify(struct terminal *term, char *string) { @@ -553,6 +592,10 @@ osc_dispatch(struct terminal *term) osc_set_pwd(term, string); break; + case 8: + osc_uri(term, string); + break; + case 10: case 11: { /* Set default foreground/background color */ From 39c5057d4963b292651cb23adfd0e1127cf89188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 12:42:35 +0100 Subject: [PATCH 06/36] url-mode: initial support for OSC-8 URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Things left to do: since OSC-8 URLs are stored as ranges in the per-row ‘extra’ data member, we currently do not handle line-wrapping URLs very well; they will be considered two separate URLs, and assigned two different key sequences. --- url-mode.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/url-mode.c b/url-mode.c index b6025b57..83421abf 100644 --- a/url-mode.c +++ b/url-mode.c @@ -355,11 +355,42 @@ auto_detected(const struct terminal *term, enum url_action action, url_list_t *u UNIGNORE_WARNINGS +static void +osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) +{ + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (row->extra == NULL) + continue; + + tll_foreach(row->extra->uri_ranges, it) { + struct coord start = { + .col = it->item.start, + .row = r + term->grid->view, + }; + struct coord end = { + .col = it->item.end, + .row = r + term->grid->view, + }; + tll_push_back( + *urls, + ((struct url){ + .url = xstrdup(it->item.uri), + .text = xwcsdup(L""), + .start = start, + .end = end, + .action = action})); + } + } +} + void urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) { xassert(tll_length(term->urls) == 0); auto_detected(term, action, urls); + osc8_uris(term, action, urls); } static void url_destroy(struct url *url); From c600e131e28f10767d185855a41a2318292c905b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:40:39 +0100 Subject: [PATCH 07/36] main: initialize pseudo-random generator --- main.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.c b/main.c index 87f7a058..f02aa0cb 100644 --- a/main.c +++ b/main.c @@ -383,6 +383,8 @@ main(int argc, char *const *argv) name.sysname, name.machine, sizeof(void *) * 8); } + srand(time(NULL)); + setlocale(LC_CTYPE, ""); LOG_INFO("locale: %s", setlocale(LC_CTYPE, NULL)); if (!locale_is_utf8()) { From ecbfc2bbe9b1964c8b2563b52618393d63dc40ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:41:27 +0100 Subject: [PATCH 08/36] =?UTF-8?q?osc:=208:=20parse=20params,=20and=20the?= =?UTF-8?q?=20=E2=80=98id=E2=80=99=20parameter=20specifically?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applications can assign an ID to the URL. This is used to associate the parts of a split up URL with each other: ╔═ file1 ════╗ ║ ╔═ file2 ═══╗ ║http://exa║Lorem ipsum║ ║le.com ║ dolor sit ║ ║ ║amet, conse║ ╚══════════║ctetur adip║ ╚═══════════╝ In the example above, ‘http://exa’ and ‘le.com’ are part of the same URL, but by necessity split up into two OSC-8 URLs. If the application sets the same ID for those two OSC-8 URLs, the terminal can ensure they are handled as one. --- osc.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osc.c b/osc.c index 2f1cfe2b..fb4f15d6 100644 --- a/osc.c +++ b/osc.c @@ -1,5 +1,6 @@ #include "osc.h" +#include #include #include #include @@ -399,16 +400,36 @@ osc_uri(struct terminal *term, char *string) * time (e.g. when hovering over one of the parts with the mouse). */ - const char *params = string; + char *params = string; char *params_end = strchr(params, ';'); if (params_end == NULL) return; *params_end = '\0'; const char *uri = params_end + 1; + uint64_t id = (uint64_t)rand() << 32 | rand(); LOG_DBG("params=%s, URI=%s", params, uri); + char *ctx = NULL; + for (const char *key_value = strtok_r(params, ":", &ctx); + key_value != NULL; + key_value = strtok_r(NULL, ":", &ctx)) + { + const char *key = key_value; + char *operator = strchr(key_value, '='); + + if (operator == NULL) + continue; + *operator = '\0'; + + const char *value = operator + 1; + LOG_DBG("param: %s=%s", key, value); + + if (strcmp(key, "id") == 0) + id = (uintptr_t)value; + } + if (uri[0] == '\0') term_osc8_close(term); else From b934969b858d8874e480cc9ee7f4ccf5657f6f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:44:07 +0100 Subject: [PATCH 09/36] =?UTF-8?q?term:=20add=20=E2=80=98id=E2=80=99=20para?= =?UTF-8?q?meter=20to=20term=5Fosc8=5Fopen()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current OSC-8 URL’s ID is now tracked along with the URI itself, and its starting point. --- osc.c | 2 +- terminal.c | 4 +++- terminal.h | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osc.c b/osc.c index fb4f15d6..b57bdd6c 100644 --- a/osc.c +++ b/osc.c @@ -433,7 +433,7 @@ osc_uri(struct terminal *term, char *string) if (uri[0] == '\0') term_osc8_close(term); else - term_osc8_open(term, uri); + term_osc8_open(term, id, uri); } static void diff --git a/terminal.c b/terminal.c index c6afbef1..218882f0 100644 --- a/terminal.c +++ b/terminal.c @@ -3016,7 +3016,7 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, } void -term_osc8_open(struct terminal *term, const char *uri) +term_osc8_open(struct terminal *term, uint64_t id, const char *uri) { if (unlikely(term->vt.osc8.begin.row < 0)) { /* It’s valid to switch from one URI to another without @@ -3028,6 +3028,7 @@ term_osc8_open(struct terminal *term, const char *uri) .col = term->grid->cursor.point.col, .row = grid_row_absolute(term->grid, term->grid->cursor.point.row), }; + term->vt.osc8.id = id; term->vt.osc8.uri = xstrdup(uri); } @@ -3066,6 +3067,7 @@ term_osc8_close(struct terminal *term) done: free(term->vt.osc8.uri); + term->vt.osc8.id = 0; term->vt.osc8.uri = NULL; term->vt.osc8.begin = (struct coord){-1, -1}; } diff --git a/terminal.h b/terminal.h index 6dc211d9..733fc92d 100644 --- a/terminal.h +++ b/terminal.h @@ -169,8 +169,9 @@ struct vt { /* Start coordinate for current OSC-8 URI */ struct { - struct coord begin; + uint64_t id; char *uri; + struct coord begin; } osc8; struct { @@ -690,5 +691,5 @@ void term_ime_set_cursor_rect( void term_urls_reset(struct terminal *term); void term_collect_urls(struct terminal *term); -void term_osc8_open(struct terminal *term, const char *uri); +void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); void term_osc8_close(struct terminal *term); From ffbee5ff372787c13e68d413173ff2e3ba270b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:44:58 +0100 Subject: [PATCH 10/36] =?UTF-8?q?term:=20add=20an=20=E2=80=98id=E2=80=99?= =?UTF-8?q?=20member=20to=20the=20the=20=E2=80=98row=5Furi=5Frange?= =?UTF-8?q?=E2=80=99=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit term_osc8_close() sets it from the OSC-8 ID tracked in the VT sub-struct. --- terminal.c | 1 + terminal.h | 1 + 2 files changed, 2 insertions(+) diff --git a/terminal.c b/terminal.c index 218882f0..9051c566 100644 --- a/terminal.c +++ b/terminal.c @@ -3059,6 +3059,7 @@ term_osc8_close(struct terminal *term) struct row_uri_range range = { .start = start_col, .end = end_col, + .id = term->vt.osc8.id, .uri = xstrdup(term->vt.osc8.uri), }; tll_push_back(row->extra->uri_ranges, range); diff --git a/terminal.h b/terminal.h index 733fc92d..69c7c2ea 100644 --- a/terminal.h +++ b/terminal.h @@ -89,6 +89,7 @@ struct composed { struct row_uri_range { int start; int end; + uint64_t id; char *uri; }; From fb9e9513a59126831862a59ce9db2d2896299441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:45:59 +0100 Subject: [PATCH 11/36] url-mode: multiple URL (parts) with the same ID is assigned a single key sequence In case an URL is split up into multiple parts, those parts are now treated as a single URL when it comes to key assignment. Only the *first* URL part is actually assigned a key combo. The other parts are ignored. We still highlight them, but for all other purposes they are ignored. --- render.c | 6 ++++++ terminal.h | 1 + url-mode.c | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/render.c b/render.c index aaa106a3..0f23fc4e 100644 --- a/render.c +++ b/render.c @@ -2531,6 +2531,12 @@ render_urls(struct terminal *term) const wchar_t *key = url->key; const size_t entered_key_len = wcslen(term->url_keys); + if (key == NULL) { + /* TODO: if we decide to use the .text field, we cannot + * just skip the entire jump label like this */ + continue; + } + struct wl_surface *surf = it->item.surf.surf; struct wl_subsurface *sub_surf = it->item.surf.sub; diff --git a/terminal.h b/terminal.h index 69c7c2ea..284dafa9 100644 --- a/terminal.h +++ b/terminal.h @@ -250,6 +250,7 @@ typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH }; struct url { + uint64_t id; char *url; wchar_t *text; wchar_t *key; diff --git a/url-mode.c b/url-mode.c index 83421abf..57291e87 100644 --- a/url-mode.c +++ b/url-mode.c @@ -1,5 +1,6 @@ #include "url-mode.h" +#include #include #include @@ -135,6 +136,9 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, const struct url *match = NULL; tll_foreach(term->urls, it) { + if (it->item.key == NULL) + continue; + const struct url *url = &it->item; const size_t key_len = wcslen(it->item.key); @@ -165,7 +169,8 @@ urls_input(struct seat *seat, struct terminal *term, uint32_t key, IGNORE_WARNING("-Wpedantic") static void -auto_detected(const struct terminal *term, enum url_action action, url_list_t *urls) +auto_detected(const struct terminal *term, enum url_action action, + url_list_t *urls) { static const wchar_t *const prots[] = { L"http://", @@ -335,6 +340,7 @@ auto_detected(const struct terminal *term, enum url_action action, url_list_t *u tll_push_back( *urls, ((struct url){ + .id = (uint64_t)rand() << 32 | rand(), .url = url_utf8, .text = xwcsdup(L""), .start = start, @@ -376,6 +382,7 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) tll_push_back( *urls, ((struct url){ + .id = it->item.id, .url = xstrdup(it->item.uri), .text = xwcsdup(L""), .start = start, @@ -467,18 +474,38 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (count == 0) return; + uint64_t seen_ids[count]; wchar_t *combos[count]; generate_key_combos(conf, count, combos); size_t idx = 0; - tll_foreach(*urls, it) + tll_foreach(*urls, it) { + bool id_already_seen = false; + for (size_t i = 0; i < idx; i++) { + if (it->item.id == seen_ids[i]) { + id_already_seen = true; + break; + } + } + + if (id_already_seen) + continue; + + seen_ids[idx] = it->item.id; it->item.key = combos[idx++]; + } + + /* Free combos we didn’t use up */ + for (size_t i = idx; i < count; i++) + free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG tll_foreach(*urls, it) { + if (it->item.key == NULL) + continue; + char key[32]; wcstombs(key, it->item.key, sizeof(key) - 1); - LOG_DBG("URL: %s (%s)", it->item.url, key); } #endif From a99f9c9341d3e9e5133fb76006c9be54af4cbace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 13:58:16 +0100 Subject: [PATCH 12/36] =?UTF-8?q?url-mode:=20activate=5Furl():=20ensure=20?= =?UTF-8?q?=E2=80=98scheme=E2=80=99,=20=E2=80=98host=E2=80=99=20and=20?= =?UTF-8?q?=E2=80=98path=E2=80=99=20are=20free:d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uri_parse() may succeed. But if the scheme isn’t “file”, or if the hostname isn’t localhost, then we failed to free ‘scheme’, ‘host’ and ‘path’. --- url-mode.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/url-mode.c b/url-mode.c index 57291e87..0733a457 100644 --- a/url-mode.c +++ b/url-mode.c @@ -42,20 +42,24 @@ activate_url(struct seat *seat, struct terminal *term, const struct url *url) char *scheme, *host, *path; if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, - &host, NULL, &path, NULL, NULL) && - strcmp(scheme, "file") == 0 && - hostname_is_localhost(host)) + &host, NULL, &path, NULL, NULL)) { - /* - * This is a file in *this* computer. Pass only the - * filename to the URL-launcher. - * - * I.e. strip the ‘file://user@host/’ prefix. - */ - url_string = path; + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + /* + * This is a file in *this* computer. Pass only the + * filename to the URL-launcher. + * + * I.e. strip the ‘file://user@host/’ prefix. + */ + url_string = path; + } else + free(path); + free(scheme); free(host); - } else + } + + if (url_string == NULL) url_string = xstrdup(url->url); switch (url->action) { From 663c43c139a4cc9405dde4d0d2e4d45eeac78e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 13 Feb 2021 14:16:53 +0100 Subject: [PATCH 13/36] term_osc8_close(): the URL *end* column is inclusive --- terminal.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/terminal.c b/terminal.c index 9051c566..efd260f1 100644 --- a/terminal.c +++ b/terminal.c @@ -3047,6 +3047,12 @@ term_osc8_close(struct terminal *term) .row = grid_row_absolute(term->grid, term->grid->cursor.point.row), }; + /* end is *inclusive */ + if (--end.col < 0) { + end.row--; + end.col = term->cols - 1; + } + int r = start.row; int start_col = start.col; do { From 34b814349b58480b235674f892b70b41cd728652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 13:28:18 +0100 Subject: [PATCH 14/36] util: add a sdbm hash implementation --- util.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/util.h b/util.h index d2059379..aa9fc8ba 100644 --- a/util.h +++ b/util.h @@ -1,5 +1,6 @@ #pragma once +#include #include #define ALEN(v) (sizeof(v) / sizeof((v)[0])) @@ -21,3 +22,16 @@ thrd_err_as_string(int thrd_err) return "unknown error"; } + +static inline uint64_t +sdbm_hash(const char *s) +{ + uint64_t hash = 0; + + for (; *s != '\0'; s++) { + int c = *s; + hash = c + (hash << 6) + (hash << 16) - hash; + } + + return hash; +} From d44cda11bf435b7687fada29946d4a1cecbc728c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 13:28:42 +0100 Subject: [PATCH 15/36] =?UTF-8?q?osc-8:=20fix=20thinko:=20can=E2=80=99t=20?= =?UTF-8?q?use=20the=20ID=20parameters=20*address*=20as=20actual=20ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doing so means the next OSC-8 URL emitted by the client application, with the same ID, will get a *new* id internally. Instead, hash the string and use that as ID. --- osc.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osc.c b/osc.c index b57bdd6c..bf97db52 100644 --- a/osc.c +++ b/osc.c @@ -16,6 +16,7 @@ #include "selection.h" #include "terminal.h" #include "uri.h" +#include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -409,8 +410,6 @@ osc_uri(struct terminal *term, char *string) const char *uri = params_end + 1; uint64_t id = (uint64_t)rand() << 32 | rand(); - LOG_DBG("params=%s, URI=%s", params, uri); - char *ctx = NULL; for (const char *key_value = strtok_r(params, ":", &ctx); key_value != NULL; @@ -424,12 +423,13 @@ osc_uri(struct terminal *term, char *string) *operator = '\0'; const char *value = operator + 1; - LOG_DBG("param: %s=%s", key, value); if (strcmp(key, "id") == 0) - id = (uintptr_t)value; + id = sdbm_hash(value); } + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); + if (uri[0] == '\0') term_osc8_close(term); else From cf651d361f382bb572ff0ee60807a17058d4b365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 13:36:07 +0100 Subject: [PATCH 16/36] url-mode: remove duplicate URLs Remove URLs with the same start and end coordinates. Such duplicate URLs can be created by emitting an OSC-8 URL with matching grid content: \E]8;;http://foo\E\\http://foo\E]8;;\E\\ --- url-mode.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/url-mode.c b/url-mode.c index 0733a457..7568b922 100644 --- a/url-mode.c +++ b/url-mode.c @@ -396,12 +396,35 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) } } +static void +remove_duplicates(url_list_t *urls) +{ + tll_foreach(*urls, outer) { + tll_foreach(*urls, inner) { + if (outer == inner) + continue; + + if (outer->item.start.row == inner->item.start.row && + outer->item.start.col == inner->item.start.col && + outer->item.end.row == inner->item.end.row && + outer->item.end.col == inner->item.end.col) + { + free(inner->item.url); + free(inner->item.text); + free(inner->item.key); + tll_remove(*urls, inner); + } + } + } +} + void urls_collect(const struct terminal *term, enum url_action action, url_list_t *urls) { xassert(tll_length(term->urls) == 0); - auto_detected(term, action, urls); osc8_uris(term, action, urls); + auto_detected(term, action, urls); + remove_duplicates(urls); } static void url_destroy(struct url *url); From cc43c1b7048d2cf1cedb48ca63ecde40e2d0dbf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 13:42:37 +0100 Subject: [PATCH 17/36] =?UTF-8?q?urls:=20remove=20free-form=20=E2=80=98tex?= =?UTF-8?q?t=E2=80=99=20member=20from=20URL=20struct?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 14 ++------------ terminal.h | 1 - url-mode.c | 11 +++-------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/render.c b/render.c index 0f23fc4e..c9dfc1fc 100644 --- a/render.c +++ b/render.c @@ -2527,7 +2527,6 @@ render_urls(struct terminal *term) tll_foreach(win->urls, it) { 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); @@ -2563,22 +2562,13 @@ render_urls(struct terminal *term) continue; } - size_t text_len = wcslen(text); - size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); + size_t chars = wcslen(key); const size_t max_chars = 50; chars = min(chars, max_chars); wchar_t label[chars + 2]; - if (text_len == 0) - wcscpy(label, key); - else { - 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'; - } - } + wcscpy(label, key); for (size_t i = 0; i < wcslen(key); i++) label[i] = towupper(label[i]); diff --git a/terminal.h b/terminal.h index 284dafa9..cc37bcac 100644 --- a/terminal.h +++ b/terminal.h @@ -252,7 +252,6 @@ enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH }; struct url { uint64_t id; char *url; - wchar_t *text; wchar_t *key; struct coord start; struct coord end; diff --git a/url-mode.c b/url-mode.c index 7568b922..44f9bb19 100644 --- a/url-mode.c +++ b/url-mode.c @@ -16,6 +16,8 @@ #include "util.h" #include "xmalloc.h" +static void url_destroy(struct url *url); + static bool execute_binding(struct seat *seat, struct terminal *term, enum bind_action_url action, uint32_t serial) @@ -346,7 +348,6 @@ auto_detected(const struct terminal *term, enum url_action action, ((struct url){ .id = (uint64_t)rand() << 32 | rand(), .url = url_utf8, - .text = xwcsdup(L""), .start = start, .end = end, .action = action})); @@ -388,7 +389,6 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) ((struct url){ .id = it->item.id, .url = xstrdup(it->item.uri), - .text = xwcsdup(L""), .start = start, .end = end, .action = action})); @@ -409,9 +409,7 @@ remove_duplicates(url_list_t *urls) outer->item.end.row == inner->item.end.row && outer->item.end.col == inner->item.end.col) { - free(inner->item.url); - free(inner->item.text); - free(inner->item.key); + url_destroy(&inner->item); tll_remove(*urls, inner); } } @@ -427,8 +425,6 @@ urls_collect(const struct terminal *term, enum url_action action, url_list_t *ur remove_duplicates(urls); } -static void url_destroy(struct url *url); - static int wcscmp_qsort_wrapper(const void *_a, const void *_b) { @@ -595,7 +591,6 @@ static void url_destroy(struct url *url) { free(url->url); - free(url->text); free(url->key); } From 06a9ffa7632abf201b0ea0270bb5058af4fc4a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 14:18:11 +0100 Subject: [PATCH 18/36] urls: add key binding that toggles whether URLs are displayed on jump-label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, the URL isn’t shown on the jump-label. For auto-detect URLs, doing so is virtually always useless, as the URL is already visible in the grid. For OSC-8 URLs however, the URL is often _not_ visible in the grid. Many times, seeing the URL is still not needed (if you’re doing ‘ls --hyperlink’, you already know what the URIs are). But it is still useful to have a way to show the URLs. This patch adds a new key binding action that can be used in url-mode to toggle the URL on and off in the jump label. It is bound to ctrl+t by default. --- config.c | 2 ++ doc/foot.ini.5.scd | 14 ++++++++++++++ foot.ini | 1 + render.c | 21 +++++++++++++++++++-- terminal.h | 1 + url-mode.c | 6 ++++++ wayland.h | 1 + 7 files changed, 44 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index 9ec6960f..3b09b882 100644 --- a/config.c +++ b/config.c @@ -119,6 +119,7 @@ static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, static const char *const url_binding_action_map[] = { [BIND_ACTION_URL_NONE] = NULL, [BIND_ACTION_URL_CANCEL] = "cancel", + [BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible", }; static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, @@ -2107,6 +2108,7 @@ add_default_url_bindings(struct config *conf) 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); + add_binding(BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, ctrl, XKB_KEY_t); #undef add_binding } diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 0d25fe12..6c1938a2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -610,6 +610,20 @@ mode. The syntax is exactly the same as the regular **key-bindings**. Exits URL mode without opening an URL. Default: _Control+g Control+d Escape_. +*toggle-url-visible* + By default, the jump label only shows the key sequence required to + activate it. This is fine as long as the URL is visible in the + original text. + + But with e.g. OSC-8 URLs (the terminal version of HTML anchors, + i.e. "links"), the text on the screen can be something completey + different than the URL. + + This action toggles between showing and hiding the URL on the jump + label. + + Default: _Control+t_ + # SECTION: mouse-bindings diff --git a/foot.ini b/foot.ini index 5a5256c4..216ef45d 100644 --- a/foot.ini +++ b/foot.ini @@ -125,6 +125,7 @@ [url-bindings] # cancel=Control+g Control+d Escape +# toggle-url-visible=Control+t [mouse-bindings] # primary-paste=BTN_MIDDLE diff --git a/render.c b/render.c index c9dfc1fc..1db813bc 100644 --- a/render.c +++ b/render.c @@ -2525,6 +2525,8 @@ render_urls(struct terminal *term) + term->grid->num_rows) & (term->grid->num_rows - 1); const int view_end = view_start + term->rows - 1; + const bool show_url = term->urls_show_uri_on_jump_label; + tll_foreach(win->urls, it) { const struct url *url = it->item.url; const wchar_t *key = url->key; @@ -2562,13 +2564,28 @@ render_urls(struct terminal *term) continue; } - size_t chars = wcslen(key); + const size_t key_len = wcslen(key); + + size_t url_len = mbstowcs(NULL, url->url, 0); + if (url_len == (size_t)-1) + url_len = 0; + + wchar_t url_wchars[url_len + 1]; + mbstowcs(url_wchars, url->url, url_len + 1); + + size_t chars = key_len + (show_url ? (2 + url_len) : 0); const size_t max_chars = 50; chars = min(chars, max_chars); wchar_t label[chars + 2]; - wcscpy(label, key); + label[chars] = L'…'; + label[chars + 1] = L'\0'; + + if (show_url) + swprintf(label, chars + 1, L"%ls: %ls", key, url_wchars); + else + wcsncpy(label, key, chars + 1); for (size_t i = 0; i < wcslen(key); i++) label[i] = towupper(label[i]); diff --git a/terminal.h b/terminal.h index cc37bcac..7d6d0616 100644 --- a/terminal.h +++ b/terminal.h @@ -539,6 +539,7 @@ struct terminal { url_list_t urls; wchar_t url_keys[5]; + bool urls_show_uri_on_jump_label; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct { diff --git a/url-mode.c b/url-mode.c index 44f9bb19..09e170e6 100644 --- a/url-mode.c +++ b/url-mode.c @@ -30,6 +30,11 @@ execute_binding(struct seat *seat, struct terminal *term, urls_reset(term); return true; + case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL: + term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label; + render_refresh_urls(term); + return true; + case BIND_ACTION_URL_COUNT: return false; @@ -613,6 +618,7 @@ urls_reset(struct terminal *term) tll_remove(term->urls, it); } + term->urls_show_uri_on_jump_label = false; memset(term->url_keys, 0, sizeof(term->url_keys)); render_refresh(term); } diff --git a/wayland.h b/wayland.h index b2d7c64f..7aecaee0 100644 --- a/wayland.h +++ b/wayland.h @@ -82,6 +82,7 @@ enum bind_action_search { enum bind_action_url { BIND_ACTION_URL_NONE, BIND_ACTION_URL_CANCEL, + BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, BIND_ACTION_URL_COUNT, }; From 4ff5154cb8ce932bdbdb21fc96eb4d49d1371bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 16:58:34 +0100 Subject: [PATCH 19/36] =?UTF-8?q?config:=20change=20default=20key=20bindin?= =?UTF-8?q?g=20for=20toggle-url-visible=20to=20=E2=80=98t=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 3b09b882..c2eaf382 100644 --- a/config.c +++ b/config.c @@ -2108,7 +2108,7 @@ add_default_url_bindings(struct config *conf) 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); - add_binding(BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, ctrl, XKB_KEY_t); + add_binding(BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, none, XKB_KEY_t); #undef add_binding } diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index 6c1938a2..cdcdcfaa 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -622,7 +622,7 @@ mode. The syntax is exactly the same as the regular **key-bindings**. This action toggles between showing and hiding the URL on the jump label. - Default: _Control+t_ + Default: _t_. # SECTION: mouse-bindings diff --git a/foot.ini b/foot.ini index 216ef45d..1b309210 100644 --- a/foot.ini +++ b/foot.ini @@ -125,7 +125,7 @@ [url-bindings] # cancel=Control+g Control+d Escape -# toggle-url-visible=Control+t +# toggle-url-visible=t [mouse-bindings] # primary-paste=BTN_MIDDLE From 20ff492d3341d73c43d0dfbfbb10ea0aac7deeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 17:12:43 +0100 Subject: [PATCH 20/36] =?UTF-8?q?term:=20osc-8:=20open:=20typo:=20close=20?= =?UTF-8?q?previous=20URL=20if=20=E2=80=98begin=E2=80=99=20coords=20are=20?= =?UTF-8?q?*non-negative*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index efd260f1..d9834f9e 100644 --- a/terminal.c +++ b/terminal.c @@ -3018,7 +3018,7 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, void term_osc8_open(struct terminal *term, uint64_t id, const char *uri) { - if (unlikely(term->vt.osc8.begin.row < 0)) { + if (unlikely(term->vt.osc8.begin.row >= 0)) { /* It’s valid to switch from one URI to another without * closing the first one */ term_osc8_close(term); From 0e6b1f7508e9f2e683227d42964c1c7092eb181a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 17:13:46 +0100 Subject: [PATCH 21/36] term: osc-8: close: ignore zero-length URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the client application emitted e.g: \E]8;;http://foo\E\\\E]8;;\E\\ i.e. an anchor without content, then we ended up with an ‘end’ coordinate that lied *before* the ‘start’ coordinate, since we subtract one (column) from the end point to make it inclusive. --- terminal.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/terminal.c b/terminal.c index d9834f9e..3e2eb97f 100644 --- a/terminal.c +++ b/terminal.c @@ -3047,6 +3047,11 @@ term_osc8_close(struct terminal *term) .row = grid_row_absolute(term->grid, term->grid->cursor.point.row), }; + if (start.row == end.row && start.col == end.col) { + /* Zero-length URL, e.g: \E]8;;http://foo\E\\\E]8;;\E\\ */ + goto done; + } + /* end is *inclusive */ if (--end.col < 0) { end.row--; From 00977fcc156b322516d50db38f27a8a050820c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:32:38 +0100 Subject: [PATCH 22/36] grid: add new function, grid_row_reset_extra() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This function resets (free:s) all ‘extra’ data associated with a row. --- grid.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/grid.h b/grid.h index 98b34570..fffeca8a 100644 --- a/grid.h +++ b/grid.h @@ -72,3 +72,18 @@ grid_row_in_view(struct grid *grid, int row_no) xassert(row != NULL); return row; } + +static inline void +grid_row_reset_extra(struct row *row) +{ + if (likely(row->extra == NULL)) + return; + + tll_foreach(row->extra->uri_ranges, it) { + free(it->item.uri); + tll_remove(row->extra->uri_ranges, it); + } + + free(row->extra); + row->extra = NULL; +} From 3e06362d74ebd68e9d6d675f8feb0daaa9610bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:33:59 +0100 Subject: [PATCH 23/36] terminal: scroll: use grid_row_reset_extra() --- terminal.c | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/terminal.c b/terminal.c index 3e2eb97f..52494c90 100644 --- a/terminal.c +++ b/terminal.c @@ -2162,14 +2162,7 @@ term_scroll_partial(struct terminal *term, struct scroll_region region, int rows /* Erase scrolled in lines */ for (int r = region.end - rows; r < region.end; r++) { struct row *row = grid_row_and_alloc(term->grid, r); - if (unlikely(row->extra != NULL)) { - tll_foreach(row->extra->uri_ranges, it) { - free(it->item.uri); - tll_remove(row->extra->uri_ranges, it); - } - free(row->extra); - row->extra = NULL; - } + grid_row_reset_extra(row); erase_line(term, row); } @@ -2240,14 +2233,7 @@ term_scroll_reverse_partial(struct terminal *term, /* Erase scrolled in lines */ for (int r = region.start; r < region.start + rows; r++) { struct row *row = grid_row_and_alloc(term->grid, r); - if (unlikely(row->extra != NULL)) { - tll_foreach(row->extra->uri_ranges, it) { - free(it->item.uri); - tll_remove(row->extra->uri_ranges, it); - } - free(row->extra); - row->extra = NULL; - } + grid_row_reset_extra(row); erase_line(term, row); } From 17f90eeec44d272e926582d2bc4fae98c7073c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:34:25 +0100 Subject: [PATCH 24/36] grid: grid_row_free(): use grid_row_reset_extra() --- grid.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/grid.c b/grid.c index 2f0033ec..8a04bb07 100644 --- a/grid.c +++ b/grid.c @@ -51,13 +51,7 @@ grid_row_free(struct row *row) if (row == NULL) return; - if (row->extra != NULL) { - tll_foreach(row->extra->uri_ranges, it) { - free(it->item.uri); - tll_remove(row->extra->uri_ranges, it); - } - } - + grid_row_reset_extra(row); free(row->extra); free(row->cells); free(row); From 3ca5a65c339c1214d903e4b897f9b288a1083137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:34:49 +0100 Subject: [PATCH 25/36] grid: reflow: translate URI ranges URI ranges are per row. Translate by detecting URI range start/end coordinates, and opening and closing a corresponding URI range on the new grid. We need to take care when line-wrapping the new grid; here we need to manually close the still-open URI ranges (on the new grid), and re-opening them on the next row. --- grid.c | 159 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 22 deletions(-) diff --git a/grid.c b/grid.c index 8a04bb07..d81e92c4 100644 --- a/grid.c +++ b/grid.c @@ -169,6 +169,109 @@ grid_resize_without_reflow( #endif } +static void +reflow_uri_ranges(const struct row *old_row, struct row *new_row, + int old_col_idx, int new_col_idx) +{ + if (old_row->extra == NULL) + return; + + /* + * Check for URI range start/end points on the “old” row, and + * open/close a corresponding URI range on the “new” row. + */ + + tll_foreach(old_row->extra->uri_ranges, it) { + if (it->item.start == old_col_idx) { + struct row_uri_range new_range = { + .start = new_col_idx, + .end = -1, + .id = it->item.id, + .uri = xstrdup(it->item.uri), + }; + + if (new_row->extra == NULL) + new_row->extra = xcalloc(1, sizeof(*new_row->extra)); + tll_push_back(new_row->extra->uri_ranges, new_range); + } + + else if (it->item.end == old_col_idx) { + xassert(new_row->extra != NULL); + + bool found_it = false; + tll_foreach(new_row->extra->uri_ranges, it2) { + if (it2->item.id != it->item.id) + continue; + if (it2->item.end >= 0) + continue; + + it2->item.end = new_col_idx; + found_it = true; + break; + } + xassert(found_it); + } + } +} + +static struct row * +_line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, + int *row_idx, int *col_idx, int row_count, int col_count) +{ + *col_idx = 0; + *row_idx = (*row_idx + 1) & (row_count - 1); + + struct row *new_row = new_grid[*row_idx]; + + if (new_row == NULL) { + /* Scrollback not yet full, allocate a completely new row */ + new_row = grid_row_alloc(col_count, true); + new_grid[*row_idx] = new_row; + } else { + /* Scrollback is full, need to re-use a row */ + memset(new_row->cells, 0, col_count * sizeof(new_row->cells[0])); + grid_row_reset_extra(new_row); + new_row->linebreak = false; + + tll_foreach(old_grid->sixel_images, it) { + if (it->item.pos.row == *row_idx) { + sixel_destroy(&it->item); + tll_remove(old_grid->sixel_images, it); + } + } + } + + if (row->extra == NULL) + return new_row; + + /* + * URI ranges are per row. Thus, we need to ‘close’ the still-open + * ranges on the previous row, and re-open them on the + * next/current row. + */ + tll_foreach(row->extra->uri_ranges, it) { + if (it->item.end >= 0) + continue; + + /* Terminate URI range on the previous row */ + it->item.end = col_count - 1; + + /* Open a new range on the new/current row */ + struct row_uri_range new_range = { + .start = 0, + .end = -1, + .id = it->item.id, + .uri = xstrdup(it->item.uri), + }; + + if (new_row->extra == NULL) + new_row->extra = xcalloc(1, sizeof(*new_row->extra)); + tll_push_back(new_row->extra->uri_ranges, new_range); + } + + return new_row; +} + void grid_resize_and_reflow( struct grid *grid, int new_rows, int new_cols, @@ -248,29 +351,13 @@ grid_resize_and_reflow( tll_remove(untranslated_sixels, it); } -#define line_wrap() \ - do { \ - new_col_idx = 0; \ - new_row_idx = (new_row_idx + 1) & (new_rows - 1); \ - \ - new_row = new_grid[new_row_idx]; \ - if (new_row == NULL) { \ - new_row = grid_row_alloc(new_cols, true); \ - new_grid[new_row_idx] = new_row; \ - } else { \ - memset(new_row->cells, 0, new_cols * sizeof(new_row->cells[0])); \ - new_row->linebreak = false; \ - tll_foreach(grid->sixel_images, it) { \ - if (it->item.pos.row == new_row_idx) { \ - sixel_destroy(&it->item); \ - tll_remove(grid->sixel_images, it); \ - } \ - } \ - } \ - } while(0) +#define line_wrap() \ + new_row = _line_wrap( \ + grid, new_grid, new_row, &new_row_idx, &new_col_idx, \ + new_rows, new_cols) -#define print_spacer() \ - do { \ +#define print_spacer() \ + do { \ new_row->cells[new_col_idx].wc = CELL_MULT_COL_SPACER; \ new_row->cells[new_col_idx].attrs = old_cell->attrs; \ new_row->cells[new_col_idx].attrs.clean = 1; \ @@ -297,6 +384,17 @@ grid_resize_and_reflow( } } + /* If there’s an URI start/end point here, we need to make + * sure we handle it */ + if (old_row->extra != NULL) { + tll_foreach(old_row->extra->uri_ranges, it) { + if (it->item.start == c || it->item.end == c) { + is_tracking_point = true; + break; + } + } + } + if (old_row->cells[c].wc == 0 && !is_tracking_point) { empty_count++; continue; @@ -359,6 +457,8 @@ grid_resize_and_reflow( tll_remove(tracking_points, it); } } + + reflow_uri_ranges(old_row, new_row, c, new_col_idx); } new_col_idx++; } @@ -385,6 +485,21 @@ grid_resize_and_reflow( #undef line_wrap } +#if defined(_DEBUG) + /* Verify all URI ranges have been “closed” */ + for (int r = 0; r < new_rows; r++) { + const struct row *row = new_grid[r]; + + if (row == NULL) + continue; + if (row->extra == NULL) + continue; + + tll_foreach(row->extra->uri_ranges, it) + xassert(it->item.end >= 0); + } +#endif + /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; while (grid->offset < 0) From 8da82c897b82220bb96a55500e33b06850296c1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:42:42 +0100 Subject: [PATCH 26/36] grid: grid_resize_without_reflow: transfer URI ranges --- grid.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index d81e92c4..16924d1c 100644 --- a/grid.c +++ b/grid.c @@ -114,6 +114,27 @@ grid_resize_without_reflow( sixel_destroy(&it->item); tll_remove(untranslated_sixels, it); } + + /* Copy URI ranges, truncating them if necessary */ + if (old_row->extra == NULL) + continue; + + new_row->extra = xcalloc(1, sizeof(*new_row->extra)); + tll_foreach(old_row->extra->uri_ranges, it) { + if (it->item.start >= new_rows) { + /* The whole range is truncated */ + continue; + } + + struct row_uri_range range = { + .start = it->item.start, + .end = min(it->item.end, new_cols - 1), + .id = it->item.id, + .uri = xstrdup(it->item.uri), + }; + + tll_push_back(new_row->extra->uri_ranges, range); + } } /* Clear "new" lines */ @@ -122,7 +143,6 @@ grid_resize_without_reflow( new_grid[(new_offset + r) & (new_rows - 1)] = new_row; memset(new_row->cells, 0, sizeof(struct cell) * new_cols); - new_row->linebreak = false; new_row->dirty = true; } From fd505f2274343d03e26847217c68e3c8f1f86e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:45:12 +0100 Subject: [PATCH 27/36] =?UTF-8?q?grid:=20resize=5Fwithout=5Freflow:=20allo?= =?UTF-8?q?cate=20=E2=80=98extra=E2=80=99=20on-demand=20on=20=E2=80=98new?= =?UTF-8?q?=E2=80=99=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even if we have URI ranges on the old row, all those ranges may lay outside the new grid’s range. --- grid.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/grid.c b/grid.c index 16924d1c..7e000b42 100644 --- a/grid.c +++ b/grid.c @@ -119,7 +119,6 @@ grid_resize_without_reflow( if (old_row->extra == NULL) continue; - new_row->extra = xcalloc(1, sizeof(*new_row->extra)); tll_foreach(old_row->extra->uri_ranges, it) { if (it->item.start >= new_rows) { /* The whole range is truncated */ @@ -133,6 +132,8 @@ grid_resize_without_reflow( .uri = xstrdup(it->item.uri), }; + if (new_row->extra == NULL) + new_row->extra = xcalloc(1, sizeof(*new_row->extra)); tll_push_back(new_row->extra->uri_ranges, range); } } From 5eea06cff99abbac234d517e84e31576234c7e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:50:33 +0100 Subject: [PATCH 28/36] grid: add new function grid_row_add_uri_range() --- grid.c | 9 +++++++++ grid.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/grid.c b/grid.c index 7e000b42..72d8505e 100644 --- a/grid.c +++ b/grid.c @@ -588,3 +588,12 @@ grid_resize_and_reflow( tll_free(tracking_points); } + +void +grid_row_add_uri_range(struct row *row, struct row_uri_range range) +{ + if (row->extra == NULL) + row->extra = xcalloc(1, sizeof(*row->extra)); + + tll_push_back(row->extra->uri_ranges, range); +} diff --git a/grid.h b/grid.h index fffeca8a..4cb339f2 100644 --- a/grid.h +++ b/grid.h @@ -73,6 +73,8 @@ grid_row_in_view(struct grid *grid, int row_no) return row; } +void grid_row_add_uri_range(struct row *row, struct row_uri_range range); + static inline void grid_row_reset_extra(struct row *row) { From d42b129814e120bf01ee3bfa06f399a81dec6c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:50:46 +0100 Subject: [PATCH 29/36] grid: refactor: use grid_row_add_uri_range() --- grid.c | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/grid.c b/grid.c index 72d8505e..93e4f8c8 100644 --- a/grid.c +++ b/grid.c @@ -131,10 +131,7 @@ grid_resize_without_reflow( .id = it->item.id, .uri = xstrdup(it->item.uri), }; - - if (new_row->extra == NULL) - new_row->extra = xcalloc(1, sizeof(*new_row->extra)); - tll_push_back(new_row->extra->uri_ranges, range); + grid_row_add_uri_range(new_row, range); } } @@ -210,10 +207,7 @@ reflow_uri_ranges(const struct row *old_row, struct row *new_row, .id = it->item.id, .uri = xstrdup(it->item.uri), }; - - if (new_row->extra == NULL) - new_row->extra = xcalloc(1, sizeof(*new_row->extra)); - tll_push_back(new_row->extra->uri_ranges, new_range); + grid_row_add_uri_range(new_row, new_range); } else if (it->item.end == old_col_idx) { @@ -284,10 +278,7 @@ _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, .id = it->item.id, .uri = xstrdup(it->item.uri), }; - - if (new_row->extra == NULL) - new_row->extra = xcalloc(1, sizeof(*new_row->extra)); - tll_push_back(new_row->extra->uri_ranges, new_range); + grid_row_add_uri_range(new_row, new_range); } return new_row; From 7f13d0084ea9498fb0cfb705724e4ee7212922ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:50:55 +0100 Subject: [PATCH 30/36] terminal: osc8_close(): refactor: use grid_row_add_uri_range() --- terminal.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/terminal.c b/terminal.c index 52494c90..ce311c3d 100644 --- a/terminal.c +++ b/terminal.c @@ -3049,17 +3049,13 @@ term_osc8_close(struct terminal *term) do { int end_col = r == end.row ? end.col : term->cols - 1; - struct row *row = term->grid->rows[r]; - if (row->extra == NULL) - row->extra = xcalloc(1, sizeof(*row->extra)); - struct row_uri_range range = { .start = start_col, .end = end_col, .id = term->vt.osc8.id, .uri = xstrdup(term->vt.osc8.uri), }; - tll_push_back(row->extra->uri_ranges, range); + grid_row_add_uri_range(term->grid->rows[r], range); start_col = 0; } while (r++ != end.row); From a0b977fcee7a3095d548b6cb9131e77f16ac3770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 20:52:26 +0100 Subject: [PATCH 31/36] =?UTF-8?q?grid:=20refactor:=20break=20out=20allocat?= =?UTF-8?q?ion=20of=20=E2=80=98extra=E2=80=99=20member=20to=20separate=20f?= =?UTF-8?q?unction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grid.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/grid.c b/grid.c index 93e4f8c8..00890a1d 100644 --- a/grid.c +++ b/grid.c @@ -580,11 +580,16 @@ grid_resize_and_reflow( tll_free(tracking_points); } -void -grid_row_add_uri_range(struct row *row, struct row_uri_range range) +static void +ensure_row_has_extra_data(struct row *row) { if (row->extra == NULL) row->extra = xcalloc(1, sizeof(*row->extra)); +} +void +grid_row_add_uri_range(struct row *row, struct row_uri_range range) +{ + ensure_row_has_extra_data(row); tll_push_back(row->extra->uri_ranges, range); } From 2074f8b6563b7a6331c94a9231cd5b5af8bd7ade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 14 Feb 2021 21:29:22 +0100 Subject: [PATCH 32/36] urls: OSC-8 URLs can now optionally be underlined outside of url-mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a new configuration option, ‘osc8-underline=url-mode|always’. When set to ‘url-mode’, OSC-8 URLs are only highlighted (i.e. underlined) in url-mode, just like auto-detected URLs. When set to ‘always’, they are always underlined, regardless of mode, and regardless of their other attributes. This is implemented by tagging collected URLs with a boolean, instructing urls_render() and urls_reset() whether they should update the cells’ ‘url’ attribute or not. The OSC-8 collecter sets this based on the value of ‘osc8-underline’. Finally, when closing an OSC-8 URL, the cells are immediately tagged with the ‘url’ attribute if ‘osc8-underline’ is set to ‘always’. --- config.c | 15 +++++++++++++++ config.h | 5 +++++ doc/foot.ini.5.scd | 14 +++++++++++++- foot.ini | 1 + terminal.c | 14 +++++++++++++- terminal.h | 1 + url-mode.c | 18 +++++++++++++++++- 7 files changed, 65 insertions(+), 3 deletions(-) diff --git a/config.c b/config.c index c2eaf382..d7d2c3a1 100644 --- a/config.c +++ b/config.c @@ -791,6 +791,19 @@ parse_section_main(const char *key, const char *value, struct config *conf, return false; } + else if (strcmp(key, "osc8-underline") == 0) { + if (strcmp(value, "url-mode") == 0) + conf->osc8_underline = OSC8_UNDERLINE_URL_MODE; + else if (strcmp(value, "always") == 0) + conf->osc8_underline = OSC8_UNDERLINE_URL_MODE; + else { + LOG_AND_NOTIFY_ERR( + "%s:%u: [default]: %s: invalid 'osc8-underline'; " + "must be one of 'url-mode', or 'always'", path, lineno, value); + return false; + } + } + else { LOG_AND_NOTIFY_ERR("%s:%u: [default]: %s: invalid key", path, lineno, key); return false; @@ -2243,6 +2256,8 @@ config_load(struct config *conf, const char *conf_path, .argv = NULL, }, + .osc8_underline = OSC8_UNDERLINE_ALWAYS, + .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, .allow_overflowing_double_width_glyphs = true, diff --git a/config.h b/config.h index 69930ec0..e8d15859 100644 --- a/config.h +++ b/config.h @@ -205,6 +205,11 @@ struct config { struct config_spawn_template notify; struct config_spawn_template url_launch; + enum { + OSC8_UNDERLINE_URL_MODE, + OSC8_UNDERLINE_ALWAYS, + } osc8_underline; + struct { enum fcft_scaling_filter fcft_filter; bool allow_overflowing_double_width_glyphs; diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index cdcdcfaa..3108c750 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -241,13 +241,25 @@ in this order: Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. - *workers* Number of threads to use for rendering. Set to 0 to disable multithreading. Default: the number of available logical CPUs (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. +*osc8-underline* + When to underline OSC-8 URLs. Possible values are *url-mode* and + *always*. + + When set to *url-mode*, OSC-8 URLs are only highlighted in URL + mode, just like auto-detected URLs. + + When set to *always*, OSC-8 URLs are always highlighted, + regardless of their other attributes (bold, italic etc). Note that + this does _not_ make them clickable. + + Default: _url-mode_ + # SECTION: scrollback diff --git a/foot.ini b/foot.ini index 1b309210..8ea060c9 100644 --- a/foot.ini +++ b/foot.ini @@ -29,6 +29,7 @@ # jump-label-letters=sadfjklewcmpgh # selection-target=primary # workers= +# osc8-underline=url-mode [scrollback] # lines=1000 diff --git a/terminal.c b/terminal.c index ce311c3d..3710f005 100644 --- a/terminal.c +++ b/terminal.c @@ -3049,13 +3049,25 @@ term_osc8_close(struct terminal *term) do { int end_col = r == end.row ? end.col : term->cols - 1; + struct row *row = term->grid->rows[r]; + + switch (term->conf->osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + for (int c = start_col; c <= end_col; c++) + row->cells[c].attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + struct row_uri_range range = { .start = start_col, .end = end_col, .id = term->vt.osc8.id, .uri = xstrdup(term->vt.osc8.uri), }; - grid_row_add_uri_range(term->grid->rows[r], range); + grid_row_add_uri_range(row, range); start_col = 0; } while (r++ != end.row); diff --git a/terminal.h b/terminal.h index 7d6d0616..acff9d06 100644 --- a/terminal.h +++ b/terminal.h @@ -256,6 +256,7 @@ struct url { struct coord start; struct coord end; enum url_action action; + bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn’t touch the cells’ attr.url */ }; typedef tll(struct url) url_list_t; diff --git a/url-mode.c b/url-mode.c index 09e170e6..a441aa86 100644 --- a/url-mode.c +++ b/url-mode.c @@ -374,6 +374,18 @@ UNIGNORE_WARNINGS static void osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) { + bool dont_touch_url_attr = false; + + switch (term->conf->osc8_underline) { + case OSC8_UNDERLINE_URL_MODE: + dont_touch_url_attr = false; + break; + + case OSC8_UNDERLINE_ALWAYS: + dont_touch_url_attr = true; + break; + } + for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); @@ -396,7 +408,8 @@ osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) .url = xstrdup(it->item.uri), .start = start, .end = end, - .action = action})); + .action = action, + .url_mode_dont_change_url_attr = dont_touch_url_attr})); } } } @@ -542,6 +555,9 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) static void tag_cells_for_url(struct terminal *term, const struct url *url, bool value) { + if (url->url_mode_dont_change_url_attr) + return; + const struct coord *start = &url->start; const struct coord *end = &url->end; From 35779ec4e51be0fa23200fdb782981de2a9fa82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 16 Feb 2021 11:24:57 +0100 Subject: [PATCH 33/36] =?UTF-8?q?config:=20osc8-underline:=20default=20to?= =?UTF-8?q?=20=E2=80=98url-mode=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index d7d2c3a1..1cd6d0cb 100644 --- a/config.c +++ b/config.c @@ -2256,7 +2256,7 @@ config_load(struct config *conf, const char *conf_path, .argv = NULL, }, - .osc8_underline = OSC8_UNDERLINE_ALWAYS, + .osc8_underline = OSC8_UNDERLINE_URL_MODE, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, From 21a355f38a03391f7d9fc2a67b46e5214102c59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 19 Feb 2021 08:44:38 +0100 Subject: [PATCH 34/36] config: typo: osc8-underline=always means ALWAYS --- config.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.c b/config.c index 1cd6d0cb..cee12a6c 100644 --- a/config.c +++ b/config.c @@ -795,7 +795,7 @@ parse_section_main(const char *key, const char *value, struct config *conf, if (strcmp(value, "url-mode") == 0) conf->osc8_underline = OSC8_UNDERLINE_URL_MODE; else if (strcmp(value, "always") == 0) - conf->osc8_underline = OSC8_UNDERLINE_URL_MODE; + conf->osc8_underline = OSC8_UNDERLINE_ALWAYS; else { LOG_AND_NOTIFY_ERR( "%s:%u: [default]: %s: invalid 'osc8-underline'; " From 11464a65de4d96f04b4abd5c32e6c092a81a7a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 21 Feb 2021 20:10:24 +0100 Subject: [PATCH 35/36] url-mode: use the same key combo for all occurrences of an URL --- url-mode.c | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/url-mode.c b/url-mode.c index a441aa86..554c17bf 100644 --- a/url-mode.c +++ b/url-mode.c @@ -519,10 +519,13 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) wchar_t *combos[count]; generate_key_combos(conf, count, combos); - size_t idx = 0; + size_t combo_idx = 0; + size_t id_idx = 0; + tll_foreach(*urls, it) { bool id_already_seen = false; - for (size_t i = 0; i < idx; i++) { + + for (size_t i = 0; i < id_idx; i++) { if (it->item.id == seen_ids[i]) { id_already_seen = true; break; @@ -531,13 +534,30 @@ urls_assign_key_combos(const struct config *conf, url_list_t *urls) if (id_already_seen) continue; + seen_ids[id_idx++] = it->item.id; - seen_ids[idx] = it->item.id; - it->item.key = combos[idx++]; + /* + * Scan previous URLs, and check if *this* URL matches any of + * them; if so, re-use the *same* key combo. + */ + bool url_already_seen = false; + tll_foreach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (strcmp(it->item.url, it2->item.url) == 0) { + it->item.key = xwcsdup(it2->item.key); + url_already_seen = true; + break; + } + } + + if (!url_already_seen) + it->item.key = combos[combo_idx++]; } /* Free combos we didn’t use up */ - for (size_t i = idx; i < count; i++) + for (size_t i = combo_idx; i < count; i++) free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG From 3c6d4f152beb01380585154f31b315f9450c5208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 21 Feb 2021 20:16:25 +0100 Subject: [PATCH 36/36] changelog: OSC-8 support --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a430a660..86bc376a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,8 @@ * Key/mouse binding `select-extend-character-wise`, which forces the selection mode to 'character-wise' when extending a selection. * `DECSET` `47`, `1047` and `1048`. -* URL detection. URLs are highlighted and activated using the keyboard - (**no** mouse support). See **foot**(1)::URLs, or +* URL detection and OSC-8 support. 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). * `-d,--log-level={info|warning|error}` to both `foot` and