diff options
| author | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:52:33 +0200 |
|---|---|---|
| committer | sl0p <sl0p@sl0p.foo> | 2026-08-13 22:52:33 +0200 |
| commit | 7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7 (patch) | |
| tree | 93f11a476bcdb6fe50f2f3e28fa865b86d3d06c3 /src/cmd.c | |
| parent | sl0ptty: M1 — layout tree, splits, focus, frames, and the hit list in use (diff) | |
| download | slosh-7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7.tar.gz slosh-7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7.tar.xz slosh-7a6fa40a9c49245cf569e10e6ed635f9a6cbbcb7.zip | |
sl0ptty: M2 — server/client split, detach/reattach, control socket
The session now outlives its client, which is the point: agents keep running
while you are gone. The server owns the panes, the layout and the renderer;
the client is a terminal in raw mode and a socket, forwarding bytes and
painting what it is sent. It does not even decode input, so the decoder can
change without redeploying clients.
sl0ptty attach to 'main', creating it if nobody is home
sl0ptty -s NAME a named session
sl0ptty ls live sessions
sl0ptty cmd LINE one control command
C-a d detach
Connections are not clients. A connection becomes the display client only by
sending HELLO; control commands and liveness probes are just connections and
never displace anybody. This was found the hard way, one hour after the server
first ran: 'sl0ptty ls' kicked the attached client off, because ls connects to
see whether a socket answers. That is precisely the fault that gave the zellij
fork a 20-25% CLI failure rate, rebuilt and then fixed structurally. The
regression runs 20 x (ls + control) against a live client and demands 0
failures and a client that can still type.
cmd.c now holds the command vocabulary once, shared by the headless driver and
the control socket, so a script written against one works against the other —
'sl0ptty cmd "snapshot text"' against a detached session prints its screen.
tests/test_session.py: 18 checks over create, split, probe-resistance, detach,
survival, drive-while-detached, reattach, displacement and cleanup.
123 checks green overall.
Diffstat (limited to 'src/cmd.c')
| -rw-r--r-- | src/cmd.c | 91 |
1 files changed, 91 insertions, 0 deletions
diff --git a/src/cmd.c b/src/cmd.c new file mode 100644 index 0000000..bfd1f08 --- /dev/null +++ b/src/cmd.c @@ -0,0 +1,91 @@ +/* One implementation of the command vocabulary, used by both the headless + * driver and the control socket. `settle` is the exception: it needs the + * caller's event loop, so the driver keeps it. */ +#define _GNU_SOURCE +#include "cmd.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* \e \n \r \t \\ \0 \xHH -> bytes */ +size_t cmd_unescape(const char *in, uint8_t *out, size_t cap) { + size_t n = 0; + for (const char *p = in; *p && n < cap; p++) { + if (*p != '\\') { + out[n++] = (uint8_t)*p; + continue; + } + p++; + switch (*p) { + case 'e': out[n++] = 0x1b; break; + case 'n': out[n++] = '\n'; break; + case 'r': out[n++] = '\r'; break; + case 't': out[n++] = '\t'; break; + case '0': out[n++] = 0; break; + case '\\': out[n++] = '\\'; break; + case 'x': { + int hi = 0, lo = 0; + sscanf(p + 1, "%1x%1x", &hi, &lo); + out[n++] = (uint8_t)(hi * 16 + lo); + p += 2; + break; + } + case 0: return n; + default: out[n++] = (uint8_t)*p; break; + } + } + return n; +} + +static void feed_event(const input_event_t *ev, void *ud) { + app_event((app_t *)ud, ev); +} + +char *cmd_exec(app_t *a, screen_t *s, input_parser_t *in, const char *line, + bool *quit) { + char verb[32] = {0}; + const char *arg = ""; + const char *sp = strchr(line, ' '); + size_t vlen = sp ? (size_t)(sp - line) : strlen(line); + if (vlen >= sizeof verb) vlen = sizeof verb - 1; + memcpy(verb, line, vlen); + if (sp) arg = sp + 1; + + if (strcmp(verb, "send") == 0) { + uint8_t buf[4096]; + size_t n = cmd_unescape(arg, buf, sizeof buf); + input_feed(in, buf, n, feed_event, a); + if (input_pending(in)) input_timeout(in, feed_event, a); + return strdup(""); + } + if (strcmp(verb, "raw") == 0) { + uint8_t buf[4096]; + size_t n = cmd_unescape(arg, buf, sizeof buf); + app_write_focused(a, buf, n); + return strdup(""); + } + if (strcmp(verb, "resize") == 0) { + int c = s->cols, r = s->rows; + sscanf(arg, "%d %d", &c, &r); + screen_resize(s, (uint16_t)c, (uint16_t)r); + app_resize(a, (uint16_t)c, (uint16_t)r); + return strdup(""); + } + if (strcmp(verb, "snapshot") == 0) { + app_compose(a, s); + return strcmp(arg, "text") == 0 ? screen_dump(s) : screen_dump_json(s); + } + if (strcmp(verb, "panes") == 0) { + app_compose(a, s); /* layout is a function of the frame: compose first */ + return app_panes_json(a); + } + if (strcmp(verb, "alive") == 0) { + return strdup(app_should_quit(a) ? "false" : "true"); + } + if (strcmp(verb, "quit") == 0) { + if (quit) *quit = true; + return strdup(""); + } + return NULL; +} |
