Merge branch 'osc8'

Closes #13
This commit is contained in:
Daniel Eklöf 2021-02-21 20:48:18 +01:00
commit 2c881b9f06
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
15 changed files with 658 additions and 98 deletions

View file

@ -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

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -29,6 +29,7 @@
# jump-label-letters=sadfjklewcmpgh
# selection-target=primary
# workers=<number of logical CPUs>
# 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

190
grid.c
View file

@ -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 theres 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);
}

17
grid.h
View file

@ -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;
}

2
main.c
View file

@ -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()) {

64
osc.c
View file

@ -1,5 +1,6 @@
#include "osc.h"
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
@ -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;<params>;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 */

View file

@ -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]);

View file

@ -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)) {
/* Its 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};
}

View file

@ -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 doesnt 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);

View file

@ -1,5 +1,6 @@
#include "url-mode.h"
#include <stdlib.h>
#include <string.h>
#include <wctype.h>
@ -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 didnt 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);
}

14
util.h
View file

@ -1,5 +1,6 @@
#pragma once
#include <stdint.h>
#include <threads.h>
#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;
}

View file

@ -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,
};