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 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.

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 <[email protected]>")
+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))))