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")