Commitc8569cf5Recorded8 Sep 2026Repositorysigil-vt
fix(vt): render DEC special graphics for tmux line drawing
Changed
README.md | 5 +++++
native/vt-fuzz.c | 7 ++++++-
native/vt.c | 49 +++++++++++++++++++++++++++++++++++++++++++++----
package.sgl | 2 +-
test/vt-test.sgl | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++---
5 files changed, 108 insertions(+), 9 deletions(-)Diff
README.mdmodified
@@ -11,6 +11,11 @@ slate's `(slate term)` rides it as a thin façade (byte-identical `take-frame`),
11
and `(sigil tui grid)` will ride it in M3. See the design note 12
`topics/sigil-terminal-handling-design`. 13
+14
The parser supports VT100 G0/G1 ASCII and DEC Special Graphics designation,+15
SI/SO selection, and saved-cursor restoration of those character sets. This+16
renders tmux/ncurses line drawing as Unicode in the grid, scrollback and row+17
runs. Ordinary Unicode output is preserved.+18
19
This is the native reimplementation of slate's `(slate term)` emulator — **the 20
Sigil emulator is the spec**; `native/vt.c` reproduces its semantics cell-for-cell, 21
proven by `test/vt-test.sgl` (a faithful port of slate's `test/term-test.sgl`).native/vt-fuzz.cmodified
@@ -143,6 +143,11 @@ static void run_input(const uint8_t *data, size_t len, int cols, int rows) {
143
144
/* ---- the seed corpus: real escape sequences from the conformance suite --- */ 145
static const char *g_corpus[] = {+146
"\033(0lqqk\033(Bqq",+147
"\033)0q\016qx\017q",+148
"\033)0\016\0337\033)B\017\0338q\017q",+149
"\033(0\033[?1049h\033(B\033[?1049lq\033(Bq",+150
"\033(\030q\033(\033(0q\033(\032q", 151
"hello", 152
"ab\r\ncd", 153
"abc\rX",@@ -241,7 +246,7 @@ int main(int argc, char **argv) {
246
* byte burst weighted toward VT-vocabulary so the state machine, OSC/CSI 247
* accumulators, scrollback and resize reflow are all reached. */ 248
static const uint8_t vocab[] = {−244
0x1b, '[', ']', ';', ':', '?', ' ', '\\',+249
0x1b, '[', ']', ';', ':', '?', ' ', '\\', '(', ')', 0x0e, 0x0f, 250
'0','1','2','3','4','5','6','7','8','9', 251
'H','f','A','B','C','D','J','K','m','r','h','l','n','c','q','P','L','M', 252
'\r','\n','\t','\x08','\x07', 0x18, 0x1a,native/vt.cmodified
@@ -75,7 +75,7 @@ enum { VT_EV_TITLE = 1, VT_EV_BELL = 2, VT_EV_CLIPBOARD = 3 };
75
76
/* ---- parser states ------------------------------------------------------- */ 77
enum {−78
ST_GROUND = 0, ST_ESC, ST_ESC_SKIP1, ST_ESC_HASH,+78
ST_GROUND = 0, ST_ESC, ST_ESC_SKIP1, ST_ESC_HASH, ST_ESC_CHARSET, 79
ST_CSI, ST_OSC, ST_OSC_ESC, ST_STR_IGNORE, ST_STR_ESC 80
}; 81
@@ -91,6 +91,7 @@ typedef struct {
91
int row, col; 92
int32_t attr, fg, bg; 93
int origin, autowrap;+94
int graphics[2], gl; 95
} SavedCursor; 96
97
typedef struct {@@ -113,6 +114,9 @@ typedef struct {
114
115
SavedCursor saved_main, saved_alt; 116
+117
/* VT100 G0/G1: ASCII or DEC Special Graphics, invoked by SI/SO. */+118
int graphics[2], gl, charset_target;+119
120
/* parser */ 121
int state; 122
int32_t groups[VT_MAX_GROUPS][VT_MAX_SUB]; /* completed groups */@@ -373,6 +377,20 @@ static void do_wrap(Vt *t) {
377
line_feed(t); 378
} 379
+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, 0x00b7+388
};+389
if (t->graphics[t->gl] && cp >= 0x60 && cp <= 0x7e)+390
return dec[cp - 0x60];+391
return cp;+392
}+393
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;@@ -381,7 +399,7 @@ static void print_cp(Vt *t, int32_t cp) {
399
for (int i = cols - 1; i > col; i--) 400
memcpy(r + i * 4, r + (i - 1) * 4, 4 * sizeof(int32_t)); 401
}−384
set_cell(r + col * 4, cp, t->attr, t->fg, t->bg);+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;@@ -400,7 +418,7 @@ static void print_run(Vt *t, const int32_t *cps, int n) {
418
int k = n - i; 419
if (k > cols - col) k = cols - col; 420
for (int x = 0; x < k; x++)−403
set_cell(r + (col + x) * 4, cps[i + x], t->attr, t->fg, t->bg);+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) {@@ -512,12 +530,16 @@ static void save_cursor(Vt *t) {
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) {@@ -837,6 +859,8 @@ static void c0(Vt *t, int b) {
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
}@@ -869,7 +893,11 @@ static void esc_dispatch(Vt *t, int32_t cp) {
893
else move_rows(t, -1); 894
break; 895
case 99: term_reset(t); break; /* c RIS */−872
case 40: case 41: case 42: case 43: /* ( ) * + */+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 */@@ -896,6 +924,18 @@ static void process_cp(Vt *t, int32_t cp) {
924
t->state = ST_GROUND; 925
break; 926
+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;+938
939
case ST_ESC_HASH: 940
t->state = ST_GROUND; 941
if (cp == 56) { /* DECALN: fill screen with E */@@ -1021,6 +1061,7 @@ static void term_reset(Vt *t) {
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;package.sglmodified
@@ -12,7 +12,7 @@
12
13
(package 14
name: "sigil-vt"−15
version: "0.1.2"+15
version: "0.1.3" 16
sigil: "0.20" 17
description: "Native VT/ANSI terminal core: parser + int32[4] grid + damage/scrollback/replies/events + row-runs" 18
url: "https://codeberg.org/sigil/sigil-vt"test/vt-test.sglmodified
@@ -11,8 +11,7 @@
11
;;; trust boundary (hostile input), plus the native additions (events, mouse 12
;;; flags, row-runs). 13
−14
(import (sigil core)−15
(sigil test)+14
(import (sigil test) 15
(sigil vt)) 16
17
;; ---- helpers --------------------------------------------------------------@@ -74,6 +73,55 @@
73
(test "tab to col 8" 74
(assert-equal (list "a b" "") (rows-of (vt* 20 2 "a\tb"))))) 75
+76
;; ==========================================================================+77
;; DEC Special Graphics (tmux/ncurses line drawing)+78
;; ==========================================================================+79
(test-group "DEC Special Graphics"+80
(test "G0 line drawing returns to ASCII"+81
(assert-equal (list "┌──┐qq" "")+82
(rows-of (vt* 20 2 ESC "(0lqqk" ESC "(Bqq"))))+83
(test "G1 designation waits for SO; SI restores G0"+84
(assert-equal (list "q─│q" "")+85
(rows-of (vt* 20 2 ESC ")0q\x0e;qx\x0f;q"))))+86
(test "byte feeds preserve designation and invocation across chunks"+87
(let ((t (vt-make 20 2)))+88
(for-each (lambda (b) (vt-feed-bytes! t (list b)))+89
'(27 41 48 14 108 113 107 15 113))+90
(assert-equal (list "┌─┐q" "") (rows-of t))))+91
(test "string feeds preserve a split designation"+92
(assert-equal (list "─q" "")+93
(rows-of (vt* 20 2 ESC "(" "0" "q" ESC "(B" "q"))))+94
(test "Unicode and ASCII outside the graphics range are unchanged"+95
(assert-equal (list "AZ_─é─" "")+96
(rows-of (vt* 20 2 ESC "(0AZ_─éq"))))+97
(test "all DEC glyphs"+98
(assert-equal (list "◆▒␉␌␍␊°±␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·" "")+99
(rows-of (vt* 40 2 ESC "(0`abcdefghijklmnopqrstuvwxyz{|}~"))))+100
(test "DECSC/DECRC restores designations and active set"+101
(assert-equal (list "─q" "")+102
(rows-of (vt* 20 2 ESC ")0\x0e;" ESC "7"+103
ESC ")B\x0f;" ESC "8q\x0f;q"))))+104
(test "alternate screen return restores graphics state"+105
(assert-equal (list "─q" "")+106
(rows-of (vt* 20 2 ESC "(0" (csi "?1049h")+107
ESC "(B" (csi "?1049l") "q" ESC "(Bq"))))+108
(test "reset clears both designations and invocation"+109
(assert-equal (list "qq" "")+110
(rows-of (vt* 20 2 ESC "(0" ESC ")0\x0e;"+111
ESC "cq\x0e;q"))))+112
(test "insert mode uses the same graphics translation"+113
(assert-equal (list "─AB" "")+114
(rows-of (vt* 20 2 "AB\r" (csi "4h") ESC "(0q"))))+115
(test "escape and cancellation interrupt an incomplete designation"+116
(assert-equal (list "q──" "")+117
(rows-of (vt* 20 2 ESC "(\x18;q" ESC "("+118
ESC "(0q" ESC "(\x1a;q"))))+119
(test "graphics remain Unicode in scrollback and row runs"+120
(let ((t (vt* 5 2 ESC "(0lqqk\r\nx x\r\nmqqj")))+121
(assert-equal #x250c+122
(vt-cell-ch (vector-ref (vt-scrollback-row t 0) 0)))+123
(assert-equal "│ │" (vector-ref (car (vt-row-runs t 0)) 0)))))+124
125
;; ========================================================================== 126
;; cursor movement 127
;; ==========================================================================@@ -282,7 +330,7 @@
330
(test "CHT clamps" 331
(assert-equal (list 0 9) (cursor-of (vt* 10 2 ESC "[999999999I" "x")))) 332
(test "unknown modes/charsets swallowed"−285
(assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "[?9999h" ESC "[<5m" ESC "(0" "ok"))))+333
(assert-equal (list "ok" "") (rows-of (vt* 10 2 ESC "[?9999h" ESC "[<5m" ESC "(X" "ok")))) 334
(test "escape split across chunks" 335
(let ((t (vt-make 10 2))) 336
(vt-feed! t ESC) (vt-feed! t "[3") (vt-feed! t "1mX")