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, ST_ESC_CHARSET,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
int graphics[2], gl;95
} SavedCursor;97
typedef struct {98
int type;99
char *payload; /* malloc'd, may be NULL (bell) */100
int payload_len;101
} VtEvent;103
typedef 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 it146
* had when it scrolled off (xterm147
* no-rewrap), which is NOT t->cols148
* after a resize. The interpreted149
* reference got this for free —150
* its rows are Scheme vectors that151
* carry their own length. Porting152
* to raw int32_t* dropped it, so153
* every reader guessed, and a154
* widening resize read off the end155
* of the allocation. Never read an156
* entry with anything but its157
* 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
/* ======================================================================== */176
static int clampi(int x, int lo, int hi) {177
return x < lo ? lo : (x > hi ? hi : x);178
}180
static 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;182
}184
/* fill a fresh row buffer with blank cells (space, default, current bg) */185
static 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);187
}189
static 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;194
}196
/* ---- damage -------------------------------------------------------------- */197
static void mark_row(Vt *t, int i) {198
if (i >= 0 && i < t->rows) t->dirty[i] = 1;199
}200
static void mark_rows(Vt *t, int from, int to) {201
for (int i = from; i <= to; i++) mark_row(t, i);202
}203
static void mark_all(Vt *t) { t->alldirty = 1; }205
/* ---- pending replies ----------------------------------------------------- */206
static 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;217
}219
/* ---- events -------------------------------------------------------------- */220
static 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
}236
}238
/* ---- title --------------------------------------------------------------- */239
static 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;249
}251
/* ---- scrollback ring ----------------------------------------------------- */252
/* push a COPY of a grid row (cols*4 int32) as the new newest entry */253
static 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));274
}276
/* newest-first index k (0 = newest); returns NULL if out of range */277
static 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];281
}283
/* the width entry k was pushed at — the ONLY safe extent for reading it */284
static 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];288
}290
/* pop the newest entry (for resize grow); returns its buffer (caller frees) */291
static 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;299
}301
/* ======================================================================== */302
/* cursor / scrolling / printing (ported from term.sgl) */303
/* ======================================================================== */305
static 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);309
}311
static 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
}319
}321
static 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);327
}329
static void move_cols(Vt *t, int delta) {330
t->wrap = 0;331
t->cur_col = clampi(t->cur_col + delta, 0, t->cols - 1);332
}334
/* scroll region [top,bot] up by n; push? => evicted rows to scrollback */335
static 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);351
}353
static 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);365
}367
static 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;372
}374
static void do_wrap(Vt *t) {375
t->wrap = 0;376
t->cur_col = 0;377
line_feed(t);378
}380
static 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, 0x00b7388
};389
if (t->graphics[t->gl] && cp >= 0x60 && cp <= 0x7e)390
return dec[cp - 0x60];391
return cp;392
}394
static 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;408
}410
/* bulk-print a run of plain printable chars (ground-state fast path) */411
static 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
}432
}434
/* ---- erase / insert / delete --------------------------------------------- */435
static 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);439
}440
static void erase_rows(Vt *t, int from, int to) {441
for (int i = from; i <= to; i++) fill_row(t, i, 0, t->cols);442
}443
static 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
}461
}462
static 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);466
}467
static 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
}475
}476
static 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
}484
}485
static 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);493
}494
static 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);502
}503
static 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));505
}507
/* ---- tabs ---------------------------------------------------------------- */508
static 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
}514
}515
static 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
}520
}521
static 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;524
}526
/* ---- alt screen / cursor save ------------------------------------------- */527
static 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;535
}536
static 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);544
}545
static 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);554
}555
static 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);560
}562
/* ======================================================================== */563
/* SGR */564
/* ======================================================================== */566
static void attr_on(Vt *t, int32_t bit) { t->attr |= bit; }567
static void attr_off(Vt *t, int32_t bit) { t->attr &= ~bit; }569
/* colon-form extended color from a group's sub-params (group[1..]) */570
static 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 */583
}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. */587
static 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;603
}605
static 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
}660
}662
/* ======================================================================== */663
/* DEC / ANSI modes */664
/* ======================================================================== */666
static 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
}688
}690
static void ansi_mode(Vt *t, int mode, int on) {691
if (mode == 4) t->insert = on;692
}694
/* ======================================================================== */695
/* CSI dispatch */696
/* ======================================================================== */698
/* Build the finalized param groups into out[][]/outlen[], returning ngroups. */699
static 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;716
}718
static 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;721
}722
static 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;725
}726
static int p1raw(int32_t g[][VT_MAX_SUB], int ng) {727
return (ng > 0) ? g[0][0] : 0;728
}730
static void term_reset(Vt *t);732
static 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
}819
}821
/* ======================================================================== */822
/* OSC dispatch */823
/* ======================================================================== */825
static 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;850
}852
/* ======================================================================== */853
/* C0 controls + parser state machine */854
/* ======================================================================== */856
static 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
}867
}869
static 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;875
}877
static void process_cp(Vt *t, int32_t cp);879
static 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
}908
}910
static 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 not955
* 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));1010
}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));1016
}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));1023
}1024
}1025
}1026
}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;1047
}1048
}1050
/* ======================================================================== */1051
/* reset / resize */1052
/* ======================================================================== */1054
static 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);1072
}1074
/* resize a single grid buffer with xterm no-rewrap semantics.1075
* `active` marks the buffer the cursor lives on; `main_screen` marks the buffer1076
* that participates in scrollback. Returns a fresh buffer (caller frees old). */1077
static 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 puts1089
* 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 resizes1093
* (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);1098
}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));1106
}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);1116
}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));1123
}1124
if (active)1125
t->cur_row = clampi(t->cur_row - drop_top, 0, rows - 1);1126
}1127
return ng;1128
}1130
static 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): they1137
* are not re-widthed here, and must never be read at t->cols. Each entry's1138
* width lives in sb_w — every reader uses it (nat_scrollback_row,1139
* nat_scrollback_runs, and the resize pull in resize_grid).1140
*1141
* This comment previously claimed ring rows "are only read during pull1142
* (min-copied) — safe". That stopped being true when the render seam added1143
* readers, and the min-copy in the pull was itself wrong after two resizes1144
* (it used ocols, not the row's width). Result: a heap over-read of up to1145
* (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;1159
}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;1172
}1174
/* ======================================================================== */1175
/* feed */1176
/* ======================================================================== */1178
static 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++;1190
}1191
}1192
}1194
static 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;1197
}1199
static 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 */1210
}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);1221
}1222
}1224
/* ======================================================================== */1225
/* palette */1226
/* ======================================================================== */1228
static int cube_level(int i) { return i == 0 ? 0 : 55 + i * 40; }1229
static 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;1237
}1238
int v = 8 + 10 * (i - 232);1239
return v * 65536 + v * 256 + v;1240
}1242
/* ======================================================================== */1243
/* lifecycle */1244
/* ======================================================================== */1246
static 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;1271
}1273
static 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]);1284
}1285
free(t->sb);1286
free(t->sb_w);1287
/* t->dirty is allocated in vt_new and realloc'd by every term_resize, and1288
* was never freed: EVERY emulator destroyed leaked its dirty array (rows1289
* 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 would1291
* have caught this on day one; it reported nothing because ASan was never1292
* 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);1297
}1299
/* ======================================================================== */1300
/* Scheme glue (excluded from the pure-C fuzz build) */1301
/* ======================================================================== */1302
#ifndef VT_FUZZ1304
static Value vt_type_tag = SIGIL_UNDEFINED;1306
static 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);1310
}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) */1317
static 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);1323
}1325
/* a cell 4-vector #(cp attr fg bg) from a cell pointer */1326
static 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;1334
}1336
/* ---- constructors -------------------------------------------------------- */1337
static 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;1341
}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));1350
}1352
static Value nat_is(SigilVM *vm, int argc, Value *args) {1353
(void)vm; (void)argc;1354
return sigil_bool(as_vt(args[0]) != NULL);1355
}1357
/* ---- feeding ------------------------------------------------------------- */1358
static 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;1364
}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; }1383
}1384
if (nc) feed_run_scan(t, cps, nc);1385
return args[0];1386
}1388
static 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);1403
}1404
}1405
return args[0];1406
}1408
/* ---- resize / reset ------------------------------------------------------ */1409
static 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;1415
}1416
term_resize(t, (int)sigil_as_fixnum(args[1]), (int)sigil_as_fixnum(args[2]));1417
return args[0];1418
}1419
static Value nat_reset(SigilVM *vm, int argc, Value *args) {1420
(void)argc; REQUIRE_VT(args[0]); term_reset(t); return args[0];1421
}1422
static Value nat_invalidate(SigilVM *vm, int argc, Value *args) {1423
(void)argc; REQUIRE_VT(args[0]); mark_all(t); return args[0];1424
}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); }1434
ACCESSOR_INT(nat_cols, t->cols)1435
ACCESSOR_INT(nat_rows, t->rows)1436
ACCESSOR_INT(nat_cursor_row, t->cur_row)1437
ACCESSOR_INT(nat_cursor_col, t->cur_col)1438
ACCESSOR_BOOL(nat_cursor_visible, t->curvis)1439
ACCESSOR_INT(nat_cursor_style, t->curstyle)1440
ACCESSOR_BOOL(nat_alt, t->alt_active)1441
ACCESSOR_BOOL(nat_bracketed, t->bracket)1442
ACCESSOR_BOOL(nat_appcur, t->appcur)1443
ACCESSOR_INT(nat_mouse, t->mouse)1444
ACCESSOR_INT(nat_scrollback_count, t->sb_size)1446
static 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;1450
}1451
static Value nat_damaged(SigilVM *vm, int argc, Value *args) {1452
(void)argc; REQUIRE_VT(args[0]); return sigil_bool(any_dirty(t));1453
}1455
static 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);1458
}1460
/* ---- pending replies ----------------------------------------------------- */1461
static 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;1467
}1469
/* ---- damage -------------------------------------------------------------- */1470
/* returns #(all? r0 r1 ...): elt0 = bool; rest = ascending dirty row indices.1471
* When all?, returns #(#t) and clears dirty. */1472
static 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;1481
}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; }1489
}1490
return v;1491
}1493
/* ---- events -------------------------------------------------------------- */1494
/* returns a list of #(type-symbol payload-or-#f); drains the queue */1495
static 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_FALSE1508
: 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;1520
}1521
for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);1522
t->nevents = 0;1523
return list;1524
}1526
/* ---- grid access --------------------------------------------------------- */1527
static 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);1539
}1540
sigil__gc_pop_temp_root(vm);1541
return v;1542
}1544
/* newest-first scrollback row k -> vector of cells, or #f */1545
static 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 had1552
* when it scrolled off. The interpreted reference returns the stored vector1553
* 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);1560
}1561
sigil__gc_pop_temp_root(vm);1562
return v;1563
}1565
/* row text: the whole row as a string (codepoints) */1566
static 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)); }1581
}1582
Value s = sigil_make_string(vm, buf, p);1583
free(buf);1584
return s;1585
}1587
/* ---- row-run extraction (THE render seam) -------------------------------- */1588
/* is a cell visually a default blank (safe to right-trim)? mirrors1589
* modes/terminal.sgl trimmable?: space, default bg, and none of1590
* inverse/underline/strike set. */1591
static 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;1594
}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 last1599
* non-trimmable cell (or the cursor col, whichever is greater). Operates over a1600
* given cell buffer (active grid or a scrollback row). */1601
static 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; }1606
}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;1623
}1624
rstart[nrun] = start; rend[nrun] = end; nrun++;1625
start = end + 1;1626
}1628
/* pass 2: cons runs right-to-left so the result is in left-to-right order1629
* (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)); }1643
}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;1659
}1660
free(buf);1661
return runs;1662
}1664
static 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);1673
}1675
static 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 blank1684
* 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);1686
}1688
/* ---- palette ------------------------------------------------------------- */1689
static 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])));1693
}1695
/* ======================================================================== */1696
/* registration */1697
/* ======================================================================== */1699
static 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 REG1739
sigil_end_module(vm);1740
}1742
/* native-init entry (CLI/test builds) */1743
SIGIL_EXPORT void sigil__init_sigil_vt_module(SigilVM *vm) {1744
vt_register_all(vm);1745
}1747
/* wasm-bridge entry (interim: generic app hook; durable: sigil_wasm_vt_register1748
* once the monorepo runtime carries the weak decl — t-4c70). */1749
SIGIL_EXPORT void sigil_wasm_vt_register(SigilVM *vm) {1750
vt_register_all(vm);1751
}1753
#endif /* !VT_FUZZ */