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, ST_ESC_CHARSET,
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 int graphics[2], gl;
95} SavedCursor;
97typedef struct {
98 int type;
99 char *payload; /* malloc'd, may be NULL (bell) */
100 int payload_len;
101} VtEvent;
103typedef struct {
104 int cols, rows;
105 int32_t *grid; /* ACTIVE grid (points at main or alt) */
106 int32_t *main; /* main-screen buffer */
107 int32_t *alt; /* alt-screen buffer (NULL until first enter) */
108 int alt_active;
110 int cur_row, cur_col;
111 int wrap; /* pending (deferred) autowrap */
112 int32_t attr, fg, bg; /* current SGR */
113 int stop, sbot; /* scroll region, 0-based inclusive */
115 SavedCursor saved_main, saved_alt;
117 /* VT100 G0/G1: ASCII or DEC Special Graphics, invoked by SI/SO. */
118 int graphics[2], gl, charset_target;
120 /* parser */
121 int state;
122 int32_t groups[VT_MAX_GROUPS][VT_MAX_SUB]; /* completed groups */
123 int group_len[VT_MAX_GROUPS];
124 int ngroups;
125 int cur_digits; /* -1 = none */
126 int32_t cur_sub[VT_MAX_SUB]; /* completed sub-params of current group */
127 int cur_sub_len;
128 int prefix; /* private marker char or 0 */
129 char inter[VT_INTER_MAX + 1];
130 int inter_len;
131 char osc[VT_OSC_MAX];
132 int osc_len;
134 /* modes */
135 int autowrap, origin, curvis, bracket, appcur, insert;
136 int mouse; /* VT_MOUSE_* bitmask */
138 uint8_t *tabs; /* length cols */
139 char *title; int title_len, title_cap;
140 int curstyle; /* DECSCUSR 0..6 */
142 /* scrollback ring: index 0 = newest */
143 int32_t **sb; /* each entry: sb_w[i]*4 int32 */
144 int *sb_w; /* WIDTH each entry was pushed at.
145 * A history row keeps the width it
146 * had when it scrolled off (xterm
147 * no-rewrap), which is NOT t->cols
148 * after a resize. The interpreted
149 * reference got this for free —
150 * its rows are Scheme vectors that
151 * carry their own length. Porting
152 * to raw int32_t* dropped it, so
153 * every reader guessed, and a
154 * widening resize read off the end
155 * of the allocation. Never read an
156 * entry with anything but its
157 * sb_w. */
158 int sb_head; /* index of newest */
159 int sb_size;
160 int sb_cap; /* == sbmax */
162 uint8_t *dirty; /* length rows */
163 int alldirty;
165 char *out; int out_len, out_cap; /* pending replies */
167 int u8need; int32_t u8acc; int32_t u8min; /* incremental UTF-8 */
169 VtEvent *events; int nevents, events_cap; /* out-of-band event queue */
170} Vt;
172/* ======================================================================== */
173/* small utilities */
174/* ======================================================================== */
176static int clampi(int x, int lo, int hi) {
177 return x < lo ? lo : (x > hi ? hi : x);
180static void set_cell(int32_t *c, int32_t cp, int32_t attr, int32_t fg, int32_t bg) {
181 c[0] = cp; c[1] = attr; c[2] = fg; c[3] = bg;
184/* fill a fresh row buffer with blank cells (space, default, current bg) */
185static void fill_blank(int32_t *row, int cols, int32_t bg) {
186 for (int i = 0; i < cols; i++) set_cell(row + i * 4, 32, 0, -1, bg);
189static int32_t *alloc_grid(int cols, int rows, int32_t bg) {
190 int32_t *g = (int32_t *)malloc((size_t)cols * rows * 4 * sizeof(int32_t));
191 if (!g) return NULL;
192 for (int r = 0; r < rows; r++) fill_blank(g + (size_t)r * cols * 4, cols, bg);
193 return g;
196/* ---- damage -------------------------------------------------------------- */
197static void mark_row(Vt *t, int i) {
198 if (i >= 0 && i < t->rows) t->dirty[i] = 1;
200static void mark_rows(Vt *t, int from, int to) {
201 for (int i = from; i <= to; i++) mark_row(t, i);
203static void mark_all(Vt *t) { t->alldirty = 1; }
205/* ---- pending replies ----------------------------------------------------- */
206static void emit_out(Vt *t, const char *s) {
207 int len = (int)strlen(s);
208 if (t->out_len + len + 1 > t->out_cap) {
209 int cap = t->out_cap ? t->out_cap * 2 : 64;
210 while (cap < t->out_len + len + 1) cap *= 2;
211 t->out = (char *)realloc(t->out, cap);
212 t->out_cap = cap;
213 }
214 memcpy(t->out + t->out_len, s, len);
215 t->out_len += len;
216 t->out[t->out_len] = 0;
219/* ---- events -------------------------------------------------------------- */
220static void push_event(Vt *t, int type, const char *payload, int len) {
221 if (t->nevents >= t->events_cap) {
222 int cap = t->events_cap ? t->events_cap * 2 : 8;
223 t->events = (VtEvent *)realloc(t->events, cap * sizeof(VtEvent));
224 t->events_cap = cap;
225 }
226 VtEvent *e = &t->events[t->nevents++];
227 e->type = type;
228 e->payload = NULL;
229 e->payload_len = 0;
230 if (payload && len >= 0) {
231 e->payload = (char *)malloc(len + 1);
232 memcpy(e->payload, payload, len);
233 e->payload[len] = 0;
234 e->payload_len = len;
235 }
238/* ---- title --------------------------------------------------------------- */
239static void set_title(Vt *t, const char *s, int len) {
240 if (len + 1 > t->title_cap) {
241 int cap = t->title_cap ? t->title_cap : 16;
242 while (cap < len + 1) cap *= 2;
243 t->title = (char *)realloc(t->title, cap);
244 t->title_cap = cap;
245 }
246 memcpy(t->title, s, len);
247 t->title[len] = 0;
248 t->title_len = len;
251/* ---- scrollback ring ----------------------------------------------------- */
252/* push a COPY of a grid row (cols*4 int32) as the new newest entry */
253static void sb_push(Vt *t, const int32_t *row) {
254 if (t->sb_cap == 0) return;
255 int cells = t->cols * 4;
256 int32_t *copy;
257 if (t->sb_size == t->sb_cap) {
258 /* evict oldest: reuse its buffer if it is cols-sized, else realloc */
259 int oldest = (t->sb_head - t->sb_size + 1 + t->sb_cap) % t->sb_cap;
260 copy = t->sb[oldest];
261 copy = (int32_t *)realloc(copy, cells * sizeof(int32_t));
262 t->sb[oldest] = copy;
263 t->sb_w[oldest] = t->cols;
264 t->sb_head = (t->sb_head + 1) % t->sb_cap;
265 /* size stays == cap */
266 } else {
267 copy = (int32_t *)malloc(cells * sizeof(int32_t));
268 t->sb_head = (t->sb_size == 0) ? 0 : (t->sb_head + 1) % t->sb_cap;
269 t->sb[t->sb_head] = copy;
270 t->sb_w[t->sb_head] = t->cols;
271 t->sb_size++;
272 }
273 memcpy(copy, row, cells * sizeof(int32_t));
276/* newest-first index k (0 = newest); returns NULL if out of range */
277static int32_t *sb_get(Vt *t, int k) {
278 if (k < 0 || k >= t->sb_size) return NULL;
279 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
280 return t->sb[idx];
283/* the width entry k was pushed at — the ONLY safe extent for reading it */
284static int sb_get_w(Vt *t, int k) {
285 if (k < 0 || k >= t->sb_size) return 0;
286 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
287 return t->sb_w[idx];
290/* pop the newest entry (for resize grow); returns its buffer (caller frees) */
291static int32_t *sb_pop(Vt *t, int *out_w) {
292 if (t->sb_size == 0) { if (out_w) *out_w = 0; return NULL; }
293 int32_t *row = t->sb[t->sb_head];
294 if (out_w) *out_w = t->sb_w[t->sb_head];
295 t->sb[t->sb_head] = NULL;
296 t->sb_head = (t->sb_head - 1 + t->sb_cap) % t->sb_cap;
297 t->sb_size--;
298 return row;
301/* ======================================================================== */
302/* cursor / scrolling / printing (ported from term.sgl) */
303/* ======================================================================== */
305static void set_cursor(Vt *t, int row, int col) {
306 t->wrap = 0;
307 t->cur_row = clampi(row, 0, t->rows - 1);
308 t->cur_col = clampi(col, 0, t->cols - 1);
311static void set_cursor_abs(Vt *t, int row, int col) {
312 if (t->origin) {
313 t->wrap = 0;
314 t->cur_row = clampi(t->stop + row, t->stop, t->sbot);
315 t->cur_col = clampi(col, 0, t->cols - 1);
316 } else {
317 set_cursor(t, row, col);
318 }
321static void move_rows(Vt *t, int delta) {
322 int row = t->cur_row;
323 int lo = (row >= t->stop) ? t->stop : 0;
324 int hi = (row <= t->sbot) ? t->sbot : t->rows - 1;
325 t->wrap = 0;
326 t->cur_row = clampi(row + delta, lo, hi);
329static void move_cols(Vt *t, int delta) {
330 t->wrap = 0;
331 t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);
334/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */
335static void scroll_up(Vt *t, int n, int push) {
336 int top = t->stop, bot = t->sbot;
337 int cols = t->cols;
338 n = clampi(n, 0, bot - top + 1);
339 if (n <= 0) return;
340 if (push && top == 0 && !t->alt_active) {
341 for (int k = 0; k < n; k++)
342 sb_push(t, t->grid + (size_t)(top + k) * cols * 4);
343 }
344 for (int i = top; i <= bot - n; i++)
345 memcpy(t->grid + (size_t)i * cols * 4,
346 t->grid + (size_t)(i + n) * cols * 4,
347 cols * 4 * sizeof(int32_t));
348 for (int i = bot - n + 1; i <= bot; i++)
349 fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);
350 mark_rows(t, top, bot);
353static void scroll_down(Vt *t, int n) {
354 int top = t->stop, bot = t->sbot;
355 int cols = t->cols;
356 n = clampi(n, 0, bot - top + 1);
357 if (n <= 0) return;
358 for (int i = bot; i >= top + n; i--)
359 memcpy(t->grid + (size_t)i * cols * 4,
360 t->grid + (size_t)(i - n) * cols * 4,
361 cols * 4 * sizeof(int32_t));
362 for (int i = top; i < top + n; i++)
363 fill_blank(t->grid + (size_t)i * cols * 4, cols, t->bg);
364 mark_rows(t, top, bot);
367static void line_feed(Vt *t) {
368 if (t->cur_row == t->sbot)
369 scroll_up(t, 1, 1);
370 else if (t->cur_row < t->rows - 1)
371 t->cur_row += 1;
374static void do_wrap(Vt *t) {
375 t->wrap = 0;
376 t->cur_col = 0;
377 line_feed(t);
380static int32_t graphic_cp(Vt *t, int32_t cp) {
381 /* DEC Special Graphics maps ASCII 0x60..0x7e to display glyphs.
382 * Store Unicode in the grid so row runs, scrollback and copying agree. */
383 static const int32_t dec[] = {
384 0x25c6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1,
385 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x23ba,
386 0x23bb, 0x2500, 0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c,
387 0x2502, 0x2264, 0x2265, 0x03c0, 0x2260, 0x00a3, 0x00b7
388 };
389 if (t->graphics[t->gl] && cp >= 0x60 && cp <= 0x7e)
390 return dec[cp - 0x60];
391 return cp;
394static void print_cp(Vt *t, int32_t cp) {
395 if (t->wrap) do_wrap(t);
396 int cols = t->cols, row = t->cur_row, col = t->cur_col;
397 int32_t *r = t->grid + (size_t)row * cols * 4;
398 if (t->insert) {
399 for (int i = cols - 1; i > col; i--)
400 memcpy(r + i * 4, r + (i - 1) * 4, 4 * sizeof(int32_t));
401 }
402 set_cell(r + col * 4, graphic_cp(t, cp), t->attr, t->fg, t->bg);
403 mark_row(t, row);
404 if (col < cols - 1)
405 t->cur_col = col + 1;
406 else if (t->autowrap)
407 t->wrap = 1;
410/* bulk-print a run of plain printable chars (ground-state fast path) */
411static void print_run(Vt *t, const int32_t *cps, int n) {
412 int cols = t->cols;
413 int i = 0;
414 while (i < n) {
415 if (t->wrap) do_wrap(t);
416 int row = t->cur_row, col = t->cur_col;
417 int32_t *r = t->grid + (size_t)row * cols * 4;
418 int k = n - i;
419 if (k > cols - col) k = cols - col;
420 for (int x = 0; x < k; x++)
421 set_cell(r + (col + x) * 4, graphic_cp(t, cps[i + x]), t->attr, t->fg, t->bg);
422 mark_row(t, row);
423 int ncol = col + k;
424 if (ncol < cols) {
425 t->cur_col = ncol;
426 } else {
427 t->cur_col = cols - 1;
428 if (t->autowrap) t->wrap = 1;
429 }
430 i += k;
431 }
434/* ---- erase / insert / delete --------------------------------------------- */
435static void fill_row(Vt *t, int row, int from, int to) {
436 int32_t *r = t->grid + (size_t)row * t->cols * 4;
437 for (int i = from; i < to; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
438 mark_row(t, row);
440static void erase_rows(Vt *t, int from, int to) {
441 for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);
443static void erase_display(Vt *t, int mode) {
444 if (mode == 0) {
445 fill_row(t, t->cur_row, t->cur_col, t->cols);
446 if (t->cur_row < t->rows - 1) erase_rows(t, t->cur_row + 1, t->rows - 1);
447 } else if (mode == 1) {
448 if (t->cur_row > 0) erase_rows(t, 0, t->cur_row - 1);
449 fill_row(t, t->cur_row, 0, t->cur_col + 1);
450 } else if (mode == 2) {
451 erase_rows(t, 0, t->rows - 1);
452 } else if (mode == 3) {
453 /* clear scrollback: free ring entries */
454 for (int k = 0; k < t->sb_size; k++) {
455 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
456 free(t->sb[idx]);
457 t->sb[idx] = NULL;
458 }
459 t->sb_size = 0; t->sb_head = 0;
460 }
462static void erase_line(Vt *t, int mode) {
463 if (mode == 0) fill_row(t, t->cur_row, t->cur_col, t->cols);
464 else if (mode == 1) fill_row(t, t->cur_row, 0, t->cur_col + 1);
465 else if (mode == 2) fill_row(t, t->cur_row, 0, t->cols);
467static void insert_lines(Vt *t, int n) {
468 if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {
469 int save = t->stop;
470 t->stop = t->cur_row;
471 scroll_down(t, n);
472 t->stop = save;
473 t->cur_col = 0; t->wrap = 0;
474 }
476static void delete_lines(Vt *t, int n) {
477 if (t->cur_row >= t->stop && t->cur_row <= t->sbot) {
478 int save = t->stop;
479 t->stop = t->cur_row;
480 scroll_up(t, n, 0);
481 t->stop = save;
482 t->cur_col = 0; t->wrap = 0;
483 }
485static void insert_chars(Vt *t, int n) {
486 int cols = t->cols, col = t->cur_col;
487 int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;
488 n = clampi(n, 0, cols - col);
489 for (int i = cols - 1; i >= col + n; i--)
490 memcpy(r + i * 4, r + (i - n) * 4, 4 * sizeof(int32_t));
491 for (int i = col; i < col + n; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
492 mark_row(t, t->cur_row);
494static void delete_chars(Vt *t, int n) {
495 int cols = t->cols, col = t->cur_col;
496 int32_t *r = t->grid + (size_t)t->cur_row * cols * 4;
497 n = clampi(n, 0, cols - col);
498 for (int i = col; i < cols - n; i++)
499 memcpy(r + i * 4, r + (i + n) * 4, 4 * sizeof(int32_t));
500 for (int i = cols - n; i < cols; i++) set_cell(r + i * 4, 32, 0, -1, t->bg);
501 mark_row(t, t->cur_row);
503static void erase_chars(Vt *t, int n) {
504 fill_row(t, t->cur_row, t->cur_col, clampi(t->cur_col + n, t->cur_col, t->cols));
507/* ---- tabs ---------------------------------------------------------------- */
508static void tab_forward(Vt *t) {
509 int cols = t->cols;
510 for (int i = t->cur_col + 1; ; i++) {
511 if (i >= cols) { t->cur_col = cols - 1; return; }
512 if (t->tabs[i]) { t->cur_col = i; return; }
513 }
515static void tab_back(Vt *t) {
516 for (int i = t->cur_col - 1; ; i--) {
517 if (i <= 0) { t->cur_col = 0; return; }
518 if (t->tabs[i]) { t->cur_col = i; return; }
519 }
521static void default_tabs(Vt *t) {
522 memset(t->tabs, 0, t->cols);
523 for (int i = 8; i < t->cols; i += 8) t->tabs[i] = 1;
526/* ---- alt screen / cursor save ------------------------------------------- */
527static void save_cursor(Vt *t) {
528 SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;
529 d->has = 1;
530 d->row = t->cur_row; d->col = t->cur_col;
531 d->attr = t->attr; d->fg = t->fg; d->bg = t->bg;
532 d->origin = t->origin; d->autowrap = t->autowrap;
533 memcpy(d->graphics, t->graphics, sizeof(d->graphics));
534 d->gl = t->gl;
536static void restore_cursor(Vt *t) {
537 SavedCursor *d = t->alt_active ? &t->saved_alt : &t->saved_main;
538 if (!d->has) return;
539 t->attr = d->attr; t->fg = d->fg; t->bg = d->bg;
540 t->origin = d->origin; t->autowrap = d->autowrap;
541 memcpy(t->graphics, d->graphics, sizeof(t->graphics));
542 t->gl = d->gl;
543 set_cursor(t, d->row, d->col);
545static void enter_alt(Vt *t) {
546 if (t->alt_active) return;
547 if (!t->alt) t->alt = alloc_grid(t->cols, t->rows, -1);
548 /* alt always starts cleared */
549 for (int r = 0; r < t->rows; r++)
550 fill_blank(t->alt + (size_t)r * t->cols * 4, t->cols, -1);
551 t->alt_active = 1;
552 t->grid = t->alt;
553 mark_all(t);
555static void leave_alt(Vt *t) {
556 if (!t->alt_active) return;
557 t->alt_active = 0;
558 t->grid = t->main;
559 mark_all(t);
562/* ======================================================================== */
563/* SGR */
564/* ======================================================================== */
566static void attr_on(Vt *t, int32_t bit) { t->attr |= bit; }
567static void attr_off(Vt *t, int32_t bit) { t->attr &= ~bit; }
569/* colon-form extended color from a group's sub-params (group[1..]) */
570static int32_t parse_colon_color(const int32_t *sub, int len) {
571 if (len >= 2 && sub[0] == 5)
572 return clampi(sub[1], 0, 255);
573 if (len >= 1 && sub[0] == 2) {
574 /* (2 r g b) or (2 colorspace r g b): 5-long skips the colorspace slot */
575 const int32_t *rgb = (len >= 5) ? sub + 2 : sub + 1;
576 int rlen = (len >= 5) ? len - 2 : len - 1;
577 if (rlen >= 3)
578 return 0x1000000 + (clampi(rgb[0], 0, 255) << 16)
579 + (clampi(rgb[1], 0, 255) << 8)
580 + clampi(rgb[2], 0, 255);
581 }
582 return -2; /* sentinel: no color */
585/* legacy semicolon-form: consume from a flat int list of following group heads.
586 * Returns the encoded color (or -2), and *consumed = # of heads eaten. */
587static int32_t parse_ext_color(const int32_t *heads, int nheads, int *consumed) {
588 if (nheads >= 1 && heads[0] == 5) {
589 if (nheads >= 2) { *consumed = 2; return clampi(heads[1], 0, 255); }
590 *consumed = 1; return -2;
591 }
592 if (nheads >= 1 && heads[0] == 2) {
593 if (nheads >= 4) {
594 *consumed = 4;
595 return 0x1000000 + (clampi(heads[1], 0, 255) << 16)
596 + (clampi(heads[2], 0, 255) << 8)
597 + clampi(heads[3], 0, 255);
598 }
599 *consumed = 1; return -2;
600 }
601 *consumed = (nheads >= 1) ? 1 : 0;
602 return -2;
605static void sgr(Vt *t, int32_t (*groups)[VT_MAX_SUB], const int *glen, int ng) {
606 /* empty -> a single {0} */
607 int32_t zero[1] = {0};
608 if (ng == 0) {
609 t->attr = 0; t->fg = -1; t->bg = -1;
610 (void)zero;
611 return;
612 }
613 int i = 0;
614 while (i < ng) {
615 int32_t p = groups[i][0];
616 const int32_t *sub = groups[i];
617 int slen = glen[i];
618 if (p == 0) { t->attr = 0; t->fg = -1; t->bg = -1; }
619 else if (p == 1) attr_on(t, VT_ATTR_BOLD);
620 else if (p == 2) attr_on(t, VT_ATTR_DIM);
621 else if (p == 3) attr_on(t, VT_ATTR_ITALIC);
622 else if (p == 4) attr_on(t, VT_ATTR_UNDERLINE);
623 else if (p == 5) attr_on(t, VT_ATTR_BLINK);
624 else if (p == 7) attr_on(t, VT_ATTR_INVERSE);
625 else if (p == 8) attr_on(t, VT_ATTR_HIDDEN);
626 else if (p == 9) attr_on(t, VT_ATTR_STRIKE);
627 else if (p == 21) attr_on(t, VT_ATTR_UNDERLINE);
628 else if (p == 22) attr_off(t, VT_ATTR_BOLD | VT_ATTR_DIM);
629 else if (p == 23) attr_off(t, VT_ATTR_ITALIC);
630 else if (p == 24) attr_off(t, VT_ATTR_UNDERLINE);
631 else if (p == 25) attr_off(t, VT_ATTR_BLINK);
632 else if (p == 27) attr_off(t, VT_ATTR_INVERSE);
633 else if (p == 28) attr_off(t, VT_ATTR_HIDDEN);
634 else if (p == 29) attr_off(t, VT_ATTR_STRIKE);
635 else if (p >= 30 && p <= 37) t->fg = p - 30;
636 else if (p == 38 || p == 48) {
637 int is_fg = (p == 38);
638 if (slen > 1) {
639 /* sub-params AFTER the 38/48 head (sub+1) are the color spec */
640 int32_t c = parse_colon_color(sub + 1, slen - 1);
641 if (c != -2) { if (is_fg) t->fg = c; else t->bg = c; }
642 } else {
643 /* legacy: gather following group heads into a flat list */
644 int32_t heads[8]; int nh = 0;
645 for (int j = i + 1; j < ng && nh < 8; j++) heads[nh++] = groups[j][0];
646 int consumed = 0;
647 int32_t c = parse_ext_color(heads, nh, &consumed);
648 if (c != -2) { if (is_fg) t->fg = c; else t->bg = c; }
649 i += consumed; /* skip the consumed groups */
650 }
651 }
652 else if (p == 39) t->fg = -1;
653 else if (p >= 40 && p <= 47) t->bg = p - 40;
654 else if (p == 49) t->bg = -1;
655 else if (p >= 90 && p <= 97) t->fg = 8 + (p - 90);
656 else if (p >= 100 && p <= 107) t->bg = 8 + (p - 100);
657 /* else: unknown, ignore */
658 i++;
659 }
662/* ======================================================================== */
663/* DEC / ANSI modes */
664/* ======================================================================== */
666static void dec_mode(Vt *t, int mode, int on) {
667 switch (mode) {
668 case 1: t->appcur = on; break;
669 case 6: t->origin = on; set_cursor_abs(t, 0, 0); break;
670 case 7: t->autowrap = on; break;
671 case 25: t->curvis = on; mark_row(t, t->cur_row); break;
672 case 47:
673 case 1047: if (on) enter_alt(t); else leave_alt(t); break;
674 case 1048: if (on) save_cursor(t); else restore_cursor(t); break;
675 case 1049:
676 if (on) { save_cursor(t); enter_alt(t); }
677 else { leave_alt(t); restore_cursor(t); }
678 break;
679 case 2004: t->bracket = on; break;
680 /* mouse-mode flags (parsed as flags in v1) */
681 case 1000: if (on) t->mouse |= VT_MOUSE_1000; else t->mouse &= ~VT_MOUSE_1000; break;
682 case 1002: if (on) t->mouse |= VT_MOUSE_1002; else t->mouse &= ~VT_MOUSE_1002; break;
683 case 1003: if (on) t->mouse |= VT_MOUSE_1003; else t->mouse &= ~VT_MOUSE_1003; break;
684 case 1006: if (on) t->mouse |= VT_MOUSE_1006; else t->mouse &= ~VT_MOUSE_1006; break;
685 case 1007: if (on) t->mouse |= VT_MOUSE_1007; else t->mouse &= ~VT_MOUSE_1007; break;
686 default: break; /* unknown modes parsed + ignored */
687 }
690static void ansi_mode(Vt *t, int mode, int on) {
691 if (mode == 4) t->insert = on;
694/* ======================================================================== */
695/* CSI dispatch */
696/* ======================================================================== */
698/* Build the finalized param groups into out[][]/outlen[], returning ngroups. */
699static int finalize_params(Vt *t, int32_t out[][VT_MAX_SUB], int *outlen) {
700 int ng = 0;
701 for (int i = 0; i < t->ngroups; i++) {
702 for (int j = 0; j < t->group_len[i]; j++) out[ng][j] = t->groups[i][j];
703 outlen[ng] = t->group_len[i];
704 ng++;
705 if (ng >= VT_MAX_GROUPS) return ng;
706 }
707 /* append the in-flight group unless there were no params at all */
708 if (!(t->ngroups == 0 && t->cur_digits < 0 && t->cur_sub_len == 0)) {
709 int j = 0;
710 for (; j < t->cur_sub_len && j < VT_MAX_SUB; j++) out[ng][j] = t->cur_sub[j];
711 if (j < VT_MAX_SUB) out[ng][j++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
712 outlen[ng] = j;
713 ng++;
714 }
715 return ng;
718static int p1(int32_t g[][VT_MAX_SUB], int ng, int deflt) {
719 int v = (ng > 0) ? g[0][0] : 0;
720 return (v == 0) ? deflt : v;
722static int p2(int32_t g[][VT_MAX_SUB], int ng, int deflt) {
723 int v = (ng > 1) ? g[1][0] : 0;
724 return (v == 0) ? deflt : v;
726static int p1raw(int32_t g[][VT_MAX_SUB], int ng) {
727 return (ng > 0) ? g[0][0] : 0;
730static void term_reset(Vt *t);
732static void csi_dispatch(Vt *t, int final) {
733 int32_t g[VT_MAX_GROUPS][VT_MAX_SUB];
734 int glen[VT_MAX_GROUPS];
735 int ng = finalize_params(t, g, glen);
736 int prefix = t->prefix;
737 const char *inter = t->inter;
739 if (prefix == '?' && final == 'h') {
740 for (int i = 0; i < ng; i++) dec_mode(t, g[i][0], 1);
741 return;
742 }
743 if (prefix == '?' && final == 'l') {
744 for (int i = 0; i < ng; i++) dec_mode(t, g[i][0], 0);
745 return;
746 }
747 if (prefix) return; /* other private-prefixed: ignore */
749 if (strcmp(inter, " ") == 0 && final == 'q') {
750 t->curstyle = clampi(p1raw(g, ng), 0, 6);
751 return;
752 }
753 if (inter[0] != 0) return; /* other intermediates: ignore */
755 switch (final) {
756 case '@': insert_chars(t, p1(g, ng, 1)); break;
757 case 'A': move_rows(t, -p1(g, ng, 1)); break;
758 case 'B': move_rows(t, p1(g, ng, 1)); break;
759 case 'C': move_cols(t, p1(g, ng, 1)); break;
760 case 'D': move_cols(t, -p1(g, ng, 1)); break;
761 case 'E': move_rows(t, p1(g, ng, 1)); t->cur_col = 0; break;
762 case 'F': move_rows(t, -p1(g, ng, 1)); t->cur_col = 0; break;
763 case 'G': case '`': set_cursor(t, t->cur_row, p1(g, ng, 1) - 1); break;
764 case 'd': set_cursor_abs(t, p1(g, ng, 1) - 1, t->cur_col); break;
765 case 'H': case 'f':
766 set_cursor_abs(t, p1(g, ng, 1) - 1, p2(g, ng, 1) - 1); break;
767 case 'I': {
768 int n = clampi(p1(g, ng, 1), 0, t->cols);
769 while (n-- > 0) tab_forward(t);
770 break;
771 }
772 case 'Z': {
773 int n = clampi(p1(g, ng, 1), 0, t->cols);
774 while (n-- > 0) tab_back(t);
775 break;
776 }
777 case 'J': erase_display(t, p1raw(g, ng)); break;
778 case 'K': erase_line(t, p1raw(g, ng)); break;
779 case 'L': insert_lines(t, p1(g, ng, 1)); break;
780 case 'M': delete_lines(t, p1(g, ng, 1)); break;
781 case 'P': delete_chars(t, p1(g, ng, 1)); break;
782 case 'X': erase_chars(t, p1(g, ng, 1)); break;
783 case 'S': scroll_up(t, p1(g, ng, 1), 1); break;
784 case 'T': scroll_down(t, p1(g, ng, 1)); break;
785 case 'm': sgr(t, g, glen, ng); break;
786 case 'h': for (int i = 0; i < ng; i++) ansi_mode(t, g[i][0], 1); break;
787 case 'l': for (int i = 0; i < ng; i++) ansi_mode(t, g[i][0], 0); break;
788 case 'g': {
789 int m = p1raw(g, ng);
790 if (m == 0) t->tabs[t->cur_col] = 0;
791 else if (m == 3) memset(t->tabs, 0, t->cols);
792 break;
793 }
794 case 'n': {
795 int m = p1raw(g, ng);
796 if (m == 5) emit_out(t, "\x1b[0n");
797 else if (m == 6) {
798 int r = t->origin ? (t->cur_row - t->stop) : t->cur_row;
799 char buf[64];
800 snprintf(buf, sizeof(buf), "\x1b[%d;%dR", r + 1, t->cur_col + 1);
801 emit_out(t, buf);
802 }
803 break;
804 }
805 case 'c': emit_out(t, "\x1b[?6c"); break;
806 case 'r': {
807 int top = p1(g, ng, 1) - 1;
808 int bot = p2(g, ng, t->rows) - 1;
809 if (top >= 0 && top < bot && bot < t->rows) {
810 t->stop = top; t->sbot = bot;
811 set_cursor_abs(t, 0, 0);
812 }
813 break;
814 }
815 case 's': save_cursor(t); break;
816 case 'u': restore_cursor(t); break;
817 default: break; /* unknown finals: parsed + ignored */
818 }
821/* ======================================================================== */
822/* OSC dispatch */
823/* ======================================================================== */
825static void osc_dispatch(Vt *t) {
826 /* find first ';' */
827 int semi = -1;
828 for (int i = 0; i < t->osc_len; i++) {
829 if (t->osc[i] == ';') { semi = i; break; }
830 }
831 if (semi >= 0) {
832 /* parse the numeric code before the ';' */
833 int code = 0, ok = (semi > 0);
834 for (int i = 0; i < semi; i++) {
835 char ch = t->osc[i];
836 if (ch < '0' || ch > '9') { ok = 0; break; }
837 code = code * 10 + (ch - '0');
838 }
839 const char *text = t->osc + semi + 1;
840 int tlen = t->osc_len - semi - 1;
841 if (ok && (code == 0 || code == 2)) {
842 set_title(t, text, tlen);
843 push_event(t, VT_EV_TITLE, text, tlen);
844 } else if (ok && code == 52) {
845 /* clipboard write (OSC 52): queued, never acted on */
846 push_event(t, VT_EV_CLIPBOARD, text, tlen);
847 }
848 }
849 t->osc_len = 0;
852/* ======================================================================== */
853/* C0 controls + parser state machine */
854/* ======================================================================== */
856static void c0(Vt *t, int b) {
857 switch (b) {
858 case 8: t->wrap = 0; if (t->cur_col > 0) t->cur_col -= 1; break; /* BS */
859 case 9: tab_forward(t); break; /* HT */
860 case 10: case 11: case 12: t->wrap = 0; line_feed(t); break; /* LF VT FF */
861 case 13: t->wrap = 0; t->cur_col = 0; break; /* CR */
862 case 14: t->gl = 1; break; /* SO */
863 case 15: t->gl = 0; break; /* SI */
864 case 7: push_event(t, VT_EV_BELL, NULL, 0); break; /* BEL */
865 default: break; /* other C0: ignore */
866 }
869static void clear_csi(Vt *t) {
870 t->ngroups = 0;
871 t->cur_digits = -1;
872 t->cur_sub_len = 0;
873 t->prefix = 0;
874 t->inter[0] = 0; t->inter_len = 0;
877static void process_cp(Vt *t, int32_t cp);
879static void esc_dispatch(Vt *t, int32_t cp) {
880 t->state = ST_GROUND;
881 switch (cp) {
882 case 91: clear_csi(t); t->state = ST_CSI; break; /* [ */
883 case 93: t->osc_len = 0; t->state = ST_OSC; break; /* ] */
884 case 80: case 88: case 94: case 95: /* P X ^ _ */
885 t->state = ST_STR_IGNORE; break;
886 case 55: save_cursor(t); break; /* 7 */
887 case 56: restore_cursor(t); break; /* 8 */
888 case 68: line_feed(t); break; /* D IND */
889 case 69: t->cur_col = 0; line_feed(t); break; /* E NEL */
890 case 72: t->tabs[t->cur_col] = 1; break; /* H HTS */
891 case 77: /* M RI */
892 if (t->cur_row == t->stop) scroll_down(t, 1);
893 else move_rows(t, -1);
894 break;
895 case 99: term_reset(t); break; /* c RIS */
896 case 40: case 41: /* ( ) G0/G1 */
897 t->charset_target = cp - 40;
898 t->state = ST_ESC_CHARSET;
899 break;
900 case 42: case 43: /* * + G2/G3 */
901 t->state = ST_ESC_SKIP1; break;
902 case 35: t->state = ST_ESC_HASH; break; /* # */
903 case 61: case 62: break; /* = > keypad: ignore */
904 case 92: break; /* \ ST stray: ignore */
905 case 27: t->state = ST_ESC; break;
906 default: break;
907 }
910static void process_cp(Vt *t, int32_t cp) {
911 switch (t->state) {
912 case ST_GROUND:
913 if (cp == 27) t->state = ST_ESC;
914 else if (cp < 32) c0(t, cp);
915 else if (cp == 127) { /* DEL: ignore */ }
916 else print_cp(t, cp);
917 break;
919 case ST_ESC:
920 esc_dispatch(t, cp);
921 break;
923 case ST_ESC_SKIP1:
924 t->state = ST_GROUND;
925 break;
927 case ST_ESC_CHARSET:
928 if (cp == 27) t->state = ST_ESC;
929 else if (cp == 24 || cp == 26) t->state = ST_GROUND;
930 else if (cp < 32) c0(t, cp);
931 else {
932 /* Unsupported designations are ignored, as before. */
933 if (cp == '0' || cp == 'B')
934 t->graphics[t->charset_target] = (cp == '0');
935 t->state = ST_GROUND;
936 }
937 break;
939 case ST_ESC_HASH:
940 t->state = ST_GROUND;
941 if (cp == 56) { /* DECALN: fill screen with E */
942 for (int r = 0; r < t->rows; r++) {
943 int32_t *row = t->grid + (size_t)r * t->cols * 4;
944 for (int cc = 0; cc < t->cols; cc++) set_cell(row + cc * 4, 69, 0, -1, -1);
945 }
946 t->stop = 0; t->sbot = t->rows - 1;
947 set_cursor(t, 0, 0);
948 mark_all(t);
949 }
950 break;
952 case ST_CSI:
953 if (cp >= 48 && cp <= 57) {
954 /* int64 accumulation then clamp: a hostile digit run must not
955 * build bignums OR overflow int (UBSan-clean). */
956 int64_t cur = (t->cur_digits < 0) ? 0 : t->cur_digits;
957 cur = cur * 10 + (cp - 48);
958 if (cur > VT_PARAM_CAP) cur = VT_PARAM_CAP;
959 t->cur_digits = (int)cur;
960 } else if (cp == 58) { /* ':' sub-param sep */
961 if (t->cur_sub_len < VT_MAX_SUB)
962 t->cur_sub[t->cur_sub_len++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
963 t->cur_digits = -1;
964 } else if (cp == 59) { /* ';' group sep */
965 if (t->ngroups < VT_MAX_GROUPS) {
966 int j = 0;
967 for (; j < t->cur_sub_len && j < VT_MAX_SUB; j++)
968 t->groups[t->ngroups][j] = t->cur_sub[j];
969 if (j < VT_MAX_SUB)
970 t->groups[t->ngroups][j++] = (t->cur_digits < 0) ? 0 : t->cur_digits;
971 t->group_len[t->ngroups] = j;
972 t->ngroups++;
973 }
974 t->cur_sub_len = 0;
975 t->cur_digits = -1;
976 } else if (cp >= 60 && cp <= 63) { /* private markers < = > ? */
977 t->prefix = cp;
978 } else if (cp >= 32 && cp <= 47) { /* intermediates */
979 if (t->inter_len < VT_INTER_MAX) {
980 t->inter[t->inter_len++] = (char)cp;
981 t->inter[t->inter_len] = 0;
982 }
983 } else if (cp >= 64 && cp <= 126) { /* final byte */
984 t->state = ST_GROUND;
985 csi_dispatch(t, cp);
986 } else if (cp == 24 || cp == 26) { /* CAN/SUB abort */
987 t->state = ST_GROUND;
988 } else if (cp == 27) {
989 t->state = ST_ESC;
990 } else if (cp < 32) { /* C0 within CSI executes */
991 c0(t, cp);
992 } else {
993 t->state = ST_GROUND;
994 }
995 break;
997 case ST_OSC:
998 if (cp == 7) { t->state = ST_GROUND; osc_dispatch(t); }
999 else if (cp == 27) { t->state = ST_OSC_ESC; }
1000 else if (cp == 24 || cp == 26) { t->osc_len = 0; t->state = ST_GROUND; }
1001 else {
1002 /* accumulate the codepoint's UTF-8 bytes (capped) */
1003 if (t->osc_len < VT_OSC_MAX) {
1004 if (cp < 0x80) {
1005 t->osc[t->osc_len++] = (char)cp;
1006 } else if (cp < 0x800) {
1007 if (t->osc_len + 2 <= VT_OSC_MAX) {
1008 t->osc[t->osc_len++] = (char)(0xC0 | (cp >> 6));
1009 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
1011 } else if (cp < 0x10000) {
1012 if (t->osc_len + 3 <= VT_OSC_MAX) {
1013 t->osc[t->osc_len++] = (char)(0xE0 | (cp >> 12));
1014 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
1015 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
1017 } else {
1018 if (t->osc_len + 4 <= VT_OSC_MAX) {
1019 t->osc[t->osc_len++] = (char)(0xF0 | (cp >> 18));
1020 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 12) & 0x3F));
1021 t->osc[t->osc_len++] = (char)(0x80 | ((cp >> 6) & 0x3F));
1022 t->osc[t->osc_len++] = (char)(0x80 | (cp & 0x3F));
1027 break;
1029 case ST_OSC_ESC:
1030 if (cp == 92) { t->state = ST_GROUND; osc_dispatch(t); } /* ESC \ = ST */
1031 else { t->osc_len = 0; t->state = ST_ESC; process_cp(t, cp); }
1032 break;
1034 case ST_STR_IGNORE:
1035 if (cp == 7) t->state = ST_GROUND;
1036 else if (cp == 27) t->state = ST_STR_ESC;
1037 break;
1039 case ST_STR_ESC:
1040 if (cp == 92) t->state = ST_GROUND;
1041 else { t->state = ST_STR_IGNORE; if (cp == 27) t->state = ST_STR_ESC; }
1042 break;
1044 default:
1045 t->state = ST_GROUND;
1046 break;
1050/* ======================================================================== */
1051/* reset / resize */
1052/* ======================================================================== */
1054static void term_reset(Vt *t) {
1055 int cols = t->cols, rows = t->rows;
1056 t->alt_active = 0;
1057 t->grid = t->main;
1058 for (int r = 0; r < rows; r++)
1059 fill_blank(t->main + (size_t)r * cols * 4, cols, -1);
1060 t->cur_row = 0; t->cur_col = 0; t->wrap = 0;
1061 t->attr = 0; t->fg = -1; t->bg = -1;
1062 t->stop = 0; t->sbot = rows - 1;
1063 t->saved_main.has = 0; t->saved_alt.has = 0;
1064 t->graphics[0] = 0; t->graphics[1] = 0; t->gl = 0;
1065 t->state = ST_GROUND;
1066 t->autowrap = 1; t->origin = 0; t->curvis = 1;
1067 t->bracket = 0; t->appcur = 0; t->insert = 0;
1068 t->mouse = 0;
1069 default_tabs(t);
1070 t->curstyle = 0;
1071 mark_all(t);
1074/* resize a single grid buffer with xterm no-rewrap semantics.
1075 * `active` marks the buffer the cursor lives on; `main_screen` marks the buffer
1076 * that participates in scrollback. Returns a fresh buffer (caller frees old). */
1077static int32_t *resize_grid(Vt *t, int32_t *g, int old_rows, int cols, int rows,
1078 int active, int main_screen) {
1079 int ocols = t->cols;
1080 int32_t *ng = alloc_grid(cols, rows, -1);
1081 if (rows >= old_rows) {
1082 int need = rows - old_rows;
1083 int pull = main_screen ? (need < t->sb_size ? need : t->sb_size) : 0;
1084 /* pull rows out of scrollback into the top */
1085 for (int k = 0; k < pull; k++) {
1086 int hist_w = 0;
1087 int32_t *hist = sb_pop(t, &hist_w); /* newest first */
1088 /* place newest adjacent to old top: slot (pull-1-k)?? term.sgl puts
1089 * the newest scrollback row at (pull-1) descending as it pops. */
1090 int slot = pull - 1 - k;
1091 int32_t *dst = ng + (size_t)slot * cols * 4;
1092 /* the history row's OWN width — NOT ocols. After two resizes
1093 * (push@40 -> 80 -> 132) ocols is 80 and the row is 40 wide. */
1094 int copy = (hist_w < cols) ? hist_w : cols;
1095 for (int c = 0; c < copy; c++)
1096 memcpy(dst + c * 4, hist + c * 4, 4 * sizeof(int32_t));
1097 free(hist);
1099 /* old grid rows */
1100 for (int j = 0; j < old_rows; j++) {
1101 int32_t *src = g + (size_t)j * ocols * 4;
1102 int32_t *dst = ng + (size_t)(pull + j) * cols * 4;
1103 int copy = (ocols < cols) ? ocols : cols;
1104 for (int c = 0; c < copy; c++)
1105 memcpy(dst + c * 4, src + c * 4, 4 * sizeof(int32_t));
1107 if (active && pull > 0)
1108 t->cur_row = clampi(t->cur_row + pull, 0, rows - 1);
1109 } else {
1110 int drop = old_rows - rows;
1111 int curs = active ? t->cur_row : old_rows - 1;
1112 int drop_bot = drop < (old_rows - 1 - curs) ? drop : (old_rows - 1 - curs);
1113 int drop_top = drop - drop_bot;
1114 for (int j = 0; j < drop_top; j++) {
1115 if (main_screen) sb_push(t, g + (size_t)j * ocols * 4);
1117 for (int j = 0; j < rows; j++) {
1118 int32_t *src = g + (size_t)(j + drop_top) * ocols * 4;
1119 int32_t *dst = ng + (size_t)j * cols * 4;
1120 int copy = (ocols < cols) ? ocols : cols;
1121 for (int c = 0; c < copy; c++)
1122 memcpy(dst + c * 4, src + c * 4, 4 * sizeof(int32_t));
1124 if (active)
1125 t->cur_row = clampi(t->cur_row - drop_top, 0, rows - 1);
1127 return ng;
1130static void term_resize(Vt *t, int cols, int rows) {
1131 int ocols = t->cols, orows = t->rows;
1132 if (cols == ocols && rows == orows) return;
1133 if (cols < 1) cols = 1;
1134 if (rows < 1) rows = 1;
1136 /* Ring entries KEEP the width they were pushed at (xterm no-rewrap): they
1137 * are not re-widthed here, and must never be read at t->cols. Each entry's
1138 * width lives in sb_w — every reader uses it (nat_scrollback_row,
1139 * nat_scrollback_runs, and the resize pull in resize_grid).
1141 * This comment previously claimed ring rows "are only read during pull
1142 * (min-copied) — safe". That stopped being true when the render seam added
1143 * readers, and the min-copy in the pull was itself wrong after two resizes
1144 * (it used ocols, not the row's width). Result: a heap over-read of up to
1145 * (t->cols - push_width) cells, rendered into the terminal. See t-d4c7. */
1147 if (t->alt_active) {
1148 int32_t *nmain = resize_grid(t, t->main, orows, cols, rows, 0, 1);
1149 int32_t *nalt = resize_grid(t, t->alt, orows, cols, rows, 1, 0);
1150 free(t->main); free(t->alt);
1151 t->main = nmain; t->alt = nalt;
1152 t->grid = t->alt;
1153 } else {
1154 int32_t *nmain = resize_grid(t, t->main, orows, cols, rows, 1, 1);
1155 free(t->main);
1156 if (t->alt) { free(t->alt); t->alt = NULL; }
1157 t->main = nmain;
1158 t->grid = t->main;
1161 t->cols = cols; t->rows = rows;
1162 t->stop = 0; t->sbot = rows - 1;
1163 t->cur_col = clampi(t->cur_col, 0, cols - 1);
1164 t->cur_row = clampi(t->cur_row, 0, rows - 1);
1165 t->wrap = 0;
1166 t->tabs = (uint8_t *)realloc(t->tabs, cols);
1167 default_tabs(t);
1168 t->dirty = (uint8_t *)realloc(t->dirty, rows);
1169 memset(t->dirty, 0, rows);
1170 mark_all(t);
1171 (void)ocols;
1174/* ======================================================================== */
1175/* feed */
1176/* ======================================================================== */
1178static void feed_run_scan(Vt *t, const int32_t *cps, int n) {
1179 int i = 0;
1180 while (i < n) {
1181 int32_t cp = cps[i];
1182 if (cp >= 32 && cp < 127 && t->state == ST_GROUND && !t->insert) {
1183 int j = i + 1;
1184 while (j < n && cps[j] >= 32 && cps[j] < 127) j++;
1185 print_run(t, cps + i, j - i);
1186 i = j;
1187 } else {
1188 process_cp(t, cp);
1189 i++;
1194static int32_t valid_cp(int32_t cp, int32_t mn) {
1195 if (cp < mn || (cp >= 0xD800 && cp < 0xE000) || cp > 0x10FFFF) return 65533;
1196 return cp;
1199static void feed_byte(Vt *t, int b) {
1200 int need = t->u8need;
1201 if (need > 0) {
1202 if (b >= 128 && b < 192) {
1203 t->u8acc = t->u8acc * 64 + (b - 128);
1204 t->u8need = need - 1;
1205 if (t->u8need == 0) process_cp(t, valid_cp(t->u8acc, t->u8min));
1206 } else {
1207 t->u8need = 0;
1208 process_cp(t, 65533);
1209 feed_byte(t, b); /* reprocess this byte fresh */
1211 } else if (b < 128) {
1212 process_cp(t, b);
1213 } else if (b >= 194 && b < 224) {
1214 t->u8need = 1; t->u8acc = b - 192; t->u8min = 0x80;
1215 } else if (b >= 224 && b < 240) {
1216 t->u8need = 2; t->u8acc = b - 224; t->u8min = 0x800;
1217 } else if (b >= 240 && b < 245) {
1218 t->u8need = 3; t->u8acc = b - 240; t->u8min = 0x10000;
1219 } else {
1220 process_cp(t, 65533);
1224/* ======================================================================== */
1225/* palette */
1226/* ======================================================================== */
1228static int cube_level(int i) { return i == 0 ? 0 : 55 + i * 40; }
1229static int32_t color_256_rgb(int i) {
1230 if (i < 16) return 0;
1231 if (i < 232) {
1232 int k = i - 16;
1233 int r = cube_level(k / 36);
1234 int g = cube_level((k / 6) % 6);
1235 int b = cube_level(k % 6);
1236 return r * 65536 + g * 256 + b;
1238 int v = 8 + 10 * (i - 232);
1239 return v * 65536 + v * 256 + v;
1242/* ======================================================================== */
1243/* lifecycle */
1244/* ======================================================================== */
1246static Vt *vt_new(int cols, int rows, int sbmax) {
1247 Vt *t = (Vt *)calloc(1, sizeof(Vt));
1248 if (!t) return NULL;
1249 t->cols = cols; t->rows = rows;
1250 t->main = alloc_grid(cols, rows, -1);
1251 t->grid = t->main;
1252 t->alt = NULL; t->alt_active = 0;
1253 t->fg = -1; t->bg = -1; t->attr = 0;
1254 t->stop = 0; t->sbot = rows - 1;
1255 t->state = ST_GROUND;
1256 t->cur_digits = -1;
1257 t->autowrap = 1; t->curvis = 1;
1258 t->tabs = (uint8_t *)malloc(cols);
1259 default_tabs(t);
1260 t->title = NULL; t->title_len = 0; t->title_cap = 0;
1261 t->sb_cap = sbmax > 0 ? sbmax : 1000;
1262 t->sb = (int32_t **)calloc(t->sb_cap, sizeof(int32_t *));
1263 t->sb_w = (int *)calloc(t->sb_cap, sizeof(int));
1264 t->sb_head = 0; t->sb_size = 0;
1265 t->dirty = (uint8_t *)calloc(rows, 1);
1266 t->alldirty = 1;
1267 t->out = NULL; t->out_len = 0; t->out_cap = 0;
1268 t->u8need = 0; t->u8acc = 0; t->u8min = 0;
1269 t->events = NULL; t->nevents = 0; t->events_cap = 0;
1270 return t;
1273static void vt_free(void *data) {
1274 Vt *t = (Vt *)data;
1275 if (!t) return;
1276 free(t->main);
1277 free(t->alt);
1278 free(t->tabs);
1279 free(t->title);
1280 free(t->out);
1281 for (int k = 0; k < t->sb_size; k++) {
1282 int idx = (t->sb_head - k + t->sb_cap) % t->sb_cap;
1283 free(t->sb[idx]);
1285 free(t->sb);
1286 free(t->sb_w);
1287 /* t->dirty is allocated in vt_new and realloc'd by every term_resize, and
1288 * was never freed: EVERY emulator destroyed leaked its dirty array (rows
1289 * bytes — unbounded across a session that opens and closes terminals).
1290 * `detect_leaks=1` has been set in the fuzz gate from the start and would
1291 * have caught this on day one; it reported nothing because ASan was never
1292 * linked (t-d4c7). Found within seconds of the gate being able to see. */
1293 free(t->dirty);
1294 for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);
1295 free(t->events);
1296 free(t);
1299/* ======================================================================== */
1300/* Scheme glue (excluded from the pure-C fuzz build) */
1301/* ======================================================================== */
1302#ifndef VT_FUZZ
1304static Value vt_type_tag = SIGIL_UNDEFINED;
1306static Vt *as_vt(Value v) {
1307 if (!sigil_is_foreign(v)) return NULL;
1308 if (sigil_foreign_type(v) != vt_type_tag) return NULL;
1309 return (Vt *)sigil_foreign_data(v);
1312#define REQUIRE_VT(v) \
1313 Vt *t = as_vt(v); \
1314 if (!t) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "expected a vt emulator"); return SIGIL_UNDEFINED; }
1316/* allocate a fresh Sigil vector of length n (young; fill before returning) */
1317static Value make_vec(SigilVM *vm, int n) {
1318 SigilVector *vec = (SigilVector *)sigil__gc_alloc(
1319 vm, SIGIL_OBJ_VECTOR, sizeof(SigilVector) + (size_t)n * sizeof(Value));
1320 vec->length = n;
1321 for (int i = 0; i < n; i++) vec->elements[i] = SIGIL_UNDEFINED;
1322 return sigil_ptr(vec);
1325/* a cell 4-vector #(cp attr fg bg) from a cell pointer */
1326static Value cell_vec(SigilVM *vm, const int32_t *c) {
1327 Value v = make_vec(vm, 4);
1328 SigilVector *vec = (SigilVector *)sigil_as_ptr(v);
1329 vec->elements[0] = sigil_fixnum(c[0]);
1330 vec->elements[1] = sigil_fixnum(c[1]);
1331 vec->elements[2] = sigil_fixnum(c[2]);
1332 vec->elements[3] = sigil_fixnum(c[3]);
1333 return v;
1336/* ---- constructors -------------------------------------------------------- */
1337static Value nat_make(SigilVM *vm, int argc, Value *args) {
1338 if (!sigil_is_fixnum(args[0]) || !sigil_is_fixnum(args[1])) {
1339 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-make: cols/rows must be integers");
1340 return SIGIL_UNDEFINED;
1342 int cols = (int)sigil_as_fixnum(args[0]);
1343 int rows = (int)sigil_as_fixnum(args[1]);
1344 int sbmax = (argc >= 3 && sigil_is_fixnum(args[2])) ? (int)sigil_as_fixnum(args[2]) : 1000;
1345 if (cols < 1) cols = 1;
1346 if (rows < 1) rows = 1;
1347 Vt *t = vt_new(cols, rows, sbmax);
1348 if (!t) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "%vt-make: out of memory"); return SIGIL_UNDEFINED; }
1349 return sigil_make_foreign(vm, vt_type_tag, t, vt_free, sizeof(Vt));
1352static Value nat_is(SigilVM *vm, int argc, Value *args) {
1353 (void)vm; (void)argc;
1354 return sigil_bool(as_vt(args[0]) != NULL);
1357/* ---- feeding ------------------------------------------------------------- */
1358static Value nat_feed(SigilVM *vm, int argc, Value *args) {
1359 (void)argc;
1360 REQUIRE_VT(args[0]);
1361 if (!sigil_is_string(args[1])) {
1362 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-feed!: expected string");
1363 return SIGIL_UNDEFINED;
1365 const char *bytes = sigil_string_bytes(args[1]);
1366 size_t nbytes = ((SigilString *)sigil_as_ptr(args[1]))->byte_length;
1367 /* decode UTF-8 -> codepoints on the stack in bounded chunks */
1368 enum { CHUNK = 4096 };
1369 int32_t cps[CHUNK];
1370 int nc = 0;
1371 size_t i = 0;
1372 while (i < nbytes) {
1373 unsigned char b = (unsigned char)bytes[i];
1374 int32_t cp; int len;
1375 if (b < 0x80) { cp = b; len = 1; }
1376 else if (b < 0xE0 && i + 1 < nbytes) { cp = ((b & 0x1F) << 6) | (bytes[i+1] & 0x3F); len = 2; }
1377 else if (b < 0xF0 && i + 2 < nbytes) { cp = ((b & 0x0F) << 12) | ((bytes[i+1] & 0x3F) << 6) | (bytes[i+2] & 0x3F); len = 3; }
1378 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; }
1379 else { cp = 0xFFFD; len = 1; }
1380 cps[nc++] = cp;
1381 i += len;
1382 if (nc == CHUNK) { feed_run_scan(t, cps, nc); nc = 0; }
1384 if (nc) feed_run_scan(t, cps, nc);
1385 return args[0];
1388static Value nat_feed_bytes(SigilVM *vm, int argc, Value *args) {
1389 (void)argc;
1390 REQUIRE_VT(args[0]);
1391 Value bv = args[1];
1392 if (sigil_is_bytevector(bv)) {
1393 uint8_t *data = sigil_bytevector_data(bv);
1394 size_t n = sigil_bytevector_length(bv);
1395 for (size_t i = 0; i < n; i++) feed_byte(t, data[i]);
1396 } else {
1397 /* accept a list of byte fixnums (term-feed-bytes! compatibility) */
1398 Value cur = bv;
1399 while (sigil_is_pair(cur)) {
1400 Value b = sigil_car(cur);
1401 if (sigil_is_fixnum(b)) feed_byte(t, (int)(sigil_as_fixnum(b) & 0xFF));
1402 cur = sigil_cdr(cur);
1405 return args[0];
1408/* ---- resize / reset ------------------------------------------------------ */
1409static Value nat_resize(SigilVM *vm, int argc, Value *args) {
1410 (void)argc;
1411 REQUIRE_VT(args[0]);
1412 if (!sigil_is_fixnum(args[1]) || !sigil_is_fixnum(args[2])) {
1413 sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-resize!: cols/rows must be integers");
1414 return SIGIL_UNDEFINED;
1416 term_resize(t, (int)sigil_as_fixnum(args[1]), (int)sigil_as_fixnum(args[2]));
1417 return args[0];
1419static Value nat_reset(SigilVM *vm, int argc, Value *args) {
1420 (void)argc; REQUIRE_VT(args[0]); term_reset(t); return args[0];
1422static Value nat_invalidate(SigilVM *vm, int argc, Value *args) {
1423 (void)argc; REQUIRE_VT(args[0]); mark_all(t); return args[0];
1426/* ---- accessors ----------------------------------------------------------- */
1427#define ACCESSOR_INT(NAME, EXPR) \
1428 static Value NAME(SigilVM *vm, int argc, Value *args) { \
1429 (void)argc; REQUIRE_VT(args[0]); return sigil_fixnum(EXPR); }
1430#define ACCESSOR_BOOL(NAME, EXPR) \
1431 static Value NAME(SigilVM *vm, int argc, Value *args) { \
1432 (void)argc; REQUIRE_VT(args[0]); return sigil_bool(EXPR); }
1434ACCESSOR_INT(nat_cols, t->cols)
1435ACCESSOR_INT(nat_rows, t->rows)
1436ACCESSOR_INT(nat_cursor_row, t->cur_row)
1437ACCESSOR_INT(nat_cursor_col, t->cur_col)
1438ACCESSOR_BOOL(nat_cursor_visible, t->curvis)
1439ACCESSOR_INT(nat_cursor_style, t->curstyle)
1440ACCESSOR_BOOL(nat_alt, t->alt_active)
1441ACCESSOR_BOOL(nat_bracketed, t->bracket)
1442ACCESSOR_BOOL(nat_appcur, t->appcur)
1443ACCESSOR_INT(nat_mouse, t->mouse)
1444ACCESSOR_INT(nat_scrollback_count, t->sb_size)
1446static int any_dirty(Vt *t) {
1447 if (t->alldirty) return 1;
1448 for (int i = 0; i < t->rows; i++) if (t->dirty[i]) return 1;
1449 return 0;
1451static Value nat_damaged(SigilVM *vm, int argc, Value *args) {
1452 (void)argc; REQUIRE_VT(args[0]); return sigil_bool(any_dirty(t));
1455static Value nat_title(SigilVM *vm, int argc, Value *args) {
1456 (void)argc; REQUIRE_VT(args[0]);
1457 return sigil_make_string(vm, t->title ? t->title : "", t->title_len);
1460/* ---- pending replies ----------------------------------------------------- */
1461static Value nat_take_output(SigilVM *vm, int argc, Value *args) {
1462 (void)argc; REQUIRE_VT(args[0]);
1463 Value s = sigil_make_string(vm, t->out ? t->out : "", t->out_len);
1464 t->out_len = 0;
1465 if (t->out) t->out[0] = 0;
1466 return s;
1469/* ---- damage -------------------------------------------------------------- */
1470/* returns #(all? r0 r1 ...): elt0 = bool; rest = ascending dirty row indices.
1471 * When all?, returns #(#t) and clears dirty. */
1472static Value nat_take_damage(SigilVM *vm, int argc, Value *args) {
1473 (void)argc; REQUIRE_VT(args[0]);
1474 int all = t->alldirty;
1475 t->alldirty = 0;
1476 if (all) {
1477 for (int i = 0; i < t->rows; i++) t->dirty[i] = 0;
1478 Value v = make_vec(vm, 1);
1479 sigil_vector_set(v, 0, SIGIL_TRUE);
1480 return v;
1482 int count = 0;
1483 for (int i = 0; i < t->rows; i++) if (t->dirty[i]) count++;
1484 Value v = make_vec(vm, count + 1);
1485 sigil_vector_set(v, 0, SIGIL_FALSE);
1486 int j = 1;
1487 for (int i = 0; i < t->rows; i++) {
1488 if (t->dirty[i]) { sigil_vector_set(v, j++, sigil_fixnum(i)); t->dirty[i] = 0; }
1490 return v;
1493/* ---- events -------------------------------------------------------------- */
1494/* returns a list of #(type-symbol payload-or-#f); drains the queue */
1495static Value nat_take_events(SigilVM *vm, int argc, Value *args) {
1496 (void)argc; REQUIRE_VT(args[0]);
1497 Value list = SIGIL_EMPTY;
1498 /* build reversed then it's newest-last; iterate backward to preserve order */
1499 for (int i = t->nevents - 1; i >= 0; i--) {
1500 VtEvent *e = &t->events[i];
1501 const char *tn = e->type == VT_EV_TITLE ? "title"
1502 : e->type == VT_EV_BELL ? "bell" : "clipboard";
1503 Value sym = sigil_intern_symbol(vm, tn, (int)strlen(tn));
1504 sigil__gc_push_temp_root(vm, list);
1505 sigil__gc_push_temp_root(vm, sym);
1506 Value payload = (e->type == VT_EV_BELL)
1507 ? SIGIL_FALSE
1508 : sigil_make_string(vm, e->payload ? e->payload : "", e->payload_len);
1509 sigil__gc_push_temp_root(vm, payload);
1510 Value rec = make_vec(vm, 2);
1511 sigil_vector_set(rec, 0, sym);
1512 sigil_vector_set(rec, 1, payload);
1513 sigil__gc_push_temp_root(vm, rec);
1514 Value cell = sigil_cons(vm, rec, list);
1515 sigil__gc_pop_temp_root(vm); /* rec */
1516 sigil__gc_pop_temp_root(vm); /* payload */
1517 sigil__gc_pop_temp_root(vm); /* sym */
1518 sigil__gc_pop_temp_root(vm); /* list */
1519 list = cell;
1521 for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);
1522 t->nevents = 0;
1523 return list;
1526/* ---- grid access --------------------------------------------------------- */
1527static Value nat_row_cells(SigilVM *vm, int argc, Value *args) {
1528 (void)argc; REQUIRE_VT(args[0]);
1529 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-row-cells: row must be integer"); return SIGIL_UNDEFINED; }
1530 int row = (int)sigil_as_fixnum(args[1]);
1531 if (row < 0 || row >= t->rows) return SIGIL_FALSE;
1532 int cols = t->cols;
1533 Value v = make_vec(vm, cols);
1534 sigil__gc_push_temp_root(vm, v);
1535 int32_t *r = t->grid + (size_t)row * cols * 4;
1536 for (int c = 0; c < cols; c++) {
1537 Value cv = cell_vec(vm, r + c * 4);
1538 sigil_vector_set(v, c, cv);
1540 sigil__gc_pop_temp_root(vm);
1541 return v;
1544/* newest-first scrollback row k -> vector of cells, or #f */
1545static Value nat_scrollback_row(SigilVM *vm, int argc, Value *args) {
1546 (void)argc; REQUIRE_VT(args[0]);
1547 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-scrollback-row: k must be integer"); return SIGIL_UNDEFINED; }
1548 int k = (int)sigil_as_fixnum(args[1]);
1549 int32_t *r = sb_get(t, k);
1550 if (!r) return SIGIL_FALSE;
1551 /* the row's OWN width, not t->cols: a history row keeps the width it had
1552 * when it scrolled off. The interpreted reference returns the stored vector
1553 * verbatim, so its length IS the push width — match that exactly. */
1554 int cols = sb_get_w(t, k);
1555 Value v = make_vec(vm, cols);
1556 sigil__gc_push_temp_root(vm, v);
1557 for (int c = 0; c < cols; c++) {
1558 Value cv = cell_vec(vm, r + c * 4);
1559 sigil_vector_set(v, c, cv);
1561 sigil__gc_pop_temp_root(vm);
1562 return v;
1565/* row text: the whole row as a string (codepoints) */
1566static Value nat_row_text(SigilVM *vm, int argc, Value *args) {
1567 (void)argc; REQUIRE_VT(args[0]);
1568 int row = sigil_is_fixnum(args[1]) ? (int)sigil_as_fixnum(args[1]) : -1;
1569 if (row < 0 || row >= t->rows) return sigil_make_string(vm, "", 0);
1570 int cols = t->cols;
1571 int32_t *r = t->grid + (size_t)row * cols * 4;
1572 /* encode codepoints to UTF-8 */
1573 char *buf = (char *)malloc((size_t)cols * 4 + 1);
1574 int p = 0;
1575 for (int c = 0; c < cols; c++) {
1576 int32_t cp = r[c * 4];
1577 if (cp < 0x80) buf[p++] = (char)cp;
1578 else if (cp < 0x800) { buf[p++] = (char)(0xC0|(cp>>6)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1579 else if (cp < 0x10000) { buf[p++]=(char)(0xE0|(cp>>12)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1580 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)); }
1582 Value s = sigil_make_string(vm, buf, p);
1583 free(buf);
1584 return s;
1587/* ---- row-run extraction (THE render seam) -------------------------------- */
1588/* is a cell visually a default blank (safe to right-trim)? mirrors
1589 * modes/terminal.sgl trimmable?: space, default bg, and none of
1590 * inverse/underline/strike set. */
1591static int trimmable(const int32_t *c) {
1592 return c[0] == 32 && c[3] == -1 &&
1593 (c[1] & (VT_ATTR_INVERSE | VT_ATTR_UNDERLINE | VT_ATTR_STRIKE)) == 0;
1596/* %vt-row-runs t row cursor-col -> list of #(text attr fg bg) runs.
1597 * Runs merge equal (attr,fg,bg); the cursor cell (when cursor-col is a fixnum)
1598 * forces a run break so it can carry its own styling. Right-trimmed to the last
1599 * non-trimmable cell (or the cursor col, whichever is greater). Operates over a
1600 * given cell buffer (active grid or a scrollback row). */
1601static Value row_runs_of(SigilVM *vm, const int32_t *r, int cols, int cursor_col) {
1602 /* find last non-trimmable */
1603 int last = -1;
1604 for (int i = cols - 1; i >= 0; i--) {
1605 if (!trimmable(r + i * 4)) { last = i; break; }
1607 int upto = last;
1608 if (cursor_col >= 0 && cursor_col > upto) upto = cursor_col;
1609 if (upto < 0) return SIGIL_EMPTY;
1611 /* pass 1: compute run boundaries (no allocation -> GC-safe) */
1612 int rstart[cols], rend[cols], nrun = 0;
1613 int start = 0;
1614 while (start <= upto) {
1615 int32_t attr = r[start * 4 + 1], fg = r[start * 4 + 2], bg = r[start * 4 + 3];
1616 int cur0 = (cursor_col >= 0 && cursor_col == start);
1617 int end = start;
1618 while (end + 1 <= upto) {
1619 int n = end + 1;
1620 int curn = (cursor_col >= 0 && cursor_col == n);
1621 if (r[n*4+1] == attr && r[n*4+2] == fg && r[n*4+3] == bg && curn == cur0) end++;
1622 else break;
1624 rstart[nrun] = start; rend[nrun] = end; nrun++;
1625 start = end + 1;
1628 /* pass 2: cons runs right-to-left so the result is in left-to-right order
1629 * (no reverse pass, and every intermediate is temp-rooted across allocs) */
1630 char *buf = (char *)malloc((size_t)(upto + 1) * 4 + 1);
1631 Value runs = SIGIL_EMPTY;
1632 for (int k = nrun - 1; k >= 0; k--) {
1633 int s = rstart[k], e = rend[k];
1634 int32_t attr = r[s * 4 + 1], fg = r[s * 4 + 2], bg = r[s * 4 + 3];
1635 int cur0 = (cursor_col >= 0 && cursor_col == s);
1636 int p = 0;
1637 for (int i = s; i <= e; i++) {
1638 int32_t cp = r[i * 4];
1639 if (cp < 0x80) buf[p++] = (char)cp;
1640 else if (cp < 0x800) { buf[p++]=(char)(0xC0|(cp>>6)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1641 else if (cp < 0x10000) { buf[p++]=(char)(0xE0|(cp>>12)); buf[p++]=(char)(0x80|((cp>>6)&0x3F)); buf[p++]=(char)(0x80|(cp&0x3F)); }
1642 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)); }
1644 sigil__gc_push_temp_root(vm, runs);
1645 Value text = sigil_make_string(vm, buf, p);
1646 sigil__gc_push_temp_root(vm, text);
1647 Value rec = make_vec(vm, 5);
1648 sigil_vector_set(rec, 0, text);
1649 sigil_vector_set(rec, 1, sigil_fixnum(attr));
1650 sigil_vector_set(rec, 2, sigil_fixnum(fg));
1651 sigil_vector_set(rec, 3, sigil_fixnum(bg));
1652 sigil_vector_set(rec, 4, sigil_bool(cur0));
1653 sigil__gc_push_temp_root(vm, rec);
1654 Value cell = sigil_cons(vm, rec, runs);
1655 sigil__gc_pop_temp_root(vm); /* rec */
1656 sigil__gc_pop_temp_root(vm); /* text */
1657 sigil__gc_pop_temp_root(vm); /* runs */
1658 runs = cell;
1660 free(buf);
1661 return runs;
1664static Value nat_row_runs(SigilVM *vm, int argc, Value *args) {
1665 REQUIRE_VT(args[0]);
1666 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-row-runs: row must be integer"); return SIGIL_UNDEFINED; }
1667 int row = (int)sigil_as_fixnum(args[1]);
1668 if (row < 0 || row >= t->rows) return SIGIL_EMPTY;
1669 int cursor_col = -1;
1670 if (argc >= 3 && sigil_is_fixnum(args[2])) cursor_col = (int)sigil_as_fixnum(args[2]);
1671 int32_t *r = t->grid + (size_t)row * t->cols * 4;
1672 return row_runs_of(vm, r, t->cols, cursor_col);
1675static Value nat_scrollback_runs(SigilVM *vm, int argc, Value *args) {
1676 REQUIRE_VT(args[0]);
1677 if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-scrollback-runs: k must be integer"); return SIGIL_UNDEFINED; }
1678 int k = (int)sigil_as_fixnum(args[1]);
1679 int32_t *r = sb_get(t, k);
1680 if (!r) return SIGIL_EMPTY;
1681 int cursor_col = -1;
1682 if (argc >= 3 && sigil_is_fixnum(args[2])) cursor_col = (int)sigil_as_fixnum(args[2]);
1683 /* the row's OWN width, not t->cols — see sb_w. Cells past it would be blank
1684 * and right-trimmed away anyway, so this is byte-identical AND in-bounds. */
1685 return row_runs_of(vm, r, sb_get_w(t, k), cursor_col);
1688/* ---- palette ------------------------------------------------------------- */
1689static Value nat_color_256(SigilVM *vm, int argc, Value *args) {
1690 (void)argc;
1691 if (!sigil_is_fixnum(args[0])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "%vt-color-256->rgb: expected integer"); return SIGIL_UNDEFINED; }
1692 return sigil_fixnum(color_256_rgb((int)sigil_as_fixnum(args[0])));
1695/* ======================================================================== */
1696/* registration */
1697/* ======================================================================== */
1699static void vt_register_all(SigilVM *vm) {
1700 vt_type_tag = sigil_intern_symbol(vm, "vt-emulator", 11);
1701 SigilModule *module = sigil_begin_module(vm, "(sigil vt)");
1702 if (!module) return;
1704#define REG(name, fn, arity, doc) \
1705 do { sigil_module_register_native(vm, name, fn, arity, doc); \
1706 sigil_module_export(vm, name); } while (0)
1708 REG("%vt-make", nat_make, SIGIL_ARITY_RANGE(2, 3), "Create a VT emulator (cols rows [scrollback-cap])");
1709 REG("%vt?", nat_is, SIGIL_ARITY_EXACT(1), "Is value a VT emulator?");
1710 REG("%vt-feed!", nat_feed, SIGIL_ARITY_EXACT(2), "Feed a decoded string chunk");
1711 REG("%vt-feed-bytes!", nat_feed_bytes, SIGIL_ARITY_EXACT(2), "Feed raw bytes (bytevector or list)");
1712 REG("%vt-resize!", nat_resize, SIGIL_ARITY_EXACT(3), "Resize (cols rows)");
1713 REG("%vt-reset!", nat_reset, SIGIL_ARITY_EXACT(1), "Full reset (RIS)");
1714 REG("%vt-invalidate!", nat_invalidate, SIGIL_ARITY_EXACT(1), "Force full-repaint damage");
1715 REG("%vt-cols", nat_cols, SIGIL_ARITY_EXACT(1), "Columns");
1716 REG("%vt-rows", nat_rows, SIGIL_ARITY_EXACT(1), "Rows");
1717 REG("%vt-cursor-row", nat_cursor_row, SIGIL_ARITY_EXACT(1), "Cursor row");
1718 REG("%vt-cursor-col", nat_cursor_col, SIGIL_ARITY_EXACT(1), "Cursor col");
1719 REG("%vt-cursor-visible?", nat_cursor_visible, SIGIL_ARITY_EXACT(1), "Cursor visible?");
1720 REG("%vt-cursor-style", nat_cursor_style, SIGIL_ARITY_EXACT(1), "DECSCUSR style");
1721 REG("%vt-alt?", nat_alt, SIGIL_ARITY_EXACT(1), "Alt screen active?");
1722 REG("%vt-title", nat_title, SIGIL_ARITY_EXACT(1), "Window title");
1723 REG("%vt-bracketed-paste?", nat_bracketed, SIGIL_ARITY_EXACT(1), "Bracketed paste mode?");
1724 REG("%vt-app-cursor?", nat_appcur, SIGIL_ARITY_EXACT(1), "Application cursor keys?");
1725 REG("%vt-mouse-flags", nat_mouse, SIGIL_ARITY_EXACT(1), "Mouse-mode flag bitmask");
1726 REG("%vt-take-output!", nat_take_output, SIGIL_ARITY_EXACT(1), "Drain pending replies (string)");
1727 REG("%vt-take-damage!", nat_take_damage, SIGIL_ARITY_EXACT(1), "Drain damage -> #(all? rows...)");
1728 REG("%vt-damaged?", nat_damaged, SIGIL_ARITY_EXACT(1), "Any damage pending?");
1729 REG("%vt-take-events!", nat_take_events, SIGIL_ARITY_EXACT(1), "Drain out-of-band events");
1730 REG("%vt-row-cells", nat_row_cells, SIGIL_ARITY_EXACT(2), "Row as vector of #(cp attr fg bg)");
1731 REG("%vt-row-text", nat_row_text, SIGIL_ARITY_EXACT(2), "Row as string");
1732 REG("%vt-scrollback-count", nat_scrollback_count, SIGIL_ARITY_EXACT(1), "Scrollback row count");
1733 REG("%vt-scrollback-row", nat_scrollback_row, SIGIL_ARITY_EXACT(2), "Scrollback row k as cells");
1734 REG("%vt-row-runs", nat_row_runs, SIGIL_ARITY_RANGE(2, 3), "Row style-merged runs");
1735 REG("%vt-scrollback-runs", nat_scrollback_runs, SIGIL_ARITY_RANGE(2, 3), "Scrollback row runs");
1736 REG("%vt-color-256->rgb", nat_color_256, SIGIL_ARITY_EXACT(1), "256-color index -> #xRRGGBB");
1738#undef REG
1739 sigil_end_module(vm);
1742/* native-init entry (CLI/test builds) */
1743SIGIL_EXPORT void sigil__init_sigil_vt_module(SigilVM *vm) {
1744 vt_register_all(vm);
1747/* wasm-bridge entry (interim: generic app hook; durable: sigil_wasm_vt_register
1748 * once the monorepo runtime carries the weak decl — t-4c70). */
1749SIGIL_EXPORT void sigil_wasm_vt_register(SigilVM *vm) {
1750 vt_register_all(vm);
1753#endif /* !VT_FUZZ */