AtlatestRepositorysigil-vt
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 */
27/* VT_FUZZ builds the pure C emulator core WITHOUT the Sigil VM glue, so the
28 * standalone ASan/UBSan fuzz driver (native/vt-fuzz.c) can #include this file
29 * and exercise the parser directly. Everything that touches the Sigil runtime
30 * is guarded out under VT_FUZZ. */
31#ifndef VT_FUZZ
32#include <sigil/sigil.h>
33#endif
34#include <stdint.h>
35#include <stdlib.h>
36#include <string.h>
37#include <stdio.h>
39#ifndef VT_FUZZ
40/* ---- internal libsigil helpers (exported from libsigil; not in the public
41 * header). Declared extern exactly as sigil-wasm-dom does. -------------------*/
42extern void *sigil__gc_alloc(SigilVM *vm, SigilObjType type, size_t size);
43extern void sigil__gc_push_temp_root(SigilVM *vm, Value v);
44extern void sigil__gc_pop_temp_root(SigilVM *vm);
46#define SIGIL_EXPORT __attribute__((visibility("default")))
47#endif
49/* ---- attribute bits (slate's superset wins) ------------------------------ */
50enum {
51 VT_ATTR_BOLD = 1,
52 VT_ATTR_DIM = 2,
53 VT_ATTR_ITALIC = 4,
54 VT_ATTR_UNDERLINE = 8,
55 VT_ATTR_BLINK = 16,
56 VT_ATTR_INVERSE = 32,
57 VT_ATTR_HIDDEN = 64,
58 VT_ATTR_STRIKE = 128,
59 /* reserved for CJK double-width (wcwidth lands later) */
60 VT_ATTR_WIDE = 256,
61 VT_ATTR_CONT = 512
62};
64/* ---- mouse-mode flags (DECSET; parsed as flags in v1) -------------------- */
65enum {
66 VT_MOUSE_1000 = 1, /* X10/normal button tracking */
67 VT_MOUSE_1002 = 2, /* button-event (drag) tracking */
68 VT_MOUSE_1003 = 4, /* any-event (motion) tracking */
69 VT_MOUSE_1006 = 8, /* SGR extended coordinates */
70 VT_MOUSE_1007 = 16 /* alternate scroll */
71};
73/* ---- out-of-band event types --------------------------------------------- */
74enum { VT_EV_TITLE = 1, VT_EV_BELL = 2, VT_EV_CLIPBOARD = 3 };
76/* ---- parser states ------------------------------------------------------- */
77enum {
78 ST_GROUND = 0, ST_ESC, ST_ESC_SKIP1, ST_ESC_HASH,
79 ST_CSI, ST_OSC, ST_OSC_ESC, ST_STR_IGNORE, ST_STR_ESC
80};
82/* ---- fixed capacities (the closed-vocabulary discipline) ----------------- */
83#define VT_MAX_GROUPS 32
84#define VT_MAX_SUB 8
85#define VT_INTER_MAX 8
86#define VT_OSC_MAX 1024
87#define VT_PARAM_CAP 999999999
89typedef struct {
90 int has;
91 int row, col;
92 int32_t attr, fg, bg;
93 int origin, autowrap;
94} SavedCursor;
96typedef struct {
97 int type;
98 char *payload; /* malloc'd, may be NULL (bell) */
99 int payload_len;
100} VtEvent;
102typedef struct {
103 int cols, rows;
104 int32_t *grid; /* ACTIVE grid (points at main or alt) */
105 int32_t *main; /* main-screen buffer */
106 int32_t *alt; /* alt-screen buffer (NULL until first enter) */
107 int alt_active;
109 int cur_row, cur_col;
110 int wrap; /* pending (deferred) autowrap */
111 int32_t attr, fg, bg; /* current SGR */
112 int stop, sbot; /* scroll region, 0-based inclusive */
114 SavedCursor saved_main, saved_alt;
116 /* parser */
117 int state;
118 int32_t groups[VT_MAX_GROUPS][VT_MAX_SUB]; /* completed groups */
119 int group_len[VT_MAX_GROUPS];
120 int ngroups;
121 int cur_digits; /* -1 = none */
122 int32_t cur_sub[VT_MAX_SUB]; /* completed sub-params of current group */
123 int cur_sub_len;
124 int prefix; /* private marker char or 0 */
125 char inter[VT_INTER_MAX + 1];
126 int inter_len;
127 char osc[VT_OSC_MAX];
128 int osc_len;
130 /* modes */
131 int autowrap, origin, curvis, bracket, appcur, insert;
132 int mouse; /* VT_MOUSE_* bitmask */
134 uint8_t *tabs; /* length cols */
135 char *title; int title_len, title_cap;
136 int curstyle; /* DECSCUSR 0..6 */
138 /* scrollback ring: index 0 = newest */
139 int32_t **sb; /* each entry: sb_w[i]*4 int32 */
140 int *sb_w; /* WIDTH each entry was pushed at.
141 * A history row keeps the width it
142 * had when it scrolled off (xterm
143 * no-rewrap), which is NOT t->cols
144 * after a resize. The interpreted
145 * reference got this for free —
146 * its rows are Scheme vectors that
147 * carry their own length. Porting
148 * to raw int32_t* dropped it, so
149 * every reader guessed, and a
150 * widening resize read off the end
151 * of the allocation. Never read an
152 * entry with anything but its
153 * sb_w. */
154 int sb_head; /* index of newest */
155 int sb_size;
156 int sb_cap; /* == sbmax */
158 uint8_t *dirty; /* length rows */
159 int alldirty;
161 char *out; int out_len, out_cap; /* pending replies */
163 int u8need; int32_t u8acc; int32_t u8min; /* incremental UTF-8 */
165 VtEvent *events; int nevents, events_cap; /* out-of-band event queue */
166} Vt;
168/* ======================================================================== */
169/* small utilities */
170/* ======================================================================== */
172static int clampi(int x, int lo, int hi) {
173 return x < lo ? lo : (x > hi ? hi : x);
176static void set_cell(int32_t *c, int32_t cp, int32_t attr, int32_t fg, int32_t bg) {
177 c[0] = cp; c[1] = attr; c[2] = fg; c[3] = bg;
180/* fill a fresh row buffer with blank cells (space, default, current bg) */
181static void fill_blank(int32_t *row, int cols, int32_t bg) {
182 for (int i = 0; i < cols; i++) set_cell(row + i * 4, 32, 0, -1, bg);
185static int32_t *alloc_grid(int cols, int rows, int32_t bg) {
186 int32_t *g = (int32_t *)malloc((size_t)cols * rows * 4 * sizeof(int32_t));
187 if (!g) return NULL;
188 for (int r = 0; r < rows; r++) fill_blank(g + (size_t)r * cols * 4, cols, bg);
189 return g;
192/* ---- damage -------------------------------------------------------------- */
193static void mark_row(Vt *t, int i) {
194 if (i >= 0 && i < t->rows) t->dirty[i] = 1;
196static void mark_rows(Vt *t, int from, int to) {
197 for (int i = from; i <= to; i++) mark_row(t, i);
199static void mark_all(Vt *t) { t->alldirty = 1; }
201/* ---- pending replies ----------------------------------------------------- */
202static void emit_out(Vt *t, const char *s) {
203 int len = (int)strlen(s);
204 if (t->out_len + len + 1 > t->out_cap) {
205 int cap = t->out_cap ? t->out_cap * 2 : 64;
206 while (cap < t->out_len + len + 1) cap *= 2;
207 t->out = (char *)realloc(t->out, cap);
208 t->out_cap = cap;
209 }
210 memcpy(t->out + t->out_len, s, len);
211 t->out_len += len;
212 t->out[t->out_len] = 0;
215/* ---- events -------------------------------------------------------------- */
216static void push_event(Vt *t, int type, const char *payload, int len) {
217 if (t->nevents >= t->events_cap) {
218 int cap = t->events_cap ? t->events_cap * 2 : 8;
219 t->events = (VtEvent *)realloc(t->events, cap * sizeof(VtEvent));
220 t->events_cap = cap;
221 }
222 VtEvent *e = &t->events[t->nevents++];
223 e->type = type;
224 e->payload = NULL;
225 e->payload_len = 0;
226 if (payload && len >= 0) {
227 e->payload = (char *)malloc(len + 1);
228 memcpy(e->payload, payload, len);
229 e->payload[len] = 0;
230 e->payload_len = len;
231 }
234/* ---- title --------------------------------------------------------------- */
235static void set_title(Vt *t, const char *s, int len) {
236 if (len + 1 > t->title_cap) {
237 int cap = t->title_cap ? t->title_cap : 16;
238 while (cap < len + 1) cap *= 2;
239 t->title = (char *)realloc(t->title, cap);
240 t->title_cap = cap;
241 }
242 memcpy(t->title, s, len);
243 t->title[len] = 0;
244 t->title_len = len;
247/* ---- scrollback ring ----------------------------------------------------- */
248/* push a COPY of a grid row (cols*4 int32) as the new newest entry */
249static void sb_push(Vt *t, const int32_t *row) {
250 if (t->sb_cap == 0) return;
251 int cells = t->cols * 4;
252 int32_t *copy;
253 if (t->sb_size == t->sb_cap) {
254 /* evict oldest: reuse its buffer if it is cols-sized, else realloc */
255 int oldest = (t->sb_head - t->sb_size + 1 + t->sb_cap) % t->sb_cap;
256 copy = t->sb[oldest];
257 copy = (int32_t *)realloc(copy, cells * sizeof(int32_t));
258 t->sb[oldest] = copy;
259 t->sb_w[oldest] = t->cols;
260 t->sb_head = (t->sb_head + 1) % t->sb_cap;
261 /* size stays == cap */
262 } else {
263 copy = (int32_t *)malloc(cells * sizeof(int32_t));
264 t->sb_head = (t->sb_size == 0) ? 0 : (t->sb_head + 1) % t->sb_cap;
265 t->sb[t->sb_head] = copy;
266 t->sb_w[t->sb_head] = t->cols;
267 t->sb_size++;
268 }
269 memcpy(copy, row, cells * sizeof(int32_t));
272/* newest-first index k (0 = newest); returns NULL if out of range */
273static int32_t *sb_get(Vt *t, int k) {
274 if (k < 0 || k >= t->sb_size) return NULL;
275 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
276 return t->sb[idx];
279/* the width entry k was pushed at — the ONLY safe extent for reading it */
280static int sb_get_w(Vt *t, int k) {
281 if (k < 0 || k >= t->sb_size) return 0;
282 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
283 return t->sb_w[idx];
286/* pop the newest entry (for resize grow); returns its buffer (caller frees) */
287static int32_t *sb_pop(Vt *t, int *out_w) {
288 if (t->sb_size == 0) { if (out_w) *out_w = 0; return NULL; }
289 int32_t *row = t->sb[t->sb_head];
290 if (out_w) *out_w = t->sb_w[t->sb_head];
291 t->sb[t->sb_head] = NULL;
292 t->sb_head = (t->sb_head - 1 + t->sb_cap) % t->sb_cap;
293 t->sb_size--;
294 return row;
297/* ======================================================================== */
298/* cursor / scrolling / printing (ported from term.sgl) */
299/* ======================================================================== */
301static void set_cursor(Vt *t, int row, int col) {
302 t->wrap = 0;
303 t->cur_row = clampi(row, 0, t->rows - 1);
304 t->cur_col = clampi(col, 0, t->cols - 1);
307static void set_cursor_abs(Vt *t, int row, int col) {
308 if (t->origin) {
309 t->wrap = 0;
310 t->cur_row = clampi(t->stop + row, t->stop, t->sbot);
311 t->cur_col = clampi(col, 0, t->cols - 1);
312 } else {
313 set_cursor(t, row, col);
314 }
317static void move_rows(Vt *t, int delta) {
318 int row = t->cur_row;
319 int lo = (row >= t->stop) ? t->stop : 0;
320 int hi = (row <= t->sbot) ? t->sbot : t->rows - 1;
321 t->wrap = 0;
322 t->cur_row = clampi(row + delta, lo, hi);
325static void move_cols(Vt *t, int delta) {
326 t->wrap = 0;
327 t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);
330/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */
331static void scroll_up(Vt *t, int n, int push) {
332 int top = t->stop, bot = t->sbot;
333 int cols = t->cols;
334 n = clampi(n, 0, bot - top + 1);
335 if (n <= 0) return;
336 if (push && top == 0 && !t->alt_active) {
337 for (int k = 0; k < n; k++)
338 sb_push(t, t->grid + (size_t)(top + k) * cols * 4);
339 }
340 for (int i = top; i <= bot - n; i++)
341 memcpy(t->grid + (size_t)i * cols * 4,
342 t->grid + (size_t)(i + n) * cols * 4,
343 cols * 4 * sizeof(int32_t));
344 for (int i = bot - n + 1; i <= bot; i++)
345 fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);
346 mark_rows(t, top, bot);
349static void scroll_down(Vt *t, int n) {
350 int top = t->stop, bot = t->sbot;
351 int cols = t->cols;
352 n = clampi(n, 0, bot - top + 1);
353 if (n <= 0) return;
354 for (int i = bot; i >= top + n; i--)
355 memcpy(t->grid + (size_t)i * cols * 4,
356 t->grid + (size_t)(i - n) * cols * 4,
357 cols * 4 * sizeof(int32_t));
358 for (int i = top; i < top + n; i++)
359 fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);
360 mark_rows(t, top, bot);
363static void line_feed(Vt *t) {
364 if (t->cur_row == t->sbot)
365 scroll_up(t, 1, 1);
366 else if (t->cur_row < t->rows - 1)
367 t->cur_row += 1;
370static void do_wrap(Vt *t) {
371 t->wrap = 0;
372 t->cur_col = 0;
373 line_feed(t);
376static void print_cp(Vt *t, int32_t cp) {
377 if (t->wrap) do_wrap(t);
378 int cols = t->cols, row = t->cur_row, col = t->cur_col;
379 int32_t *r = t->grid + (size_t)row * cols * 4;
380 if (t->insert) {
381 for (int i = cols - 1; i > col; i--)
382 memcpy(r + i * 4, r + (i - 1) * 4, 4 * sizeof(int32_t));
383 }
384 set_cell(r + col * 4, cp, t->attr, t->fg, t->bg);
385 mark_row(t, row);
386 if (col < cols - 1)
387 t->cur_col = col + 1;
388 else if (t->autowrap)
389 t->wrap = 1;
392/* bulk-print a run of plain printable chars (ground-state fast path) */
393static void print_run(Vt *t, const int32_t *cps, int n) {
394 int cols = t->cols;
395 int i = 0;
396 while (i < n) {
397 if (t->wrap) do_wrap(t);
398 int row = t->cur_row, col = t->cur_col;
399 int32_t *r = t->grid + (size_t)row * cols * 4;
400 int k = n - i;
401 if (k > cols - col) k = cols - col;
402 for (int x = 0; x < k; x++)
403 set_cell(r + (col + x) * 4, cps[i + x], t->attr, t->fg, t->bg);
404 mark_row(t, row);
405 int ncol = col + k;
406 if (ncol < cols) {
407 t->cur_col = ncol;
408 } else {
409 t->cur_col = cols - 1;
410 if (t->autowrap) t->wrap = 1;
411 }
412 i += k;
413 }
416/* ---- erase / insert / delete --------------------------------------------- */
417static void fill_row(Vt *t, int row, int from, int to) {
418 int32_t *r = t->grid + (size_t)row * t->cols * 4;
419 for (int i = from; i < to; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
420 mark_row(t, row);
422static void erase_rows(Vt *t, int from, int to) {
423 for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);
425static void erase_display(Vt *t, int mode) {
426 if (mode == 0) {
427 fill_row(t, t->cur_row, t->cur_col, t->cols);
428 if (t->cur_row < t->rows - 1) erase_rows(t, t->cur_row + 1, t->rows - 1);
429 } else if (mode == 1) {
430 if (t->cur_row > 0) erase_rows(t, 0, t->cur_row - 1);
431 fill_row(t, t->cur_row, 0, t->cur_col + 1);
432 } else if (mode == 2) {
433 erase_rows(t, 0, t->rows - 1);
434 } else if (mode == 3) {
435 /* clear scrollback: free ring entries */
436 for (int k = 0; k < t->sb_size; k++) {
437 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
438 free(t->sb[idx]);
439 t->sb[idx] = NULL;
440 }
441 t->sb_size = 0; t->sb_head = 0;
442 }
444static void erase_line(Vt *t, int mode) {
445 if (mode == 0) fill_row(t, t->cur_row, t->cur_col, t->cols);
446 else if (mode == 1) fill_row(t, t->cur_row, 0, t->cur_col + 1);
447 else if (mode == 2) fill_row(t, t->cur_row, 0, t->cols);
449static void insert_lines(Vt *t, int n) {
450 if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {
451 int save = t->stop;
452 t->stop = t->cur_row;
453 scroll_down(t, n);
454 t->stop = save;
455 t->cur_col = 0; t->wrap = 0;
456 }
458static void delete_lines(Vt *t, int n) {
459 if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {
460 int save = t->stop;
461 t->stop = t->cur_row;
462 scroll_up(t, n, 0);
463 t->stop = save;
464 t->cur_col = 0; t->wrap = 0;
465 }
467static void insert_chars(Vt *t, int n) {
468 int cols = t->cols, col = t->cur_col;
469 int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;
470 n = clampi(n, 0, cols - col);
471 for (int i = cols - 1; i >= col + n; i--)
472 memcpy(r + i * 4, r + (i - n) * 4, 4 * sizeof(int32_t));
473 for (int i = col; i < col + n; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
474 mark_row(t, t->cur_row);
476static void delete_chars(Vt *t, int n) {
477 int cols = t->cols, col = t->cur_col;
478 int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;
479 n = clampi(n, 0, cols - col);
480 for (int i = col; i < cols - n; i++)
481 memcpy(r + i * 4, r + (i + n) * 4, 4 * sizeof(int32_t));
482 for (int i = cols - n; i < cols; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
483 mark_row(t, t->cur_row);
485static void erase_chars(Vt *t, int n) {
486 fill_row(t, t->cur_row, t->cur_col, clampi(t->cur_col + n, t->cur_col, t->cols));
489/* ---- tabs ---------------------------------------------------------------- */
490static void tab_forward(Vt *t) {
491 int cols = t->cols;
492 for (int i = t->cur_col + 1; ; i++) {
493 if (i >= cols) { t->cur_col = cols - 1; return; }
494 if (t->tabs[i]) { t->cur_col = i; return; }
495 }
497static void tab_back(Vt *t) {
498 for (int i = t->cur_col - 1; ; i--) {
499 if (i <= 0) { t->cur_col = 0; return; }
500 if (t->tabs[i]) { t->cur_col = i; return; }
501 }
503static void default_tabs(Vt *t) {
504 memset(t->tabs, 0, t->cols);
505 for (int i = 8; i < t->cols; i += 8) t->tabs[i] = 1;
508/* ---- alt screen / cursor save ------------------------------------------- */
509static void save_cursor(Vt *t) {
510 SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;
511 d->has = 1;
512 d->row = t->cur_row; d->col = t->cur_col;
513 d->attr = t->attr; d->fg = t->fg; d->bg = t->bg;
514 d->origin = t->origin; d->autowrap = t->autowrap;
516static void restore_cursor(Vt *t) {
517 SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;
518 if (!d->has) return;
519 t->attr = d->attr; t->fg = d->fg; t->bg = d->bg;
520 t->origin = d->origin; t->autowrap = d->autowrap;
521 set_cursor(t, d->row, d->col);
523static void enter_alt(Vt *t) {
524 if (t->alt_active) return;
525 if (!t->alt) t->alt = alloc_grid(t->cols, t->rows, -1);
526 /* alt always starts cleared */
527 for (int r = 0; r < t->rows; r++)
528 fill_blank(t->alt + (size_t)r * t->cols * 4, t->cols, -1);
529 t->alt_active = 1;
530 t->grid = t->alt;
531 mark_all(t);
533static void leave_alt(Vt *t) {
534 if (!t->alt_active) return;
535 t->alt_active = 0;
536 t->grid = t->main;
537 mark_all(t);
540/* ======================================================================== */
541/* SGR */
542/* ======================================================================== */
544static void attr_on(Vt *t, int32_t bit) { t->attr |= bit; }
545static void attr_off(Vt *t, int32_t bit) { t->attr &= ~bit; }
547/* colon-form extended color from a group's sub-params (group[1..]) */
548static int32_t parse_colon_color(const int32_t *sub, int len) {
549 if (len >= 2 && sub[0] == 5)
550 return clampi(sub[1], 0, 255);
551 if (len >= 1 && sub[0] == 2) {
552 /* (2 r g b) or (2 colorspace r g b): 5-long skips the colorspace slot */
553 const int32_t *rgb = (len >= 5) ? sub + 2 : sub + 1;
554 int rlen = (len >= 5) ? len - 2 : len - 1;
555 if (rlen >= 3)
556 return 0x1000000 + (clampi(rgb[0], 0, 255) << 16)
557 + (clampi(rgb[1], 0, 255) << 8)
558 + clampi(rgb[2], 0, 255);
559 }
560 return -2; /* sentinel: no color */
563/* legacy semicolon-form: consume from a flat int list of following group heads.
564 * Returns the encoded color (or -2), and *consumed = # of heads eaten. */
565static int32_t parse_ext_color(const int32_t *heads, int nheads, int *consumed) {
566 if (nheads >= 1 && heads[0] == 5) {
567 if (nheads >= 2) { *consumed = 2; return clampi(heads[1], 0, 255); }
568 *consumed = 1; return -2;
569 }
570 if (nheads >= 1 && heads[0] == 2) {
571 if (nheads >= 4) {
572 *consumed = 4;
573 return 0x1000000 + (clampi(heads[1], 0, 255) << 16)
574 + (clampi(heads[2], 0, 255) << 8)
575 + clampi(heads[3], 0, 255);
576 }
577 *consumed = 1; return -2;
578 }
579 *consumed = (nheads >= 1) ? 1 : 0;
580 return -2;
583static void sgr(Vt *t, int32_t (*groups)[VT_MAX_SUB], const int *glen, int ng) {
584 /* empty -> a single {0} */
585 int32_t zero[1] = {0};
586 if (ng == 0) {
587 t->attr = 0; t->fg = -1; t->bg = -1;
588 (void)zero;
589 return;
590 }
591 int i = 0;
592 while (i < ng) {
593 int32_t p = groups[i][0];
594 const int32_t *sub = groups[i];
595 int slen = glen[i];
596 if (p == 0) { t->attr = 0; t->fg = -1; t->bg = -1; }
597 else if (p == 1) attr_on(t, VT_ATTR_BOLD);
598 else if (p == 2) attr_on(t, VT_ATTR_DIM);
599 else if (p == 3) attr_on(t, VT_ATTR_ITALIC);
600 else if (p == 4) attr_on(t, VT_ATTR_UNDERLINE);
601 else if (p == 5) attr_on(t, VT_ATTR_BLINK);
602 else if (p == 7) attr_on(t, VT_ATTR_INVERSE);
603 else if (p == 8) attr_on(t, VT_ATTR_HIDDEN);
604 else if (p == 9) attr_on(t, VT_ATTR_STRIKE);
605 else if (p == 21) attr_on(t, VT_ATTR_UNDERLINE);
606 else if (p == 22) attr_off(t, VT_ATTR_BOLD | VT_ATTR_DIM);
607 else if (p == 23) attr_off(t, VT_ATTR_ITALIC);
608 else if (p == 24) attr_off(t, VT_ATTR_UNDERLINE);
609 else if (p == 25) attr_off(t, VT_ATTR_BLINK);
610 else if (p == 27) attr_off(t, VT_ATTR_INVERSE);
611 else if (p == 28) attr_off(t, VT_ATTR_HIDDEN);
612 else if (p == 29) attr_off(t, VT_ATTR_STRIKE);
613 else if (p >= 30 && p <= 37) t->fg = p - 30;
614 else if (p == 38 || p == 48) {
615 int is_fg = (p == 38);
616 if (slen > 1) {
617 /* sub-params AFTER the 38/48 head (sub+1) are the color spec */
618 int32_t c = parse_colon_color(sub + 1, slen - 1);
619 if (c != -2) { if (is_fg) t->fg = c; else t->bg = c; }
620 } else {
621 /* legacy: gather following group heads into a flat list */
622 int32_t heads[8]; int nh = 0;
623 for (int j = i + 1; j < ng && nh < 8; j++) heads[nh++] = groups[j][0];
624 int consumed = 0;
625 int32_t c = parse_ext_color(heads, nh, &consumed);
626 if (c != -2) { if (is_fg) t->fg = c; else t->bg = c; }
627 i += consumed; /* skip the consumed groups */
628 }
629 }
630 else if (p == 39) t->fg = -1;
631 else if (p >= 40 && p <= 47) t->bg = p - 40;
632 else if (p == 49) t->bg = -1;
633 else if (p >= 90 && p <= 97) t->fg = 8 + (p - 90);
634 else if (p >= 100 && p <= 107) t->bg = 8 + (p - 100);
635 /* else: unknown, ignore */
636 i++;
637 }
640/* ======================================================================== */
641/* DEC / ANSI modes */
642/* ======================================================================== */
644static void dec_mode(Vt *t, int mode, int on) {
645 switch (mode) {
646 case 1: t->appcur = on; break;
647 case 6: t->origin = on; set_cursor_abs(t, 0, 0); break;
648 case 7: t->autowrap = on; break;
649 case 25: t->curvis = on; mark_row(t, t->cur_row); break;
650 case 47:
651 case 1047: if (on) enter_alt(t); else leave_alt(t); break;
652 case 1048: if (on) save_cursor(t); else restore_cursor(t); break;
653 case 1049:
654 if (on) { save_cursor(t); enter_alt(t); }
655 else { leave_alt(t); restore_cursor(t); }
656 break;
657 case 2004: t->bracket = on; break;
658 /* mouse-mode flags (parsed as flags in v1) */
659 case 1000: if (on) t->mouse |= VT_MOUSE_1000; else t->mouse &= ~VT_MOUSE_1000; break;
660 case 1002: if (on) t->mouse |= VT_MOUSE_1002; else t->mouse &= ~VT_MOUSE_1002; break;
661 case 1003: if (on) t->mouse |= VT_MOUSE_1003; else t->mouse &= ~VT_MOUSE_1003; break;
662 case 1006: if (on) t->mouse |= VT_MOUSE_1006; else t->mouse &= ~VT_MOUSE_1006; break;
663 case 1007: if (on) t->mouse |= VT_MOUSE_1007; else t->mouse &= ~VT_MOUSE_1007; break;
664 default: break; /* unknown modes parsed + ignored */
665 }
668static void ansi_mode(Vt *t, int mode, int on) {
669 if (mode == 4) t->insert = on;
672/* ======================================================================== */
673/* CSI dispatch */
674/* ======================================================================== */
676/* Build the finalized param groups into out[][]/outlen[], returning ngroups. */
677static int finalize_params(Vt *t, int32_t out[][VT_MAX_SUB], int *outlen) {
678 int ng = 0;
679 for (int i = 0; i < t->ngroups; i++) {
680 for (int j = 0; j < t->group_len[i]; j++) out[ng][j] = t->groups[i][j];
681 outlen[ng] = t->group_len[i];
682 ng++;
683 if (ng >= VT_MAX_GROUPS) return ng;
684 }
685 /* append the in-flight group unless there were no params at all */
686 if (!(t->ngroups == 0 && t->cur_digits < 0 && t->cur_sub_len == 0)) {
687 int j = 0;
688 for (; j < t->cur_sub_len && j < VT_MAX_SUB; j++) out[ng][j] = t->cur_sub[j];
689 if (j < VT_MAX_SUB) out[ng][j++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
690 outlen[ng] = j;
691 ng++;
692 }
693 return ng;
696static int p1(int32_t g[][VT_MAX_SUB], int ng, int deflt) {
697 int v = (ng > 0) ? g[0][0] : 0;
698 return (v == 0) ? deflt : v;
700static int p2(int32_t g[][VT_MAX_SUB], int ng, int deflt) {
701 int v = (ng > 1) ? g[1][0] : 0;
702 return (v == 0) ? deflt : v;
704static int p1raw(int32_t g[][VT_MAX_SUB], int ng) {
705 return (ng > 0) ? g[0][0] : 0;
708static void term_reset(Vt *t);
710static void csi_dispatch(Vt *t, int final) {
711 int32_t g[VT_MAX_GROUPS][VT_MAX_SUB];
712 int glen[VT_MAX_GROUPS];
713 int ng = finalize_params(t, g, glen);
714 int prefix = t->prefix;
715 const char *inter = t->inter;
717 if (prefix == '?' && final == 'h') {
718 for (int i = 0; i < ng; i++) dec_mode(t, g[i][0], 1);
719 return;
720 }
721 if (prefix == '?' && final == 'l') {
722 for (int i = 0; i < ng; i++) dec_mode(t, g[i][0], 0);
723 return;
724 }
725 if (prefix) return; /* other private-prefixed: ignore */
727 if (strcmp(inter, " ") == 0 && final == 'q') {
728 t->curstyle = clampi(p1raw(g, ng), 0, 6);
729 return;
730 }
731 if (inter[0] != 0) return; /* other intermediates: ignore */
733 switch (final) {
734 case '@': insert_chars(t, p1(g, ng, 1)); break;
735 case 'A': move_rows(t, -p1(g, ng, 1)); break;
736 case 'B': move_rows(t, p1(g, ng, 1)); break;
737 case 'C': move_cols(t, p1(g, ng, 1)); break;
738 case 'D': move_cols(t, -p1(g, ng, 1)); break;
739 case 'E': move_rows(t, p1(g, ng, 1)); t->cur_col = 0; break;
740 case 'F': move_rows(t, -p1(g, ng, 1)); t->cur_col = 0; break;
741 case 'G': case '`': set_cursor(t, t->cur_row, p1(g, ng, 1) - 1); break;
742 case 'd': set_cursor_abs(t, p1(g, ng, 1) - 1, t->cur_col); break;
743 case 'H': case 'f':
744 set_cursor_abs(t, p1(g, ng, 1) - 1, p2(g, ng, 1) - 1); break;
745 case 'I': {
746 int n = clampi(p1(g, ng, 1), 0, t->cols);
747 while (n-- > 0) tab_forward(t);
748 break;
749 }
750 case 'Z': {
751 int n = clampi(p1(g, ng, 1), 0, t->cols);
752 while (n-- > 0) tab_back(t);
753 break;
754 }
755 case 'J': erase_display(t, p1raw(g, ng)); break;
756 case 'K': erase_line(t, p1raw(g, ng)); break;
757 case 'L': insert_lines(t, p1(g, ng, 1)); break;
758 case 'M': delete_lines(t, p1(g, ng, 1)); break;
759 case 'P': delete_chars(t, p1(g, ng, 1)); break;
760 case 'X': erase_chars(t, p1(g, ng, 1)); break;
761 case 'S': scroll_up(t, p1(g, ng, 1), 1); break;
762 case 'T': scroll_down(t, p1(g, ng, 1)); break;
763 case 'm': sgr(t, g, glen, ng); break;
764 case 'h': for (int i = 0; i < ng; i++) ansi_mode(t, g[i][0], 1); break;
765 case 'l': for (int i = 0; i < ng; i++) ansi_mode(t, g[i][0], 0); break;
766 case 'g': {
767 int m = p1raw(g, ng);
768 if (m == 0) t->tabs[t->cur_col] = 0;
769 else if (m == 3) memset(t->tabs, 0, t->cols);
770 break;
771 }
772 case 'n': {
773 int m = p1raw(g, ng);
774 if (m == 5) emit_out(t, "\x1b[0n");
775 else if (m == 6) {
776 int r = t->origin ? (t->cur_row - t->stop) : t->cur_row;
777 char buf[64];
778 snprintf(buf, sizeof(buf), "\x1b[%d;%dR", r + 1, t->cur_col + 1);
779 emit_out(t, buf);
780 }
781 break;
782 }
783 case 'c': emit_out(t, "\x1b[?6c"); break;
784 case 'r': {
785 int top = p1(g, ng, 1) - 1;
786 int bot = p2(g, ng, t->rows) - 1;
787 if (top >= 0 && top < bot && bot < t->rows) {
788 t->stop = top; t->sbot = bot;
789 set_cursor_abs(t, 0, 0);
790 }
791 break;
792 }
793 case 's': save_cursor(t); break;
794 case 'u': restore_cursor(t); break;
795 default: break; /* unknown finals: parsed + ignored */
796 }
799/* ======================================================================== */
800/* OSC dispatch */
801/* ======================================================================== */
803static void osc_dispatch(Vt *t) {
804 /* find first ';' */
805 int semi = -1;
806 for (int i = 0; i < t->osc_len; i++) {
807 if (t->osc[i] == ';') { semi = i; break; }
808 }
809 if (semi >= 0) {
810 /* parse the numeric code before the ';' */
811 int code = 0, ok = (semi > 0);
812 for (int i = 0; i < semi; i++) {
813 char ch = t->osc[i];
814 if (ch < '0' || ch > '9') { ok = 0; break; }
815 code = code * 10 + (ch - '0');
816 }
817 const char *text = t->osc + semi + 1;
818 int tlen = t->osc_len - semi - 1;
819 if (ok && (code == 0 || code == 2)) {
820 set_title(t, text, tlen);
821 push_event(t, VT_EV_TITLE, text, tlen);
822 } else if (ok && code == 52) {
823 /* clipboard write (OSC 52): queued, never acted on */
824 push_event(t, VT_EV_CLIPBOARD, text, tlen);
825 }
826 }
827 t->osc_len = 0;
830/* ======================================================================== */
831/* C0 controls + parser state machine */
832/* ======================================================================== */
834static void c0(Vt *t, int b) {
835 switch (b) {
836 case 8: t->wrap = 0; if (t->cur_col > 0) t->cur_col -= 1; break; /* BS */
837 case 9: tab_forward(t); break; /* HT */
838 case 10: case 11: case 12: t->wrap = 0; line_feed(t); break; /* LF VT FF */
839 case 13: t->wrap = 0; t->cur_col = 0; break; /* CR */
840 case 7: push_event(t, VT_EV_BELL, NULL, 0); break; /* BEL */
841 default: break; /* other C0: ignore */
842 }
845static void clear_csi(Vt *t) {
846 t->ngroups = 0;
847 t->cur_digits = -1;
848 t->cur_sub_len = 0;
849 t->prefix = 0;
850 t->inter[0] = 0; t->inter_len = 0;
853static void process_cp(Vt *t, int32_t cp);
855static void esc_dispatch(Vt *t, int32_t cp) {
856 t->state = ST_GROUND;
857 switch (cp) {
858 case 91: clear_csi(t); t->state = ST_CSI; break; /* [ */
859 case 93: t->osc_len = 0; t->state = ST_OSC; break; /* ] */
860 case 80: case 88: case 94: case 95: /* P X ^ _ */
861 t->state = ST_STR_IGNORE; break;
862 case 55: save_cursor(t); break; /* 7 */
863 case 56: restore_cursor(t); break; /* 8 */
864 case 68: line_feed(t); break; /* D IND */
865 case 69: t->cur_col = 0; line_feed(t); break; /* E NEL */
866 case 72: t->tabs[t->cur_col] = 1; break; /* H HTS */
867 case 77: /* M RI */
868 if (t->cur_row == t->stop) scroll_down(t, 1);
869 else move_rows(t, -1);
870 break;
871 case 99: term_reset(t); break; /* c RIS */
872 case 40: case 41: case 42: case 43: /* ( ) * + */
873 t->state = ST_ESC_SKIP1; break;
874 case 35: t->state = ST_ESC_HASH; break; /* # */
875 case 61: case 62: break; /* = > keypad: ignore */
876 case 92: break; /* \ ST stray: ignore */
877 case 27: t->state = ST_ESC; break;
878 default: break;
879 }
882static void process_cp(Vt *t, int32_t cp) {
883 switch (t->state) {
884 case ST_GROUND:
885 if (cp == 27) t->state = ST_ESC;
886 else if (cp < 32) c0(t, cp);
887 else if (cp == 127) { /* DEL: ignore */ }
888 else print_cp(t, cp);
889 break;
891 case ST_ESC:
892 esc_dispatch(t, cp);
893 break;
895 case ST_ESC_SKIP1:
896 t->state = ST_GROUND;
897 break;
899 case ST_ESC_HASH:
900 t->state = ST_GROUND;
901 if (cp == 56) { /* DECALN: fill screen with E */
902 for (int r = 0; r < t->rows; r++) {
903 int32_t *row = t->grid + (size_t)r * t->cols * 4;
904 for (int cc = 0; cc < t->cols; cc++) set_cell(row + cc * 4, 69, 0, -1, -1);
905 }
906 t->stop = 0; t->sbot = t->rows - 1;
907 set_cursor(t, 0, 0);
908 mark_all(t);
909 }
910 break;
912 case ST_CSI:
913 if (cp >= 48 && cp <= 57) {
914 /* int64 accumulation then clamp: a hostile digit run must not
915 * build bignums OR overflow int (UBSan-clean). */
916 int64_t cur = (t->cur_digits < 0) ? 0 : t->cur_digits;
917 cur = cur * 10 + (cp - 48);
918 if (cur > VT_PARAM_CAP) cur = VT_PARAM_CAP;
919 t->cur_digits = (int)cur;
920 } else if (cp == 58) { /* ':' sub-param sep */
921 if (t->cur_sub_len < VT_MAX_SUB)
922 t->cur_sub[t->cur_sub_len++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
923 t->cur_digits = -1;
924 } else if (cp == 59) { /* ';' group sep */
925 if (t->ngroups < VT_MAX_GROUPS) {
926 int j = 0;
927 for (; j < t->cur_sub_len && j < VT_MAX_SUB; j++)
928 t->groups[t->ngroups][j] = t->cur_sub[j];
929 if (j < VT_MAX_SUB)
930 t->groups[t->ngroups][j++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
931 t->group_len[t->ngroups] = j;
932 t->ngroups++;
933 }
934 t->cur_sub_len = 0;
935 t->cur_digits = -1;
936 } else if (cp >= 60 && cp <= 63) { /* private markers < = > ? */
937 t->prefix = cp;
938 } else if (cp >= 32 && cp <= 47) { /* intermediates */
939 if (t->inter_len < VT_INTER_MAX) {
940 t->inter[t->inter_len++] = (char)cp;
941 t->inter[t->inter_len] = 0;
942 }
943 } else if (cp >= 64 && cp <= 126) { /* final byte */
944 t->state = ST_GROUND;
945 csi_dispatch(t, cp);
946 } else if (cp == 24 || cp == 26) { /* CAN/SUB abort */
947 t->state = ST_GROUND;
948 } else if (cp == 27) {
949 t->state = ST_ESC;
950 } else if (cp < 32) { /* C0 within CSI executes */
951 c0(t, cp);
952 } else {
953 t->state = ST_GROUND;
954 }
955 break;
957 case ST_OSC:
958 if (cp == 7) { t->state = ST_GROUND; osc_dispatch(t); }
959 else if (cp == 27) { t->state = ST_OSC_ESC; }
960 else if (cp == 24 || cp == 26) { t->osc_len = 0; t->state = ST_GROUND; }
961 else {
962 /* accumulate the codepoint's UTF-8 bytes (capped) */
963 if (t->osc_len < VT_OSC_MAX) {
964 if (cp < 0x80) {
965 t->osc[t->osc_len++] = (char)cp;
966 } else if (cp < 0x800) {
967 if (t->osc_len + 2 <= VT_OSC_MAX) {
968 t->osc[t->osc_len++] = (char)(0xC0 | (cp >> 6));
969 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
970 }
971 } else if (cp < 0x10000) {
972 if (t->osc_len + 3 <= VT_OSC_MAX) {
973 t->osc[t->osc_len++] = (char)(0xE0 | (cp >> 12));
974 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
975 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
976 }
977 } else {
978 if (t->osc_len + 4 <= VT_OSC_MAX) {
979 t->osc[t->osc_len++] = (char)(0xF0 | (cp >> 18));
980 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 12) & 0x3F));
981 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
982 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
983 }
984 }
985 }
986 }
987 break;
989 case ST_OSC_ESC:
990 if (cp == 92) { t->state = ST_GROUND; osc_dispatch(t); } /* ESC \ = ST */
991 else { t->osc_len = 0; t->state = ST_ESC; process_cp(t, cp); }
992 break;
994 case ST_STR_IGNORE:
995 if (cp == 7) t->state = ST_GROUND;
996 else if (cp == 27) t->state = ST_STR_ESC;
997 break;
999 case ST_STR_ESC:
1000 if (cp == 92) t->state = ST_GROUND;
1001 else { t->state = ST_STR_IGNORE; if (cp == 27) t->state = ST_STR_ESC; }
1002 break;
1004 default:
1005 t->state = ST_GROUND;
1006 break;
1010/* ======================================================================== */
1011/* reset / resize */
1012/* ======================================================================== */
1014static void term_reset(Vt *t) {
1015 int cols = t->cols, rows = t->rows;
1016 t->alt_active = 0;
1017 t->grid = t->main;
1018 for (int r = 0; r < rows; r++)
1019 fill_blank(t->main + (size_t)r * cols * 4, cols, -1);
1020 t->cur_row = 0; t->cur_col = 0; t->wrap = 0;
1021 t->attr = 0; t->fg = -1; t->bg = -1;
1022 t->stop = 0; t->sbot = rows - 1;
1023 t->saved_main.has = 0; t->saved_alt.has = 0;
1024 t->state = ST_GROUND;
1025 t->autowrap = 1; t->origin = 0; t->curvis = 1;
1026 t->bracket = 0; t->appcur = 0; t->insert = 0;
1027 t->mouse = 0;
1028 default_tabs(t);
1029 t->curstyle = 0;
1030 mark_all(t);
1033/* resize a single grid buffer with xterm no-rewrap semantics.
1034 * `active` marks the buffer the cursor lives on; `main_screen` marks the buffer
1035 * that participates in scrollback. Returns a fresh buffer (caller frees old). */
1036static int32_t *resize_grid(Vt *t, int32_t *g, int old_rows, int cols, int rows,
1037 int active, int main_screen) {
1038 int ocols = t->cols;
1039 int32_t *ng = alloc_grid(cols, rows, -1);
1040 if (rows >= old_rows) {
1041 int need = rows - old_rows;
1042 int pull = main_screen ? (need < t->sb_size ? need : t->sb_size) : 0;
1043 /* pull rows out of scrollback into the top */
1044 for (int k = 0; k < pull; k++) {
1045 int hist_w = 0;
1046 int32_t *hist = sb_pop(t, &hist_w); /* newest first */
1047 /* place newest adjacent to old top: slot (pull-1-k)?? term.sgl puts
1048 * the newest scrollback row at (pull-1) descending as it pops. */
1049 int slot = pull - 1 - k;
1050 int32_t *dst = ng + (size_t)slot * cols * 4;
1051 /* the history row's OWN width — NOT ocols. After two resizes
1052 * (push@40 -> 80 -> 132) ocols is 80 and the row is 40 wide. */
1053 int copy = (hist_w < cols) ? hist_w : cols;
1054 for (int c = 0; c < copy; c++)
1055 memcpy(dst + c * 4, hist + c * 4, 4 * sizeof(int32_t));
1056 free(hist);
1058 /* old grid rows */
1059 for (int j = 0; j < old_rows; j++) {
1060 int32_t *src = g + (size_t)j * ocols * 4;
1061 int32_t *dst = ng + (size_t)(pull + j) * cols * 4;
1062 int copy = (ocols < cols) ? ocols : cols;
1063 for (int c = 0; c < copy; c++)
1064 memcpy(dst + c * 4, src + c * 4, 4 * sizeof(int32_t));
1066 if (active && pull > 0)
1067 t->cur_row = clampi(t->cur_row + pull, 0, rows - 1);
1068 } else {
1069 int drop = old_rows - rows;
1070 int curs = active ? t->cur_row : old_rows - 1;
1071 int drop_bot = drop < (old_rows - 1 - curs) ? drop : (old_rows - 1 - curs);
1072 int drop_top = drop - drop_bot;
1073 for (int j = 0; j < drop_top; j++) {
1074 if (main_screen) sb_push(t, g + (size_t)j * ocols * 4);
1076 for (int j = 0; j < rows; j++) {
1077 int32_t *src = g + (size_t)(j + drop_top) * ocols * 4;
1078 int32_t *dst = ng + (size_t)j * cols * 4;
1079 int copy = (ocols < cols) ? ocols : cols;
1080 for (int c = 0; c < copy; c++)
1081 memcpy(dst + c * 4, src + c * 4, 4 * sizeof(int32_t));
1083 if (active)
1084 t->cur_row = clampi(t->cur_row - drop_top, 0, rows - 1);
1086 return ng;
1089static void term_resize(Vt *t, int cols, int rows) {
1090 int ocols = t->cols, orows = t->rows;
1091 if (cols == ocols && rows == orows) return;
1092 if (cols < 1) cols = 1;
1093 if (rows < 1) rows = 1;
1095 /* Ring entries KEEP the width they were pushed at (xterm no-rewrap): they
1096 * are not re-widthed here, and must never be read at t->cols. Each entry's
1097 * width lives in sb_w — every reader uses it (nat_scrollback_row,
1098 * nat_scrollback_runs, and the resize pull in resize_grid).
1100 * This comment previously claimed ring rows "are only read during pull
1101 * (min-copied) — safe". That stopped being true when the render seam added
1102 * readers, and the min-copy in the pull was itself wrong after two resizes
1103 * (it used ocols, not the row's width). Result: a heap over-read of up to
1104 * (t->cols - push_width) cells, rendered into the terminal. See t-d4c7. */
1106 if (t->alt_active) {
1107 int32_t *nmain = resize_grid(t, t->main, orows, cols, rows, 0, 1);
1108 int32_t *nalt = resize_grid(t, t->alt, orows, cols, rows, 1, 0);
1109 free(t->main); free(t->alt);
1110 t->main = nmain; t->alt = nalt;
1111 t->grid = t->alt;
1112 } else {
1113 int32_t *nmain = resize_grid(t, t->main, orows, cols, rows, 1, 1);
1114 free(t->main);
1115 if (t->alt) { free(t->alt); t->alt = NULL; }
1116 t->main = nmain;
1117 t->grid = t->main;
1120 t->cols = cols; t->rows = rows;
1121 t->stop = 0; t->sbot = rows - 1;
1122 t->cur_col = clampi(t->cur_col, 0, cols - 1);
1123 t->cur_row = clampi(t->cur_row, 0, rows - 1);
1124 t->wrap = 0;
1125 t->tabs = (uint8_t *)realloc(t->tabs, cols);
1126 default_tabs(t);
1127 t->dirty = (uint8_t *)realloc(t->dirty, rows);
1128 memset(t->dirty, 0, rows);
1129 mark_all(t);
1130 (void)ocols;
1133/* ======================================================================== */
1134/* feed */
1135/* ======================================================================== */
1137static void feed_run_scan(Vt *t, const int32_t *cps, int n) {
1138 int i = 0;
1139 while (i < n) {
1140 int32_t cp = cps[i];
1141 if (cp >= 32 && cp < 127 && t->state == ST_GROUND && !t->insert) {
1142 int j = i + 1;
1143 while (j < n && cps[j] >= 32 && cps[j] < 127) j++;
1144 print_run(t, cps + i, j - i);
1145 i = j;
1146 } else {
1147 process_cp(t, cp);
1148 i++;
1153static int32_t valid_cp(int32_t cp, int32_t mn) {
1154 if (cp < mn || (cp >= 0xD800 && cp < 0xE000) || cp > 0x10FFFF) return 65533;
1155 return cp;
1158static void feed_byte(Vt *t, int b) {
1159 int need = t->u8need;
1160 if (need > 0) {
1161 if (b >= 128 && b < 192) {
1162 t->u8acc = t->u8acc * 64 + (b - 128);
1163 t->u8need = need - 1;
1164 if (t->u8need == 0) process_cp(t, valid_cp(t->u8acc, t->u8min));
1165 } else {
1166 t->u8need = 0;
1167 process_cp(t, 65533);
1168 feed_byte(t, b); /* reprocess this byte fresh */
1170 } else if (b < 128) {
1171 process_cp(t, b);
1172 } else if (b >= 194 && b < 224) {
1173 t->u8need = 1; t->u8acc = b - 192; t->u8min = 0x80;
1174 } else if (b >= 224 && b < 240) {
1175 t->u8need = 2; t->u8acc = b - 224; t->u8min = 0x800;
1176 } else if (b >= 240 && b < 245) {
1177 t->u8need = 3; t->u8acc = b - 240; t->u8min = 0x10000;
1178 } else {
1179 process_cp(t, 65533);
1183/* ======================================================================== */
1184/* palette */
1185/* ======================================================================== */
1187static int cube_level(int i) { return i == 0 ? 0 : 55 + i * 40; }
1188static int32_t color_256_rgb(int i) {
1189 if (i < 16) return 0;
1190 if (i < 232) {
1191 int k = i - 16;
1192 int r = cube_level(k / 36);
1193 int g = cube_level((k / 6) % 6);
1194 int b = cube_level(k % 6);
1195 return r * 65536 + g * 256 + b;
1197 int v = 8 + 10 * (i - 232);
1198 return v * 65536 + v * 256 + v;
1201/* ======================================================================== */
1202/* lifecycle */
1203/* ======================================================================== */
1205static Vt *vt_new(int cols, int rows, int sbmax) {
1206 Vt *t = (Vt *)calloc(1, sizeof(Vt));
1207 if (!t) return NULL;
1208 t->cols = cols; t->rows = rows;
1209 t->main = alloc_grid(cols, rows, -1);
1210 t->grid = t->main;
1211 t->alt = NULL; t->alt_active = 0;
1212 t->fg = -1; t->bg = -1; t->attr = 0;
1213 t->stop = 0; t->sbot = rows - 1;
1214 t->state = ST_GROUND;
1215 t->cur_digits = -1;
1216 t->autowrap = 1; t->curvis = 1;
1217 t->tabs = (uint8_t *)malloc(cols);
1218 default_tabs(t);
1219 t->title = NULL; t->title_len = 0; t->title_cap = 0;
1220 t->sb_cap = sbmax > 0 ? sbmax : 1000;
1221 t->sb = (int32_t **)calloc(t->sb_cap, sizeof(int32_t *));
1222 t->sb_w = (int *)calloc(t->sb_cap, sizeof(int));
1223 t->sb_head = 0; t->sb_size = 0;
1224 t->dirty = (uint8_t *)calloc(rows, 1);
1225 t->alldirty = 1;
1226 t->out = NULL; t->out_len = 0; t->out_cap = 0;
1227 t->u8need = 0; t->u8acc = 0; t->u8min = 0;
1228 t->events = NULL; t->nevents = 0; t->events_cap = 0;
1229 return t;
1232static void vt_free(void *data) {
1233 Vt *t = (Vt *)data;
1234 if (!t) return;
1235 free(t->main);
1236 free(t->alt);
1237 free(t->tabs);
1238 free(t->title);
1239 free(t->out);
1240 for (int k = 0; k < t->sb_size; k++) {
1241 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
1242 free(t->sb[idx]);
1244 free(t->sb);
1245 free(t->sb_w);
1246 /* t->dirty is allocated in vt_new and realloc'd by every term_resize, and
1247 * was never freed: EVERY emulator destroyed leaked its dirty array (rows
1248 * bytes — unbounded across a session that opens and closes terminals).
1249 * `detect_leaks=1` has been set in the fuzz gate from the start and would
1250 * have caught this on day one; it reported nothing because ASan was never
1251 * linked (t-d4c7). Found within seconds of the gate being able to see. */
1252 free(t->dirty);
1253 for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);
1254 free(t->events);
1255 free(t);
1258/* ======================================================================== */
1259/* Scheme glue (excluded from the pure-C fuzz build) */
1260/* ======================================================================== */
1261#ifndef VT_FUZZ
1263static Value vt_type_tag = SIGIL_UNDEFINED;
1265static Vt *as_vt(Value v) {
1266 if (!sigil_is_foreign(v)) return NULL;
1267 if (sigil_foreign_type(v) != vt_type_tag) return NULL;
1268 return (Vt *)sigil_foreign_data(v);
1271#define REQUIRE_VT(v) \
1272 Vt *t = as_vt(v); \
1273 if (!t) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "expected a vt emulator"); return SIGIL_UNDEFINED; }
1275/* allocate a fresh Sigil vector of length n (young; fill before returning) */
1276static Value make_vec(SigilVM *vm, int n) {
1277 SigilVector *vec = (SigilVector *)sigil__gc_alloc(
1278 vm, SIGIL_OBJ_VECTOR, sizeof(SigilVector) + (size_t)n * sizeof(Value));
1279 vec->length = n;
1280 for (int i = 0; i < n; i++) vec->elements[i] = SIGIL_UNDEFINED;
1281 return sigil_ptr(vec);
1284/* a cell 4-vector #(cp attr fg bg) from a cell pointer */
1285static Value cell_vec(SigilVM *vm, const int32_t *c) {
1286 Value v = make_vec(vm, 4);
1287 SigilVector *vec = (SigilVector *)sigil_as_ptr(v);
1288 vec->elements[0] = sigil_fixnum(c[0]);
1289 vec->elements[1] = sigil_fixnum(c[1]);
1290 vec->elements[2] = sigil_fixnum(c[2]);
1291 vec->elements[3] = sigil_fixnum(c[3]);
1292 return v;
1295/* ---- constructors -------------------------------------------------------- */
1296static Value nat_make(SigilVM *vm, int argc, Value *args) {
1297 if (!sigil_is_fixnum(args[0]) || !sigil_is_fixnum(args[1])) {
1298 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-make: cols/rows must be integers");
1299 return SIGIL_UNDEFINED;
1301 int cols = (int)sigil_as_fixnum(args[0]);
1302 int rows = (int)sigil_as_fixnum(args[1]);
1303 int sbmax = (argc >= 3 && sigil_is_fixnum(args[2])) ? (int)sigil_as_fixnum(args[2]) : 1000;
1304 if (cols < 1) cols = 1;
1305 if (rows < 1) rows = 1;
1306 Vt *t = vt_new(cols, rows, sbmax);
1307 if (!t) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "%vt-make: out of memory"); return SIGIL_UNDEFINED; }
1308 return sigil_make_foreign(vm, vt_type_tag, t, vt_free, sizeof(Vt));
1311static Value nat_is(SigilVM *vm, int argc, Value *args) {
1312 (void)vm; (void)argc;
1313 return sigil_bool(as_vt(args[0]) != NULL);
1316/* ---- feeding ------------------------------------------------------------- */
1317static Value nat_feed(SigilVM *vm, int argc, Value *args) {
1318 (void)argc;
1319 REQUIRE_VT(args[0]);
1320 if (!sigil_is_string(args[1])) {
1321 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-feed!: expected string");
1322 return SIGIL_UNDEFINED;
1324 const char *bytes = sigil_string_bytes(args[1]);
1325 size_t nbytes = ((SigilString *)sigil_as_ptr(args[1]))->byte_length;
1326 /* decode UTF-8 -> codepoints on the stack in bounded chunks */
1327 enum { CHUNK = 4096 };
1328 int32_t cps[CHUNK];
1329 int nc = 0;
1330 size_t i = 0;
1331 while (i < nbytes) {
1332 unsigned char b = (unsigned char)bytes[i];
1333 int32_t cp; int len;
1334 if (b < 0x80) { cp = b; len = 1; }
1335 else if (b < 0xE0 && i + 1 < nbytes) { cp = ((b & 0x1F) << 6) | (bytes[i+1] & 0x3F); len = 2; }
1336 else if (b < 0xF0 && i + 2 < nbytes) { cp = ((b & 0x0F) << 12) | ((bytes[i+1] & 0x3F) << 6) | (bytes[i+2] & 0x3F); len = 3; }
1337 else if (i + 3 < nbytes) { cp = ((b & 0x07) << 18) | ((bytes[i+1] & 0x3F) << 12) | ((bytes[i+2] & 0x3F) << 6) | (bytes[i+3] & 0x3F); len = 4; }
1338 else { cp = 0xFFFD; len = 1; }
1339 cps[nc++] = cp;
1340 i += len;
1341 if (nc == CHUNK) { feed_run_scan(t, cps, nc); nc = 0; }
1343 if (nc) feed_run_scan(t, cps, nc);
1344 return args[0];
1347static Value nat_feed_bytes(SigilVM *vm, int argc, Value *args) {
1348 (void)argc;
1349 REQUIRE_VT(args[0]);
1350 Value bv = args[1];
1351 if (sigil_is_bytevector(bv)) {
1352 uint8_t *data = sigil_bytevector_data(bv);
1353 size_t n = sigil_bytevector_length(bv);
1354 for (size_t i = 0; i < n; i++) feed_byte(t, data[i]);
1355 } else {
1356 /* accept a list of byte fixnums (term-feed-bytes! compatibility) */
1357 Value cur = bv;
1358 while (sigil_is_pair(cur)) {
1359 Value b = sigil_car(cur);
1360 if (sigil_is_fixnum(b)) feed_byte(t, (int)(sigil_as_fixnum(b) & 0xFF));
1361 cur = sigil_cdr(cur);
1364 return args[0];
1367/* ---- resize / reset ------------------------------------------------------ */
1368static Value nat_resize(SigilVM *vm, int argc, Value *args) {
1369 (void)argc;
1370 REQUIRE_VT(args[0]);
1371 if (!sigil_is_fixnum(args[1]) || !sigil_is_fixnum(args[2])) {
1372 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-resize!: cols/rows must be integers");
1373 return SIGIL_UNDEFINED;
1375 term_resize(t, (int)sigil_as_fixnum(args[1]), (int)sigil_as_fixnum(args[2]));
1376 return args[0];
1378static Value nat_reset(SigilVM *vm, int argc, Value *args) {
1379 (void)argc; REQUIRE_VT(args[0]); term_reset(t); return args[0];
1381static Value nat_invalidate(SigilVM *vm, int argc, Value *args) {
1382 (void)argc; REQUIRE_VT(args[0]); mark_all(t); return args[0];
1385/* ---- accessors ----------------------------------------------------------- */
1386#define ACCESSOR_INT(NAME, EXPR) \
1387 static Value NAME(SigilVM *vm, int argc, Value *args) { \
1388 (void)argc; REQUIRE_VT(args[0]); return sigil_fixnum(EXPR); }
1389#define ACCESSOR_BOOL(NAME, EXPR) \
1390 static Value NAME(SigilVM *vm, int argc, Value *args) { \
1391 (void)argc; REQUIRE_VT(args[0]); return sigil_bool(EXPR); }
1393ACCESSOR_INT(nat_cols, t->cols)
1394ACCESSOR_INT(nat_rows, t->rows)
1395ACCESSOR_INT(nat_cursor_row, t->cur_row)
1396ACCESSOR_INT(nat_cursor_col, t->cur_col)
1397ACCESSOR_BOOL(nat_cursor_visible, t->curvis)
1398ACCESSOR_INT(nat_cursor_style, t->curstyle)
1399ACCESSOR_BOOL(nat_alt, t->alt_active)
1400ACCESSOR_BOOL(nat_bracketed, t->bracket)
1401ACCESSOR_BOOL(nat_appcur, t->appcur)
1402ACCESSOR_INT(nat_mouse, t->mouse)
1403ACCESSOR_INT(nat_scrollback_count, t->sb_size)
1405static int any_dirty(Vt *t) {
1406 if (t->alldirty) return 1;
1407 for (int i = 0; i < t->rows; i++) if (t->dirty[i]) return 1;
1408 return 0;
1410static Value nat_damaged(SigilVM *vm, int argc, Value *args) {
1411 (void)argc; REQUIRE_VT(args[0]); return sigil_bool(any_dirty(t));
1414static Value nat_title(SigilVM *vm, int argc, Value *args) {
1415 (void)argc; REQUIRE_VT(args[0]);
1416 return sigil_make_string(vm, t->title ? t->title : "", t->title_len);
1419/* ---- pending replies ----------------------------------------------------- */
1420static Value nat_take_output(SigilVM *vm, int argc, Value *args) {
1421 (void)argc; REQUIRE_VT(args[0]);
1422 Value s = sigil_make_string(vm, t->out ? t->out : "", t->out_len);
1423 t->out_len = 0;
1424 if (t->out) t->out[0] = 0;
1425 return s;
1428/* ---- damage -------------------------------------------------------------- */
1429/* returns #(all? r0 r1 ...): elt0 = bool; rest = ascending dirty row indices.
1430 * When all?, returns #(#t) and clears dirty. */
1431static Value nat_take_damage(SigilVM *vm, int argc, Value *args) {
1432 (void)argc; REQUIRE_VT(args[0]);
1433 int all = t->alldirty;
1434 t->alldirty = 0;
1435 if (all) {
1436 for (int i = 0; i < t->rows; i++) t->dirty[i] = 0;
1437 Value v = make_vec(vm, 1);
1438 sigil_vector_set(v, 0, SIGIL_TRUE);
1439 return v;
1441 int count = 0;
1442 for (int i = 0; i < t->rows; i++) if (t->dirty[i]) count++;
1443 Value v = make_vec(vm, count + 1);
1444 sigil_vector_set(v, 0, SIGIL_FALSE);
1445 int j = 1;
1446 for (int i = 0; i < t->rows; i++) {
1447 if (t->dirty[i]) { sigil_vector_set(v, j++, sigil_fixnum(i)); t->dirty[i] = 0; }
1449 return v;
1452/* ---- events -------------------------------------------------------------- */
1453/* returns a list of #(type-symbol payload-or-#f); drains the queue */
1454static Value nat_take_events(SigilVM *vm, int argc, Value *args) {
1455 (void)argc; REQUIRE_VT(args[0]);
1456 Value list = SIGIL_EMPTY;
1457 /* build reversed then it's newest-last; iterate backward to preserve order */
1458 for (int i = t->nevents - 1; i >= 0; i--) {
1459 VtEvent *e = &t->events[i];
1460 const char *tn = e->type == VT_EV_TITLE ? "title"
1461 : e->type == VT_EV_BELL ? "bell" : "clipboard";
1462 Value sym = sigil_intern_symbol(vm, tn, (int)strlen(tn));
1463 sigil__gc_push_temp_root(vm, list);
1464 sigil__gc_push_temp_root(vm, sym);
1465 Value payload = (e->type == VT_EV_BELL)
1466 ? SIGIL_FALSE
1467 : sigil_make_string(vm, e->payload ? e->payload : "", e->payload_len);
1468 sigil__gc_push_temp_root(vm, payload);
1469 Value rec = make_vec(vm, 2);
1470 sigil_vector_set(rec, 0, sym);
1471 sigil_vector_set(rec, 1, payload);
1472 sigil__gc_push_temp_root(vm, rec);
1473 Value cell = sigil_cons(vm, rec, list);
1474 sigil__gc_pop_temp_root(vm); /* rec */
1475 sigil__gc_pop_temp_root(vm); /* payload */
1476 sigil__gc_pop_temp_root(vm); /* sym */
1477 sigil__gc_pop_temp_root(vm); /* list */
1478 list = cell;
1480 for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);
1481 t->nevents = 0;
1482 return list;
1485/* ---- grid access --------------------------------------------------------- */
1486static Value nat_row_cells(SigilVM *vm, int argc, Value *args) {
1487 (void)argc; REQUIRE_VT(args[0]);
1488 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-row-cells: row must be integer"); return SIGIL_UNDEFINED; }
1489 int row = (int)sigil_as_fixnum(args[1]);
1490 if (row < 0 || row >= t->rows) return SIGIL_FALSE;
1491 int cols = t->cols;
1492 Value v = make_vec(vm, cols);
1493 sigil__gc_push_temp_root(vm, v);
1494 int32_t *r = t->grid + (size_t)row * cols * 4;
1495 for (int c = 0; c < cols; c++) {
1496 Value cv = cell_vec(vm, r + c * 4);
1497 sigil_vector_set(v, c, cv);
1499 sigil__gc_pop_temp_root(vm);
1500 return v;
1503/* newest-first scrollback row k -> vector of cells, or #f */
1504static Value nat_scrollback_row(SigilVM *vm, int argc, Value *args) {
1505 (void)argc; REQUIRE_VT(args[0]);
1506 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-scrollback-row: k must be integer"); return SIGIL_UNDEFINED; }
1507 int k = (int)sigil_as_fixnum(args[1]);
1508 int32_t *r = sb_get(t, k);
1509 if (!r) return SIGIL_FALSE;
1510 /* the row's OWN width, not t->cols: a history row keeps the width it had
1511 * when it scrolled off. The interpreted reference returns the stored vector
1512 * verbatim, so its length IS the push width — match that exactly. */
1513 int cols = sb_get_w(t, k);
1514 Value v = make_vec(vm, cols);
1515 sigil__gc_push_temp_root(vm, v);
1516 for (int c = 0; c < cols; c++) {
1517 Value cv = cell_vec(vm, r + c * 4);
1518 sigil_vector_set(v, c, cv);
1520 sigil__gc_pop_temp_root(vm);
1521 return v;
1524/* row text: the whole row as a string (codepoints) */
1525static Value nat_row_text(SigilVM *vm, int argc, Value *args) {
1526 (void)argc; REQUIRE_VT(args[0]);
1527 int row = sigil_is_fixnum(args[1]) ? (int)sigil_as_fixnum(args[1]) : -1;
1528 if (row < 0 || row >= t->rows) return sigil_make_string(vm, "", 0);
1529 int cols = t->cols;
1530 int32_t *r = t->grid + (size_t)row * cols * 4;
1531 /* encode codepoints to UTF-8 */
1532 char *buf = (char *)malloc((size_t)cols * 4 + 1);
1533 int p = 0;
1534 for (int c = 0; c < cols; c++) {
1535 int32_t cp = r[c * 4];
1536 if (cp < 0x80) buf[p++] = (char)cp;
1537 else if (cp < 0x800) { buf[p++] = (char)(0xC0|(cp>>6)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1538 else if (cp < 0x10000) { buf[p++]=(char)(0xE0|(cp>>12)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1539 else { buf[p++]=(char)(0xF0|(cp>>18)); buf[p++]=(char)(0x80|((cp>>12)&0x3F)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1541 Value s = sigil_make_string(vm, buf, p);
1542 free(buf);
1543 return s;
1546/* ---- row-run extraction (THE render seam) -------------------------------- */
1547/* is a cell visually a default blank (safe to right-trim)? mirrors
1548 * modes/terminal.sgl trimmable?: space, default bg, and none of
1549 * inverse/underline/strike set. */
1550static int trimmable(const int32_t *c) {
1551 return c[0] == 32 && c[3] == -1 &&
1552 (c[1] & (VT_ATTR_INVERSE | VT_ATTR_UNDERLINE | VT_ATTR_STRIKE)) == 0;
1555/* %vt-row-runs t row cursor-col -> list of #(text attr fg bg) runs.
1556 * Runs merge equal (attr,fg,bg); the cursor cell (when cursor-col is a fixnum)
1557 * forces a run break so it can carry its own styling. Right-trimmed to the last
1558 * non-trimmable cell (or the cursor col, whichever is greater). Operates over a
1559 * given cell buffer (active grid or a scrollback row). */
1560static Value row_runs_of(SigilVM *vm, const int32_t *r, int cols, int cursor_col) {
1561 /* find last non-trimmable */
1562 int last = -1;
1563 for (int i = cols - 1; i >= 0; i--) {
1564 if (!trimmable(r + i * 4)) { last = i; break; }
1566 int upto = last;
1567 if (cursor_col >= 0 && cursor_col > upto) upto = cursor_col;
1568 if (upto < 0) return SIGIL_EMPTY;
1570 /* pass 1: compute run boundaries (no allocation -> GC-safe) */
1571 int rstart[cols], rend[cols], nrun = 0;
1572 int start = 0;
1573 while (start <= upto) {
1574 int32_t attr = r[start * 4 + 1], fg = r[start * 4 + 2], bg = r[start * 4 + 3];
1575 int cur0 = (cursor_col >= 0 && cursor_col == start);
1576 int end = start;
1577 while (end + 1 <= upto) {
1578 int n = end + 1;
1579 int curn = (cursor_col >= 0 && cursor_col == n);
1580 if (r[n*4+1] == attr && r[n*4+2] == fg && r[n*4+3] == bg && curn == cur0) end++;
1581 else break;
1583 rstart[nrun] = start; rend[nrun] = end; nrun++;
1584 start = end + 1;
1587 /* pass 2: cons runs right-to-left so the result is in left-to-right order
1588 * (no reverse pass, and every intermediate is temp-rooted across allocs) */
1589 char *buf = (char *)malloc((size_t)(upto + 1) * 4 + 1);
1590 Value runs = SIGIL_EMPTY;
1591 for (int k = nrun - 1; k >= 0; k--) {
1592 int s = rstart[k], e = rend[k];
1593 int32_t attr = r[s * 4 + 1], fg = r[s * 4 + 2], bg = r[s * 4 + 3];
1594 int cur0 = (cursor_col >= 0 && cursor_col == s);
1595 int p = 0;
1596 for (int i = s; i <= e; i++) {
1597 int32_t cp = r[i * 4];
1598 if (cp < 0x80) buf[p++] = (char)cp;
1599 else if (cp < 0x800) { buf[p++]=(char)(0xC0|(cp>>6)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1600 else if (cp < 0x10000) { buf[p++]=(char)(0xE0|(cp>>12)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1601 else { buf[p++]=(char)(0xF0|(cp>>18)); buf[p++]=(char)(0x80|((cp>>12)&0x3F)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1603 sigil__gc_push_temp_root(vm, runs);
1604 Value text = sigil_make_string(vm, buf, p);
1605 sigil__gc_push_temp_root(vm, text);
1606 Value rec = make_vec(vm, 5);
1607 sigil_vector_set(rec, 0, text);
1608 sigil_vector_set(rec, 1, sigil_fixnum(attr));
1609 sigil_vector_set(rec, 2, sigil_fixnum(fg));
1610 sigil_vector_set(rec, 3, sigil_fixnum(bg));
1611 sigil_vector_set(rec, 4, sigil_bool(cur0));
1612 sigil__gc_push_temp_root(vm, rec);
1613 Value cell = sigil_cons(vm, rec, runs);
1614 sigil__gc_pop_temp_root(vm); /* rec */
1615 sigil__gc_pop_temp_root(vm); /* text */
1616 sigil__gc_pop_temp_root(vm); /* runs */
1617 runs = cell;
1619 free(buf);
1620 return runs;
1623static Value nat_row_runs(SigilVM *vm, int argc, Value *args) {
1624 REQUIRE_VT(args[0]);
1625 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-row-runs: row must be integer"); return SIGIL_UNDEFINED; }
1626 int row = (int)sigil_as_fixnum(args[1]);
1627 if (row < 0 || row >= t->rows) return SIGIL_EMPTY;
1628 int cursor_col = -1;
1629 if (argc >= 3 && sigil_is_fixnum(args[2])) cursor_col = (int)sigil_as_fixnum(args[2]);
1630 int32_t *r = t->grid + (size_t)row * t->cols * 4;
1631 return row_runs_of(vm, r, t->cols, cursor_col);
1634static Value nat_scrollback_runs(SigilVM *vm, int argc, Value *args) {
1635 REQUIRE_VT(args[0]);
1636 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-scrollback-runs: k must be integer"); return SIGIL_UNDEFINED; }
1637 int k = (int)sigil_as_fixnum(args[1]);
1638 int32_t *r = sb_get(t, k);
1639 if (!r) return SIGIL_EMPTY;
1640 int cursor_col = -1;
1641 if (argc >= 3 && sigil_is_fixnum(args[2])) cursor_col = (int)sigil_as_fixnum(args[2]);
1642 /* the row's OWN width, not t->cols — see sb_w. Cells past it would be blank
1643 * and right-trimmed away anyway, so this is byte-identical AND in-bounds. */
1644 return row_runs_of(vm, r, sb_get_w(t, k), cursor_col);
1647/* ---- palette ------------------------------------------------------------- */
1648static Value nat_color_256(SigilVM *vm, int argc, Value *args) {
1649 (void)argc;
1650 if (!sigil_is_fixnum(args[0])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-color-256->rgb: expected integer"); return SIGIL_UNDEFINED; }
1651 return sigil_fixnum(color_256_rgb((int)sigil_as_fixnum(args[0])));
1654/* ======================================================================== */
1655/* registration */
1656/* ======================================================================== */
1658static void vt_register_all(SigilVM *vm) {
1659 vt_type_tag = sigil_intern_symbol(vm, "vt-emulator", 11);
1660 SigilModule *module = sigil_begin_module(vm, "(sigil vt)");
1661 if (!module) return;
1663#define REG(name, fn, arity, doc) \
1664 do { sigil_module_register_native(vm, name, fn, arity, doc); \
1665 sigil_module_export(vm, name); } while (0)
1667 REG("%vt-make", nat_make, SIGIL_ARITY_RANGE(2, 3), "Create a VT emulator (cols rows [scrollback-cap])");
1668 REG("%vt?", nat_is, SIGIL_ARITY_EXACT(1), "Is value a VT emulator?");
1669 REG("%vt-feed!", nat_feed, SIGIL_ARITY_EXACT(2), "Feed a decoded string chunk");
1670 REG("%vt-feed-bytes!", nat_feed_bytes, SIGIL_ARITY_EXACT(2), "Feed raw bytes (bytevector or list)");
1671 REG("%vt-resize!", nat_resize, SIGIL_ARITY_EXACT(3), "Resize (cols rows)");
1672 REG("%vt-reset!", nat_reset, SIGIL_ARITY_EXACT(1), "Full reset (RIS)");
1673 REG("%vt-invalidate!", nat_invalidate, SIGIL_ARITY_EXACT(1), "Force full-repaint damage");
1674 REG("%vt-cols", nat_cols, SIGIL_ARITY_EXACT(1), "Columns");
1675 REG("%vt-rows", nat_rows, SIGIL_ARITY_EXACT(1), "Rows");
1676 REG("%vt-cursor-row", nat_cursor_row, SIGIL_ARITY_EXACT(1), "Cursor row");
1677 REG("%vt-cursor-col", nat_cursor_col, SIGIL_ARITY_EXACT(1), "Cursor col");
1678 REG("%vt-cursor-visible?", nat_cursor_visible, SIGIL_ARITY_EXACT(1), "Cursor visible?");
1679 REG("%vt-cursor-style", nat_cursor_style, SIGIL_ARITY_EXACT(1), "DECSCUSR style");
1680 REG("%vt-alt?", nat_alt, SIGIL_ARITY_EXACT(1), "Alt screen active?");
1681 REG("%vt-title", nat_title, SIGIL_ARITY_EXACT(1), "Window title");
1682 REG("%vt-bracketed-paste?", nat_bracketed, SIGIL_ARITY_EXACT(1), "Bracketed paste mode?");
1683 REG("%vt-app-cursor?", nat_appcur, SIGIL_ARITY_EXACT(1), "Application cursor keys?");
1684 REG("%vt-mouse-flags", nat_mouse, SIGIL_ARITY_EXACT(1), "Mouse-mode flag bitmask");
1685 REG("%vt-take-output!", nat_take_output, SIGIL_ARITY_EXACT(1), "Drain pending replies (string)");
1686 REG("%vt-take-damage!", nat_take_damage, SIGIL_ARITY_EXACT(1), "Drain damage -> #(all? rows...)");
1687 REG("%vt-damaged?", nat_damaged, SIGIL_ARITY_EXACT(1), "Any damage pending?");
1688 REG("%vt-take-events!", nat_take_events, SIGIL_ARITY_EXACT(1), "Drain out-of-band events");
1689 REG("%vt-row-cells", nat_row_cells, SIGIL_ARITY_EXACT(2), "Row as vector of #(cp attr fg bg)");
1690 REG("%vt-row-text", nat_row_text, SIGIL_ARITY_EXACT(2), "Row as string");
1691 REG("%vt-scrollback-count", nat_scrollback_count, SIGIL_ARITY_EXACT(1), "Scrollback row count");
1692 REG("%vt-scrollback-row", nat_scrollback_row, SIGIL_ARITY_EXACT(2), "Scrollback row k as cells");
1693 REG("%vt-row-runs", nat_row_runs, SIGIL_ARITY_RANGE(2, 3), "Row style-merged runs");
1694 REG("%vt-scrollback-runs", nat_scrollback_runs, SIGIL_ARITY_RANGE(2, 3), "Scrollback row runs");
1695 REG("%vt-color-256->rgb", nat_color_256, SIGIL_ARITY_EXACT(1), "256-color index -> #xRRGGBB");
1697#undef REG
1698 sigil_end_module(vm);
1701/* native-init entry (CLI/test builds) */
1702SIGIL_EXPORT void sigil__init_sigil_vt_module(SigilVM *vm) {
1703 vt_register_all(vm);
1706/* wasm-bridge entry (interim: generic app hook; durable: sigil_wasm_vt_register
1707 * once the monorepo runtime carries the weak decl — t-4c70). */
1708SIGIL_EXPORT void sigil_wasm_vt_register(SigilVM *vm) {
1709 vt_register_all(vm);
1712#endif /* !VT_FUZZ */