aboutsummaryrefslogtreecommitdiffstats
path: root/src/server.c
diff options
context:
space:
mode:
authorsl0p <sl0p@sl0p.foo>2026-08-14 00:04:03 +0200
committersl0p <sl0p@sl0p.foo>2026-08-14 00:04:03 +0200
commit1532581aa3765512b6629aed94f7f529a53f92b5 (patch)
tree9b0c62922c53c3399b461be6f92b5c85cf52c8ea /src/server.c
parentsl0ptty: scrollback — wheel, keyboard, and a clickable indicator (diff)
downloadslosh-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.c49
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();
}