aboutsummaryrefslogtreecommitdiffstats
path: root/src/screen.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/screen.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/screen.c')
-rw-r--r--src/screen.c139
1 files changed, 139 insertions, 0 deletions
diff --git a/src/screen.c b/src/screen.c
index 73d2fb0..9d31640 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -9,6 +9,35 @@
#include <string.h>
#include <unistd.h>
+#include "json.h"
+
+/* ---- hit list ----------------------------------------------------------- */
+
+void hit_reset(hitlist_t *hl) { hl->len = 0; }
+
+void hit_add(hitlist_t *hl, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
+ const char *action) {
+ if (hl->len == hl->cap) {
+ hl->cap = hl->cap ? hl->cap * 2 : 16;
+ hl->items = realloc(hl->items, hl->cap * sizeof *hl->items);
+ }
+ hit_t *e = &hl->items[hl->len++];
+ e->x = x;
+ e->y = y;
+ e->w = w;
+ e->h = h;
+ snprintf(e->action, sizeof e->action, "%s", action);
+}
+
+const char *hit_test(const hitlist_t *hl, uint16_t x, uint16_t y) {
+ for (size_t i = hl->len; i-- > 0;) {
+ const hit_t *e = &hl->items[i];
+ if (x >= e->x && x < e->x + e->w && y >= e->y && y < e->y + e->h)
+ return e->action;
+ }
+ return NULL;
+}
+
static void out_reserve(screen_t *s, size_t n) {
if (s->out_len + n <= s->out_cap) return;
size_t cap = s->out_cap ? s->out_cap : 8192;
@@ -57,6 +86,7 @@ void screen_free(screen_t *s) {
free(s->cur);
free(s->prev);
free(s->out);
+ free(s->hits.items);
memset(s, 0, sizeof *s);
}
@@ -185,6 +215,115 @@ void screen_flush(screen_t *s, int fd) {
}
}
+/* Style runs rather than per-cell objects: a test wants to say "columns 0..5 of
+ * row 0 are bold red", and 1920 cell objects per snapshot is unreadable. */
+static void dump_style_runs(screen_t *s, json_t *j) {
+ json_arr_open(j, "styles");
+ for (uint16_t y = 0; y < s->rows; y++) {
+ uint16_t x = 0;
+ while (x < s->cols) {
+ cell_t *c = &s->cur[(size_t)y * s->cols + x];
+ bool plain = !c->attrs && !c->fg.set && !c->bg.set;
+ if (plain) {
+ x++;
+ continue;
+ }
+ uint16_t start = x;
+ while (x < s->cols) {
+ cell_t *n = &s->cur[(size_t)y * s->cols + x];
+ if (n->attrs != c->attrs || !color_eq(n->fg, c->fg) ||
+ !color_eq(n->bg, c->bg))
+ break;
+ x++;
+ }
+ json_obj_open(j, NULL);
+ json_int(j, "x", start);
+ json_int(j, "y", y);
+ json_int(j, "w", x - start);
+ char col[8];
+ if (c->fg.set) {
+ snprintf(col, sizeof col, "#%02x%02x%02x", c->fg.r, c->fg.g, c->fg.b);
+ json_str(j, "fg", col, strlen(col));
+ } else {
+ json_null(j, "fg");
+ }
+ if (c->bg.set) {
+ snprintf(col, sizeof col, "#%02x%02x%02x", c->bg.r, c->bg.g, c->bg.b);
+ json_str(j, "bg", col, strlen(col));
+ } else {
+ json_null(j, "bg");
+ }
+ json_arr_open(j, "attrs");
+ static const struct {
+ uint16_t bit;
+ const char *name;
+ } names[] = {
+ {ATTR_BOLD, "bold"}, {ATTR_DIM, "dim"},
+ {ATTR_ITALIC, "italic"}, {ATTR_UNDERLINE, "underline"},
+ {ATTR_BLINK, "blink"}, {ATTR_INVERSE, "inverse"},
+ {ATTR_INVISIBLE, "invisible"}, {ATTR_STRIKE, "strike"},
+ };
+ for (size_t i = 0; i < sizeof names / sizeof *names; i++)
+ if (c->attrs & names[i].bit)
+ json_str(j, NULL, names[i].name, strlen(names[i].name));
+ json_arr_close(j);
+ json_obj_close(j);
+ }
+ }
+ json_arr_close(j);
+}
+
+char *screen_dump_json(screen_t *s) {
+ json_t j;
+ json_init(&j);
+ json_obj_open(&j, NULL);
+ json_int(&j, "cols", s->cols);
+ json_int(&j, "rows", s->rows);
+
+ json_arr_open(&j, "text");
+ char *line = malloc((size_t)s->cols * 4 + 1);
+ for (uint16_t y = 0; y < s->rows; y++) {
+ size_t o = 0;
+ for (uint16_t x = 0; x < s->cols; x++) {
+ cell_t *c = &s->cur[(size_t)y * s->cols + x];
+ if (c->width == 0) continue;
+ if (c->len) {
+ memcpy(line + o, c->text, c->len);
+ o += c->len;
+ } else {
+ line[o++] = ' ';
+ }
+ }
+ json_str(&j, NULL, line, o);
+ }
+ free(line);
+ json_arr_close(&j);
+
+ dump_style_runs(s, &j);
+
+ json_obj_open(&j, "cursor");
+ json_bool(&j, "visible", s->cursor_visible);
+ json_int(&j, "x", s->cursor_x);
+ json_int(&j, "y", s->cursor_y);
+ json_obj_close(&j);
+
+ json_arr_open(&j, "hits");
+ for (size_t i = 0; i < s->hits.len; i++) {
+ hit_t *e = &s->hits.items[i];
+ json_obj_open(&j, NULL);
+ json_int(&j, "x", e->x);
+ json_int(&j, "y", e->y);
+ json_int(&j, "w", e->w);
+ json_int(&j, "h", e->h);
+ json_str(&j, "action", e->action, strlen(e->action));
+ json_obj_close(&j);
+ }
+ json_arr_close(&j);
+
+ json_obj_close(&j);
+ return j.buf; /* caller frees */
+}
+
char *screen_dump(screen_t *s) {
size_t cap = (size_t)(s->cols + 1) * s->rows * 4 + 1;
char *buf = malloc(cap);