osc8: update URI ranges as we print data, *not* when the URI is closed

At first, an OSC-8 URI range was added when we received the closing
OSC-8 escape (i.e. with an empty URI).

But, this meant that cursor movements while the OSC-8 escape was in
effect wasn’t handled correctly, since we’d add a range that spanned
the cursor movements.

Attempts were made to handle this in the cursor movement functions, by
closing and re-opening the URI.

However, there are too many corner cases to make this a viable
approach. Scrolling is one such example, line-wrapping another.

This patch takes a different approach; emit, or update the URI range
when we print to the grid. This models the intended behavior much more
closely, where an active OSC-8 URI act like any other SGR attribute -
it is applied to all cells printed to, but otherwise have no effect.

To avoid killing performance, this is only done in the “generic”
printer. This means OSC-8 open/close calls must now “switch” the ASCII
printer.

Note that the “fast” printer still needs to *erase* pre-existing OSC-8
URIs.

Closes #816
This commit is contained in:
Daniel Eklöf 2021-11-25 19:22:52 +01:00
parent 82219713cc
commit 8c50a7afd4
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
5 changed files with 143 additions and 116 deletions

View file

@ -48,6 +48,9 @@
* An ongoing mouse selection is now finalized on a pointer leave event
(for example by switching workspace while doing a mouse selection).
* OSC-8 URIs in the last column
* OSC-8 URIs sometimes being applied to too many, and seemingly
unrelated cells (https://codeberg.org/dnkl/foot/issues/816).
### Security

152
grid.c
View file

@ -844,6 +844,117 @@ ensure_row_has_extra_data(struct row *row)
row->extra = xcalloc(1, sizeof(*row->extra));
}
static void
verify_no_overlapping_uris(const struct row *row)
{
#if defined(_DEBUG)
const struct row_data *extra = row->extra;
tll_foreach(extra->uri_ranges, it1) {
const struct row_uri_range *r1 = &it1->item;
tll_foreach(extra->uri_ranges, it2) {
const struct row_uri_range *r2 = &it2->item;
if (r1 == r2)
continue;
if ((r1->start <= r2->start && r1->end >= r2->start) ||
(r1->start <= r2->end && r1->end >= r2->end))
{
BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d",
r1->uri, r1->start, r1->end,
r2->uri, r2->start, r2->end);
}
}
}
#endif
}
void
grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id)
{
ensure_row_has_extra_data(row);
struct row_data *extra = row->extra;
tll_rforeach(extra->uri_ranges, it) {
struct row_uri_range *r = &it->item;
if (r->end + 1 < col) {
/* Ranges are sorted, and this means all the remaining
* URIs *end* before col */
break;
}
if (r->start <= col && r->end >= col) {
/*
* col is *inside* an existing range
*/
if (r->id == id) {
/* col is already inside a matching URI range */
goto out;
}
/* Splice old range */
if (r->start == r->end)
tll_remove(extra->uri_ranges, it);
else if (r->start == col)
r->start++;
else if (r->end == col)
r->end--;
else {
xassert(r->start < col);
xassert(r->end > col);
struct row_uri_range new_tail = {
.start = col + 1,
.end = r->end,
.id = r->id,
.uri = xstrdup(r->uri),
};
r->end = col - 1;
xassert(r->start <= r->end);
xassert(new_tail.start <= new_tail.end);
tll_insert_after(extra->uri_ranges, it, new_tail);
}
break; /* Break out add and a new range for col */
}
else if (r->id != id)
continue;
#if 0 /* Trust the URI ID, for now... */
if (strcmp(r->uri, uri) != 0)
continue;
#endif
else if (likely(r->end + 1 == col)) {
r->end = col;
goto out;
}
else if (col + 1 == r->start) {
r->start = col;
goto out;
}
}
struct row_uri_range new_range = {
.start = col,
.end = col,
.id = id,
.uri = xstrdup(uri),
};
grid_row_uri_range_add(row, new_range);
out:
verify_no_overlapping_uris(row);
}
void
grid_row_uri_range_add(struct row *row, struct row_uri_range range)
{
@ -858,20 +969,7 @@ grid_row_uri_range_add(struct row *row, struct row_uri_range range)
tll_push_front(row->extra->uri_ranges, range);
out:
;
#if defined(_DEBUG)
tll_foreach(row->extra->uri_ranges, it1) {
tll_foreach(row->extra->uri_ranges, it2) {
if (&it1->item == &it2->item)
continue;
xassert(it1->item.start != it2->item.start);
xassert(it1->item.start != it2->item.end);
xassert(it1->item.end != it2->item.start);
xassert(it1->item.end != it2->item.end);
}
}
#endif
verify_no_overlapping_uris(row);
}
void
@ -929,36 +1027,22 @@ UNITTEST
struct row_data row_data = {.uri_ranges = tll_init()};
struct row row = {.extra = &row_data};
#define row_has_no_overlapping_uris(row) \
do { \
tll_foreach((row)->extra->uri_ranges, it1) { \
tll_foreach((row)->extra->uri_ranges, it2) { \
if (&it1->item == &it2->item) \
continue; \
xassert(it1->item.start != it2->item.start); \
xassert(it1->item.start != it2->item.end); \
xassert(it1->item.end != it2->item.start); \
xassert(it1->item.end != it2->item.end); \
} \
} \
} while (0)
grid_row_uri_range_add(&row, (struct row_uri_range){1, 10});
xassert(tll_length(row_data.uri_ranges) == 1);
xassert(tll_front(row_data.uri_ranges).start == 1);
xassert(tll_front(row_data.uri_ranges).end == 10);
row_has_no_overlapping_uris(&row);
verify_no_overlapping_uris(&row);
grid_row_uri_range_add(&row, (struct row_uri_range){11, 20});
xassert(tll_length(row_data.uri_ranges) == 2);
xassert(tll_back(row_data.uri_ranges).start == 11);
xassert(tll_back(row_data.uri_ranges).end == 20);
row_has_no_overlapping_uris(&row);
verify_no_overlapping_uris(&row);
/* Erase both URis */
grid_row_uri_range_erase(&row, 1, 20);
xassert(tll_length(row_data.uri_ranges) == 0);
row_has_no_overlapping_uris(&row);
verify_no_overlapping_uris(&row);
/* Two URIs, then erase second half of the first, first half of
the second */
@ -970,7 +1054,7 @@ UNITTEST
xassert(tll_front(row_data.uri_ranges).end == 4);
xassert(tll_back(row_data.uri_ranges).start == 16);
xassert(tll_back(row_data.uri_ranges).end == 20);
row_has_no_overlapping_uris(&row);
verify_no_overlapping_uris(&row);
tll_pop_back(row_data.uri_ranges);
tll_pop_back(row_data.uri_ranges);
@ -984,9 +1068,7 @@ UNITTEST
xassert(tll_front(row_data.uri_ranges).end == 4);
xassert(tll_back(row_data.uri_ranges).start == 7);
xassert(tll_back(row_data.uri_ranges).end == 10);
row_has_no_overlapping_uris(&row);
#undef row_has_no_overlapping_uris
verify_no_overlapping_uris(&row);
tll_free(row_data.uri_ranges);
}

