diff options
| author | user <user@clank> | 2026-08-20 23:23:07 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-20 23:23:07 +0200 |
| commit | 965966f9fb3808fbc69e6cb55960ad7a3c72c5f4 (patch) | |
| tree | 388d17a48155b9fae25c65fdc6c8765b6bb3ec01 /src/pane.c | |
| parent | slosh: floating panes and the drop grammar (diff) | |
| download | slosh-965966f9fb3808fbc69e6cb55960ad7a3c72c5f4.tar.gz slosh-965966f9fb3808fbc69e6cb55960ad7a3c72c5f4.tar.xz slosh-965966f9fb3808fbc69e6cb55960ad7a3c72c5f4.zip | |
slosh: OSC 8 hyperlinks pass through the compositor
lib-vt tracks a hyperlink per cell and slosh dropped every one of them: the
client's terminal can only offer a link it was sent, so `ls --hyperlink`,
gcc diagnostics and friends silently lost theirs inside the session.
The link rides beside the cell -- cell_t is the shader ABI and a link is
not a colour -- as a plane of ids into an interned URI table, append-only
so an id in prev_link still means the same URI next frame (which is what
lets the diff compare ids instead of strings), and capped so a hostile pane
cannot grow it forever. URIs with control bytes are refused outright: they
are emitted verbatim inside an OSC, and a control byte in one is a way to
write escape sequences into the client's terminal. The diff wraps emitted
runs in OSC 8, closes what it opens before the frame ends, and treats a
link-only change as a change. Chrome painted over a link takes the link
with it, so a frame, modal or float across a URL is never clickable with
the pane's link; links survive scrollback with the text that carries them.
Plain-text URLs stay the outer terminal's own matcher, with the catch
documented in panes.md: under mouse capture Ghostty only offers links with
shift added to the gesture (shift+cmd hover/click), shift being its
capture escape. Wrapped URLs can never be regex-matched through a
multiplexer -- rows repaint as hard lines -- which is exactly what OSC 8
links, passing through whole, are for.
`snapshot format:"bytes"` returns the emitter's own output for a frame (a
second call is the delta): the model and the bytes are two different
things, and this codebase already paid once for asserting on one while the
other was wrong.
Diffstat (limited to '')
| -rw-r--r-- | src/pane.c | 40 |
1 files changed, 39 insertions, 1 deletions
@@ -1145,6 +1145,30 @@ static color_t to_color(const GhosttyColorRgb *c, bool ok) { return out; } +/* The hyperlink on a cell, as this screen's interned id. The raw cell only + * says *whether* there is one; the URI takes a grid-ref lookup, so the flag + * gates it and a run of cells sharing one link costs one lookup — the last + * answer is remembered for exactly as long as the flag keeps being true with + * the same URI, which is what a link is. */ +static uint16_t cell_link_id(pane_t *p, screen_t *s, uint16_t x, uint16_t y, + char *last_uri, size_t *last_len, + uint16_t *last_id) { + GhosttyGridRef ref; + if (!grid_ref_at(p, x, y, &ref)) return 0; + uint8_t uri[1024]; + size_t len = 0; + if (ghostty_grid_ref_hyperlink_uri(&ref, uri, sizeof uri, &len) != + GHOSTTY_SUCCESS || + !len) + return 0; + if (len == *last_len && memcmp(uri, last_uri, len) == 0) return *last_id; + uint16_t id = screen_link_id(s, (const char *)uri, len); + memcpy(last_uri, uri, len); + *last_len = len; + *last_id = id; + return id; +} + void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0, bool focused) { if (ghostty_render_state_update(p->rstate, p->term) != GHOSTTY_SUCCESS) @@ -1155,6 +1179,10 @@ void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0, &p->rows) != GHOSTTY_SUCCESS) return; + char last_uri[1024]; + size_t last_len = 0; + uint16_t last_id = 0; + uint16_t y = 0; while (ghostty_render_state_row_iterator_next(p->rows)) { if (y >= p->rows_n) break; @@ -1179,18 +1207,28 @@ void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0, GhosttyCell raw = {0}; GhosttyCellWide wide = GHOSTTY_CELL_WIDE_NARROW; + bool linked = false; if (ghostty_render_state_row_cells_get( p->cells, GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW, &raw) == - GHOSTTY_SUCCESS) + GHOSTTY_SUCCESS) { ghostty_cell_get(raw, GHOSTTY_CELL_DATA_WIDE, &wide); + ghostty_cell_get(raw, GHOSTTY_CELL_DATA_HAS_HYPERLINK, &linked); + } if (wide == GHOSTTY_CELL_WIDE_SPACER_TAIL) { dst->width = 0; dst->len = 0; + screen_set_link(s, (uint16_t)(x0 + x), (uint16_t)(y0 + y), 0); x++; continue; } + /* OSC 8 hyperlinks ride beside the cell, or they would be lost: the + * client's terminal can only offer a link it was sent. */ + screen_set_link( + s, (uint16_t)(x0 + x), (uint16_t)(y0 + y), + linked ? cell_link_id(p, s, x, y, last_uri, &last_len, &last_id) : 0); + char utf8[16] = {0}; GhosttyBuffer gb = {.ptr = (uint8_t *)utf8, .cap = sizeof utf8, .len = 0}; if (ghostty_render_state_row_cells_get( |
