aboutsummaryrefslogtreecommitdiffstats
path: root/src/headless.c
diff options
context:
space:
mode:
authorsl0p <sl0p@sl0p.foo>2026-08-13 22:26:02 +0200
committersl0p <sl0p@sl0p.foo>2026-08-13 22:26:02 +0200
commit34fcd16e59e3087e9bf84ebce4e05d439eea9198 (patch)
treee07b9dfc235e3a0b67047d59a9c521c6e98a28ae /src/headless.c
parentsl0ptty: input decoder + per-pane re-encode (diff)
downloadslosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.tar.gz
slosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.tar.xz
slosh-34fcd16e59e3087e9bf84ebce4e05d439eea9198.zip
sl0ptty: M0.5 — the headless driver and the harness everything rests on
--script runs the whole compositor with no tty: commands in (send, raw, settle, resize, snapshot, alive), composited screen out as JSON — rows of text, style runs, cursor, and the hit list. Styles are emitted as runs rather than per-cell objects, because a test wants 'columns 0..5 of row 0 are bold red', not 1920 objects. The hit list is empty until M1 paints something clickable, but the type, the JSON, and hit_test() exist now: one geometry, established before there is anything to get wrong. Also fixes two host-policy defaults found by the cursor never appearing. lib-vt is a VT library, not a terminal, so it starts with DECTCEM (mode 25) off and grapheme clustering (2027) off. Both are now set via MODE_DEFAULT, so they survive RIS. tests/harness.py + tests/test_screen.py: 19 checks over composition, styling, wide chars/clusters, scrollback, resize and the full input round trip — including the kitty/legacy re-encoding matrix, which no longer needs a pty. make test is now 2.4s for 59 checks; make test-live keeps the four that genuinely need a tty.
Diffstat (limited to 'src/headless.c')
-rw-r--r--src/headless.c164
1 files changed, 164 insertions, 0 deletions
diff --git a/src/headless.c b/src/headless.c
new file mode 100644
index 0000000..3da8ee7
--- /dev/null
+++ b/src/headless.c
@@ -0,0 +1,164 @@
+/* The headless driver (DESIGN.md, "headless from day one").
+ *
+ * The whole compositor with no tty: commands in on stdin, screen snapshots out
+ * on stdout. Tests drive this instead of screen-scraping a pty, so they are
+ * deterministic and take milliseconds.
+ *
+ * The command set is deliberately the vocabulary the M2 control socket will
+ * speak, so the harness moves over unchanged.
+ *
+ * send <escaped> bytes as if typed at the outer terminal (decoded first)
+ * raw <escaped> bytes straight to the pane's pty, decoder bypassed
+ * settle [ms] pump the pane until it has been quiet for ms
+ * resize <c> <r>
+ * snapshot [json|text]
+ * quit
+ *
+ * <escaped> understands \e \n \r \t \\ \xHH, so a test can write \e[1;5A.
+ */
+#define _GNU_SOURCE
+#include "sl0ptty.h"
+
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+typedef struct {
+ pane_t *pane;
+} hl_dispatch_t;
+
+static void hl_event(const input_event_t *ev, void *ud) {
+ hl_dispatch_t *d = ud;
+ switch (ev->kind) {
+ case EV_KEY: pane_send_key(d->pane, ev); break;
+ case EV_MOUSE: pane_send_mouse(d->pane, ev); break;
+ case EV_PASTE: pane_send_paste(d->pane, ev->paste, ev->paste_len); break;
+ default: break;
+ }
+}
+
+static int64_t ms_now(void) {
+ struct timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+}
+
+/* \e \n \r \t \\ \xHH -> bytes */
+static size_t 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;
+}
+
+/* Pump the pane until it has produced nothing for `quiet` ms. */
+static void settle(pane_t *p, int quiet) {
+ int64_t last = ms_now();
+ int64_t deadline = last + 3000;
+ while (pane_alive(p) && ms_now() < deadline) {
+ struct pollfd pfd = {.fd = pane_fd(p), .events = POLLIN};
+ int n = poll(&pfd, 1, 10);
+ if (n > 0) {
+ if (pane_pump(p) > 0) last = ms_now();
+ if (!pane_alive(p)) break;
+ }
+ if (ms_now() - last >= quiet) break;
+ }
+}
+
+int run_headless(const char *const argv[], uint16_t cols, uint16_t rows,
+ int idle_ms, bool script) {
+ pane_t *p = pane_new(argv, cols, rows, NULL);
+ if (!p) {
+ fprintf(stderr, "sl0ptty: cannot spawn pane\n");
+ return 1;
+ }
+ screen_t s;
+ screen_init(&s, cols, rows);
+ input_parser_t *in = input_new();
+ hl_dispatch_t d = {.pane = p};
+
+ /* one-shot: run it, let it settle, print the screen */
+ if (!script) {
+ settle(p, idle_ms);
+ pane_compose(p, &s, 0, 0, true);
+ char *dump = screen_dump(&s);
+ fputs(dump, stdout);
+ free(dump);
+ goto done;
+ }
+
+ char line[4096];
+ while (fgets(line, sizeof line, stdin)) {
+ line[strcspn(line, "\r\n")] = 0;
+ char *sp = strchr(line, ' ');
+ const char *arg = sp ? sp + 1 : "";
+ if (sp) *sp = 0;
+
+ if (strcmp(line, "send") == 0) {
+ uint8_t buf[4096];
+ size_t n = unescape(arg, buf, sizeof buf);
+ input_feed(in, buf, n, hl_event, &d);
+ if (input_pending(in)) input_timeout(in, hl_event, &d);
+ } else if (strcmp(line, "raw") == 0) {
+ uint8_t buf[4096];
+ size_t n = unescape(arg, buf, sizeof buf);
+ pane_write(p, buf, n);
+ } else if (strcmp(line, "settle") == 0) {
+ settle(p, *arg ? atoi(arg) : idle_ms);
+ } else if (strcmp(line, "resize") == 0) {
+ int c = cols, r = rows;
+ sscanf(arg, "%d %d", &c, &r);
+ cols = (uint16_t)c;
+ rows = (uint16_t)r;
+ screen_resize(&s, cols, rows);
+ pane_resize(p, cols, rows);
+ } else if (strcmp(line, "snapshot") == 0) {
+ pane_compose(p, &s, 0, 0, true);
+ char *dump = strcmp(arg, "text") == 0 ? screen_dump(&s)
+ : screen_dump_json(&s);
+ fputs(dump, stdout);
+ fputs("\n", stdout);
+ free(dump);
+ fflush(stdout);
+ } else if (strcmp(line, "alive") == 0) {
+ printf("%s\n", pane_alive(p) ? "true" : "false");
+ fflush(stdout);
+ } else if (strcmp(line, "quit") == 0) {
+ break;
+ } else if (line[0]) {
+ fprintf(stderr, "sl0ptty: unknown command: %s\n", line);
+ }
+ }
+
+done:
+ input_free(in);
+ screen_free(&s);
+ pane_free(p);
+ return 0;
+}