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 diff --git a/config.c b/config.c index 9ec6960f..cee12a6c 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, @@ -790,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_ALWAYS; + 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; @@ -2107,6 +2121,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, none, XKB_KEY_t); #undef add_binding } @@ -2241,6 +2256,8 @@ config_load(struct config *conf, const char *conf_path, .argv = NULL, }, + .osc8_underline = OSC8_UNDERLINE_URL_MODE, + .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 0d25fe12..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 @@ -610,6 +622,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: _t_. + # SECTION: mouse-bindings diff --git a/foot.ini b/foot.ini index 5a5256c4..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 @@ -125,6 +126,7 @@ [url-bindings] # cancel=Control+g Control+d Escape +# toggle-url-visible=t [mouse-bindings] # primary-paste=BTN_MIDDLE diff --git a/grid.c b/grid.c index 6d6b6b3c..00890a1d 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,8 @@ grid_row_free(struct row *row) if (row == NULL) return; + grid_row_reset_extra(row); + free(row->extra); free(row->cells); free(row); } @@ -111,6 +114,25 @@ 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; + + 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), + }; + grid_row_add_uri_range(new_row, range); + } } /* Clear "new" lines */ @@ -119,7 +141,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; } @@ -166,6 +187,103 @@ 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), + }; + grid_row_add_uri_range(new_row, 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), + }; + grid_row_add_uri_range(new_row, new_range); + } + + return new_row; +} + void grid_resize_and_reflow( struct grid *grid, int new_rows, int new_cols, @@ -245,29 +363,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; \ @@ -294,6 +396,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; @@ -356,6 +469,8 @@ grid_resize_and_reflow( tll_remove(tracking_points, it); } } + + reflow_uri_ranges(old_row, new_row, c, new_col_idx); } new_col_idx++; } @@ -382,6 +497,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) @@ -449,3 +579,17 @@ grid_resize_and_reflow( tll_free(tracking_points); } + +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); +} diff --git a/grid.h b/grid.h index 98b34570..4cb339f2 100644 --- a/grid.h +++ b/grid.h @@ -72,3 +72,20 @@ grid_row_in_view(struct grid *grid, int row_no) xassert(row != NULL); 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) +{ + 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; +} 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()) { diff --git a/osc.c b/osc.c index 801b04e9..bf97db52 100644 --- a/osc.c +++ b/osc.c @@ -1,5 +1,6 @@ #include "osc.h" +#include #include #include #include @@ -15,6 +16,7 @@ #include "selection.h" #include "terminal.h" #include "uri.h" +#include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -376,6 +378,64 @@ 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). + */ + + 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(); + + 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; + + if (strcmp(key, "id") == 0) + id = sdbm_hash(value); + } + + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); + + if (uri[0] == '\0') + term_osc8_close(term); + else + term_osc8_open(term, id, uri); +} + static void osc_notify(struct terminal *term, char *string) { @@ -553,6 +613,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 */ diff --git a/render.c b/render.c index aaa106a3..1db813bc 100644 --- a/render.c +++ b/render.c @@ -2525,12 +2525,19 @@ 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 *text = url->text; 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; @@ -2557,22 +2564,28 @@ render_urls(struct terminal *term) continue; } - size_t text_len = wcslen(text); - size_t chars = wcslen(key) + (text_len > 0 ? 3 + text_len : 0); + 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]; - 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'; - } - } + 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.c b/terminal.c index 0051ca6d..3710f005 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; @@ -2154,8 +2160,11 @@ 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); + grid_row_reset_extra(row); + 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 +2231,11 @@ 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); + grid_row_reset_extra(row); + 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); @@ -2989,3 +3001,79 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, #endif } +void +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 + * 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.id = id; + 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), + }; + + 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--; + end.col = term->cols - 1; + } + + 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]; + + 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(row, range); + start_col = 0; + } while (r++ != end.row); + +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 94433178..acff9d06 100644 --- a/terminal.h +++ b/terminal.h @@ -86,10 +86,22 @@ struct composed { uint8_t count; }; +struct row_uri_range { + int start; + int end; + uint64_t id; + 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 { @@ -144,12 +156,25 @@ 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 { + uint64_t id; + char *uri; + struct coord begin; + } osc8; + struct { uint8_t *data; size_t size; @@ -157,8 +182,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 }; @@ -227,12 +250,13 @@ 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; + uint64_t id; + char *url; wchar_t *key; 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; @@ -516,6 +540,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 { @@ -668,3 +693,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, uint64_t id, const char *uri); +void term_osc8_close(struct terminal *term); diff --git a/url-mode.c b/url-mode.c index ac379c8e..554c17bf 100644 --- a/url-mode.c +++ b/url-mode.c @@ -1,5 +1,6 @@ #include "url-mode.h" +#include #include #include @@ -11,9 +12,12 @@ #include "selection.h" #include "spawn.h" #include "terminal.h" +#include "uri.h" #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) @@ -26,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; @@ -36,42 +45,59 @@ 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)) + { + 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); - 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); + free(scheme); + free(host); } + + if (url_string == NULL) + url_string = xstrdup(url->url); + + 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; + + 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 @@ -121,6 +147,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); @@ -151,7 +180,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://", @@ -313,14 +343,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){ + .id = (uint64_t)rand() << 32 | rand(), + .url = url_utf8, + .start = start, + .end = end, + .action = action})); + } state = STATE_PROTOCOL; len = 0; @@ -335,15 +371,78 @@ 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) +{ + 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); + + 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){ + .id = it->item.id, + .url = xstrdup(it->item.uri), + .start = start, + .end = end, + .action = action, + .url_mode_dont_change_url_attr = dont_touch_url_attr})); + } + } +} + +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) + { + url_destroy(&inner->item); + 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); + osc8_uris(term, action, urls); auto_detected(term, action, urls); + remove_duplicates(urls); } -static void url_destroy(struct url *url); - static int wcscmp_qsort_wrapper(const void *_a, const void *_b) { @@ -416,22 +515,59 @@ 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) - it->item.key = combos[idx++]; + 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 < id_idx; i++) { + if (it->item.id == seen_ids[i]) { + id_already_seen = true; + break; + } + } + + if (id_already_seen) + continue; + seen_ids[id_idx++] = it->item.id; + + /* + * 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 = combo_idx; i < count; i++) + free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG tll_foreach(*urls, it) { - char url[1024]; - wcstombs(url, it->item.url, sizeof(url) - 1); + if (it->item.key == NULL) + continue; 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 } @@ -439,6 +575,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; @@ -493,7 +632,6 @@ static void url_destroy(struct url *url) { free(url->url); - free(url->text); free(url->key); } @@ -516,6 +654,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/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; +} 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, };