diff options
| author | sl0p <sl0p@sl0p.foo> | 2026-08-14 00:04:03 +0200 |
|---|---|---|
| committer | sl0p <sl0p@sl0p.foo> | 2026-08-14 00:04:03 +0200 |
| commit | 1532581aa3765512b6629aed94f7f529a53f92b5 (patch) | |
| tree | 9b0c62922c53c3399b461be6f92b5c85cf52c8ea /src/pane.c | |
| parent | sl0ptty: scrollback — wheel, keyboard, and a clickable indicator (diff) | |
| download | slosh-1532581aa3765512b6629aed94f7f529a53f92b5.tar.gz slosh-1532581aa3765512b6629aed94f7f529a53f92b5.tar.xz slosh-1532581aa3765512b6629aed94f7f529a53f92b5.zip | |
sl0ptty: select-to-copy, middle-click paste, and toasts
Drag over text, release, and it is on your clipboard — no chord, no menu, the
selection is the copy. The clipboard lives on the client's machine, so the
copy travels as OSC 52 and therefore works over ssh; the session keeps its own
copy as the primary selection, which is what middle click pastes back.
Selected cells are inverted rather than recoloured, so a selection reads the
same over whatever theme the program inside is using. A program that is
tracking the mouse keeps its own clicks (shift is the escape hatch), which is
tested rather than assumed.
Toasts: transient announcements in the bottom-right that expire on their own,
stacked up to three. 'copied 13 chars', 'config reloaded', a pane's OSC 9
notification, or {"cmd":"notify","text":"..."} from an orchestrator — an agent
in a pane can now say 'build finished' and have it appear. OSC 52 from a pane
reaches the same clipboard.
Two real bugs found on the way:
- mouse events are hit-tested against what was painted, and nothing had been
painted before the first frame, so a click that arrived first was dropped.
Both front ends now compose once before accepting input, which is what
really happens in a session anyway.
- the plain leaf_new never installed the clipboard and notification
handlers; only panes built from a layout file had them. Every ordinary
pane was silently missing both.
The test escaping traps continue to earn their comments: a real newline inside
a driver argument terminates the *command* rather than reaching the shell, \a
is not a driver escape (\x07 is), and an interactive shell's line editor eats
a typed ESC — so a pane driven with escape sequences must not be one.
356 checks green (329 headless + 27 live).
Diffstat (limited to '')
| -rw-r--r-- | src/pane.c | 121 |
1 files changed, 121 insertions, 0 deletions
@@ -37,12 +37,20 @@ struct pane { char *cwd; char label[128]; + /* Selection anchor, in viewport coordinates, while a drag is in progress. */ + bool selecting; + uint16_t sel_x, sel_y; + osc_scan_t scan; char status[256]; pane_button_t buttons[8]; size_t nbuttons; pane_osc_fn osc_cb; void *osc_ud; + pane_clip_fn clip_cb; + void *clip_ud; + pane_notify_fn notify_cb; + void *notify_ud; }; void pane_set_osc_handler(pane_t *p, pane_osc_fn fn, void *ud) { @@ -128,6 +136,45 @@ static void on_write_pty(GhosttyTerminal t, void *ud, const uint8_t *data, } } +/* A program in the pane wrote the clipboard (OSC 52 / OSC 1337). lib-vt has + * already normalised the base64, the chunking and the selectors. */ +static GhosttyClipboardWriteResult on_clipboard_write( + GhosttyTerminal t, void *ud, const GhosttyClipboardWrite *write) { + pane_t *p = ud; + if (!p->clip_cb || !write || !write->contents_len) + return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS; + const GhosttyString *data = &write->contents[0].data; + if (!data->len) return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS; + char *copy = malloc(data->len + 1); + memcpy(copy, data->ptr, data->len); + copy[data->len] = 0; + p->clip_cb(p, copy, p->clip_ud); /* the handler takes ownership */ + return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS; +} + +void pane_set_clipboard_handler(pane_t *p, pane_clip_fn fn, void *ud) { + p->clip_cb = fn; + p->clip_ud = ud; +} + +void pane_set_notify_handler(pane_t *p, pane_notify_fn fn, void *ud) { + p->notify_cb = fn; + p->notify_ud = ud; +} + +/* OSC 9 / OSC 777: a program in the pane wants to say something. */ +static void on_notify(GhosttyTerminal t, void *ud, + const GhosttyTerminalDesktopNotification *n) { + pane_t *p = ud; + if (!p->notify_cb || !n) return; + char title[96] = {0}, body[96] = {0}; + if (n->title.len) + snprintf(title, sizeof title, "%.*s", (int)n->title.len, (const char *)n->title.ptr); + if (n->body.len) + snprintf(body, sizeof body, "%.*s", (int)n->body.len, (const char *)n->body.ptr); + p->notify_cb(p, title, body, p->notify_ud); +} + static void on_title_changed(GhosttyTerminal t, void *ud) { pane_t *p = ud; GhosttyString s = {0}; @@ -213,6 +260,10 @@ pane_t *pane_new(const char *const argv[], uint16_t cols, uint16_t rows, (const void *)(uintptr_t)on_write_pty); ghostty_terminal_set(p->term, GHOSTTY_TERMINAL_OPT_TITLE_CHANGED, (const void *)(uintptr_t)on_title_changed); + ghostty_terminal_set(p->term, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE, + (const void *)(uintptr_t)on_clipboard_write); + ghostty_terminal_set(p->term, GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION, + (const void *)(uintptr_t)on_notify); /* lib-vt makes no assumptions a host terminal would: it starts with DECTCEM * (mode 25) off, so the cursor is invisible until someone says otherwise. @@ -373,6 +424,65 @@ void pane_send_paste(pane_t *p, const char *text, size_t len) { free(buf); } +/* ---- selection ---------------------------------------------------------- * + * + * A selection is two grid references and a flag, so it can be built directly + * from two viewport positions — no gesture machinery, and the render state + * reports the per-row range for us to highlight. + */ + +static bool grid_ref_at(pane_t *p, uint16_t x, uint16_t y, GhosttyGridRef *out) { + GhosttyPoint pt = {.tag = GHOSTTY_POINT_TAG_VIEWPORT, + .value.coordinate = {.x = x, .y = y}}; + return ghostty_terminal_grid_ref(p->term, pt, out) == GHOSTTY_SUCCESS; +} + +void pane_select_start(pane_t *p, uint16_t x, uint16_t y) { + p->selecting = true; + p->sel_x = x; + p->sel_y = y; + pane_select_clear(p); +} + +void pane_select_extend(pane_t *p, uint16_t x, uint16_t y) { + if (!p->selecting) return; + GhosttyGridRef a, b; + if (!grid_ref_at(p, p->sel_x, p->sel_y, &a) || !grid_ref_at(p, x, y, &b)) + return; + GhosttySelection sel = GHOSTTY_INIT_SIZED(GhosttySelection); + sel.start = a; + sel.end = b; + sel.rectangle = false; + ghostty_terminal_set(p->term, GHOSTTY_TERMINAL_OPT_SELECTION, &sel); + p->dirty = true; +} + +void pane_select_clear(pane_t *p) { + ghostty_terminal_set(p->term, GHOSTTY_TERMINAL_OPT_SELECTION, NULL); + p->dirty = true; +} + +bool pane_selecting(const pane_t *p) { return p->selecting; } +void pane_select_done(pane_t *p) { p->selecting = false; } + +/* The selected text, or NULL. Caller frees. */ +char *pane_selection_text(pane_t *p) { + GhosttyTerminalSelectionFormatOptions opts = + GHOSTTY_INIT_SIZED(GhosttyTerminalSelectionFormatOptions); + opts.trim = true; + uint8_t *ptr = NULL; + size_t len = 0; + if (ghostty_terminal_selection_format_alloc(p->term, NULL, opts, &ptr, &len) != + GHOSTTY_SUCCESS || + !len) + return NULL; + char *out = malloc(len + 1); + memcpy(out, ptr, len); + out[len] = 0; + ghostty_free(NULL, ptr, len); + return out; +} + /* ---- scrollback --------------------------------------------------------- */ void pane_scroll(pane_t *p, int delta) { @@ -455,6 +565,12 @@ void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0, continue; } + GhosttyRenderStateRowSelection rowsel = + GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection); + bool has_sel = ghostty_render_state_row_get( + p->rows, GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION, + &rowsel) == GHOSTTY_SUCCESS; + uint16_t x = 0; while (ghostty_render_state_row_cells_next(p->cells)) { if (x >= p->cols) break; @@ -517,6 +633,11 @@ void pane_compose(pane_t *p, screen_t *s, uint16_t x0, uint16_t y0, else { dst->text[0] = ' '; n = 1; } dst->len = (uint8_t)n; dst->width = wide == GHOSTTY_CELL_WIDE_WIDE ? 2 : 1; + /* Selected cells are inverted rather than recoloured, so a selection + * reads the same over any theme the program inside is using. */ + if (has_sel && x >= rowsel.start_x && x <= rowsel.end_x) + attrs ^= ATTR_INVERSE; + dst->attrs = attrs; dst->fg = to_color(&fg, have_fg); dst->bg = to_color(&bg, have_bg); |
