aboutsummaryrefslogtreecommitdiffstats
path: root/src/pane.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-20 23:23:07 +0200
committeruser <user@clank>2026-08-20 23:23:07 +0200
commit965966f9fb3808fbc69e6cb55960ad7a3c72c5f4 (patch)
tree388d17a48155b9fae25c65fdc6c8765b6bb3ec01 /src/pane.c
parentslosh: floating panes and the drop grammar (diff)
downloadslosh-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.c40
1 files changed, 39 insertions, 1 deletions
diff --git a/src/pane.c b/src/pane.c
index 00b1e59..5cf0bab 100644
--- a/src/pane.c
+++ b/src/pane.c
@@ -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(