2
grid.h
View file

@ -74,6 +74,8 @@ grid_row_in_view(struct grid *grid, int row_no)
return row;
}
void grid_row_uri_range_put(
struct row *row, int col, const char *uri, uint64_t id);
void grid_row_uri_range_add(struct row *row, struct row_uri_range range);
void grid_row_uri_range_erase(struct row *row, int start, int end);

View file

@ -1134,9 +1134,6 @@ 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,
@ -1826,7 +1823,6 @@ term_reset(struct terminal *term, bool hard)
term->vt = (struct vt){
.state = 0, /* STATE_GROUND */
.osc8 = {.begin = (struct coord){-1, -1}},
};
if (term->grid == &term->alt) {
@ -3151,7 +3147,8 @@ print_insert(struct terminal *term, int width)
static void
print_spacer(struct terminal *term, int col, int remaining)
{
struct row *row = term->grid->cur_row;
struct grid *grid = term->grid;
struct row *row = grid->cur_row;
struct cell *cell = &row->cells[col];
cell->wc = CELL_SPACER + remaining;
@ -3210,7 +3207,19 @@ term_print(struct terminal *term, wchar_t wc, int width)
cell->wc = term->vt.last_printed = wc;
cell->attrs = term->vt.attrs;
const int uri_start = col;
if (term->vt.osc8.uri != NULL) {
grid_row_uri_range_put(
row, col, term->vt.osc8.uri, term->vt.osc8.id);
switch (term->conf->url.osc8_underline) {
case OSC8_UNDERLINE_ALWAYS:
cell->attrs.url = true;
break;
case OSC8_UNDERLINE_URL_MODE:
break;
}
}
/* Advance cursor the 'additional' columns while dirty:ing the cells */
for (int i = 1; i < width && col < term->cols - 1; i++) {
@ -3226,9 +3235,6 @@ term_print(struct terminal *term, wchar_t wc, int width)
xassert(!grid->cursor.lcf);
grid->cursor.point.col = col;
if (unlikely(row->extra != NULL))
grid_row_uri_range_erase(row, uri_start, uri_start + width - 1);
}
static void
@ -3260,7 +3266,6 @@ ascii_printer_fast(struct terminal *term, wchar_t wc)
cell->wc = term->vt.last_printed = wc;
cell->attrs = term->vt.attrs;
/* Advance cursor */
if (unlikely(++col >= term->cols)) {
grid->cursor.lcf = true;
@ -3287,6 +3292,7 @@ term_update_ascii_printer(struct terminal *term)
{
void (*new_printer)(struct terminal *term, wchar_t wc) =
unlikely(tll_length(term->grid->sixel_images) > 0 ||
term->vt.osc8.uri != NULL ||
term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC ||
term->insert_mode)
? &ascii_printer_generic
@ -3294,7 +3300,7 @@ term_update_ascii_printer(struct terminal *term)
#if defined(_DEBUG) && LOG_ENABLE_DBG
if (term->ascii_printer != new_printer) {
LOG_DBG("switching ASCII printer %s -> %s",
LOG_DBG("§switching ASCII printer %s -> %s",
term->ascii_printer == &ascii_printer_fast ? "fast" : "generic",
new_printer == &ascii_printer_fast ? "fast" : "generic");
}
@ -3491,84 +3497,19 @@ term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width,
void
term_osc8_open(struct terminal *term, uint64_t id, const char *uri)
{
if (unlikely(term->vt.osc8.begin.row >= 0)) {
/* Its valid to switch from one URI to another without
* closing the first one */
term_osc8_close(term);
}
term_osc8_close(term);
xassert(term->vt.osc8.uri == NULL);
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);
term_update_ascii_printer(term);
}
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;
while (true) {
int end_col = r == end.row ? end.col : term->cols - 1;
struct row *row = term->grid->rows[r];
switch (term->conf->url.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_uri_range_add(row, range);
start_col = 0;
if (r == end.row)
break;
r++;
r &= term->grid->num_rows - 1;
}
done:
free(term->vt.osc8.uri);
term->vt.osc8.id = 0;
term->vt.osc8.uri = NULL;
term->vt.osc8.begin = (struct coord){-1, -1};
term->vt.osc8.id = 0;
term_update_ascii_printer(term);
}

View file

@ -185,7 +185,6 @@ struct vt {
struct {
uint64_t id;
char *uri;
struct coord begin;
} osc8;
struct {