Commitc952db6dRecorded16 Jul 2026Repositorysigil-vt
M1: sigil-vt native VT core + conformance suite
Message
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 SIGIL_OBJ_FOREIGN 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.
Changed
.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(+)Diff
.gitignoreadded
@@ -0,0 +1,2 @@
+1
build/+2
.sigil/README.mdadded
@@ -0,0 +1,53 @@
+1
# sigil-vt+2
+3
The native VT/ANSI terminal core for the Sigil ecosystem — a VT100/xterm-subset+4
terminal emulator in C: an incremental byte stream → a damage-tracked+5
`int32[4]`-per-cell grid, with scrollback, cursor-report / device-attribute+6
replies, an out-of-band event queue (title / bell / OSC-52 clipboard), mouse-mode+7
flags, and native style-merged **row-run** extraction (the render seam).+8
+9
It is the shared native foundation for terminal handling across the ecosystem:+10
slate's `(slate term)` rides it as a thin façade (byte-identical `take-frame`),+11
and `(sigil tui grid)` will ride it in M3. See the design note+12
`topics/sigil-terminal-handling-design`.+13
+14
This is the native reimplementation of slate's `(slate term)` emulator — **the+15
Sigil emulator is the spec**; `native/vt.c` reproduces its semantics cell-for-cell,+16
proven by `test/vt-test.sgl` (a faithful port of slate's `test/term-test.sgl`).+17
+18
## Layout+19
+20
- `native/vt.c` — the C core (parser state machine + grid + damage/scrollback/+21
replies/events + row-runs). Registered as `%vt-*` builtins in module `(sigil vt)`.+22
- `src/sigil/vt.sgl` — the `(sigil vt)` Scheme surface (thin wrappers + the+23
public API, shaping damage into a dict etc.).+24
- `test/vt-test.sgl` — the conformance suite.+25
+26
## Cell model+27
+28
Flat `int32_t` buffer, cell `(row,col)` at `((row*cols+col)*4)`:+29
`[codepoint, attrs, fg, bg]`. Attr bits: bold 1, dim 2, italic 4, underline 8,+30
blink 16, inverse 32, hidden 64, strike 128; high bits reserved for+31
wide/continuation (wcwidth deferred). Colors: `-1` default, `0..255` indexed,+32
`#x1000000 + #xRRGGBB` truecolor.+33
+34
## Trust boundary+35
+36
Terminal bytes are UNTRUSTED program output. The parser converts arbitrary bytes+37
into a CLOSED vocabulary of grid operations — no escape sequence executes+38
anything, reaches an eval, or emits markup. Params/sub-params/OSC accumulators+39
are fixed-capacity; no allocation is sized by input params. The core never calls+40
back into Scheme. Fuzzed + ASan/UBSan-clean on `vt-feed-bytes!` (see `spike/`).+41
+42
## Build / test (development, against a local monorepo checkout)+43
+44
```+45
sigil deps install --redirects dev-redirects.sgl+46
sigil build --redirects dev-redirects.sgl+47
sigil test --redirects dev-redirects.sgl+48
```+49
+50
## Not here+51
+52
No `vt-diff` (the grid-diff ANSI emitter) — deferred to M3 (the sigil-tui+53
revival). The cell repr is designed to account for it.dev-redirects.sgladded
@@ -0,0 +1,7 @@
+1
;; Development redirects — point dependencies at the local Sigil monorepo+2
;; checkout. Use: sigil build --redirects ./dev-redirects.sgl+3
(redirects+4
repos: (list+5
(for-repo+6
url: "codeberg:sigil/sigil"+7
use: (from-path dir: "../sigil"))))native/vt.cadded
@@ -0,0 +1,1654 @@
+1
/*+2
* sigil-vt — the native VT/ANSI terminal core.+3
*+4
* A VT100/xterm-subset terminal emulator: an incremental byte stream -> a+5
* damage-tracked int32[4]-per-cell grid. This is the native reimplementation+6
* of slate's (slate term) emulator (src/slate/term.sgl) — the Sigil emulator+7
* IS the spec; this C port reproduces its semantics cell-for-cell (proven by+8
* the ported conformance suite, test/vt-test.sgl).+9
*+10
* THE TRUST BOUNDARY. Terminal bytes are UNTRUSTED program output. This parser+11
* converts arbitrary bytes into a CLOSED vocabulary of grid operations. No+12
* escape sequence executes anything, reaches an eval, or emits markup. Params+13
* are clamped, param/sub-param/OSC accumulators are fixed-capacity, and no+14
* allocation is sized by input params. The core never calls back into Scheme+15
* (all APIs are call-in / return-out), so the fiber-suspension constraint is+16
* structurally satisfied. Fuzzed + ASan/UBSan clean on vt-feed-bytes! before+17
* the slate cutover (the M2 gate).+18
*+19
* Cell model: flat int32_t buffer, cell (row,col) at ((row*cols+col)*4):+20
* [0] codepoint [1] attrs [2] fg [3] bg+21
* attrs bits: bold 1, dim 2, italic 4, underline 8, blink 16, inverse 32,+22
* hidden 64, strike 128; high bits reserved for wide/continuation (wcwidth+23
* deferred to M3/M4 — the model carries a home for it from day 1).+24
* colors: -1 default, 0..255 indexed, 0x1000000 + 0xRRGGBB truecolor.+25
*/+26
+27
#include <sigil/sigil.h>+28
#include <stdint.h>+29
#include <stdlib.h>+30
#include <string.h>+31
#include <stdio.h>+32
+33
/* ---- internal libsigil helpers (exported from libsigil; not in the public+34
* header). Declared extern exactly as sigil-wasm-dom does. -------------------*/+35
extern void *sigil__gc_alloc(SigilVM *vm, SigilObjType type, size_t size);+36
extern void sigil__gc_push_temp_root(SigilVM *vm, Value v);+37
extern void sigil__gc_pop_temp_root(SigilVM *vm);+38
+39
#define SIGIL_EXPORT __attribute__((visibility("default")))+40
+41
/* ---- attribute bits (slate's superset wins) ------------------------------ */+42
enum {+43
VT_ATTR_BOLD = 1,+44
VT_ATTR_DIM = 2,+45
VT_ATTR_ITALIC = 4,+46
VT_ATTR_UNDERLINE = 8,+47
VT_ATTR_BLINK = 16,+48
VT_ATTR_INVERSE = 32,+49
VT_ATTR_HIDDEN = 64,+50
VT_ATTR_STRIKE = 128,+51
/* reserved for CJK double-width (wcwidth lands later) */+52
VT_ATTR_WIDE = 256,+53
VT_ATTR_CONT = 512+54
};+55
+56
/* ---- mouse-mode flags (DECSET; parsed as flags in v1) -------------------- */+57
enum {+58
VT_MOUSE_1000 = 1, /* X10/normal button tracking */+59
VT_MOUSE_1002 = 2, /* button-event (drag) tracking */+60
VT_MOUSE_1003 = 4, /* any-event (motion) tracking */+61
VT_MOUSE_1006 = 8, /* SGR extended coordinates */+62
VT_MOUSE_1007 = 16 /* alternate scroll */+63
};+64
+65
/* ---- out-of-band event types --------------------------------------------- */+66
enum { VT_EV_TITLE = 1, VT_EV_BELL = 2, VT_EV_CLIPBOARD = 3 };+67
+68
/* ---- parser states ------------------------------------------------------- */+69
enum {+70
ST_GROUND = 0, ST_ESC, ST_ESC_SKIP1, ST_ESC_HASH,+71
ST_CSI, ST_OSC, ST_OSC_ESC, ST_STR_IGNORE, ST_STR_ESC+72
};+73
+74
/* ---- fixed capacities (the closed-vocabulary discipline) ----------------- */+75
#define VT_MAX_GROUPS 32+76
#define VT_MAX_SUB 8+77
#define VT_INTER_MAX 8+78
#define VT_OSC_MAX 1024+79
#define VT_PARAM_CAP 999999999+80
+81
typedef struct {+82
int has;+83
int row, col;+84
int32_t attr, fg, bg;+85
int origin, autowrap;+86
} SavedCursor;+87
+88
typedef struct {+89
int type;+90
char *payload; /* malloc'd, may be NULL (bell) */+91
int payload_len;+92
} VtEvent;+93
+94
typedef struct {+95
int cols, rows;+96
int32_t *grid; /* ACTIVE grid (points at main or alt) */+97
int32_t *main; /* main-screen buffer */+98
int32_t *alt; /* alt-screen buffer (NULL until first enter) */+99
int alt_active;+100
+101
int cur_row, cur_col;+102
int wrap; /* pending (deferred) autowrap */+103
int32_t attr, fg, bg; /* current SGR */+104
int stop, sbot; /* scroll region, 0-based inclusive */+105
+106
SavedCursor saved_main, saved_alt;+107
+108
/* parser */+109
int state;+110
int32_t groups[VT_MAX_GROUPS][VT_MAX_SUB]; /* completed groups */+111
int group_len[VT_MAX_GROUPS];+112
int ngroups;+113
int cur_digits; /* -1 = none */+114
int32_t cur_sub[VT_MAX_SUB]; /* completed sub-params of current group */+115
int cur_sub_len;+116
int prefix; /* private marker char or 0 */+117
char inter[VT_INTER_MAX + 1];+118
int inter_len;+119
char osc[VT_OSC_MAX];+120
int osc_len;+121
+122
/* modes */+123
int autowrap, origin, curvis, bracket, appcur, insert;+124
int mouse; /* VT_MOUSE_* bitmask */+125
+126
uint8_t *tabs; /* length cols */+127
char *title; int title_len, title_cap;+128
int curstyle; /* DECSCUSR 0..6 */+129
+130
/* scrollback ring: index 0 = newest */+131
int32_t **sb; /* each entry: cols*4 int32 */+132
int sb_head; /* index of newest */+133
int sb_size;+134
int sb_cap; /* == sbmax */+135
+136
uint8_t *dirty; /* length rows */+137
int alldirty;+138
+139
char *out; int out_len, out_cap; /* pending replies */+140
+141
int u8need; int32_t u8acc; int32_t u8min; /* incremental UTF-8 */+142
+143
VtEvent *events; int nevents, events_cap; /* out-of-band event queue */+144
} Vt;+145
+146
static Value vt_type_tag = SIGIL_UNDEFINED;+147
+148
/* ======================================================================== */+149
/* small utilities */+150
/* ======================================================================== */+151
+152
static int clampi(int x, int lo, int hi) {+153
return x < lo ? lo : (x > hi ? hi : x);+154
}+155
+156
static void set_cell(int32_t *c, int32_t cp, int32_t attr, int32_t fg, int32_t bg) {+157
c[0] = cp; c[1] = attr; c[2] = fg; c[3] = bg;+158
}+159
+160
/* fill a fresh row buffer with blank cells (space, default, current bg) */+161
static void fill_blank(int32_t *row, int cols, int32_t bg) {+162
for (int i = 0; i < cols; i++) set_cell(row + i * 4, 32, 0, -1, bg);+163
}+164
+165
static int32_t *alloc_grid(int cols, int rows, int32_t bg) {+166
int32_t *g = (int32_t *)malloc((size_t)cols * rows * 4 * sizeof(int32_t));+167
if (!g) return NULL;+168
for (int r = 0; r < rows; r++) fill_blank(g + (size_t)r * cols * 4, cols, bg);+169
return g;+170
}+171
+172
/* ---- damage -------------------------------------------------------------- */+173
static void mark_row(Vt *t, int i) {+174
if (i >= 0 && i < t->rows) t->dirty[i] = 1;+175
}+176
static void mark_rows(Vt *t, int from, int to) {+177
for (int i = from; i <= to; i++) mark_row(t, i);+178
}+179
static void mark_all(Vt *t) { t->alldirty = 1; }+180
+181
/* ---- pending replies ----------------------------------------------------- */+182
static void emit_out(Vt *t, const char *s) {+183
int len = (int)strlen(s);+184
if (t->out_len + len + 1 > t->out_cap) {+185
int cap = t->out_cap ? t->out_cap * 2 : 64;+186
while (cap < t->out_len + len + 1) cap *= 2;+187
t->out = (char *)realloc(t->out, cap);+188
t->out_cap = cap;+189
}+190
memcpy(t->out + t->out_len, s, len);+191
t->out_len += len;+192
t->out[t->out_len] = 0;+193
}+194
+195
/* ---- events -------------------------------------------------------------- */+196
static void push_event(Vt *t, int type, const char *payload, int len) {+197
if (t->nevents >= t->events_cap) {+198
int cap = t->events_cap ? t->events_cap * 2 : 8;+199
t->events = (VtEvent *)realloc(t->events, cap * sizeof(VtEvent));+200
t->events_cap = cap;+201
}+202
VtEvent *e = &t->events[t->nevents++];+203
e->type = type;+204
e->payload = NULL;+205
e->payload_len = 0;+206
if (payload && len >= 0) {+207
e->payload = (char *)malloc(len + 1);+208
memcpy(e->payload, payload, len);+209
e->payload[len] = 0;+210
e->payload_len = len;+211
}+212
}+213
+214
/* ---- title --------------------------------------------------------------- */+215
static void set_title(Vt *t, const char *s, int len) {+216
if (len + 1 > t->title_cap) {+217
int cap = t->title_cap ? t->title_cap : 16;+218
while (cap < len + 1) cap *= 2;+219
t->title = (char *)realloc(t->title, cap);+220
t->title_cap = cap;+221
}+222
memcpy(t->title, s, len);+223
t->title[len] = 0;+224
t->title_len = len;+225
}+226
+227
/* ---- scrollback ring ----------------------------------------------------- */+228
/* push a COPY of a grid row (cols*4 int32) as the new newest entry */+229
static void sb_push(Vt *t, const int32_t *row) {+230
if (t->sb_cap == 0) return;+231
int cells = t->cols * 4;+232
int32_t *copy;+233
if (t->sb_size == t->sb_cap) {+234
/* evict oldest: reuse its buffer if it is cols-sized, else realloc */+235
int oldest = (t->sb_head - t->sb_size + 1 + t->sb_cap) % t->sb_cap;+236
copy = t->sb[oldest];+237
copy = (int32_t *)realloc(copy, cells * sizeof(int32_t));+238
t->sb[oldest] = copy;+239
t->sb_head = (t->sb_head + 1) % t->sb_cap;+240
/* size stays == cap */+241
} else {+242
copy = (int32_t *)malloc(cells * sizeof(int32_t));+243
t->sb_head = (t->sb_size == 0) ? 0 : (t->sb_head + 1) % t->sb_cap;+244
t->sb[t->sb_head] = copy;+245
t->sb_size++;+246
}+247
memcpy(copy, row, cells * sizeof(int32_t));+248
}+249
+250
/* newest-first index k (0 = newest); returns NULL if out of range */+251
static int32_t *sb_get(Vt *t, int k) {+252
if (k < 0 || k >= t->sb_size) return NULL;+253
int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;+254
return t->sb[idx];+255
}+256
+257
/* pop the newest entry (for resize grow); returns its buffer (caller frees) */+258
static int32_t *sb_pop(Vt *t) {+259
if (t->sb_size == 0) return NULL;+260
int32_t *row = t->sb[t->sb_head];+261
t->sb[t->sb_head] = NULL;+262
t->sb_head = (t->sb_head - 1 + t->sb_cap) % t->sb_cap;+263
t->sb_size--;+264
return row;+265
}+266
+267
/* ======================================================================== */+268
/* cursor / scrolling / printing (ported from term.sgl) */+269
/* ======================================================================== */+270
+271
static void set_cursor(Vt *t, int row, int col) {+272
t->wrap = 0;+273
t->cur_row = clampi(row, 0, t->rows - 1);+274
t->cur_col = clampi(col, 0, t->cols - 1);+275
}+276
+277
static void set_cursor_abs(Vt *t, int row, int col) {+278
if (t->origin) {+279
t->wrap = 0;+280
t->cur_row = clampi(t->stop + row, t->stop, t->sbot);+281
t->cur_col = clampi(col, 0, t->cols - 1);+282
} else {+283
set_cursor(t, row, col);+284
}+285
}+286
+287
static void move_rows(Vt *t, int delta) {+288
int row = t->cur_row;+289
int lo = (row >= t->stop) ? t->stop : 0;+290
int hi = (row <= t->sbot) ? t->sbot : t->rows - 1;+291
t->wrap = 0;+292
t->cur_row = clampi(row + delta, lo, hi);+293
}+294
+295
static void move_cols(Vt *t, int delta) {+296
t->wrap = 0;+297
t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);+298
}+299
+300
/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */+301
static void scroll_up(Vt *t, int n, int push) {+302
int top = t->stop, bot = t->sbot;+303
int cols = t->cols;+304
n = clampi(n, 0, bot - top + 1);+305
if (n <= 0) return;+306
if (push && top == 0 && !t->alt_active) {+307
for (int k = 0; k < n; k++)+308
sb_push(t, t->grid + (size_t)(top + k) * cols * 4);+309
}+310
for (int i = top; i <= bot - n; i++)+311
memcpy(t->grid + (size_t)i * cols * 4,+312
t->grid + (size_t)(i + n) * cols * 4,+313
cols * 4 * sizeof(int32_t));+314
for (int i = bot - n + 1; i <= bot; i++)+315
fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);+316
mark_rows(t, top, bot);+317
}+318
+319
static void scroll_down(Vt *t, int n) {+320
int top = t->stop, bot = t->sbot;+321
int cols = t->cols;+322
n = clampi(n, 0, bot - top + 1);+323
if (n <= 0) return;+324
for (int i = bot; i >= top + n; i--)+325
memcpy(t->grid + (size_t)i * cols * 4,+326
t->grid + (size_t)(i - n) * cols * 4,+327
cols * 4 * sizeof(int32_t));+328
for (int i = top; i < top + n; i++)+329
fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);+330
mark_rows(t, top, bot);+331
}+332
+333
static void line_feed(Vt *t) {+334
if (t->cur_row == t->sbot)+335
scroll_up(t, 1, 1);+336
else if (t->cur_row < t->rows - 1)+337
t->cur_row += 1;+338
}+339
+340
static void do_wrap(Vt *t) {+341
t->wrap = 0;+342
t->cur_col = 0;+343
line_feed(t);+344
}+345
+346
static void print_cp(Vt *t, int32_t cp) {+347
if (t->wrap) do_wrap(t);+348
int cols = t->cols, row = t->cur_row, col = t->cur_col;+349
int32_t *r = t->grid + (size_t)row * cols * 4;+350
if (t->insert) {+351
for (int i = cols - 1; i > col; i--)+352
memcpy(r + i * 4, r + (i - 1) * 4, 4 * sizeof(int32_t));+353
}+354
set_cell(r + col * 4, cp, t->attr, t->fg, t->bg);+355
mark_row(t, row);+356
if (col < cols - 1)+357
t->cur_col = col + 1;+358
else if (t->autowrap)+359
t->wrap = 1;+360
}+361
+362
/* bulk-print a run of plain printable chars (ground-state fast path) */+363
static void print_run(Vt *t, const int32_t *cps, int n) {+364
int cols = t->cols;+365
int i = 0;+366
while (i < n) {+367
if (t->wrap) do_wrap(t);+368
int row = t->cur_row, col = t->cur_col;+369
int32_t *r = t->grid + (size_t)row * cols * 4;+370
int k = n - i;+371
if (k > cols - col) k = cols - col;+372
for (int x = 0; x < k; x++)+373
set_cell(r + (col + x) * 4, cps[i + x], t->attr, t->fg, t->bg);+374
mark_row(t, row);+375
int ncol = col + k;+376
if (ncol < cols) {+377
t->cur_col = ncol;+378
} else {+379
t->cur_col = cols - 1;+380
if (t->autowrap) t->wrap = 1;+381
}+382
i += k;+383
}+384
}+385
+386
/* ---- erase / insert / delete --------------------------------------------- */+387
static void fill_row(Vt *t, int row, int from, int to) {+388
int32_t *r = t->grid + (size_t)row * t->cols * 4;+389
for (int i = from; i < to; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);+390
mark_row(t, row);+391
}+392
static void erase_rows(Vt *t, int from, int to) {+393
for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);+394
}+395
static void erase_display(Vt *t, int mode) {+396
if (mode == 0) {+397
fill_row(t, t->cur_row, t->cur_col, t->cols);+398
if (t->cur_row < t->rows - 1) erase_rows(t, t->cur_row + 1, t->rows - 1);+399
} else if (mode == 1) {+400
if (t->cur_row > 0) erase_rows(t, 0, t->cur_row - 1);+401
fill_row(t, t->cur_row, 0, t->cur_col + 1);+402
} else if (mode == 2) {+403
erase_rows(t, 0, t->rows - 1);+404
} else if (mode == 3) {+405
/* clear scrollback: free ring entries */+406
for (int k = 0; k < t->sb_size; k++) {+407
int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;+408
free(t->sb[idx]);+409
t->sb[idx] = NULL;+410
}+411
t->sb_size = 0; t->sb_head = 0;+412
}+413
}+414
static void erase_line(Vt *t, int mode) {+415
if (mode == 0) fill_row(t, t->cur_row, t->cur_col, t->cols);+416
else if (mode == 1) fill_row(t, t->cur_row, 0, t->cur_col + 1);+417
else if (mode == 2) fill_row(t, t->cur_row, 0, t->cols);+418
}+419
static void insert_lines(Vt *t, int n) {+420
if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {+421
int save = t->stop;+422
t->stop = t->cur_row;+423
scroll_down(t, n);+424
t->stop = save;+425
t->cur_col = 0; t->wrap = 0;+426
}+427
}+428
static void delete_lines(Vt *t, int n) {+429
if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {+430
int save = t->stop;+431
t->stop = t->cur_row;+432
scroll_up(t, n, 0);+433
t->stop = save;+434
t->cur_col = 0; t->wrap = 0;+435
}+436
}+437
static void insert_chars(Vt *t, int n) {+438
int cols = t->cols, col = t->cur_col;+439
int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;+440
n = clampi(n, 0, cols - col);+441
for (int i = cols - 1; i >= col + n; i--)+442
memcpy(r + i * 4, r + (i - n) * 4, 4 * sizeof(int32_t));+443
for (int i = col; i < col + n; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);+444
mark_row(t, t->cur_row);+445
}+446
static void delete_chars(Vt *t, int n) {+447
int cols = t->cols, col = t->cur_col;+448
int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;+449
n = clampi(n, 0, cols - col);+450
for (int i = col; i < cols - n; i++)+451
memcpy(r + i * 4, r + (i + n) * 4, 4 * sizeof(int32_t));+452
for (int i = cols - n; i < cols; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);+453
mark_row(t, t->cur_row);+454
}+455
static void erase_chars(Vt *t, int n) {+456
fill_row(t, t->cur_row, t->cur_col, clampi(t->cur_col + n, t->cur_col, t->cols));+457
}+458
+459
/* ---- tabs ---------------------------------------------------------------- */+460
static void tab_forward(Vt *t) {+461
int cols = t->cols;+462
for (int i = t->cur_col + 1; ; i++) {+463
if (i >= cols) { t->cur_col = cols - 1; return; }+464
if (t->tabs[i]) { t->cur_col = i; return; }+465
}+466
}+467
static void tab_back(Vt *t) {+468
for (int i = t->cur_col - 1; ; i--) {+469
if (i <= 0) { t->cur_col = 0; return; }+470
if (t->tabs[i]) { t->cur_col = i; return; }+471
}+472
}+473
static void default_tabs(Vt *t) {+474
memset(t->tabs, 0, t->cols);+475
for (int i = 8; i < t->cols; i += 8) t->tabs[i] = 1;+476
}+477
+478
/* ---- alt screen / cursor save ------------------------------------------- */+479
static void save_cursor(Vt *t) {+480
SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;+481
d->has = 1;+482
d->row = t->cur_row; d->col = t->cur_col;+483
d->attr = t->attr; d->fg = t->fg; d->bg = t->bg;+484
d->origin = t->origin; d->autowrap = t->autowrap;+485
}+486
static void restore_cursor(Vt *t) {+487
SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;+488
if (!d->has) return;+489
t->attr = d->attr; t->fg = d->fg; t->bg = d->bg;+490
t->origin = d->origin; t->autowrap = d->autowrap;+491
set_cursor(t, d->row, d->col);+492
}+493
static void enter_alt(Vt *t) {+494
if (t->alt_active) return;+495
if (!t->alt) t->alt = alloc_grid(t->cols, t->rows, -1);+496
/* alt always starts cleared */+497
for (int r = 0; r < t->rows; r++)+498
fill_blank(t->alt + (size_t)r * t->cols * 4, t->cols, -1);+499
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
@@ -0,0 +1,55 @@
+1
;;; sigil-vt — the native VT/ANSI terminal core.+2
;;;+3
;;; A VT100/xterm-subset terminal emulator in C: an incremental byte stream ->+4
;;; a damage-tracked int32[4]-per-cell grid, with scrollback, cursor-report /+5
;;; device-attribute replies, an out-of-band event queue (title / bell /+6
;;; OSC-52 clipboard), mouse-mode flags, and native style-merged ROW-RUN+7
;;; extraction (the render seam). It is the native reimplementation of slate's+8
;;; (slate term) emulator — the Sigil emulator is the spec; this reproduces its+9
;;; semantics cell-for-cell (test/vt-test.sgl is the ported conformance suite).+10
;;;+11
;;; NO vt-diff (the grid-diff ANSI emitter) — deferred to M3 (sigil-tui revival).+12
+13
(package+14
name: "sigil-vt"+15
version: "0.1.0"+16
sigil: "^0.17"+17
description: "Native VT/ANSI terminal core: parser + int32[4] grid + damage/scrollback/replies/events + row-runs"+18
url: "https://codeberg.org/sigil/sigil-vt"+19
license: "BSD-3-Clause"+20
authors: (list "David Wilson <david@daviwil.com>")+21
+22
dependencies: (list)+23
+24
;; Native test harness needs sigil-test + sigil-test-runner (resolved to the+25
;; local monorepo via dev-redirects.sgl during development).+26
dev-dependencies: (list+27
(from-git url: "codeberg:sigil/sigil" package: "sigil-test" version: "^0.17")+28
(from-git url: "codeberg:sigil/sigil" package: "sigil-test-runner" version: "^0.17")+29
;; sigil-build (pulled transitively by the test-runner to build the native+30
;; harness) imports (sigil version) from the separate sigil-version repo.+31
(from-git url: "codeberg:sigil/sigil-version" package: "sigil-version" version: "^0.16"))+32
+33
;; Native library: the C core + its native-init hook (collected into CLI/test+34
;; entrypoints so %vt-* builtins register at VM startup).+35
libraries: (list+36
(library+37
name: 'sigil-vt+38
c-sources: '("native/vt.c")+39
native-init: "sigil__init_sigil_vt_module"))+40
+41
tasks: (list+42
(task+43
name: 'build+44
description: "Build the sigil-vt native library"+45
steps: (list+46
(compile-c-sources+47
sources: '("native/vt.c")+48
flags: (with-sigil-c-flags '("-std=c99"+49
"-Wall" "-Wextra"+50
"-Wno-unused-parameter"+51
"-D_GNU_SOURCE")))+52
(create-static-library+53
name: "sigil-vt")+54
(compile-sigil-modules+55
sources: "src/**/*.sgl")))))src/sigil/vt.sgladded
@@ -0,0 +1,146 @@
+1
;;; (sigil vt) — the native VT/ANSI terminal core (Scheme surface).+2
;;;+3
;;; A thin wrapper over the %vt-* native builtins (native/vt.c). The emulator+4
;;; is a pure bytes->grid state machine: feed pty output, drain damage /+5
;;; replies / events, read the grid as cells or style-merged row-runs. All+6
;;; state leaves through drained queues; the core never calls back into Scheme.+7
;;;+8
;;; Cells are immutable 4-vectors #(codepoint attr fg bg):+9
;;; attr bitmask (see vt-attr-*); fg/bg: -1 default, 0..255 indexed,+10
;;; #x1000000 + #xRRGGBB truecolor.+11
;;;+12
;;; This is the shared foundation slate's (slate term) façade rides on (M2);+13
;;; the sigil-tui grid façade + vt-diff come in M3.+14
+15
(define-library (sigil vt)+16
(import (sigil core))+17
+18
(export+19
;; lifecycle+20
vt-make vt?+21
vt-feed! vt-feed-bytes!+22
vt-resize! vt-reset! vt-invalidate!+23
;; geometry / cursor / modes+24
vt-cols vt-rows+25
vt-cursor-row vt-cursor-col+26
vt-cursor-visible? vt-cursor-style+27
vt-alt? vt-title+28
vt-bracketed-paste? vt-app-cursor?+29
vt-mouse-flags+30
vt-mouse-1000 vt-mouse-1002 vt-mouse-1003 vt-mouse-1006 vt-mouse-1007+31
;; pending replies (DSR/DA) the consumer writes back to the pty+32
vt-take-output!+33
;; damage (drained by the renderer)+34
vt-take-damage! vt-damaged?+35
;; out-of-band events (title / bell / OSC-52 clipboard)+36
vt-take-events!+37
;; grid access+38
vt-row-cells vt-row-text+39
vt-scrollback-count vt-scrollback-row+40
;; the render seam: style-merged row runs #(text attr fg bg cursor?)+41
vt-row-runs vt-scrollback-runs+42
;; cells+43
vt-cell-ch vt-cell-attr vt-cell-fg vt-cell-bg vt-blank-cell+44
vt-attr-bold vt-attr-dim vt-attr-italic vt-attr-underline+45
vt-attr-blink vt-attr-inverse vt-attr-hidden vt-attr-strike+46
vt-attr-wide vt-attr-continuation+47
;; 256-color palette (index -> #xRRGGBB), for renderers+48
vt-color-256->rgb)+49
+50
(begin+51
+52
;; ---- attribute bits (slate's superset) --------------------------------+53
(define vt-attr-bold 1)+54
(define vt-attr-dim 2)+55
(define vt-attr-italic 4)+56
(define vt-attr-underline 8)+57
(define vt-attr-blink 16)+58
(define vt-attr-inverse 32)+59
(define vt-attr-hidden 64)+60
(define vt-attr-strike 128)+61
;; reserved for CJK double-width (wcwidth deferred to M3/M4)+62
(define vt-attr-wide 256)+63
(define vt-attr-continuation 512)+64
+65
;; ---- mouse-mode flag bits (DECSET) ------------------------------------+66
(define vt-mouse-1000 1)+67
(define vt-mouse-1002 2)+68
(define vt-mouse-1003 4)+69
(define vt-mouse-1006 8)+70
(define vt-mouse-1007 16)+71
+72
;; ---- cells (immutable 4-vectors) --------------------------------------+73
(define (vt-cell-ch c) (vector-ref c 0)) ; a CODEPOINT (int)+74
(define (vt-cell-attr c) (vector-ref c 1))+75
(define (vt-cell-fg c) (vector-ref c 2))+76
(define (vt-cell-bg c) (vector-ref c 3))+77
(define vt-blank-cell (vector 32 0 -1 -1))+78
+79
;; ---- lifecycle --------------------------------------------------------+80
(define (vt-make cols rows . rest)+81
(if (pair? rest)+82
(%vt-make cols rows (car rest))+83
(%vt-make cols rows 1000)))+84
(define (vt? x) (%vt? x))+85
(define (vt-feed! t s) (%vt-feed! t s))+86
(define (vt-feed-bytes! t bytes) (%vt-feed-bytes! t bytes))+87
(define (vt-resize! t cols rows) (%vt-resize! t cols rows))+88
(define (vt-reset! t) (%vt-reset! t))+89
(define (vt-invalidate! t) (%vt-invalidate! t))+90
+91
;; ---- accessors --------------------------------------------------------+92
(define (vt-cols t) (%vt-cols t))+93
(define (vt-rows t) (%vt-rows t))+94
(define (vt-cursor-row t) (%vt-cursor-row t))+95
(define (vt-cursor-col t) (%vt-cursor-col t))+96
(define (vt-cursor-visible? t) (%vt-cursor-visible? t))+97
(define (vt-cursor-style t) (%vt-cursor-style t))+98
(define (vt-alt? t) (%vt-alt? t))+99
(define (vt-title t) (%vt-title t))+100
(define (vt-bracketed-paste? t) (%vt-bracketed-paste? t))+101
(define (vt-app-cursor? t) (%vt-app-cursor? t))+102
(define (vt-mouse-flags t) (%vt-mouse-flags t))+103
+104
;; ---- pending replies --------------------------------------------------+105
(define (vt-take-output! t) (%vt-take-output! t))+106
+107
;; ---- damage -----------------------------------------------------------+108
;; Drain -> #{ all?: bool rows: (ascending list of dirty row indices) }.+109
;; rows is '() when all? is #t (the renderer repaints everything).+110
(define (vt-take-damage! t)+111
(let* ((v (%vt-take-damage! t))+112
(all? (vector-ref v 0)))+113
(if all?+114
#{ all?: #t rows: '() }+115
(let loop ((i (- (vector-length v) 1)) (acc '()))+116
(if (< i 1)+117
#{ all?: #f rows: acc }+118
(loop (- i 1) (cons (vector-ref v i) acc)))))))+119
(define (vt-damaged? t) (%vt-damaged? t))+120
+121
;; ---- events -----------------------------------------------------------+122
;; Drain -> a list of #{ type: <'title|'bell|'clipboard> payload: <string|#f> }+123
;; in arrival order. Title also updates vt-title; OSC-52 clipboard is queued+124
;; only (never acted on — the consumer decides).+125
(define (vt-take-events! t)+126
(map (lambda (e)+127
#{ type: (vector-ref e 0) payload: (vector-ref e 1) })+128
(%vt-take-events! t)))+129
+130
;; ---- grid access ------------------------------------------------------+131
(define (vt-row-cells t i) (%vt-row-cells t i))+132
(define (vt-row-text t i) (%vt-row-text t i))+133
(define (vt-scrollback-count t) (%vt-scrollback-count t))+134
(define (vt-scrollback-row t k) (%vt-scrollback-row t k))+135
+136
;; ---- the render seam --------------------------------------------------+137
;; Style-merged, right-trimmed runs for a visible row (or a scrollback row).+138
;; cursor-col (optional) forces a run break at that cell. Each run is+139
;; #(text attr fg bg cursor?).+140
(define (vt-row-runs t i . rest)+141
(if (pair? rest) (%vt-row-runs t i (car rest)) (%vt-row-runs t i #f)))+142
(define (vt-scrollback-runs t k . rest)+143
(if (pair? rest) (%vt-scrollback-runs t k (car rest)) (%vt-scrollback-runs t k #f)))+144
+145
;; ---- palette ----------------------------------------------------------+146
(define (vt-color-256->rgb i) (%vt-color-256->rgb i))))test/vt-test.sgladded
@@ -0,0 +1,433 @@
+1
;;; Conformance suite for (sigil vt) — the native VT core.+2
;;;+3
;;; A faithful port of slate's test/term-test.sgl (the emulator's behavior IS+4
;;; the spec). Each fixture feeds a byte/escape sequence into a fresh emulator+5
;;; and asserts the resulting grid text, cursor, attrs, or mode flags. Covers+6
;;; the MUST tier: C0, CSI cursor/erase/insert/delete/scroll-region, SGR+7
;;; 16/256/truecolor (+ colon sub-params), alt-screen, autowrap (incl. the+8
;;; deferred-wrap edge), tabs, DECSC/DECRC, origin mode, DECALN, OSC title,+9
;;; DECSCUSR, bracketed paste, DSR/DA replies, scrollback, resize, incremental+10
;;; UTF-8 across chunk boundaries, the ground-state bulk-run fast path, the+11
;;; trust boundary (hostile input), plus the native additions (events, mouse+12
;;; flags, row-runs).+13
+14
(import (sigil core)+15
(sigil test)+16
(sigil vt))+17
+18
;; ---- helpers --------------------------------------------------------------+19
(define ESC "\x1b;")+20
(define (csi . parts) (apply string-append ESC "[" parts))+21
+22
(define (vt* cols rows . feeds)+23
(let ((t (vt-make cols rows)))+24
(for-each (lambda (s) (vt-feed! t s)) feeds)+25
t))+26
+27
(define (rtrim s)+28
(let loop ((i (string-length s)))+29
(cond ((= i 0) "")+30
((char=? (string-ref s (- i 1)) #\space) (loop (- i 1)))+31
(else (substring s 0 i)))))+32
(define (rows-of t)+33
(let loop ((i (- (vt-rows t) 1)) (acc '()))+34
(if (< i 0) acc (loop (- i 1) (cons (rtrim (vt-row-text t i)) acc)))))+35
(define (cursor-of t) (list (vt-cursor-row t) (vt-cursor-col t)))+36
(define (cell-at t row col)+37
(let ((c (vector-ref (vt-row-cells t row) col)))+38
(list (string (integer->char (vt-cell-ch c)))+39
(vt-cell-attr c) (vt-cell-fg c) (vt-cell-bg c))))+40
+41
;; ==========================================================================+42
;; plain text, C0, wrapping+43
;; ==========================================================================+44
(test-group "print / C0 / wrap"+45
(test "print: text lands on row 0"+46
(let ((t (vt* 10 3 "hello")))+47
(assert-equal (list "hello" "" "") (rows-of t))+48
(assert-equal (list 0 5) (cursor-of t))))+49
(test "CRLF: second line"+50
(let ((t (vt* 10 3 "ab\r\ncd")))+51
(assert-equal (list "ab" "cd" "") (rows-of t))+52
(assert-equal (list 1 2) (cursor-of t))))+53
(test "CR overprint"+54
(assert-equal (list "Xbc" "" "") (rows-of (vt* 10 3 "abc\rX"))))+55
(test "BS then overprint"+56
(assert-equal (list "aX" "" "") (rows-of (vt* 10 3 "ab\x08;X"))))+57
(test "autowrap wraps"+58
(let ((t (vt* 10 3 "0123456789AB")))+59
(assert-equal (list "0123456789" "AB" "") (rows-of t))+60
(assert-equal (list 1 2) (cursor-of t))))+61
(test "pending wrap: cursor stays on last col, CR cancels"+62
(let ((t (vt* 10 3 "0123456789")))+63
(assert-equal (list 0 9) (cursor-of t))+64
(vt-feed! t "\rX")+65
(assert-equal (list "X123456789" "" "") (rows-of t))))+66
(test "DECAWM off: no wrap"+67
(assert-equal (list "012345678B" "" "")+68
(rows-of (vt* 10 3 (csi "?7l") "0123456789AB"))))+69
(test "LF at bottom scrolls; evicted row to scrollback"+70
(let ((t (vt* 5 2 "aa\r\nbb\r\ncc")))+71
(assert-equal (list "bb" "cc") (rows-of t))+72
(assert-equal 1 (vt-scrollback-count t))+73
(assert-true (= (vt-cell-ch (vector-ref (vt-scrollback-row t 0) 0)) 97))))+74
(test "tab to col 8"+75
(assert-equal (list "a b" "") (rows-of (vt* 20 2 "a\tb")))))+76
+77
;; ==========================================================================+78
;; cursor movement+79
;; ==========================================================================+80
(test-group "cursor movement"+81
(test "CUP 3;4"+82
(assert-equal (list "" "" " X" "" "") (rows-of (vt* 10 5 (csi "3;4H") "X"))))+83
(test "CUU + CUB"+84
(assert-equal (list "" " X" "" "" "")+85
(rows-of (vt* 10 5 (csi "3;4H") (csi "A") (csi "2D") "X"))))+86
(test "CUP clamps"+87
(let ((t (vt* 10 5 "abc" (csi "10;20H") "Z")))+88
(assert-equal (list "abc" "" "" "" " Z") (rows-of t))+89
(assert-equal (list 4 9) (cursor-of t))))+90
(test "CUD + CUF"+91
(assert-equal (list 4 4)+92
(cursor-of (vt* 10 5 (csi "2;2H") (csi "3B") (csi "2C") "X"))))+93
(test "CHA column"+94
(assert-equal (list "hi X" "" "" "" "") (rows-of (vt* 10 5 "hi" (csi "5G") "X"))))+95
(test "VPA row keeps col"+96
(assert-equal (list "hi" "" " X" "" "") (rows-of (vt* 10 5 "hi" (csi "3d") "X")))))+97
+98
;; ==========================================================================+99
;; erase / insert / delete+100
;; ==========================================================================+101
(test-group "erase / insert / delete"+102
(test "EL 0: erase to right"+103
(assert-equal (list "abc" "" "") (rows-of (vt* 10 3 "abcdef" (csi "4G") (csi "K")))))+104
(test "EL 1: erase to left (incl cursor)"+105
(assert-equal (list " ef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "4G") (csi "1K")))))+106
(test "EL 2: whole line"+107
(assert-equal (list "" "" "") (rows-of (vt* 10 3 "abcdef" (csi "2K")))))+108
(test "ED 0: erase below"+109
(assert-equal (list "aaaaaa" "bb" "")+110
(rows-of (vt* 6 3 "aaaaaa\r\nbbbbbb\r\ncccccc" (csi "2;3H") (csi "J")))))+111
(test "ED 1: erase above"+112
(assert-equal (list "" " bbb" "cccccc")+113
(rows-of (vt* 6 3 "aaaaaa\r\nbbbbbb\r\ncccccc" (csi "2;3H") (csi "1J")))))+114
(test "ED 2: erase all + cursor left on last col"+115
(let ((t (vt* 6 3 "aaaaaa\r\nbbbbbb" (csi "2J"))))+116
(assert-equal (list "" "" "") (rows-of t))+117
(assert-equal (list 1 5) (cursor-of t))))+118
(test "ICH inserts blanks (then overtyped)"+119
(assert-equal (list "abXYcdef" "" "")+120
(rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2@") "XY"))))+121
(test "DCH deletes chars"+122
(assert-equal (list "abef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2P")))))+123
(test "ECH erases chars in place"+124
(assert-equal (list "ab ef" "" "") (rows-of (vt* 10 3 "abcdef" (csi "3G") (csi "2X")))))+125
(test "IL inserts a line"+126
(assert-equal (list "a" "" "b" "c")+127
(rows-of (vt* 5 4 "a\r\nb\r\nc\r\nd" (csi "2;1H") (csi "L")))))+128
(test "DL deletes a line"+129
(assert-equal (list "a" "c" "d" "")+130
(rows-of (vt* 5 4 "a\r\nb\r\nc\r\nd" (csi "2;1H") (csi "M"))))))+131
+132
;; ==========================================================================+133
;; scroll region+134
;; ==========================================================================+135
(test-group "scroll region"+136
(test "DECSTBM: region scrolls, top intact"+137
(assert-equal (list "top" "l2" "l3" "l4" "")+138
(rows-of (vt* 5 5 "top" (csi "2;4r") (csi "2;1H") "l1\r\nl2\r\nl3\r\nl4"))))+139
(test "SU in region"+140
(assert-equal (list "a" "d" "" "" "e")+141
(rows-of (vt* 5 5 "a\r\nb\r\nc\r\nd\r\ne" (csi "2;4r") (csi "2S")))))+142
(test "SD in region"+143
(assert-equal (list "a" "" "b" "c" "e")+144
(rows-of (vt* 5 5 "a\r\nb\r\nc\r\nd\r\ne" (csi "2;4r") (csi "1T")))))+145
(test "RI at region top"+146
(assert-equal (list "a" "" "b" "c" "")+147
(rows-of (vt* 5 5 "a\r\nb\r\nc" (csi "2;4r") (csi "2;1H") ESC "M"))))+148
(test "non-top region scroll: no scrollback"+149
(assert-equal 0 (vt-scrollback-count+150
(vt* 5 5 "x\r\ny" (csi "2;4r") (csi "2;1H") "1\r\n2\r\n3\r\n4"))))+151
(test "DECOM: home is region top"+152
(assert-equal (list "" "X" "" "" "")+153
(rows-of (vt* 10 5 (csi "2;4r") (csi "?6h") (csi "1;1H") "X")))))+154
+155
;; ==========================================================================+156
;; SGR+157
;; ==========================================================================+158
(test-group "SGR"+159
(test "SGR bold red + reset"+160
(let ((t (vt* 10 2 (csi "1;31m") "R" (csi "0m") "p")))+161
(assert-equal (list "R" vt-attr-bold 1 -1) (cell-at t 0 0))+162
(assert-equal (list "p" 0 -1 -1) (cell-at t 0 1))))+163
(test "SGR italic+underline+inverse"+164
(assert-equal (list "x" (+ vt-attr-italic vt-attr-underline vt-attr-inverse) -1 -1)+165
(cell-at (vt* 10 2 (csi "3;4;7m") "x") 0 0)))+166
(test "SGR 256-color fg/bg"+167
(assert-equal (list "c" 0 196 22)+168
(cell-at (vt* 10 2 (csi "38;5;196m") (csi "48;5;22m") "c") 0 0)))+169
(test "SGR truecolor fg"+170
(assert-equal (list "t" 0 (+ #x1000000 (* 255 65536) (* 128 256) 0) -1)+171
(cell-at (vt* 10 2 (csi "38;2;255;128;0m") "t") 0 0)))+172
(test "SGR colon syntax"+173
(assert-equal (list "Q" 0 99 -1) (cell-at (vt* 10 2 ESC "[38:5:99mQ") 0 0)))+174
(test "SGR colon colorspace form + following param"+175
(assert-equal (list "W" vt-attr-underline (+ #x1000000 (* 255 65536) (* 128 256) 0) -1)+176
(cell-at (vt* 10 2 ESC "[38:2::255:128:0;4mW") 0 0)))+177
(test "SGR colon truecolor (no colorspace)"+178
(assert-equal (list "V" 0 (+ #x1000000 (* 10 65536) (* 20 256) 30) -1)+179
(cell-at (vt* 10 2 ESC "[38:2:10:20:30mV") 0 0)))+180
(test "SGR bright fg + 39 default"+181
(let ((t (vt* 10 2 (csi "91m") "b" (csi "39m") "d")))+182
(assert-equal (list "b" 0 9 -1) (cell-at t 0 0))+183
(assert-equal (list "d" 0 -1 -1) (cell-at t 0 1))))+184
(test "SGR 22 clears bold, keeps color"+185
(assert-equal (list "b" 0 1 -1)+186
(cell-at (vt* 10 2 (csi "1;31m") "a" (csi "22m") "b") 0 1)))+187
(test "BCE: ED fills with cur bg"+188
(assert-equal 19 (vt-cell-bg (vector-ref (vt-row-cells (vt* 4 2 (csi "48;5;19m") (csi "2J")) 1) 3))))+189
(test "palette 16 = cube 0,0,0" (assert-equal 0 (vt-color-256->rgb 16)))+190
(test "palette 196 = red" (assert-equal #xff0000 (vt-color-256->rgb 196)))+191
(test "palette 231 = white" (assert-equal #xffffff (vt-color-256->rgb 231)))+192
(test "palette 244 gray" (assert-equal #x808080 (vt-color-256->rgb 244))))+193
+194
;; ==========================================================================+195
;; alt screen+196
;; ==========================================================================+197
(test-group "alt screen"+198
(test "1049: alt starts cleared / restores main + cursor"+199
(let ((t (vt* 10 3 "main" (csi "?1049h") (csi "1;1H") "ALT")))+200
(assert-equal (list "ALT" "" "") (rows-of t))+201
(assert-true (vt-alt? t))+202
(vt-feed! t (csi "?1049l"))+203
(assert-equal (list "main" "" "") (rows-of t))+204
(assert-equal (list 0 4) (cursor-of t))+205
(assert-true (not (vt-alt? t)))))+206
(test "alt: no scrollback"+207
(assert-equal 0 (vt-scrollback-count (vt* 5 2 (csi "?1049h") "a\r\nb\r\nc\r\nd")))))+208
+209
;; ==========================================================================+210
;; DECSC/DECRC, DECALN, RIS+211
;; ==========================================================================+212
(test-group "DECSC/DECRC, DECALN, RIS"+213
(test "DECSC/DECRC restores pos + SGR"+214
(let ((t (vt* 10 3 (csi "31m") (csi "2;3H") ESC "7" (csi "0m") (csi "1;1H") ESC "8" "X")))+215
(assert-equal (list "" " X" "") (rows-of t))+216
(assert-equal (list "X" 0 1 -1) (cell-at t 1 2))))+217
(test "DECALN fills E"+218
(assert-equal (list "EEE" "EEE") (rows-of (vt* 3 2 ESC "#8"))))+219
(test "RIS clears + resets SGR"+220
(let ((t (vt* 5 2 "hi" (csi "31m") ESC "c" "x")))+221
(assert-equal (list "x" "") (rows-of t))+222
(assert-equal (list "x" 0 -1 -1) (cell-at t 0 0)))))+223
+224
;; ==========================================================================+225
;; OSC title, DECSCUSR, bracketed paste, modes, events+226
;; ==========================================================================+227
(test-group "OSC / modes / events"+228
(test "OSC 0 BEL: title"+229
(let ((t (vt* 10 2 ESC "]0;my title\x07;" "x")))+230
(assert-equal "my title" (vt-title t))+231
(assert-equal (list "x" "") (rows-of t))))+232
(test "OSC 2 ST: title"+233
(let ((t (vt* 10 2 ESC "]2;st title" ESC "\\" "y")))+234
(assert-equal "st title" (vt-title t))+235
(assert-equal (list "y" "") (rows-of t))))+236
(test "OSC 52 (clipboard) not in title, queued as event"+237
(let ((t (vt* 10 2 ESC "]52;c;aGVsbG8=\x07;" "z")))+238
(assert-equal "" (vt-title t))+239
(assert-equal (list "z" "") (rows-of t))+240
(let ((evs (vt-take-events! t)))+241
(assert-true (memv 'clipboard (map (lambda (e) (dict-ref e type: #f)) evs))))))+242
(test "DECSCUSR style" (assert-equal 4 (vt-cursor-style (vt* 10 2 (csi "4 q")))))+243
(test "bracketed paste on" (assert-true (vt-bracketed-paste? (vt* 10 2 (csi "?2004h")))))+244
(test "bracketed paste off"+245
(assert-true (not (vt-bracketed-paste? (vt* 10 2 (csi "?2004h") (csi "?2004l"))))))+246
(test "cursor hidden" (assert-true (not (vt-cursor-visible? (vt* 10 2 (csi "?25l"))))))+247
(test "app cursor mode" (assert-true (vt-app-cursor? (vt* 10 2 (csi "?1h")))))+248
(test "IRM: chars shift right"+249
(assert-equal (list "aXYbc" "") (rows-of (vt* 10 2 "abc" (csi "2G") (csi "4h") "XY"))))+250
(test "mouse-mode flags parsed"+251
(assert-equal (+ vt-mouse-1002 vt-mouse-1006)+252
(vt-mouse-flags (vt* 10 2 (csi "?1002h") (csi "?1006h")))))+253
(test "mouse-mode flags cleared"+254
(assert-equal vt-mouse-1006+255
(vt-mouse-flags (vt* 10 2 (csi "?1002h") (csi "?1006h") (csi "?1002l"))))))+256
+257
;; ==========================================================================+258
;; replies (DSR / DA)+259
;; ==========================================================================+260
(test-group "replies"+261
(test "DSR 6: cursor report + drain"+262
(let ((t (vt* 10 5 (csi "3;4H") (csi "6n"))))+263
(assert-equal (string-append ESC "[3;4R") (vt-take-output! t))+264
(assert-equal "" (vt-take-output! t))))+265
(test "DSR 5: status ok"+266
(assert-equal (string-append ESC "[0n") (vt-take-output! (vt* 10 5 (csi "5n")))))+267
(test "DA reply"+268
(assert-equal (string-append ESC "[?6c") (vt-take-output! (vt* 10 5 (csi "c"))))))+269
+270
;; ==========================================================================+271
;; the trust boundary+272
;; ==========================================================================+273
(test-group "trust boundary"+274
(test "huge params clamp"+275
(let ((t (vt* 10 2 ESC "[999999999999H" "ok")))+276
(assert-equal (list 1 2) (cursor-of t))+277
(assert-equal (list "" "ok") (rows-of t))))+278
(test "truncated SGR ignored"+279
(assert-equal (list "x" 0 -1 -1) (cell-at (vt* 10 2 ESC "[38;2m" "x") 0 0)))+280
(test "DCS swallowed"+281
(assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "P malicious dcs payload " ESC "\\" "ok"))))+282
(test "CHT clamps"+283
(assert-equal (list 0 9) (cursor-of (vt* 10 2 ESC "[999999999I" "x"))))+284
(test "unknown modes/charsets swallowed"+285
(assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "[?9999h" ESC "[<5m" ESC "(0" "ok"))))+286
(test "escape split across chunks"+287
(let ((t (vt-make 10 2)))+288
(vt-feed! t ESC) (vt-feed! t "[3") (vt-feed! t "1mX")+289
(assert-equal (list "X" 0 1 -1) (cell-at t 0 0)))))+290
+291
;; ==========================================================================+292
;; incremental UTF-8 (byte feed)+293
;; ==========================================================================+294
(test-group "incremental UTF-8"+295
(test "utf-8 across chunks"+296
(let ((t (vt-make 10 2)))+297
(vt-feed-bytes! t (list 97 195))+298
(vt-feed-bytes! t (list 169 32 226 134))+299
(vt-feed-bytes! t (list 146))+300
(assert-equal "aé → " (vt-row-text t 0))))+301
(test "invalid utf-8 -> U+FFFD, stream recovers"+302
(let ((t (vt-make 10 2)))+303
(vt-feed-bytes! t (list 195 195 169))+304
(assert-true (and (= (vt-cell-ch (vector-ref (vt-row-cells t 0) 0)) 65533)+305
(= (vt-cell-ch (vector-ref (vt-row-cells t 0) 1)) 233)))))+306
(test "surrogate bytes -> U+FFFD"+307
(let ((t (vt-make 10 2)))+308
(vt-feed-bytes! t (list 237 160 128))+309
(assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0)))))+310
(test "past-U+10FFFF -> U+FFFD"+311
(let ((t (vt-make 10 2)))+312
(vt-feed-bytes! t (list 244 144 128 128))+313
(assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0)))))+314
(test "overlong encoding -> U+FFFD"+315
(let ((t (vt-make 10 2)))+316
(vt-feed-bytes! t (list 224 128 168))+317
(assert-equal 65533 (vt-cell-ch (vector-ref (vt-row-cells t 0) 0)))))+318
(test "DL never scrollbacks"+319
(let ((t (vt* 5 3 "a\r\nb\r\nc" (csi "1;1H") (csi "M"))))+320
(assert-equal 0 (vt-scrollback-count t))+321
(assert-equal (list "b" "c" "") (rows-of t)))))+322
+323
;; ==========================================================================+324
;; damage tracking+325
;; ==========================================================================+326
(test-group "damage"+327
(test "damage: one row / drained"+328
(let ((t (vt-make 10 3)))+329
(vt-take-damage! t)+330
(vt-feed! t "x")+331
(let ((d (vt-take-damage! t)))+332
(assert-equal (list 0) (dict-ref d rows: '()))+333
(assert-true (not (dict-ref d all?: #f))))+334
(assert-equal '() (dict-ref (vt-take-damage! t) rows: '()))))+335
(test "ED marks all its rows"+336
(let ((t (vt-make 10 3)))+337
(vt-take-damage! t)+338
(vt-feed! t (csi "2J"))+339
(assert-equal (list 0 1 2) (dict-ref (vt-take-damage! t) rows: '()))))+340
(test "scroll damages the region"+341
(let ((t (vt-make 5 2)))+342
(vt-take-damage! t)+343
(vt-feed! t "a\r\nb\r\nc")+344
(let ((d (vt-take-damage! t)))+345
(assert-true (or (dict-ref d all?: #f)+346
(equal? (dict-ref d rows: '()) (list 0 1))))))))+347
+348
;; ==========================================================================+349
;; resize+350
;; ==========================================================================+351
(test-group "resize"+352
(test "narrower truncates / wider pads / cols updated"+353
(let ((t (vt* 10 4 "aa\r\nbb")))+354
(vt-resize! t 5 4)+355
(assert-equal (list "aa" "bb" "" "") (rows-of t))+356
(vt-resize! t 20 4)+357
(assert-equal (list "aa" "bb" "" "") (rows-of t))+358
(assert-equal 20 (vt-cols t))))+359
(test "shrink drops blank bottom rows (no scrollback)"+360
(let ((t (vt* 10 5 "aa\r\nbb")))+361
(vt-resize! t 10 3)+362
(assert-equal (list "aa" "bb" "") (rows-of t))+363
(assert-equal 0 (vt-scrollback-count t))))+364
(test "shrink onto content keeps cursor rows, evicts to scrollback"+365
(let ((t (vt* 10 4 "a\r\nb\r\nc\r\nd")))+366
(vt-resize! t 10 2)+367
(assert-equal (list "c" "d") (rows-of t))+368
(assert-equal 2 (vt-scrollback-count t))+369
(assert-equal (list 1 1) (cursor-of t))))+370
(test "grow pulls back out of scrollback"+371
(let ((t (vt* 10 2 "a\r\nb\r\nc")))+372
(assert-equal 1 (vt-scrollback-count t))+373
(vt-resize! t 10 4)+374
(assert-equal (list "a" "b" "c" "") (rows-of t))+375
(assert-equal 0 (vt-scrollback-count t))))+376
(test "post-resize feed is sane"+377
(let ((t (vt* 10 4 (csi "2;3r") "x")))+378
(vt-resize! t 8 3)+379
(vt-feed! t (string-append (csi "3;1H") "\n\n"))+380
(assert-true (>= (vt-rows t) 3)))))+381
+382
;; ==========================================================================+383
;; the ground-state bulk-run fast path+384
;; ==========================================================================+385
(test-group "bulk-run fast path"+386
(test "run wraps across the margin"+387
(let ((t (vt* 5 3 "abcdefgh")))+388
(assert-equal (list "abcde" "fgh" "") (rows-of t))+389
(assert-equal (list 1 3) (cursor-of t))))+390
(test "run deferred wrap parks / fires next char"+391
(let ((t (vt* 5 3 "abcde")))+392
(assert-equal (list 0 4) (cursor-of t))+393
(vt-feed! t "f")+394
(assert-equal (list "abcde" "f" "") (rows-of t))))+395
(test "run no-autowrap overwrites last col"+396
(let ((t (vt* 5 3 (csi "?7l") "abcdefgh")))+397
(assert-equal (list "abcdh" "" "") (rows-of t))+398
(assert-equal (list 0 4) (cursor-of t))))+399
(test "esc splits runs, text intact"+400
(assert-equal (list "abcdef" "" "")+401
(rows-of (vt* 10 3 (string-append "ab" (csi "31m") "cd" (csi "0m") "ef")))))+402
(test "insert mode shifts, not overwrites"+403
(assert-equal (list "XYabc" "" "")+404
(rows-of (vt* 10 3 "abc" (csi "1;1H") (csi "4h") "XY"))))+405
(test "mixed unicode intact"+406
(assert-equal (list "aλbμc" "" "") (rows-of (vt* 10 3 "aλbμc")))))+407
+408
;; ==========================================================================+409
;; row-runs (the render seam) — new native primitive+410
;; ==========================================================================+411
(test-group "row-runs"+412
(test "plain run merges to one, right-trimmed"+413
(let* ((t (vt* 10 2 "hello"))+414
(runs (vt-row-runs t 0)))+415
(assert-equal 1 (length runs))+416
(assert-equal "hello" (vector-ref (car runs) 0))+417
(assert-equal 0 (vector-ref (car runs) 1))))+418
(test "style change splits runs"+419
(let* ((t (vt* 10 2 "ab" (csi "31m") "cd"))+420
(runs (vt-row-runs t 0)))+421
(assert-equal 2 (length runs))+422
(assert-equal "ab" (vector-ref (car runs) 0))+423
(assert-equal "cd" (vector-ref (cadr runs) 0))+424
(assert-equal 1 (vector-ref (cadr runs) 2))))+425
(test "cursor-col forces a break with cursor? flag"+426
(let* ((t (vt* 10 2 "hello"))+427
(runs (vt-row-runs t 0 1)))+428
;; "h" | "e"(cursor) | "llo"+429
(assert-equal 3 (length runs))+430
(assert-true (vector-ref (cadr runs) 4))+431
(assert-equal "e" (vector-ref (cadr runs) 0))))+432
(test "blank row -> no runs"+433
(assert-equal '() (vt-row-runs (vt-make 10 2) 1))))