AtlatestRepositorysigil-vt
1
/*2
* sigil-vt — the native VT/ANSI terminal core.3
*4
* A VT100/xterm-subset terminal emulator: an incremental byte stream -> a5
* damage-tracked int32[4]-per-cell grid. This is the native reimplementation6
* of slate's (slate term) emulator (src/slate/term.sgl) — the Sigil emulator7
* IS the spec; this C port reproduces its semantics cell-for-cell (proven by8
* the ported conformance suite, test/vt-test.sgl).9
*10
* THE TRUST BOUNDARY. Terminal bytes are UNTRUSTED program output. This parser11
* converts arbitrary bytes into a CLOSED vocabulary of grid operations. No12
* escape sequence executes anything, reaches an eval, or emits markup. Params13
* are clamped, param/sub-param/OSC accumulators are fixed-capacity, and no14
* allocation is sized by input params. The core never calls back into Scheme15
* (all APIs are call-in / return-out), so the fiber-suspension constraint is16
* structurally satisfied. Fuzzed + ASan/UBSan clean on vt-feed-bytes! before17
* 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] bg21
* 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 (wcwidth23
* 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 the28
* standalone ASan/UBSan fuzz driver (native/vt-fuzz.c) can #include this file29
* and exercise the parser directly. Everything that touches the Sigil runtime30
* is guarded out under VT_FUZZ. */31
#ifndef VT_FUZZ32
#include <sigil/sigil.h>33
#endif34
#include <stdint.h>35
#include <stdlib.h>36
#include <string.h>37
#include <stdio.h>39
#ifndef VT_FUZZ40
/* ---- internal libsigil helpers (exported from libsigil; not in the public41
* header). Declared extern exactly as sigil-wasm-dom does. -------------------*/42
extern void *sigil__gc_alloc(SigilVM *vm, SigilObjType type, size_t size);43
extern void sigil__gc_push_temp_root(SigilVM *vm, Value v);44
extern void sigil__gc_pop_temp_root(SigilVM *vm);46
#define SIGIL_EXPORT __attribute__((visibility("default")))47
#endif49
/* ---- attribute bits (slate's superset wins) ------------------------------ */50
enum {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 = 51262
};64
/* ---- mouse-mode flags (DECSET; parsed as flags in v1) -------------------- */65
enum {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 --------------------------------------------- */74
enum { VT_EV_TITLE = 1, VT_EV_BELL = 2, VT_EV_CLIPBOARD = 3 };76
/* ---- parser states ------------------------------------------------------- */77
enum {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_ESC80
};82
/* ---- fixed capacities (the closed-vocabulary discipline) ----------------- */83
#define VT_MAX_GROUPS 3284
#define VT_MAX_SUB 885
#define VT_INTER_MAX 886
#define VT_OSC_MAX 102487
#define VT_PARAM_CAP 99999999989
typedef struct {90
int has;91
int row, col;92
int32_t attr, fg, bg;93
int origin, autowrap;94
} SavedCursor;96
typedef struct {97
int type;98
char *payload; /* malloc'd, may be NULL (bell) */99
int payload_len;100
} VtEvent;102
typedef 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 it142
* had when it scrolled off (xterm143
* no-rewrap), which is NOT t->cols144
* after a resize. The interpreted145
* reference got this for free —146
* its rows are Scheme vectors that147
* carry their own length. Porting148
* to raw int32_t* dropped it, so149
* every reader guessed, and a150
* widening resize read off the end151
* of the allocation. Never read an152
* entry with anything but its153
* 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
/* ======================================================================== */172
static int clampi(int x, int lo, int hi) {173
return x < lo ? lo : (x > hi ? hi : x);174
}176
static 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;178
}180
/* fill a fresh row buffer with blank cells (space, default, current bg) */181
static 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);183
}185
static 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;190
}192
/* ---- damage -------------------------------------------------------------- */193
static void mark_row(Vt *t, int i) {194
if (i >= 0 && i < t->rows) t->dirty[i] = 1;195
}196
static void mark_rows(Vt *t, int from, int to) {197
for (int i = from; i <= to; i++) mark_row(t, i);198
}199
static void mark_all(Vt *t) { t->alldirty = 1; }201
/* ---- pending replies ----------------------------------------------------- */202
static 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;213
}215
/* ---- events -------------------------------------------------------------- */216
static 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
}232
}234
/* ---- title --------------------------------------------------------------- */235
static 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;245
}247
/* ---- scrollback ring ----------------------------------------------------- */248
/* push a COPY of a grid row (cols*4 int32) as the new newest entry */249
static 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));270
}272
/* newest-first index k (0 = newest); returns NULL if out of range */273
static 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];277
}279
/* the width entry k was pushed at — the ONLY safe extent for reading it */280
static 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];284
}286
/* pop the newest entry (for resize grow); returns its buffer (caller frees) */287
static 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;295
}297
/* ======================================================================== */298
/* cursor / scrolling / printing (ported from term.sgl) */299
/* ======================================================================== */301
static 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);305
}307
static 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
}315
}317
static 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);323
}325
static void move_cols(Vt *t, int delta) {326
t->wrap = 0;327
t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);328
}330
/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */331
static 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);347
}349
static 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);361
}363
static 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;368
}370
static void do_wrap(Vt *t) {371
t->wrap = 0;372
t->cur_col = 0;373
line_feed(t);374
}376
static 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;390
}392
/* bulk-print a run of plain printable chars (ground-state fast path) */393
static 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
}414
}416
/* ---- erase / insert / delete --------------------------------------------- */417
static 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);421
}422
static void erase_rows(Vt *t, int from, int to) {423
for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);424
}425
static 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
}443
}444
static 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);448
}449
static 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
}457
}458
static 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
}466
}467
static 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);475
}476
static 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);484
}485
static 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));487
}489
/* ---- tabs ---------------------------------------------------------------- */490
static 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
}496
}497
static 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
}502
}503
static 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;506
}508
/* ---- alt screen / cursor save ------------------------------------------- */509
static 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;515
}516
static 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);522
}523
static 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);532
}533
static 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);538
}540
/* ======================================================================== */541
/* SGR */542
/* ======================================================================== */544
static void attr_on(Vt *t, int32_t bit) { t->attr |= bit; }545
static void attr_off(Vt *t, int32_t bit) { t->attr &= ~bit; }547
/* colon-form extended color from a group's sub-params (group[1..]) */548
static 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 */561
}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. */565
static 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;581
}583
static 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
}638
}640
/* ======================================================================== */641
/* DEC / ANSI modes */642
/* ======================================================================== */644
static 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
}666
}668
static void ansi_mode(Vt *t, int mode, int on) {669
if (mode == 4) t->insert = on;670
}672
/* ======================================================================== */673
/* CSI dispatch */674
/* ======================================================================== */676
/* Build the finalized param groups into out[][]/outlen[], returning ngroups. */677
static 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;694
}696
static 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;699
}700
static 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;703
}704
static int p1raw(int32_t g[][VT_MAX_SUB], int ng) {705
return (ng > 0) ? g[0][0] : 0;706
}708
static void term_reset(Vt *t);710
static 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
}797
}799
/* ======================================================================== */800
/* OSC dispatch */801
/* ======================================================================== */803
static 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;828
}830
/* ======================================================================== */831
/* C0 controls + parser state machine */832
/* ======================================================================== */834
static 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
}843
}845
static 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;851
}853
static void process_cp(Vt *t, int32_t cp);855
static 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
}880
}882
static 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 not915
* 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;1007
}1008
}1010
/* ======================================================================== */1011
/* reset / resize */1012
/* ======================================================================== */1014
static 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);1031
}1033
/* resize a single grid buffer with xterm no-rewrap semantics.1034
* `active` marks the buffer the cursor lives on; `main_screen` marks the buffer1035
* that participates in scrollback. Returns a fresh buffer (caller frees old). */1036
static 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 puts1048
* 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 resizes1052
* (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);1057
}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));1065
}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);1075
}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));1082
}1083
if (active)1084
t->cur_row = clampi(t->cur_row - drop_top, 0, rows - 1);1085
}1086
return ng;1087
}1089
static 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): they1096
* are not re-widthed here, and must never be read at t->cols. Each entry's1097
* width lives in sb_w — every reader uses it (nat_scrollback_row,1098
* nat_scrollback_runs, and the resize pull in resize_grid).1099
*1100
* This comment previously claimed ring rows "are only read during pull1101
* (min-copied) — safe". That stopped being true when the render seam added1102
* readers, and the min-copy in the pull was itself wrong after two resizes1103
* (it used ocols, not the row's width). Result: a heap over-read of up to1104
* (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;1118
}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;1131
}1133
/* ======================================================================== */1134
/* feed */1135
/* ======================================================================== */1137
static 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++;1149
}1150
}1151
}1153
static 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;1156
}1158
static 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 */1169
}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);1180
}1181
}1183
/* ======================================================================== */1184
/* palette */1185
/* ======================================================================== */1187
static int cube_level(int i) { return i == 0 ? 0 : 55 + i * 40; }1188
static 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;1196
}1197
int v = 8 + 10 * (i - 232);1198
return v * 65536 + v * 256 + v;1199
}1201
/* ======================================================================== */1202
/* lifecycle */1203
/* ======================================================================== */1205
static 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;1230
}1232
static 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]);1243
}1244
free(t->sb);1245
free(t->sb_w);1246
/* t->dirty is allocated in vt_new and realloc'd by every term_resize, and1247
* was never freed: EVERY emulator destroyed leaked its dirty array (rows1248
* 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 would1250
* have caught this on day one; it reported nothing because ASan was never1251
* 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);1256
}1258
/* ======================================================================== */1259
/* Scheme glue (excluded from the pure-C fuzz build) */1260
/* ======================================================================== */1261
#ifndef VT_FUZZ1263
static Value vt_type_tag = SIGIL_UNDEFINED;1265
static 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);1269
}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) */1276
static 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);1282
}1284
/* a cell 4-vector #(cp attr fg bg) from a cell pointer */1285
static 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;1293
}1295
/* ---- constructors -------------------------------------------------------- */1296
static 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;1300
}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));1309
}1311
static Value nat_is(SigilVM *vm, int argc, Value *args) {1312
(void)vm; (void)argc;1313
return sigil_bool(as_vt(args[0]) != NULL);1314
}1316
/* ---- feeding ------------------------------------------------------------- */1317
static 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;1323
}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; }1342
}1343
if (nc) feed_run_scan(t, cps, nc);1344
return args[0];1345
}1347
static 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);1362
}1363
}1364
return args[0];1365
}1367
/* ---- resize / reset ------------------------------------------------------ */1368
static 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;1374
}1375
term_resize(t, (int)sigil_as_fixnum(args[1]), (int)sigil_as_fixnum(args[2]));1376
return args[0];1377
}1378
static Value nat_reset(SigilVM *vm, int argc, Value *args) {1379
(void)argc; REQUIRE_VT(args[0]); term_reset(t); return args[0];1380
}1381
static Value nat_invalidate(SigilVM *vm, int argc, Value *args) {1382
(void)argc; REQUIRE_VT(args[0]); mark_all(t); return args[0];1383
}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); }1393
ACCESSOR_INT(nat_cols, t->cols)1394
ACCESSOR_INT(nat_rows, t->rows)1395
ACCESSOR_INT(nat_cursor_row, t->cur_row)1396
ACCESSOR_INT(nat_cursor_col, t->cur_col)1397
ACCESSOR_BOOL(nat_cursor_visible, t->curvis)1398
ACCESSOR_INT(nat_cursor_style, t->curstyle)1399
ACCESSOR_BOOL(nat_alt, t->alt_active)1400
ACCESSOR_BOOL(nat_bracketed, t->bracket)1401
ACCESSOR_BOOL(nat_appcur, t->appcur)1402
ACCESSOR_INT(nat_mouse, t->mouse)1403
ACCESSOR_INT(nat_scrollback_count, t->sb_size)1405
static 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;1409
}1410
static Value nat_damaged(SigilVM *vm, int argc, Value *args) {1411
(void)argc; REQUIRE_VT(args[0]); return sigil_bool(any_dirty(t));1412
}1414
static 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);1417
}1419
/* ---- pending replies ----------------------------------------------------- */1420
static 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;1426
}1428
/* ---- damage -------------------------------------------------------------- */1429
/* returns #(all? r0 r1 ...): elt0 = bool; rest = ascending dirty row indices.1430
* When all?, returns #(#t) and clears dirty. */1431
static 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;1440
}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; }1448
}1449
return v;1450
}1452
/* ---- events -------------------------------------------------------------- */1453
/* returns a list of #(type-symbol payload-or-#f); drains the queue */1454
static 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_FALSE1467
: 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;1479
}1480
for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);1481
t->nevents = 0;1482
return list;1483
}1485
/* ---- grid access --------------------------------------------------------- */1486
static 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);1498
}1499
sigil__gc_pop_temp_root(vm);1500
return v;1501
}1503
/* newest-first scrollback row k -> vector of cells, or #f */1504
static 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 had1511
* when it scrolled off. The interpreted reference returns the stored vector1512
* 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);1519
}1520
sigil__gc_pop_temp_root(vm);1521
return v;1522
}1524
/* row text: the whole row as a string (codepoints) */1525
static 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)); }1540
}1541
Value s = sigil_make_string(vm, buf, p);1542
free(buf);1543
return s;1544
}1546
/* ---- row-run extraction (THE render seam) -------------------------------- */1547
/* is a cell visually a default blank (safe to right-trim)? mirrors1548
* modes/terminal.sgl trimmable?: space, default bg, and none of1549
* inverse/underline/strike set. */1550
static 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;1553
}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 last1558
* non-trimmable cell (or the cursor col, whichever is greater). Operates over a1559
* given cell buffer (active grid or a scrollback row). */1560
static 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; }1565
}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;1582
}1583
rstart[nrun] = start; rend[nrun] = end; nrun++;1584
start = end + 1;1585
}1587
/* pass 2: cons runs right-to-left so the result is in left-to-right order1588
* (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)); }1602
}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;1618
}1619
free(buf);1620
return runs;1621
}1623
static 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);1632
}1634
static 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 blank1643
* 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);1645
}1647
/* ---- palette ------------------------------------------------------------- */1648
static 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])));1652
}1654
/* ======================================================================== */1655
/* registration */1656
/* ======================================================================== */1658
static 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 REG1698
sigil_end_module(vm);1699
}1701
/* native-init entry (CLI/test builds) */1702
SIGIL_EXPORT void sigil__init_sigil_vt_module(SigilVM *vm) {1703
vt_register_all(vm);1704
}1706
/* wasm-bridge entry (interim: generic app hook; durable: sigil_wasm_vt_register1707
* once the monorepo runtime carries the weak decl — t-4c70). */1708
SIGIL_EXPORT void sigil_wasm_vt_register(SigilVM *vm) {1709
vt_register_all(vm);1710
}1712
#endif /* !VT_FUZZ */