M1: sigil-vt native VT core + conformance suite
New extracted package sigil-vt: a native C VT/ANSI terminal emulator reimplementing slate's (slate term) semantics cell-for-cell.
- native/vt.c: int32[4]-per-cell grid; Paul Williams VT state machine (C0/CSI/SGR/OSC/DCS); damage tracking; scrollback ring; DSR/DA replies; out-of-band event queue (title/bell/OSC-52 clipboard); mouse-mode flags (DECSET 1000/1002/1003/1006/1007); incremental UTF-8 with validation; ground-state bulk-run fast path; native style-merged row-run extraction (the render seam). Emulator is a SIGILOBJFOREIGN handle with a GC finalizer (no monorepo changes). No vt-diff (deferred to M3). - src/sigil/vt.sgl: the (sigil vt) Scheme surface. - test/vt-test.sgl: 95-check conformance suite ported from slate's term-test.sgl (the spec) — all green, clean under the dev build's UBSan. - package.sgl + dev-redirects.sgl: native-init wiring; builds against the local monorepo checkout.
Trust boundary hardened: fixed-capacity param/OSC accumulators, int64 param accumulation (no overflow), input never sizes allocation.
.gitignore | 2 +
README.md | 53 ++++++
dev-redirects.sgl | 7 +
native/vt.c | 1654 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.sgl | 55 ++++++
src/sigil/vt.sgl | 146 ++++++++++++++++
test/vt-test.sgl | 433 ++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 2350 insertions(+).gitignoreadded
build/.sigil/README.mdadded
# sigil-vtThe native VT/ANSI terminal core for the Sigil ecosystem — a VT100/xterm-subsetterminal emulator in C: an incremental byte stream → a damage-tracked`int32[4]`-per-cell grid, with scrollback, cursor-report / device-attributereplies, an out-of-band event queue (title / bell / OSC-52 clipboard), mouse-modeflags, and native style-merged **row-run** extraction (the render seam).It is the shared native foundation for terminal handling across the ecosystem:slate's `(slate term)` rides it as a thin façade (byte-identical `take-frame`),and `(sigil tui grid)` will ride it in M3. See the design note`topics/sigil-terminal-handling-design`.This is the native reimplementation of slate's `(slate term)` emulator — **theSigil emulator is the spec**; `native/vt.c` reproduces its semantics cell-for-cell,proven by `test/vt-test.sgl` (a faithful port of slate's `test/term-test.sgl`).## Layout- `native/vt.c` — the C core (parser state machine + grid + damage/scrollback/ replies/events + row-runs). Registered as `%vt-*` builtins in module `(sigil vt)`.- `src/sigil/vt.sgl` — the `(sigil vt)` Scheme surface (thin wrappers + the public API, shaping damage into a dict etc.).- `test/vt-test.sgl` — the conformance suite.## Cell modelFlat `int32_t` buffer, cell `(row,col)` at `((row*cols+col)*4)`:`[codepoint, attrs, fg, bg]`. Attr bits: bold 1, dim 2, italic 4, underline 8,blink 16, inverse 32, hidden 64, strike 128; high bits reserved forwide/continuation (wcwidth deferred). Colors: `-1` default, `0..255` indexed,`#x1000000 + #xRRGGBB` truecolor.## Trust boundaryTerminal bytes are UNTRUSTED program output. The parser converts arbitrary bytesinto a CLOSED vocabulary of grid operations — no escape sequence executesanything, reaches an eval, or emits markup. Params/sub-params/OSC accumulatorsare fixed-capacity; no allocation is sized by input params. The core never callsback into Scheme. Fuzzed + ASan/UBSan-clean on `vt-feed-bytes!` (see `spike/`).## Build / test (development, against a local monorepo checkout)```sigil deps install --redirects dev-redirects.sglsigil build --redirects dev-redirects.sglsigil test --redirects dev-redirects.sgl```## Not hereNo `vt-diff` (the grid-diff ANSI emitter) — deferred to M3 (the sigil-tuirevival). The cell repr is designed to account for it.dev-redirects.sgladded
;; Development redirects — point dependencies at the local Sigil monorepo;; checkout. Use: sigil build --redirects ./dev-redirects.sgl(redirects repos: (list (for-repo url: "codeberg:sigil/sigil" use: (from-path dir: "../sigil"))))native/vt.cadded
/* * sigil-vt — the native VT/ANSI terminal core. * * A VT100/xterm-subset terminal emulator: an incremental byte stream -> a * damage-tracked int32[4]-per-cell grid. This is the native reimplementation * of slate's (slate term) emulator (src/slate/term.sgl) — the Sigil emulator * IS the spec; this C port reproduces its semantics cell-for-cell (proven by * the ported conformance suite, test/vt-test.sgl). * * THE TRUST BOUNDARY. Terminal bytes are UNTRUSTED program output. This parser * converts arbitrary bytes into a CLOSED vocabulary of grid operations. No * escape sequence executes anything, reaches an eval, or emits markup. Params * are clamped, param/sub-param/OSC accumulators are fixed-capacity, and no * allocation is sized by input params. The core never calls back into Scheme * (all APIs are call-in / return-out), so the fiber-suspension constraint is * structurally satisfied. Fuzzed + ASan/UBSan clean on vt-feed-bytes! before * the slate cutover (the M2 gate). * * Cell model: flat int32_t buffer, cell (row,col) at ((row*cols+col)*4): * [0] codepoint [1] attrs [2] fg [3] bg * attrs bits: bold 1, dim 2, italic 4, underline 8, blink 16, inverse 32, * hidden 64, strike 128; high bits reserved for wide/continuation (wcwidth * deferred to M3/M4 — the model carries a home for it from day 1). * colors: -1 default, 0..255 indexed, 0x1000000 + 0xRRGGBB truecolor. */#include <sigil/sigil.h>#include <stdint.h>#include <stdlib.h>#include <string.h>#include <stdio.h>/* ---- internal libsigil helpers (exported from libsigil; not in the public * header). Declared extern exactly as sigil-wasm-dom does. -------------------*/extern void *sigil__gc_alloc(SigilVM *vm, SigilObjType type, size_t size);extern void sigil__gc_push_temp_root(SigilVM *vm, Value v);extern void sigil__gc_pop_temp_root(SigilVM *vm);#define SIGIL_EXPORT __attribute__((visibility("default")))/* ---- attribute bits (slate's superset wins) ------------------------------ */enum { VT_ATTR_BOLD = 1, VT_ATTR_DIM = 2, VT_ATTR_ITALIC = 4, VT_ATTR_UNDERLINE = 8, VT_ATTR_BLINK = 16, VT_ATTR_INVERSE = 32, VT_ATTR_HIDDEN = 64, VT_ATTR_STRIKE = 128, /* reserved for CJK double-width (wcwidth lands later) */ VT_ATTR_WIDE = 256, VT_ATTR_CONT = 512};/* ---- mouse-mode flags (DECSET; parsed as flags in v1) -------------------- */enum { VT_MOUSE_1000 = 1, /* X10/normal button tracking */ VT_MOUSE_1002 = 2, /* button-event (drag) tracking */ VT_MOUSE_1003 = 4, /* any-event (motion) tracking */ VT_MOUSE_1006 = 8, /* SGR extended coordinates */ VT_MOUSE_1007 = 16 /* alternate scroll */};/* ---- out-of-band event types --------------------------------------------- */enum { VT_EV_TITLE = 1, VT_EV_BELL = 2, VT_EV_CLIPBOARD = 3 };/* ---- parser states ------------------------------------------------------- */enum { ST_GROUND = 0, ST_ESC, ST_ESC_SKIP1, ST_ESC_HASH, ST_CSI, ST_OSC, ST_OSC_ESC, ST_STR_IGNORE, ST_STR_ESC};/* ---- fixed capacities (the closed-vocabulary discipline) ----------------- */#define VT_MAX_GROUPS 32#define VT_MAX_SUB 8#define VT_INTER_MAX 8#define VT_OSC_MAX 1024#define VT_PARAM_CAP 999999999typedef struct { int has; int row, col; int32_t attr, fg, bg; int origin, autowrap;} SavedCursor;typedef struct { int type; char *payload; /* malloc'd, may be NULL (bell) */ int payload_len;} VtEvent;typedef struct { int cols, rows; int32_t *grid; /* ACTIVE grid (points at main or alt) */ int32_t *main; /* main-screen buffer */ int32_t *alt; /* alt-screen buffer (NULL until first enter) */ int alt_active; int cur_row, cur_col; int wrap; /* pending (deferred) autowrap */ int32_t attr, fg, bg; /* current SGR */ int stop, sbot; /* scroll region, 0-based inclusive */ SavedCursor saved_main, saved_alt; /* parser */ int state; int32_t groups[VT_MAX_GROUPS][VT_MAX_SUB]; /* completed groups */ int group_len[VT_MAX_GROUPS]; int ngroups; int cur_digits; /* -1 = none */ int32_t cur_sub[VT_MAX_SUB]; /* completed sub-params of current group */ int cur_sub_len; int prefix; /* private marker char or 0 */ char inter[VT_INTER_MAX + 1]; int inter_len; char osc[VT_OSC_MAX]; int osc_len; /* modes */ int autowrap, origin, curvis, bracket, appcur, insert; int mouse; /* VT_MOUSE_* bitmask */ uint8_t *tabs; /* length cols */ char *title; int title_len, title_cap; int curstyle; /* DECSCUSR 0..6 */ /* scrollback ring: index 0 = newest */ int32_t **sb; /* each entry: cols*4 int32 */ int sb_head; /* index of newest */ int sb_size; int sb_cap; /* == sbmax */ uint8_t *dirty; /* length rows */ int alldirty; char *out; int out_len, out_cap; /* pending replies */ int u8need; int32_t u8acc; int32_t u8min; /* incremental UTF-8 */ VtEvent *events; int nevents, events_cap; /* out-of-band event queue */} Vt;static Value vt_type_tag = SIGIL_UNDEFINED;/* ======================================================================== *//* small utilities *//* ======================================================================== */static int clampi(int x, int lo, int hi) { return x < lo ? lo : (x > hi ? hi : x);}static void set_cell(int32_t *c, int32_t cp, int32_t attr, int32_t fg, int32_t bg) { c[0] = cp; c[1] = attr; c[2] = fg; c[3] = bg;}/* fill a fresh row buffer with blank cells (space, default, current bg) */static void fill_blank(int32_t *row, int cols, int32_t bg) { for (int i = 0; i < cols; i++) set_cell(row + i * 4, 32, 0, -1, bg);}static int32_t *alloc_grid(int cols, int rows, int32_t bg) { int32_t *g = (int32_t *)malloc((size_t)cols * rows * 4 * sizeof(int32_t)); if (!g) return NULL; for (int r = 0; r < rows; r++) fill_blank(g + (size_t)r * cols * 4, cols, bg); return g;}/* ---- damage -------------------------------------------------------------- */static void mark_row(Vt *t, int i) { if (i >= 0 && i < t->rows) t->dirty[i] = 1;}static void mark_rows(Vt *t, int from, int to) { for (int i = from; i <= to; i++) mark_row(t, i);}static void mark_all(Vt *t) { t->alldirty = 1; }/* ---- pending replies ----------------------------------------------------- */static void emit_out(Vt *t, const char *s) { int len = (int)strlen(s); if (t->out_len + len + 1 > t->out_cap) { int cap = t->out_cap ? t->out_cap * 2 : 64; while (cap < t->out_len + len + 1) cap *= 2; t->out = (char *)realloc(t->out, cap); t->out_cap = cap; } memcpy(t->out + t->out_len, s, len); t->out_len += len; t->out[t->out_len] = 0;}/* ---- events -------------------------------------------------------------- */static void push_event(Vt *t, int type, const char *payload, int len) { if (t->nevents >= t->events_cap) { int cap = t->events_cap ? t->events_cap * 2 : 8; t->events = (VtEvent *)realloc(t->events, cap * sizeof(VtEvent)); t->events_cap = cap; } VtEvent *e = &t->events[t->nevents++]; e->type = type; e->payload = NULL; e->payload_len = 0; if (payload && len >= 0) { e->payload = (char *)malloc(len + 1); memcpy(e->payload, payload, len); e->payload[len] = 0; e->payload_len = len; }}/* ---- title --------------------------------------------------------------- */static void set_title(Vt *t, const char *s, int len) { if (len + 1 > t->title_cap) { int cap = t->title_cap ? t->title_cap : 16; while (cap < len + 1) cap *= 2; t->title = (char *)realloc(t->title, cap); t->title_cap = cap; } memcpy(t->title, s, len); t->title[len] = 0; t->title_len = len;}/* ---- scrollback ring ----------------------------------------------------- *//* push a COPY of a grid row (cols*4 int32) as the new newest entry */static void sb_push(Vt *t, const int32_t *row) { if (t->sb_cap == 0) return; int cells = t->cols * 4; int32_t *copy; if (t->sb_size == t->sb_cap) { /* evict oldest: reuse its buffer if it is cols-sized, else realloc */ int oldest = (t->sb_head - t->sb_size + 1 + t->sb_cap) % t->sb_cap; copy = t->sb[oldest]; copy = (int32_t *)realloc(copy, cells * sizeof(int32_t)); t->sb[oldest] = copy; t->sb_head = (t->sb_head + 1) % t->sb_cap; /* size stays == cap */ } else { copy = (int32_t *)malloc(cells * sizeof(int32_t)); t->sb_head = (t->sb_size == 0) ? 0 : (t->sb_head + 1) % t->sb_cap; t->sb[t->sb_head] = copy; t->sb_size++; } memcpy(copy, row, cells * sizeof(int32_t));}/* newest-first index k (0 = newest); returns NULL if out of range */static int32_t *sb_get(Vt *t, int k) { if (k < 0 || k >= t->sb_size) return NULL; int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap; return t->sb[idx];}/* pop the newest entry (for resize grow); returns its buffer (caller frees) */static int32_t *sb_pop(Vt *t) { if (t->sb_size == 0) return NULL; int32_t *row = t->sb[t->sb_head]; t->sb[t->sb_head] = NULL; t->sb_head = (t->sb_head - 1 + t->sb_cap) % t->sb_cap; t->sb_size--; return row;}/* ======================================================================== *//* cursor / scrolling / printing (ported from term.sgl) *//* ======================================================================== */static void set_cursor(Vt *t, int row, int col) { t->wrap = 0; t->cur_row = clampi(row, 0, t->rows - 1); t->cur_col = clampi(col, 0, t->cols - 1);}static void set_cursor_abs(Vt *t, int row, int col) { if (t->origin) { t->wrap = 0; t->cur_row = clampi(t->stop + row, t->stop, t->sbot); t->cur_col = clampi(col, 0, t->cols - 1); } else { set_cursor(t, row, col); }}static void move_rows(Vt *t, int delta) { int row = t->cur_row; int lo = (row >= t->stop) ? t->stop : 0; int hi = (row <= t->sbot) ? t->sbot : t->rows - 1; t->wrap = 0; t->cur_row = clampi(row + delta, lo, hi);}static void move_cols(Vt *t, int delta) { t->wrap = 0; t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);}/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */static void scroll_up(Vt *t, int n, int push) { int top = t->stop, bot = t->sbot; int cols = t->cols; n = clampi(n, 0, bot - top + 1); if (n <= 0) return; if (push && top == 0 && !t->alt_active) { for (int k = 0; k < n; k++) sb_push(t, t->grid + (size_t)(top + k) * cols * 4); } for (int i = top; i <= bot - n; i++) memcpy(t->grid + (size_t)i * cols * 4, t->grid + (size_t)(i + n) * cols * 4, cols * 4 * sizeof(int32_t)); for (int i = bot - n + 1; i <= bot; i++) fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg); mark_rows(t, top, bot);}static void scroll_down(Vt *t, int n) { int top = t->stop, bot = t->sbot; int cols = t->cols; n = clampi(n, 0, bot - top + 1); if (n <= 0) return; for (int i = bot; i >= top + n; i--) memcpy(t->grid + (size_t)i * cols * 4, t->grid + (size_t)(i - n) * cols * 4, cols * 4 * sizeof(int32_t)); for (int i = top; i < top + n; i++) fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg); mark_rows(t, top, bot);}static void line_feed(Vt *t) { if (t->cur_row == t->sbot) scroll_up(t, 1, 1); else if (t->cur_row < t->rows - 1) t->cur_row += 1;}static void do_wrap(Vt *t) { t->wrap = 0; t->cur_col = 0; line_feed(t);}static void print_cp(Vt *t, int32_t cp) { if (t->wrap) do_wrap(t); int cols = t->cols, row = t->cur_row, col = t->cur_col; int32_t *r = t->grid + (size_t)row * cols * 4; if (t->insert) { for (int i = cols - 1; i > col; i--) memcpy(r + i * 4, r + (i - 1) * 4, 4 * sizeof(int32_t)); } set_cell(r + col * 4, cp, t->attr, t->fg, t->bg); mark_row(t, row); if (col < cols - 1) t->cur_col = col + 1; else if (t->autowrap) t->wrap = 1;}/* bulk-print a run of plain printable chars (ground-state fast path) */static void print_run(Vt *t, const int32_t *cps, int n) { int cols = t->cols; int i = 0; while (i < n) { if (t->wrap) do_wrap(t); int row = t->cur_row, col = t->cur_col; int32_t *r = t->grid + (size_t)row * cols * 4; int k = n - i; if (k > cols - col) k = cols - col; for (int x = 0; x < k; x++) set_cell(r + (col + x) * 4, cps[i + x], t->attr, t->fg, t->bg); mark_row(t, row); int ncol = col + k; if (ncol < cols) { t->cur_col = ncol; } else { t->cur_col = cols - 1; if (t->autowrap) t->wrap = 1; } i += k; }}/* ---- erase / insert / delete --------------------------------------------- */static void fill_row(Vt *t, int row, int from, int to) { int32_t *r = t->grid + (size_t)row * t->cols * 4; for (int i = from; i < to; i++) set_cell(r + i * 4, 32, 0, -1, t->bg); mark_row(t, row);}static void erase_rows(Vt *t, int from, int to) { for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);}static void erase_display(Vt *t, int mode) { if (mode == 0) { fill_row(t, t->cur_row, t->cur_col, t->cols); if (t->cur_row < t->rows - 1) erase_rows(t, t->cur_row + 1, t->rows - 1); } else if (mode == 1) { if (t->cur_row > 0) erase_rows(t, 0, t->cur_row - 1); fill_row(t, t->cur_row, 0, t->cur_col + 1); } else if (mode == 2) { erase_rows(t, 0, t->rows - 1); } else if (mode == 3) { /* clear scrollback: free ring entries */ for (int k = 0; k < t->sb_size; k++) { int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap; free(t->sb[idx]); t->sb[idx] = NULL; } t->sb_size = 0; t->sb_head = 0; }}static void erase_line(Vt *t, int mode) { if (mode == 0) fill_row(t, t->cur_row, t->cur_col, t->cols); else if (mode == 1) fill_row(t, t->cur_row, 0, t->cur_col + 1); else if (mode == 2) fill_row(t, t->cur_row, 0, t->cols);}static void insert_lines(Vt *t, int n) { if (t->cur_row >= t->stop && t->cur_row <= t->sbot) { int save = t->stop; t->stop = t->cur_row; scroll_down(t, n); t->stop = save; t->cur_col = 0; t->wrap = 0; }}static void delete_lines(Vt *t, int n) { if (t->cur_row >= t->stop && t->cur_row <= t->sbot) { int save = t->stop; t->stop = t->cur_row; scroll_up(t, n, 0); t->stop = save; t->cur_col = 0; t->wrap = 0; }}static void insert_chars(Vt *t, int n) { int cols = t->cols, col = t->cur_col; int32_t *r = t->grid + (size_t)t->cur_row * cols * 4; n = clampi(n, 0, cols - col); for (int i = cols - 1; i >= col + n; i--) memcpy(r + i * 4, r + (i - n) * 4, 4 * sizeof(int32_t)); for (int i = col; i < col + n; i++) set_cell(r + i * 4, 32, 0, -1, t->bg); mark_row(t, t->cur_row);}static void delete_chars(Vt *t, int n) { int cols = t->cols, col = t->cur_col; int32_t *r = t->grid + (size_t)t->cur_row * cols * 4; n = clampi(n, 0, cols - col); for (int i = col; i < cols - n; i++) memcpy(r + i * 4, r + (i + n) * 4, 4 * sizeof(int32_t)); for (int i = cols - n; i < cols; i++) set_cell(r + i * 4, 32, 0, -1, t->bg); mark_row(t, t->cur_row);}static void erase_chars(Vt *t, int n) { fill_row(t, t->cur_row, t->cur_col, clampi(t->cur_col + n, t->cur_col, t->cols));}/* ---- tabs ---------------------------------------------------------------- */static void tab_forward(Vt *t) { int cols = t->cols; for (int i = t->cur_col + 1; ; i++) { if (i >= cols) { t->cur_col = cols - 1; return; } if (t->tabs[i]) { t->cur_col = i; return; } }}static void tab_back(Vt *t) { for (int i = t->cur_col - 1; ; i--) { if (i <= 0) { t->cur_col = 0; return; } if (t->tabs[i]) { t->cur_col = i; return; } }}static void default_tabs(Vt *t) { memset(t->tabs, 0, t->cols); for (int i = 8; i < t->cols; i += 8) t->tabs[i] = 1;}/* ---- alt screen / cursor save ------------------------------------------- */static void save_cursor(Vt *t) { SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main; d->has = 1; d->row = t->cur_row; d->col = t->cur_col; d->attr = t->attr; d->fg = t->fg; d->bg = t->bg; d->origin = t->origin; d->autowrap = t->autowrap;}static void restore_cursor(Vt *t) { SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main; if (!d->has) return; t->attr = d->attr; t->fg = d->fg; t->bg = d->bg; t->origin = d->origin; t->autowrap = d->autowrap; set_cursor(t, d->row, d->col);}static void enter_alt(Vt *t) { if (t->alt_active) return; if (!t->alt) t->alt = alloc_grid(t->cols, t->rows, -1); /* alt always starts cleared */ for (int r = 0; r < t->rows; r++) fill_blank(t->alt + (size_t)r * t->cols * 4, t->cols, -1); t->alt_active = 1;Showing the first 500 of 1655 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
package.sgladded
;;; sigil-vt — the native VT/ANSI terminal core.;;;;;; A VT100/xterm-subset terminal emulator in C: an incremental byte stream ->;;; a damage-tracked int32[4]-per-cell grid, with scrollback, cursor-report /;;; device-attribute replies, an out-of-band event queue (title / bell /;;; OSC-52 clipboard), mouse-mode flags, and native style-merged ROW-RUN;;; extraction (the render seam). It is the native reimplementation of slate's;;; (slate term) emulator — the Sigil emulator is the spec; this reproduces its;;; semantics cell-for-cell (test/vt-test.sgl is the ported conformance suite).;;;;;; NO vt-diff (the grid-diff ANSI emitter) — deferred to M3 (sigil-tui revival).(package name: "sigil-vt" version: "0.1.0" sigil: "^0.17" description: "Native VT/ANSI terminal core: parser + int32[4] grid + damage/scrollback/replies/events + row-runs" url: "https://codeberg.org/sigil/sigil-vt" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>") dependencies: (list) ;; Native test harness needs sigil-test + sigil-test-runner (resolved to the ;; local monorepo via dev-redirects.sgl during development). dev-dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-test" version: "^0.17") (from-git url: "codeberg:sigil/sigil" package: "sigil-test-runner" version: "^0.17") ;; sigil-build (pulled transitively by the test-runner to build the native ;; harness) imports (sigil version) from the separate sigil-version repo. (from-git url: "codeberg:sigil/sigil-version" package: "sigil-version" version: "^0.16")) ;; Native library: the C core + its native-init hook (collected into CLI/test ;; entrypoints so %vt-* builtins register at VM startup). libraries: (list (library name: 'sigil-vt c-sources: '("native/vt.c") native-init: "sigil__init_sigil_vt_module")) tasks: (list (task name: 'build description: "Build the sigil-vt native library" steps: (list (compile-c-sources sources: '("native/vt.c") flags: (with-sigil-c-flags '("-std=c99" "-Wall" "-Wextra" "-Wno-unused-parameter" "-D_GNU_SOURCE"))) (create-static-library name: "sigil-vt") (compile-sigil-modules sources: "src/**/*.sgl")))))src/sigil/vt.sgladded
;;; (sigil vt) — the native VT/ANSI terminal core (Scheme surface).;;;;;; A thin wrapper over the %vt-* native builtins (native/vt.c). The emulator;;; is a pure bytes->grid state machine: feed pty output, drain damage /;;; replies / events, read the grid as cells or style-merged row-runs. All;;; state leaves through drained queues; the core never calls back into Scheme.;;;;;; Cells are immutable 4-vectors #(codepoint attr fg bg):;;; attr bitmask (see vt-attr-*); fg/bg: -1 default, 0..255 indexed,;;; #x1000000 + #xRRGGBB truecolor.;;;;;; This is the shared foundation slate's (slate term) façade rides on (M2);;;; the sigil-tui grid façade + vt-diff come in M3.(define-library (sigil vt) (import (sigil core)) (export ;; lifecycle vt-make vt? vt-feed! vt-feed-bytes! vt-resize! vt-reset! vt-invalidate! ;; geometry / cursor / modes vt-cols vt-rows vt-cursor-row vt-cursor-col vt-cursor-visible? vt-cursor-style vt-alt? vt-title vt-bracketed-paste? vt-app-cursor? vt-mouse-flags vt-mouse-1000 vt-mouse-1002 vt-mouse-1003 vt-mouse-1006 vt-mouse-1007 ;; pending replies (DSR/DA) the consumer writes back to the pty vt-take-output! ;; damage (drained by the renderer) vt-take-damage! vt-damaged? ;; out-of-band events (title / bell / OSC-52 clipboard) vt-take-events! ;; grid access vt-row-cells vt-row-text vt-scrollback-count vt-scrollback-row ;; the render seam: style-merged row runs #(text attr fg bg cursor?) vt-row-runs vt-scrollback-runs ;; cells vt-cell-ch vt-cell-attr vt-cell-fg vt-cell-bg vt-blank-cell vt-attr-bold vt-attr-dim vt-attr-italic vt-attr-underline vt-attr-blink vt-attr-inverse vt-attr-hidden vt-attr-strike vt-attr-wide vt-attr-continuation ;; 256-color palette (index -> #xRRGGBB), for renderers vt-color-256->rgb) (begin ;; ---- attribute bits (slate's superset) -------------------------------- (define vt-attr-bold 1) (define vt-attr-dim 2) (define vt-attr-italic 4) (define vt-attr-underline 8) (define vt-attr-blink 16) (define vt-attr-inverse 32) (define vt-attr-hidden 64) (define vt-attr-strike 128) ;; reserved for CJK double-width (wcwidth deferred to M3/M4) (define vt-attr-wide 256) (define vt-attr-continuation 512) ;; ---- mouse-mode flag bits (DECSET) ------------------------------------ (define vt-mouse-1000 1) (define vt-mouse-1002 2) (define vt-mouse-1003 4) (define vt-mouse-1006 8) (define vt-mouse-1007 16) ;; ---- cells (immutable 4-vectors) -------------------------------------- (define (vt-cell-ch c) (vector-ref c 0)) ; a CODEPOINT (int) (define (vt-cell-attr c) (vector-ref c 1)) (define (vt-cell-fg c) (vector-ref c 2)) (define (vt-cell-bg c) (vector-ref c 3)) (define vt-blank-cell (vector 32 0 -1 -1)) ;; ---- lifecycle -------------------------------------------------------- (define (vt-make cols rows . rest) (if (pair? rest) (%vt-make cols rows (car rest)) (%vt-make cols rows 1000))) (define (vt? x) (%vt? x)) (define (vt-feed! t s) (%vt-feed! t s)) (define (vt-feed-bytes! t bytes) (%vt-feed-bytes! t bytes)) (define (vt-resize! t cols rows) (%vt-resize! t cols rows)) (define (vt-reset! t) (%vt-reset! t)) (define (vt-invalidate! t) (%vt-invalidate! t)) ;; ---- accessors -------------------------------------------------------- (define (vt-cols t) (%vt-cols t)) (define (vt-rows t) (%vt-rows t)) (define (vt-cursor-row t) (%vt-cursor-row t)) (define (vt-cursor-col t) (%vt-cursor-col t)) (define (vt-cursor-visible? t) (%vt-cursor-visible? t)) (define (vt-cursor-style t) (%vt-cursor-style t)) (define (vt-alt? t) (%vt-alt? t)) (define (vt-title t) (%vt-title t)) (define (vt-bracketed-paste? t) (%vt-bracketed-paste? t)) (define (vt-app-cursor? t) (%vt-app-cursor? t)) (define (vt-mouse-flags t) (%vt-mouse-flags t)) ;; ---- pending replies -------------------------------------------------- (define (vt-take-output! t) (%vt-take-output! t)) ;; ---- damage ----------------------------------------------------------- ;; Drain -> #{ all?: bool rows: (ascending list of dirty row indices) }. ;; rows is '() when all? is #t (the renderer repaints everything). (define (vt-take-damage! t) (let* ((v (%vt-take-damage! t)) (all? (vector-ref v 0))) (if all? #{ all?: #t rows: '() } (let loop ((i (- (vector-length v) 1)) (acc '())) (if (< i 1) #{ all?: #f rows: acc } (loop (- i 1) (cons (vector-ref v i) acc))))))) (define (vt-damaged? t) (%vt-damaged? t)) ;; ---- events ----------------------------------------------------------- ;; Drain -> a list of #{ type: <'title|'bell|'clipboard> payload: <string|#f> } ;; in arrival order. Title also updates vt-title; OSC-52 clipboard is queued ;; only (never acted on — the consumer decides). (define (vt-take-events! t) (map (lambda (e) #{ type: (vector-ref e 0) payload: (vector-ref e 1) }) (%vt-take-events! t))) ;; ---- grid access ------------------------------------------------------ (define (vt-row-cells t i) (%vt-row-cells t i)) (define (vt-row-text t i) (%vt-row-text t i)) (define (vt-scrollback-count t) (%vt-scrollback-count t)) (define (vt-scrollback-row t k) (%vt-scrollback-row t k)) ;; ---- the render seam -------------------------------------------------- ;; Style-merged, right-trimmed runs for a visible row (or a scrollback row). ;; cursor-col (optional) forces a run break at that cell. Each run is ;; #(text attr fg bg cursor?). (define (vt-row-runs t i . rest) (if (pair? rest) (%vt-row-runs t i (car rest)) (%vt-row-runs t i #f))) (define (vt-scrollback-runs t k . rest) (if (pair? rest) (%vt-scrollback-runs t k (car rest)) (%vt-scrollback-runs t k #f))) ;; ---- palette ---------------------------------------------------------- (define (vt-color-256->rgb i) (%vt-color-256->rgb i))))test/vt-test.sgladded
;;; Conformance suite for (sigil vt) — the native VT core.;;;;;; A faithful port of slate's test/term-test.sgl (the emulator's behavior IS;;; the spec). Each fixture feeds a byte/escape sequence into a fresh emulator;;; and asserts the resulting grid text, cursor, attrs, or mode flags. Covers;;; the MUST tier: C0, CSI cursor/erase/insert/delete/scroll-region, SGR;;; 16/256/truecolor (+ colon sub-params), alt-screen, autowrap (incl. the;;; deferred-wrap edge), tabs, DECSC/DECRC, origin mode, DECALN, OSC title,;;; DECSCUSR, bracketed paste, DSR/DA replies, scrollback, resize, incremental;;; UTF-8 across chunk boundaries, the ground-state bulk-run fast path, the;;; trust boundary (hostile input), plus the native additions (events, mouse;;; flags, row-runs).(import (sigil core) (sigil test) (sigil vt));; ---- helpers --------------------------------------------------------------(define ESC "\x1b;")(define (csi . parts) (apply string-append ESC "[" parts))(define (vt* cols rows . feeds) (let ((t (vt-make cols rows))) (for-each (lambda (s) (vt-feed! t s)) feeds) t))(define (rtrim s) (let loop ((i (string-length s))) (cond ((= i 0) "") ((char=? (string-ref s (- i 1)) #\space) (loop (- i 1))) (else (substring s 0 i)))))(define (rows-of t) (let loop ((i (- (vt-rows t) 1)) (acc '())) (if (< i 0) acc (loop (- i 1) (cons (rtrim (vt-row-text t i)) acc)))))(define (cursor-of t) (list (vt-cursor-row t) (vt-cursor-col t)))(define (cell-at t row col) (let ((c (vector-ref (vt-row-cells t row) col))) (list (string (integer->char (vt-cell-ch c))) (vt-cell-attr c) (vt-cell-fg c) (vt-cell-bg c))));; ==========================================================================;; plain text, C0, wrapping;; ==========================================================================(test-group "print / C0 / wrap" (test "print: text lands on row 0" (let ((t (vt* 10 3 "hello"))) (assert-equal (list "hello" "" "") (rows-of t)) (assert-equal (list 0 5) (cursor-of t)))) (test "CRLF: second line" (let ((t (vt* 10 3 "ab\r\ncd"))) (assert-equal (list "ab" "cd" "") (rows-of t)) (assert-equal (list 1 2) (cursor-of t)))) (test "CR overprint" (assert-equal (list "Xbc" "" "") (rows-of (vt* 10 3 "abc\rX")))) (test "BS then overprint" (assert-equal (list "aX" "" "") (rows-of (vt* 10 3 "ab\x08;X")))) (test "autowrap wraps" (let ((t (vt* 10 3 "0123456789AB"))) (assert-equal (list "0123456789" "AB" "") (rows-of t)) (assert-equal (list 1 2) (cursor-of t)))) (test "pending wrap: cursor stays on last col, CR cancels" (let ((t (vt* 10 3 "0123456789"))) (assert-equal (list 0 9) (cursor-of t)) (vt-feed! t "\rX") (assert-equal (list "X123456789" "" "") (rows-of t)))) (test "DECAWM off: no wrap" (assert-equal (list "012345678B" "" "") (rows-of (vt* 10 3 (csi "?7l") "0123456789AB")))) (test "LF at bottom scrolls; evicted row to scrollback" (let ((t (vt* 5 2 "aa\r\nbb\r\ncc"))) (assert-equal (list "bb" "cc") (rows-of t)) (assert-equal 1 (vt-scrollback-count t)) (assert-true (= (vt-cell-ch (vector-ref (vt-scrollback-row t 0) 0)) 97)))) (test "tab to col 8" (assert-equal (list "a b" "") (rows-of (vt* 20 2 "a\tb")))));; ==========================================================================;; cursor movement;; ==========================================================================(test-group "cursor movement" (test "CUP 3;4" (assert-equal (list "" "" " X" "" "") (rows-of (vt* 10 5 (csi "3;4H") "X")))) (test "CUU + CUB" (assert-equal (list "" " X" "" "" "") (rows-of (vt* 10 5 (csi "3;4H") (csi "A") (csi "2D") "X")))) (test "CUP clamps" (let ((t (vt* 10 5 "abc" (csi "10;20H") "Z"))) (assert-equal (list "abc" "" "" "" " Z") (rows-of t)) (assert-equal (list 4 9) (cursor-of t)))) (test "CUD + CUF" (assert-equal (list 4 4) (cursor-of (vt* 10 5 (csi "2;2H") (csi "3B") (csi "2C") "X")))) (test "CHA column" (assert-equal (list "hi X" "" "" "" "") (rows-of (vt* 10 5 "hi" (csi "5G") "X")))) (test "VPA row keeps col" (assert-equal (list "hi" "" " X" "" "") (rows-of (vt* 10 5 "hi" (csi "3d") "X")))));; ==========================================================================;; erase / insert / delete;; ==========================================================================(test-group "erase / insert / delete" (test "EL 0: erase to right" (assert-equal (list "abc" "" "") (rows-of (vt* 10 3 "abcdef" (csi "4G") (csi "K"))))) (test "EL 1: erase to left (incl cursor)" (assert-equal (list " ef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "4G") (csi "1K"))))) (test "EL 2: whole line" (assert-equal (list "" "" "") (rows-of (vt* 10 3 "abcdef" (csi "2K"))))) (test "ED 0: erase below" (assert-equal (list "aaaaaa" "bb" "") (rows-of (vt* 6 3 "aaaaaa\r\nbbbbbb\r\ncccccc" (csi "2;3H") (csi "J"))))) (test "ED 1: erase above" (assert-equal (list "" " bbb" "cccccc") (rows-of (vt* 6 3 "aaaaaa\r\nbbbbbb\r\ncccccc" (csi "2;3H") (csi "1J"))))) (test "ED 2: erase all + cursor left on last col" (let ((t (vt* 6 3 "aaaaaa\r\nbbbbbb" (csi "2J")))) (assert-equal (list "" "" "") (rows-of t)) (assert-equal (list 1 5) (cursor-of t)))) (test "ICH inserts blanks (then overtyped)" (assert-equal (list "abXYcdef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2@") "XY")))) (test "DCH deletes chars" (assert-equal (list "abef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2P"))))) (test "ECH erases chars in place" (assert-equal (list "ab ef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2X"))))) (test "IL inserts a line" (assert-equal (list "a" "" "b" "c") (rows-of (vt* 5 4 "a\r\nb\r\nc\r\nd" (csi "2;1H") (csi "L"))))) (test "DL deletes a line" (assert-equal (list "a" "c" "d" "") (rows-of (vt* 5 4 "a\r\nb\r\nc\r\nd" (csi "2;1H") (csi "M"))))));; ==========================================================================;; scroll region;; ==========================================================================(test-group "scroll region" (test "DECSTBM: region scrolls, top intact" (assert-equal (list "top" "l2" "l3" "l4" "") (rows-of (vt* 5 5 "top" (csi "2;4r") (csi "2;1H") "l1\r\nl2\r\nl3\r\nl4")))) (test "SU in region" (assert-equal (list "a" "d" "" "" "e") (rows-of (vt* 5 5 "a\r\nb\r\nc\r\nd\r\ne" (csi "2;4r") (csi "2S"))))) (test "SD in region" (assert-equal (list "a" "" "b" "c" "e") (rows-of (vt* 5 5 "a\r\nb\r\nc\r\nd\r\ne" (csi "2;4r") (csi "1T"))))) (test "RI at region top" (assert-equal (list "a" "" "b" "c" "") (rows-of (vt* 5 5 "a\r\nb\r\nc" (csi "2;4r") (csi "2;1H") ESC "M")))) (test "non-top region scroll: no scrollback" (assert-equal 0 (vt-scrollback-count (vt* 5 5 "x\r\ny" (csi "2;4r") (csi "2;1H") "1\r\n2\r\n3\r\n4")))) (test "DECOM: home is region top" (assert-equal (list "" "X" "" "" "") (rows-of (vt* 10 5 (csi "2;4r") (csi "?6h") (csi "1;1H") "X")))));; ==========================================================================;; SGR;; ==========================================================================(test-group "SGR" (test "SGR bold red + reset" (let ((t (vt* 10 2 (csi "1;31m") "R" (csi "0m") "p"))) (assert-equal (list "R" vt-attr-bold 1 -1) (cell-at t 0 0)) (assert-equal (list "p" 0 -1 -1) (cell-at t 0 1)))) (test "SGR italic+underline+inverse" (assert-equal (list "x" (+ vt-attr-italic vt-attr-underline vt-attr-inverse) -1 -1) (cell-at (vt* 10 2 (csi "3;4;7m") "x") 0 0))) (test "SGR 256-color fg/bg" (assert-equal (list "c" 0 196 22) (cell-at (vt* 10 2 (csi "38;5;196m") (csi "48;5;22m") "c") 0 0))) (test "SGR truecolor fg" (assert-equal (list "t" 0 (+ #x1000000 (* 255 65536) (* 128 256) 0) -1) (cell-at (vt* 10 2 (csi "38;2;255;128;0m") "t") 0 0))) (test "SGR colon syntax" (assert-equal (list "Q" 0 99 -1) (cell-at (vt* 10 2 ESC "[38:5:99mQ") 0 0))) (test "SGR colon colorspace form + following param" (assert-equal (list "W" vt-attr-underline (+ #x1000000 (* 255 65536) (* 128 256) 0) -1) (cell-at (vt* 10 2 ESC "[38:2::255:128:0;4mW") 0 0))) (test "SGR colon truecolor (no colorspace)" (assert-equal (list "V" 0 (+ #x1000000 (* 10 65536) (* 20 256) 30) -1) (cell-at (vt* 10 2 ESC "[38:2:10:20:30mV") 0 0))) (test "SGR bright fg + 39 default" (let ((t (vt* 10 2 (csi "91m") "b" (csi "39m") "d"))) (assert-equal (list "b" 0 9 -1) (cell-at t 0 0)) (assert-equal (list "d" 0 -1 -1) (cell-at t 0 1)))) (test "SGR 22 clears bold, keeps color" (assert-equal (list "b" 0 1 -1) (cell-at (vt* 10 2 (csi "1;31m") "a" (csi "22m") "b") 0 1))) (test "BCE: ED fills with cur bg" (assert-equal 19 (vt-cell-bg (vector-ref (vt-row-cells (vt* 4 2 (csi "48;5;19m") (csi "2J")) 1) 3)))) (test "palette 16 = cube 0,0,0" (assert-equal 0 (vt-color-256->rgb 16))) (test "palette 196 = red" (assert-equal #xff0000 (vt-color-256->rgb 196))) (test "palette 231 = white" (assert-equal #xffffff (vt-color-256->rgb 231))) (test "palette 244 gray" (assert-equal #x808080 (vt-color-256->rgb 244))));; ==========================================================================;; alt screen;; ==========================================================================(test-group "alt screen" (test "1049: alt starts cleared / restores main + cursor" (let ((t (vt* 10 3 "main" (csi "?1049h") (csi "1;1H") "ALT"))) (assert-equal (list "ALT" "" "") (rows-of t)) (assert-true (vt-alt? t)) (vt-feed! t (csi "?1049l")) (assert-equal (list "main" "" "") (rows-of t)) (assert-equal (list 0 4) (cursor-of t)) (assert-true (not (vt-alt? t))))) (test "alt: no scrollback" (assert-equal 0 (vt-scrollback-count (vt* 5 2 (csi "?1049h") "a\r\nb\r\nc\r\nd")))));; ==========================================================================;; DECSC/DECRC, DECALN, RIS;; ==========================================================================(test-group "DECSC/DECRC, DECALN, RIS" (test "DECSC/DECRC restores pos + SGR" (let ((t (vt* 10 3 (csi "31m") (csi "2;3H") ESC "7" (csi "0m") (csi "1;1H") ESC "8" "X"))) (assert-equal (list "" " X" "") (rows-of t)) (assert-equal (list "X" 0 1 -1) (cell-at t 1 2)))) (test "DECALN fills E" (assert-equal (list "EEE" "EEE") (rows-of (vt* 3 2 ESC "#8")))) (test "RIS clears + resets SGR" (let ((t (vt* 5 2 "hi" (csi "31m") ESC "c" "x"))) (assert-equal (list "x" "") (rows-of t)) (assert-equal (list "x" 0 -1 -1) (cell-at t 0 0)))));; ==========================================================================;; OSC title, DECSCUSR, bracketed paste, modes, events;; ==========================================================================(test-group "OSC / modes / events" (test "OSC 0 BEL: title" (let ((t (vt* 10 2 ESC "]0;my title\x07;" "x"))) (assert-equal "my title" (vt-title t)) (assert-equal (list "x" "") (rows-of t)))) (test "OSC 2 ST: title" (let ((t (vt* 10 2 ESC "]2;st title" ESC "\\" "y"))) (assert-equal "st title" (vt-title t)) (assert-equal (list "y" "") (rows-of t)))) (test "OSC 52 (clipboard) not in title, queued as event" (let ((t (vt* 10 2 ESC "]52;c;aGVsbG8=\x07;" "z"))) (assert-equal "" (vt-title t)) (assert-equal (list "z" "") (rows-of t)) (let ((evs (vt-take-events! t))) (assert-true (memv 'clipboard (map (lambda (e) (dict-ref e type: #f)) evs)))))) (test "DECSCUSR style" (assert-equal 4 (vt-cursor-style (vt* 10 2 (csi "4 q"))))) (test "bracketed paste on" (assert-true (vt-bracketed-paste? (vt* 10 2 (csi "?2004h"))))) (test "bracketed paste off" (assert-true (not (vt-bracketed-paste? (vt* 10 2 (csi "?2004h") (csi "?2004l")))))) (test "cursor hidden" (assert-true (not (vt-cursor-visible? (vt* 10 2 (csi "?25l")))))) (test "app cursor mode" (assert-true (vt-app-cursor? (vt* 10 2 (csi "?1h"))))) (test "IRM: chars shift right" (assert-equal (list "aXYbc" "") (rows-of (vt* 10 2 "abc" (csi "2G") (csi "4h") "XY")))) (test "mouse-mode flags parsed" (assert-equal (+ vt-mouse-1002 vt-mouse-1006) (vt-mouse-flags (vt* 10 2 (csi "?1002h") (csi "?1006h"))))) (test "mouse-mode flags cleared" (assert-equal vt-mouse-1006 (vt-mouse-flags (vt* 10 2 (csi "?1002h") (csi "?1006h") (csi "?1002l"))))));; ==========================================================================;; replies (DSR / DA);; ==========================================================================(test-group "replies" (test "DSR 6: cursor report + drain" (let ((t (vt* 10 5 (csi "3;4H") (csi "6n")))) (assert-equal (string-append ESC "[3;4R") (vt-take-output! t)) (assert-equal "" (vt-take-output! t)))) (test "DSR 5: status ok" (assert-equal (string-append ESC "[0n") (vt-take-output! (vt* 10 5 (csi "5n"))))) (test "DA reply" (assert-equal (string-append ESC "[?6c") (vt-take-output! (vt* 10 5 (csi "c"))))));; ==========================================================================;; the trust boundary;; ==========================================================================(test-group "trust boundary" (test "huge params clamp" (let ((t (vt* 10 2 ESC "[999999999999H" "ok"))) (assert-equal (list 1 2) (cursor-of t)) (assert-equal (list "" "ok") (rows-of t)))) (test "truncated SGR ignored" (assert-equal (list "x" 0 -1 -1) (cell-at (vt* 10 2 ESC "[38;2m" "x") 0 0))) (test "DCS swallowed" (assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "P malicious dcs payload " ESC "\\" "ok")))) (test "CHT clamps" (assert-equal (list 0 9) (cursor-of (vt* 10 2 ESC "[999999999I" "x")))) (test "unknown modes/charsets swallowed" (assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "[?9999h" ESC "[<5m" ESC "(0" "ok")))) (test "escape split across chunks" (let ((t (vt-make 10 2))) (vt-feed! t ESC) (vt-feed! t "[3") (vt-feed! t "1mX") (assert-equal (list "X" 0 1 -1) (cell-at t 0 0)))));; ==========================================================================;; incremental UTF-8 (byte feed);; ==========================================================================(test-group "incremental UTF-8" (test "utf-8 across chunks" (let ((t (vt-make 10 2))) (vt-feed-bytes! t (list 97 195)) (vt-feed-bytes! t (list 169 32 226 134)) (vt-feed-bytes! t (list 146)) (assert-equal "aé → " (vt-row-text t 0)))) (test "invalid utf-8 -> U+FFFD, stream recovers" (let ((t (vt-make 10 2))) (vt-feed-bytes! t (list 195 195 169)) (assert-true (and (= (vt-cell-ch (vector-ref (vt-row-cells t 0) 0)) 65533) (= (vt-cell-ch (vector-ref (vt-row-cells t 0) 1)) 233))))) (test "surrogate bytes -> U+FFFD" (let ((t (vt-make 10 2))) (vt-feed-bytes! t (list 237 160 128)) (assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0))))) (test "past-U+10FFFF -> U+FFFD" (let ((t (vt-make 10 2))) (vt-feed-bytes! t (list 244 144 128 128)) (assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0))))) (test "overlong encoding -> U+FFFD" (let ((t (vt-make 10 2))) (vt-feed-bytes! t (list 224 128 168)) (assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0))))) (test "DL never scrollbacks" (let ((t (vt* 5 3 "a\r\nb\r\nc" (csi "1;1H") (csi "M")))) (assert-equal 0 (vt-scrollback-count t)) (assert-equal (list "b" "c" "") (rows-of t)))));; ==========================================================================;; damage tracking;; ==========================================================================(test-group "damage" (test "damage: one row / drained" (let ((t (vt-make 10 3))) (vt-take-damage! t) (vt-feed! t "x") (let ((d (vt-take-damage! t))) (assert-equal (list 0) (dict-ref d rows: '())) (assert-true (not (dict-ref d all?: #f)))) (assert-equal '() (dict-ref (vt-take-damage! t) rows: '())))) (test "ED marks all its rows" (let ((t (vt-make 10 3))) (vt-take-damage! t) (vt-feed! t (csi "2J")) (assert-equal (list 0 1 2) (dict-ref (vt-take-damage! t) rows: '())))) (test "scroll damages the region" (let ((t (vt-make 5 2))) (vt-take-damage! t) (vt-feed! t "a\r\nb\r\nc") (let ((d (vt-take-damage! t))) (assert-true (or (dict-ref d all?: #f) (equal? (dict-ref d rows: '()) (list 0 1))))))));; ==========================================================================;; resize;; ==========================================================================(test-group "resize" (test "narrower truncates / wider pads / cols updated" (let ((t (vt* 10 4 "aa\r\nbb"))) (vt-resize! t 5 4) (assert-equal (list "aa" "bb" "" "") (rows-of t)) (vt-resize! t 20 4) (assert-equal (list "aa" "bb" "" "") (rows-of t)) (assert-equal 20 (vt-cols t)))) (test "shrink drops blank bottom rows (no scrollback)" (let ((t (vt* 10 5 "aa\r\nbb"))) (vt-resize! t 10 3) (assert-equal (list "aa" "bb" "") (rows-of t)) (assert-equal 0 (vt-scrollback-count t)))) (test "shrink onto content keeps cursor rows, evicts to scrollback" (let ((t (vt* 10 4 "a\r\nb\r\nc\r\nd"))) (vt-resize! t 10 2) (assert-equal (list "c" "d") (rows-of t)) (assert-equal 2 (vt-scrollback-count t)) (assert-equal (list 1 1) (cursor-of t)))) (test "grow pulls back out of scrollback" (let ((t (vt* 10 2 "a\r\nb\r\nc"))) (assert-equal 1 (vt-scrollback-count t)) (vt-resize! t 10 4) (assert-equal (list "a" "b" "c" "") (rows-of t)) (assert-equal 0 (vt-scrollback-count t)))) (test "post-resize feed is sane" (let ((t (vt* 10 4 (csi "2;3r") "x"))) (vt-resize! t 8 3) (vt-feed! t (string-append (csi "3;1H") "\n\n")) (assert-true (>= (vt-rows t) 3)))));; ==========================================================================;; the ground-state bulk-run fast path;; ==========================================================================(test-group "bulk-run fast path" (test "run wraps across the margin" (let ((t (vt* 5 3 "abcdefgh"))) (assert-equal (list "abcde" "fgh" "") (rows-of t)) (assert-equal (list 1 3) (cursor-of t)))) (test "run deferred wrap parks / fires next char" (let ((t (vt* 5 3 "abcde"))) (assert-equal (list 0 4) (cursor-of t)) (vt-feed! t "f") (assert-equal (list "abcde" "f" "") (rows-of t)))) (test "run no-autowrap overwrites last col" (let ((t (vt* 5 3 (csi "?7l") "abcdefgh"))) (assert-equal (list "abcdh" "" "") (rows-of t)) (assert-equal (list 0 4) (cursor-of t)))) (test "esc splits runs, text intact" (assert-equal (list "abcdef" "" "") (rows-of (vt* 10 3 (string-append "ab" (csi "31m") "cd" (csi "0m") "ef"))))) (test "insert mode shifts, not overwrites" (assert-equal (list "XYabc" "" "") (rows-of (vt* 10 3 "abc" (csi "1;1H") (csi "4h") "XY")))) (test "mixed unicode intact" (assert-equal (list "aλbμc" "" "") (rows-of (vt* 10 3 "aλbμc")))));; ==========================================================================;; row-runs (the render seam) — new native primitive;; ==========================================================================(test-group "row-runs" (test "plain run merges to one, right-trimmed" (let* ((t (vt* 10 2 "hello")) (runs (vt-row-runs t 0))) (assert-equal 1 (length runs)) (assert-equal "hello" (vector-ref (car runs) 0)) (assert-equal 0 (vector-ref (car runs) 1)))) (test "style change splits runs" (let* ((t (vt* 10 2 "ab" (csi "31m") "cd")) (runs (vt-row-runs t 0))) (assert-equal 2 (length runs)) (assert-equal "ab" (vector-ref (car runs) 0)) (assert-equal "cd" (vector-ref (cadr runs) 0)) (assert-equal 1 (vector-ref (cadr runs) 2)))) (test "cursor-col forces a break with cursor? flag" (let* ((t (vt* 10 2 "hello")) (runs (vt-row-runs t 0 1))) ;; "h" | "e"(cursor) | "llo" (assert-equal 3 (length runs)) (assert-true (vector-ref (cadr runs) 4)) (assert-equal "e" (vector-ref (cadr runs) 0)))) (test "blank row -> no runs" (assert-equal '() (vt-row-runs (vt-make 10 2) 1))))