mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-17 22:05:22 -05:00
commit
2c881b9f06
15 changed files with 658 additions and 98 deletions
|
|
@ -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
|
||||
|
|
|
|||
17
config.c
17
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,
|
||||
|
|
|
|||
5
config.h
5
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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
2
foot.ini
2
foot.ini
|
|
@ -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
190
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);
|
||||
}
|
||||
|
|
|
|||
17
grid.h
17
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;
|
||||
}
|
||||
|
|
|
|||
2
main.c
2
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()) {
|
||||
|
|
|
|||
64
osc.c
64
osc.c
|
|
@ -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 */
|
||||
|
|
|
|||
37
render.c
37
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]);
|
||||
|
|
|
|||
96
terminal.c
96
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};
|
||||
}
|
||||
|
|
|
|||
36
terminal.h
36
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);
|
||||
|
|
|
|||
243
url-mode.c
243
url-mode.c
|
|
@ -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 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);
|
||||
}
|
||||
|
|
|
|||
14
util.h
14
util.h
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue