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/server.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 'src/server.c')
| -rw-r--r-- | src/server.c | 49 |
1 files changed, 49 insertions, 0 deletions
diff --git a/src/server.c b/src/server.c index 55696d1..94f55ed 100644 --- a/src/server.c +++ b/src/server.c @@ -98,6 +98,46 @@ static int listen_socket(const char *path) { return fd; } +/* base64, for OSC 52. */ +static char *b64(const char *in, size_t len, size_t *out_len) { + static const char T[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t n = ((len + 2) / 3) * 4; + char *out = malloc(n + 1); + size_t o = 0; + for (size_t i = 0; i < len; i += 3) { + unsigned v = (unsigned char)in[i] << 16; + if (i + 1 < len) v |= (unsigned char)in[i + 1] << 8; + if (i + 2 < len) v |= (unsigned char)in[i + 2]; + out[o++] = T[(v >> 18) & 63]; + out[o++] = T[(v >> 12) & 63]; + out[o++] = i + 1 < len ? T[(v >> 6) & 63] : '='; + out[o++] = i + 2 < len ? T[v & 63] : '='; + } + out[o] = 0; + *out_len = o; + return out; +} + +/* The clipboard lives on the client's machine, not ours, so a copy travels as + * OSC 52 for the outer terminal to honour. */ +static void push_clipboard(server_t *s) { + char *text = app_take_clipboard(s->app); + if (!text) return; + conn_t *c = display_conn(s); + if (c) { + size_t b64len = 0; + char *enc = b64(text, strlen(text), &b64len); + size_t n = b64len + 32; + char *msg = malloc(n); + int len = snprintf(msg, n, "\x1b]52;c;%s\x1b\\", enc); + if (len > 0) msg_send(c->fd, MSG_OUTPUT, msg, (size_t)len); + free(msg); + free(enc); + } + free(text); +} + /* Send whatever the last compose produced to the display client. */ static void push_frame(server_t *s) { conn_t *c = display_conn(s); @@ -148,6 +188,8 @@ int server_run(const char *name, const char *const argv[], uint16_t cols, screen_init(&s.screen, cols, rows); s.in = input_new(); + app_compose(s.app, &s.screen); /* a click resolves against a painted frame */ + bool pending_paint = true; int64_t next_frame = now_ms(); int64_t esc_due = 0; @@ -175,6 +217,12 @@ int server_run(const char *name, const char *const argv[], uint16_t cols, int t = due <= 0 ? 0 : (int)due; if (timeout < 0 || t < timeout) timeout = t; } + /* a toast expires on its own, so the loop has to wake for it */ + int toast_due = app_next_deadline_ms(s.app); + if (toast_due >= 0 && (timeout < 0 || toast_due < timeout)) { + timeout = toast_due; + pending_paint = true; + } int r = poll(pfds, (nfds_t)n, timeout); if (r < 0 && errno != EINTR) break; @@ -288,6 +336,7 @@ int server_run(const char *name, const char *const argv[], uint16_t cols, if (pending_paint && now_ms() >= next_frame) { app_compose(s.app, &s.screen); push_frame(&s); + push_clipboard(&s); pending_paint = false; next_frame = now_ms(); } |
