Commit137af6fdRecorded1 Mar 2026Repositorysigil-tui
Add define-native compiler form and native grid for sigil-tui
Message
Introduces define-native as a compiler special form for declaring bindings provided by native C code at runtime. This lets Scheme modules export native functions without needing fallback implementations.
Compiler changes:
- OP_DEFINE_IF_UNBOUND (0x17): only defines a binding if one doesn't already exist, so bytecode won't overwrite native bindings
- compile_define_native() handles function/variable forms, creates placeholder bindings for export resolution, collects docstrings, and emits %set-spec! calls for type annotations
- %set-spec! guards against non-procedure values for graceful degradation when native code isn't linked
Native grid (sigil-tui):
- native/grid.c with C implementations of all hot-path grid operations using flat int32_t cell arrays instead of Scheme vectors
- grid.sgl replaced with define-native declarations using grid? specs
- Grid tests skip cleanly when native code isn't linked
Changed
native/grid.c | 677 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package.sgl | 31 +++++++-
src/sigil/tui/components.sgl | 2 +-
src/sigil/tui/grid.sgl | 287 ++++++++++-----------------------------------------------------------
test/test-grid.sgl | 5 ++
5 files changed, 751 insertions(+), 251 deletions(-)Diff
native/grid.cadded
@@ -0,0 +1,677 @@
+1
/*+2
* Native grid implementation for (sigil tui grid)+3
*+4
* Provides a high-performance character grid for terminal UI rendering.+5
* Each cell stores a character, foreground color, background color, and+6
* text attributes as flat C arrays, avoiding per-cell heap allocation.+7
*/+8
+9
#include "sigil-internal.h"+10
#include <stdio.h>+11
#include <stdlib.h>+12
#include <string.h>+13
+14
/* ============================================================+15
* Cell layout: 4 int32_t values per cell+16
* [0] = character (Unicode codepoint)+17
* [1] = foreground color+18
* [2] = background color+19
* [3] = text attributes (bitmask)+20
* ============================================================ */+21
+22
#define CELL_FIELDS 4+23
#define CELL_CHAR 0+24
#define CELL_FG 1+25
#define CELL_BG 2+26
#define CELL_ATTRS 3+27
+28
/* Attribute flags */+29
#define ATTR_NONE 0+30
#define ATTR_BOLD 1+31
#define ATTR_DIM 2+32
#define ATTR_ITALIC 4+33
#define ATTR_UNDERLINE 8+34
#define ATTR_INVERSE 16+35
#define ATTR_STRIKETHROUGH 32+36
+37
/* Default cell values */+38
#define DEFAULT_CHAR ' '+39
#define DEFAULT_COLOR (-1)+40
#define DEFAULT_ATTRS 0+41
+42
typedef struct {+43
int width;+44
int height;+45
int32_t *cells; /* Flat array: width * height * CELL_FIELDS */+46
} Grid;+47
+48
static Value grid_type_tag = SIGIL_UNDEFINED;+49
+50
static void grid_finalizer(void *data)+51
{+52
Grid *g = (Grid *)data;+53
if (g) {+54
free(g->cells);+55
free(g);+56
}+57
}+58
+59
static int is_grid(Value v)+60
{+61
if (!sigil_is_foreign(v)) return 0;+62
return sigil_foreign_type(v) == grid_type_tag;+63
}+64
+65
static Grid *as_grid(Value v)+66
{+67
return (Grid *)sigil_foreign_data(v);+68
}+69
+70
/* Get cell pointer for (col, row) */+71
static inline int32_t *cell_at(Grid *g, int col, int row)+72
{+73
return &g->cells[(row * g->width + col) * CELL_FIELDS];+74
}+75
+76
/* Initialize a cell to defaults */+77
static inline void cell_clear(int32_t *cell)+78
{+79
cell[CELL_CHAR] = DEFAULT_CHAR;+80
cell[CELL_FG] = DEFAULT_COLOR;+81
cell[CELL_BG] = DEFAULT_COLOR;+82
cell[CELL_ATTRS] = DEFAULT_ATTRS;+83
}+84
+85
/* ============================================================+86
* Grid operations+87
* ============================================================ */+88
+89
/*+90
* %make-grid width height -> grid+91
*/+92
static Value native_make_grid(SigilVM *vm, int argc, Value *args)+93
{+94
(void)argc;+95
+96
int w = (int)sigil_as_fixnum(args[0]);+97
int h = (int)sigil_as_fixnum(args[1]);+98
+99
if (w <= 0 || h <= 0) {+100
sigil__vm_error(vm, SIGIL_ERR_TYPE,+101
"make-grid: dimensions must be positive");+102
return SIGIL_UNDEFINED;+103
}+104
+105
Grid *g = malloc(sizeof(Grid));+106
if (!g) return SIGIL_FALSE;+107
+108
size_t size = (size_t)w * h * CELL_FIELDS;+109
g->cells = malloc(size * sizeof(int32_t));+110
if (!g->cells) {+111
free(g);+112
return SIGIL_FALSE;+113
}+114
+115
g->width = w;+116
g->height = h;+117
+118
/* Initialize all cells to defaults */+119
for (size_t i = 0; i < (size_t)w * h; i++) {+120
int32_t *cell = &g->cells[i * CELL_FIELDS];+121
cell_clear(cell);+122
}+123
+124
return sigil_make_foreign(vm, grid_type_tag, g, grid_finalizer,+125
sizeof(Grid) + size * sizeof(int32_t));+126
}+127
+128
/*+129
* %grid-width grid -> integer+130
*/+131
static Value native_grid_width(SigilVM *vm, int argc, Value *args)+132
{+133
(void)vm; (void)argc;+134
return sigil_fixnum(as_grid(args[0])->width);+135
}+136
+137
/*+138
* %grid-height grid -> integer+139
*/+140
static Value native_grid_height(SigilVM *vm, int argc, Value *args)+141
{+142
(void)vm; (void)argc;+143
return sigil_fixnum(as_grid(args[0])->height);+144
}+145
+146
/* Helper: allocate a 4-element Scheme vector for cell data */+147
static Value make_cell_vector(SigilVM *vm, int32_t *cell)+148
{+149
SigilVector *vec = sigil__gc_alloc(vm, SIGIL_OBJ_VECTOR,+150
sizeof(SigilVector) + 4 * sizeof(Value));+151
vec->length = 4;+152
vec->elements[0] = sigil_char((uint32_t)cell[CELL_CHAR]);+153
vec->elements[1] = sigil_fixnum(cell[CELL_FG]);+154
vec->elements[2] = sigil_fixnum(cell[CELL_BG]);+155
vec->elements[3] = sigil_fixnum(cell[CELL_ATTRS]);+156
return sigil_ptr(vec);+157
}+158
+159
/*+160
* %grid-ref grid col row -> vector #(char fg bg attrs)+161
*+162
* Returns a fresh vector for compatibility with existing code.+163
*/+164
static Value native_grid_ref(SigilVM *vm, int argc, Value *args)+165
{+166
(void)argc;+167
+168
Grid *g = as_grid(args[0]);+169
int col = (int)sigil_as_fixnum(args[1]);+170
int row = (int)sigil_as_fixnum(args[2]);+171
+172
if (col < 0 || col >= g->width || row < 0 || row >= g->height) {+173
sigil__vm_error(vm, SIGIL_ERR_TYPE,+174
"grid-ref: index out of bounds");+175
return SIGIL_UNDEFINED;+176
}+177
+178
return make_cell_vector(vm, cell_at(g, col, row));+179
}+180
+181
/*+182
* %grid-set! grid col row ch fg bg attrs -> void+183
*/+184
static Value native_grid_set(SigilVM *vm, int argc, Value *args)+185
{+186
(void)argc;+187
+188
Grid *g = as_grid(args[0]);+189
int col = (int)sigil_as_fixnum(args[1]);+190
int row = (int)sigil_as_fixnum(args[2]);+191
+192
if (col < 0 || col >= g->width || row < 0 || row >= g->height) {+193
/* Silently ignore out-of-bounds writes (truncation) */+194
(void)vm;+195
return SIGIL_UNDEFINED;+196
}+197
+198
int32_t *cell = cell_at(g, col, row);+199
cell[CELL_CHAR] = (int32_t)sigil_as_char(args[3]);+200
cell[CELL_FG] = (int32_t)sigil_as_fixnum(args[4]);+201
cell[CELL_BG] = (int32_t)sigil_as_fixnum(args[5]);+202
cell[CELL_ATTRS] = (int32_t)sigil_as_fixnum(args[6]);+203
+204
return SIGIL_UNDEFINED;+205
}+206
+207
/*+208
* %grid-clear! grid -> void+209
*/+210
static Value native_grid_clear(SigilVM *vm, int argc, Value *args)+211
{+212
(void)vm; (void)argc;+213
+214
Grid *g = as_grid(args[0]);+215
size_t count = (size_t)g->width * g->height;+216
+217
for (size_t i = 0; i < count; i++) {+218
int32_t *cell = &g->cells[i * CELL_FIELDS];+219
cell_clear(cell);+220
}+221
+222
return SIGIL_UNDEFINED;+223
}+224
+225
/*+226
* %grid-write-string! grid col row str fg bg attrs -> void+227
*+228
* Write a string to the grid at (col, row), truncating at the grid edge.+229
*/+230
static Value native_grid_write_string(SigilVM *vm, int argc, Value *args)+231
{+232
(void)vm; (void)argc;+233
+234
Grid *g = as_grid(args[0]);+235
int col = (int)sigil_as_fixnum(args[1]);+236
int row = (int)sigil_as_fixnum(args[2]);+237
/* args[3] = string */+238
int32_t fg = (int32_t)sigil_as_fixnum(args[4]);+239
int32_t bg = (int32_t)sigil_as_fixnum(args[5]);+240
int32_t attrs = (int32_t)sigil_as_fixnum(args[6]);+241
+242
if (row < 0 || row >= g->height || col >= g->width) {+243
return SIGIL_UNDEFINED;+244
}+245
+246
SigilString *str = (SigilString *)sigil_as_ptr(args[3]);+247
const char *data = str->data;+248
size_t byte_len = str->byte_length;+249
int w = g->width;+250
int c = col;+251
size_t pos = 0;+252
+253
while (pos < byte_len && c < w) {+254
/* Decode UTF-8 codepoint */+255
uint32_t cp;+256
uint8_t b = (uint8_t)data[pos];+257
if (b < 0x80) {+258
cp = b;+259
pos += 1;+260
} else if ((b & 0xE0) == 0xC0) {+261
cp = (b & 0x1F) << 6;+262
if (pos + 1 < byte_len) cp |= ((uint8_t)data[pos + 1] & 0x3F);+263
pos += 2;+264
} else if ((b & 0xF0) == 0xE0) {+265
cp = (b & 0x0F) << 12;+266
if (pos + 1 < byte_len) cp |= ((uint8_t)data[pos + 1] & 0x3F) << 6;+267
if (pos + 2 < byte_len) cp |= ((uint8_t)data[pos + 2] & 0x3F);+268
pos += 3;+269
} else if ((b & 0xF8) == 0xF0) {+270
cp = (b & 0x07) << 18;+271
if (pos + 1 < byte_len) cp |= ((uint8_t)data[pos + 1] & 0x3F) << 12;+272
if (pos + 2 < byte_len) cp |= ((uint8_t)data[pos + 2] & 0x3F) << 6;+273
if (pos + 3 < byte_len) cp |= ((uint8_t)data[pos + 3] & 0x3F);+274
pos += 4;+275
} else {+276
cp = '?';+277
pos += 1;+278
}+279
+280
if (c >= 0) {+281
int32_t *cell = cell_at(g, c, row);+282
cell[CELL_CHAR] = (int32_t)cp;+283
cell[CELL_FG] = fg;+284
cell[CELL_BG] = bg;+285
cell[CELL_ATTRS] = attrs;+286
}+287
c++;+288
}+289
+290
return SIGIL_UNDEFINED;+291
}+292
+293
/*+294
* %grid-fill-rect! grid x y w h ch fg bg attrs -> void+295
*/+296
static Value native_grid_fill_rect(SigilVM *vm, int argc, Value *args)+297
{+298
(void)vm; (void)argc;+299
+300
Grid *g = as_grid(args[0]);+301
int x = (int)sigil_as_fixnum(args[1]);+302
int y = (int)sigil_as_fixnum(args[2]);+303
int w = (int)sigil_as_fixnum(args[3]);+304
int h = (int)sigil_as_fixnum(args[4]);+305
int32_t ch = (int32_t)sigil_as_char(args[5]);+306
int32_t fg = (int32_t)sigil_as_fixnum(args[6]);+307
int32_t bg = (int32_t)sigil_as_fixnum(args[7]);+308
int32_t attrs = (int32_t)sigil_as_fixnum(args[8]);+309
+310
int gw = g->width;+311
int gh = g->height;+312
+313
for (int r = y; r < y + h && r < gh; r++) {+314
if (r < 0) continue;+315
for (int c = x; c < x + w && c < gw; c++) {+316
if (c < 0) continue;+317
int32_t *cell = cell_at(g, c, r);+318
cell[CELL_CHAR] = ch;+319
cell[CELL_FG] = fg;+320
cell[CELL_BG] = bg;+321
cell[CELL_ATTRS] = attrs;+322
}+323
}+324
+325
return SIGIL_UNDEFINED;+326
}+327
+328
/*+329
* %grid-copy grid -> grid+330
*+331
* Create a deep copy of a grid.+332
*/+333
static Value native_grid_copy(SigilVM *vm, int argc, Value *args)+334
{+335
(void)argc;+336
+337
Grid *src = as_grid(args[0]);+338
Grid *dst = malloc(sizeof(Grid));+339
if (!dst) return SIGIL_FALSE;+340
+341
size_t size = (size_t)src->width * src->height * CELL_FIELDS;+342
dst->cells = malloc(size * sizeof(int32_t));+343
if (!dst->cells) {+344
free(dst);+345
return SIGIL_FALSE;+346
}+347
+348
dst->width = src->width;+349
dst->height = src->height;+350
memcpy(dst->cells, src->cells, size * sizeof(int32_t));+351
+352
return sigil_make_foreign(vm, grid_type_tag, dst, grid_finalizer,+353
sizeof(Grid) + size * sizeof(int32_t));+354
}+355
+356
/* ============================================================+357
* Grid diffing — the performance-critical hot path+358
*+359
* Scans cell-by-cell, emitting minimal ANSI escape sequences to+360
* update only changed cells. Returns a string to write atomically+361
* via terminal-write-raw.+362
* ============================================================ */+363
+364
/* Dynamic buffer for building diff output */+365
typedef struct {+366
char *data;+367
size_t len;+368
size_t cap;+369
} DiffBuf;+370
+371
static void buf_init(DiffBuf *buf)+372
{+373
buf->cap = 4096;+374
buf->data = malloc(buf->cap);+375
buf->len = 0;+376
}+377
+378
static void buf_ensure(DiffBuf *buf, size_t need)+379
{+380
if (buf->len + need > buf->cap) {+381
while (buf->len + need > buf->cap) {+382
buf->cap *= 2;+383
}+384
buf->data = realloc(buf->data, buf->cap);+385
}+386
}+387
+388
static void buf_append(DiffBuf *buf, const char *str, size_t len)+389
{+390
buf_ensure(buf, len);+391
memcpy(buf->data + buf->len, str, len);+392
buf->len += len;+393
}+394
+395
static void buf_append_str(DiffBuf *buf, const char *str)+396
{+397
buf_append(buf, str, strlen(str));+398
}+399
+400
static void buf_append_int(DiffBuf *buf, int n)+401
{+402
char tmp[16];+403
int len = snprintf(tmp, sizeof(tmp), "%d", n);+404
buf_append(buf, tmp, len);+405
}+406
+407
static void buf_append_char_utf8(DiffBuf *buf, uint32_t cp)+408
{+409
char tmp[4];+410
int len;+411
if (cp < 0x80) {+412
tmp[0] = (char)cp;+413
len = 1;+414
} else if (cp < 0x800) {+415
tmp[0] = (char)(0xC0 | (cp >> 6));+416
tmp[1] = (char)(0x80 | (cp & 0x3F));+417
len = 2;+418
} else if (cp < 0x10000) {+419
tmp[0] = (char)(0xE0 | (cp >> 12));+420
tmp[1] = (char)(0x80 | ((cp >> 6) & 0x3F));+421
tmp[2] = (char)(0x80 | (cp & 0x3F));+422
len = 3;+423
} else {+424
tmp[0] = (char)(0xF0 | (cp >> 18));+425
tmp[1] = (char)(0x80 | ((cp >> 12) & 0x3F));+426
tmp[2] = (char)(0x80 | ((cp >> 6) & 0x3F));+427
tmp[3] = (char)(0x80 | (cp & 0x3F));+428
len = 4;+429
}+430
buf_append(buf, tmp, len);+431
}+432
+433
/* Emit foreground color SGR to buffer */+434
static void emit_fg(DiffBuf *buf, int32_t color)+435
{+436
if (color == -1) {+437
buf_append_str(buf, "39");+438
} else if (color >= 0 && color <= 7) {+439
buf_append_int(buf, color + 30);+440
} else if (color >= 8 && color <= 15) {+441
buf_append_int(buf, color + 82); /* 90-97 */+442
} else if (color >= 16 && color <= 255) {+443
buf_append_str(buf, "38;5;");+444
buf_append_int(buf, color);+445
} else {+446
/* Truecolor: decode packed RGB */+447
int v = color - 65536; /* undo +1 offset from color-rgb */+448
int r = v / 65536;+449
int rem = v % 65536;+450
int g = rem / 256;+451
int b = rem % 256;+452
buf_append_str(buf, "38;2;");+453
buf_append_int(buf, r);+454
buf_append(buf, ";", 1);+455
buf_append_int(buf, g);+456
buf_append(buf, ";", 1);+457
buf_append_int(buf, b);+458
}+459
}+460
+461
/* Emit background color SGR to buffer */+462
static void emit_bg(DiffBuf *buf, int32_t color)+463
{+464
if (color == -1) {+465
buf_append_str(buf, "49");+466
} else if (color >= 0 && color <= 7) {+467
buf_append_int(buf, color + 40);+468
} else if (color >= 8 && color <= 15) {+469
buf_append_int(buf, color + 92); /* 100-107 */+470
} else if (color >= 16 && color <= 255) {+471
buf_append_str(buf, "48;5;");+472
buf_append_int(buf, color);+473
} else {+474
int v = color - 65536;+475
int r = v / 65536;+476
int rem = v % 65536;+477
int g = rem / 256;+478
int b = rem % 256;+479
buf_append_str(buf, "48;2;");+480
buf_append_int(buf, r);+481
buf_append(buf, ";", 1);+482
buf_append_int(buf, g);+483
buf_append(buf, ";", 1);+484
buf_append_int(buf, b);+485
}+486
}+487
+488
/* Emit attribute codes to buffer */+489
static void emit_attrs(DiffBuf *buf, int32_t attrs)+490
{+491
if (attrs & ATTR_BOLD) buf_append_str(buf, ";1");+492
if (attrs & ATTR_DIM) buf_append_str(buf, ";2");+493
if (attrs & ATTR_ITALIC) buf_append_str(buf, ";3");+494
if (attrs & ATTR_UNDERLINE) buf_append_str(buf, ";4");+495
if (attrs & ATTR_INVERSE) buf_append_str(buf, ";7");+496
if (attrs & ATTR_STRIKETHROUGH) buf_append_str(buf, ";9");+497
}+498
+499
/* Emit full SGR for a cell */Showing the first 500 of 678 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
package.sglmodified
@@ -14,4 +14,33 @@
14
dependencies: (list 15
(from-workspace name: "sigil-stdlib") 16
(from-workspace name: "sigil-ansi")−17
(from-workspace name: "sigil-socket")))+17
(from-workspace name: "sigil-socket"))+18
+19
;; Native library for high-performance grid operations+20
libraries: (list+21
(library+22
name: 'sigil-tui+23
c-sources: '("native/grid.c")+24
native-init: "sigil__init_sigil_tui_grid_module"))+25
+26
tasks: (list+27
(task+28
name: 'build+29
description: "Build the sigil-tui native library"+30
steps: (list+31
(compile-c-sources+32
sources: '("native/grid.c")+33
include-dirs: '("../sigil-lib/include"+34
"../sigil-lib/src")+35
flags: '("-std=c99"+36
"-Wall" "-Wextra"+37
"-Wno-unused-parameter"+38
"-D_GNU_SOURCE"))+39
+40
(create-static-library+41
name: "sigil-tui")+42
+43
(compile-sigil-modules+44
sources: "src/**/*.sgl")+45
+46
(copy-package-docs)))))src/sigil/tui/components.sglmodified
@@ -189,7 +189,7 @@
189
190
;;; Render a list of layout commands into a grid. 191
(define (render-commands grid commands)−192
(: vector? list? -> void?)+192
(: grid? list? -> void?) 193
(for-each 194
(lambda (cmd) 195
(let* ((type (car cmd))src/sigil/tui/grid.sglmodified
@@ -5,6 +5,10 @@
5
;;; text attributes. The diff algorithm emits minimal ANSI escape sequences 6
;;; to update only changed cells. 7
;;;+8
;;; Grid operations are implemented in native C code (native/grid.c) for+9
;;; performance. The native module is linked when building packages that+10
;;; depend on sigil-tui.+11
;;; 12
;;; Color encoding: 13
;;; - -1 = default terminal color 14
;;; - 0-7 = standard colors (black, red, green, yellow, blue, magenta, cyan, white)@@ -13,13 +17,10 @@
17
;;; - >= 256 = truecolor RGB (pack with color-rgb) 18
19
(define-library (sigil tui grid)−16
(import (sigil core)−17
(sigil io)−18
(sigil string)−19
(sigil math)−20
(sigil terminal))+20
(import (sigil core)) 21
22
(export make-grid+23
grid? 24
grid-width 25
grid-height 26
grid-ref@@ -105,14 +106,9 @@
106
b)) 107
108
;; ============================================================−108
;; Cell: 4-element vector #(char fg bg attrs)+109
;; Cell accessors (Scheme — lightweight wrappers) 110
;; ============================================================ 111
−111
;;; Create a cell with the given character, colors, and attributes.−112
(define (make-cell ch fg bg attrs)−113
(: char? integer? integer? integer? -> vector?)−114
(vector ch fg bg attrs))−115
112
;; Default cell: space with default colors, no attributes 113
(define (default-cell) 114
(: -> vector?)@@ -147,266 +143,59 @@
143
(eqv? (vector-ref a 3) (vector-ref b 3)))) 144
145
;; ============================================================−150
;; Grid: vector of cells + dimensions+146
;; Native grid operations (provided by native/grid.c) 147
;; ============================================================ 148
+149
;;; Create a cell with the given character, colors, and attributes.+150
(define-native (make-cell ch fg bg attrs)+151
(: char? integer? integer? integer? -> vector?))+152
153
;;; Create a grid of width x height cells, filled with default cells.−154
(define (make-grid width height)−155
(: integer? integer? -> vector?)−156
(let* ((size (* width height))−157
(cells (make-vector size #f)))−158
(let loop ((i 0))−159
(when (< i size)−160
(vector-set! cells i (default-cell))−161
(loop (+ i 1))))−162
(vector width height cells)))+154
(define-native (make-grid width height)+155
(: integer? integer? -> grid?))+156
+157
;;; Check if a value is a native grid.+158
(define-native (grid? obj)+159
(: any? -> boolean?)) 160
161
;;; Get the grid width.−165
(define (grid-width grid)−166
(: vector? -> integer?)−167
(vector-ref grid 0))+162
(define-native (grid-width grid)+163
(: grid? -> integer?)) 164
165
;;; Get the grid height.−170
(define (grid-height grid)−171
(: vector? -> integer?)−172
(vector-ref grid 1))−173
−174
(define (grid-cells grid)−175
(vector-ref grid 2))−176
−177
(define (grid-index grid col row)−178
(+ (* row (grid-width grid)) col))+166
(define-native (grid-height grid)+167
(: grid? -> integer?)) 168
169
;;; Get the cell at (col, row).−181
(define (grid-ref grid col row)−182
(: vector? integer? integer? -> vector?)−183
(vector-ref (grid-cells grid) (grid-index grid col row)))+170
(define-native (grid-ref grid col row)+171
(: grid? integer? integer? -> vector?)) 172
173
;;; Set a cell at (col, row) by mutating it in place.−186
;;;−187
;;; Avoids allocating a new cell vector on every write.−188
(define (grid-set! grid col row ch fg bg attrs)−189
(: vector? integer? integer? char? integer? integer? integer? -> void?)−190
(let ((cell (vector-ref (grid-cells grid) (grid-index grid col row))))−191
(vector-set! cell 0 ch)−192
(vector-set! cell 1 fg)−193
(vector-set! cell 2 bg)−194
(vector-set! cell 3 attrs)))+174
(define-native (grid-set! grid col row ch fg bg attrs)+175
(: grid? integer? integer? char? integer? integer? integer? -> void?)) 176
177
;;; Clear the grid to default cells (space, default colors, no attrs).−197
(define (grid-clear! grid)−198
(: vector? -> void?)−199
(let ((cells (grid-cells grid))−200
(size (* (grid-width grid) (grid-height grid))))−201
(let loop ((i 0))−202
(when (< i size)−203
(let ((cell (vector-ref cells i)))−204
(vector-set! cell 0 #\space)−205
(vector-set! cell 1 -1)−206
(vector-set! cell 2 -1)−207
(vector-set! cell 3 0))−208
(loop (+ i 1))))))+178
(define-native (grid-clear! grid)+179
(: grid? -> void?)) 180
181
;;; Write a string to the grid at (col, row) with the given style. 182
;;; 183
;;; Characters are written left-to-right, truncated at the grid edge.−213
(define (grid-write-string! grid col row str fg bg attrs)−214
(: vector? integer? integer? string? integer? integer? integer? -> void?)−215
(let ((w (grid-width grid))−216
(cells (grid-cells grid))−217
(gw (grid-width grid))−218
(len (string-length str)))−219
(let loop ((i 0) (c col))−220
(when (and (< i len) (< c w))−221
(let ((cell (vector-ref cells (+ (* row gw) c))))−222
(vector-set! cell 0 (string-ref str i))−223
(vector-set! cell 1 fg)−224
(vector-set! cell 2 bg)−225
(vector-set! cell 3 attrs))−226
(loop (+ i 1) (+ c 1))))))+184
(define-native (grid-write-string! grid col row str fg bg attrs)+185
(: grid? integer? integer? string? integer? integer? integer? -> void?)) 186
187
;;; Fill a rectangle with a character and style.−229
(define (grid-fill-rect! grid x y w h ch fg bg attrs)−230
(: vector? integer? integer? integer? integer? char? integer? integer? integer? -> void?)−231
(let ((cells (grid-cells grid))−232
(gw (grid-width grid))−233
(gh (grid-height grid)))−234
(let row-loop ((r y))−235
(when (and (< r (+ y h)) (< r gh))−236
(let col-loop ((c x))−237
(when (and (< c (+ x w)) (< c gw))−238
(let ((cell (vector-ref cells (+ (* r gw) c))))−239
(vector-set! cell 0 ch)−240
(vector-set! cell 1 fg)−241
(vector-set! cell 2 bg)−242
(vector-set! cell 3 attrs))−243
(col-loop (+ c 1))))−244
(row-loop (+ r 1))))))−245
−246
;;; Create a deep copy of a grid (copies cell contents, not references).−247
(define (grid-copy grid)−248
(: vector? -> vector?)−249
(let* ((w (grid-width grid))−250
(h (grid-height grid))−251
(size (* w h))−252
(src (grid-cells grid))−253
(dst (make-vector size #f)))−254
(let loop ((i 0))−255
(when (< i size)−256
(let ((cell (vector-ref src i)))−257
(vector-set! dst i (vector (vector-ref cell 0)−258
(vector-ref cell 1)−259
(vector-ref cell 2)−260
(vector-ref cell 3))))−261
(loop (+ i 1))))−262
(vector w h dst)))−263
−264
;; ============================================================−265
;; SGR escape code emission−266
;; ============================================================−267
−268
(define esc "\x1b;")−269
−270
;; Emit a foreground color SGR sequence to port−271
(define (emit-fg port color)−272
(cond−273
((= color -1)−274
(display "39" port))−275
((and (>= color 0) (<= color 7))−276
(display (number->string (+ color 30)) port))−277
((and (>= color 8) (<= color 15))−278
(display (number->string (+ color 82)) port)) ;; 90-97−279
((and (>= color 16) (<= color 255))−280
(display "38;5;" port)−281
(display (number->string color) port))−282
(else−283
;; Truecolor: decode packed RGB−284
(let* ((v (- color 65536)) ;; undo the +1 offset from color-rgb−285
(r (quotient v 65536))−286
(rem (remainder v 65536))−287
(g (quotient rem 256))−288
(b (remainder rem 256)))−289
(display "38;2;" port)−290
(display (number->string r) port)−291
(display ";" port)−292
(display (number->string g) port)−293
(display ";" port)−294
(display (number->string b) port)))))−295
−296
;; Emit a background color SGR sequence to port−297
(define (emit-bg port color)−298
(cond−299
((= color -1)−300
(display "49" port))−301
((and (>= color 0) (<= color 7))−302
(display (number->string (+ color 40)) port))−303
((and (>= color 8) (<= color 15))−304
(display (number->string (+ color 92)) port)) ;; 100-107−305
((and (>= color 16) (<= color 255))−306
(display "48;5;" port)−307
(display (number->string color) port))−308
(else−309
(let* ((v (- color 65536))−310
(r (quotient v 65536))−311
(rem (remainder v 65536))−312
(g (quotient rem 256))−313
(b (remainder rem 256)))−314
(display "48;2;" port)−315
(display (number->string r) port)−316
(display ";" port)−317
(display (number->string g) port)−318
(display ";" port)−319
(display (number->string b) port)))))−320
−321
;; Emit attribute SGR codes to port−322
(define (emit-attrs port attrs)−323
(when (not (= (bitwise-and attrs attr-bold) 0))−324
(display ";1" port))−325
(when (not (= (bitwise-and attrs attr-dim) 0))−326
(display ";2" port))−327
(when (not (= (bitwise-and attrs attr-italic) 0))−328
(display ";3" port))−329
(when (not (= (bitwise-and attrs attr-underline) 0))−330
(display ";4" port))−331
(when (not (= (bitwise-and attrs attr-inverse) 0))−332
(display ";7" port))−333
(when (not (= (bitwise-and attrs attr-strikethrough) 0))−334
(display ";9" port)))−335
−336
;; Emit full SGR for a cell−337
(define (emit-sgr port fg bg attrs)−338
(display esc port)−339
(display "[0;" port) ;; reset first−340
(emit-fg port fg)−341
(display ";" port)−342
(emit-bg port bg)−343
(emit-attrs port attrs)−344
(display "m" port))−345
−346
;; ============================================================−347
;; Grid diffing−348
;; ============================================================+188
(define-native (grid-fill-rect! grid x y w h ch fg bg attrs)+189
(: grid? integer? integer? integer? integer? char? integer? integer? integer? -> void?)) 190
191
;;; Compute the diff between prev and curr grids, return an ANSI string. 192
;;; 193
;;; Scans cell-by-cell, emitting cursor moves and SGR sequences only 194
;;; where cells differ. Returns the string to write atomically via 195
;;; terminal-write-raw.−355
(define (grid-diff prev curr)−356
(: vector? vector? -> string?)−357
(let ((w (grid-width curr))−358
(h (grid-height curr))−359
(prev-cells (grid-cells prev))−360
(curr-cells (grid-cells curr))−361
(out (open-output-string))−362
(last-fg -2) ;; -2 = no style emitted yet (different from -1=default)−363
(last-bg -2)−364
(last-attrs -1)−365
(cursor-col -1)−366
(cursor-row -1))−367
(let row-loop ((row 0))−368
(when (< row h)−369
(let col-loop ((col 0))−370
(when (< col w)−371
(let* ((idx (+ (* row w) col))−372
(pc (vector-ref prev-cells idx))−373
(cc (vector-ref curr-cells idx)))−374
(when (not (and (eqv? (vector-ref pc 0) (vector-ref cc 0))−375
(eqv? (vector-ref pc 1) (vector-ref cc 1))−376
(eqv? (vector-ref pc 2) (vector-ref cc 2))−377
(eqv? (vector-ref pc 3) (vector-ref cc 3))))−378
(let ((fg (vector-ref cc 1))−379
(bg (vector-ref cc 2))−380
(attrs (vector-ref cc 3))−381
(ch (vector-ref cc 0)))−382
;; Move cursor if not at expected position−383
(when (or (not (= cursor-row row))−384
(not (= cursor-col col)))−385
(display esc out)−386
(display "[" out)−387
(display (number->string (+ row 1)) out)−388
(display ";" out)−389
(display (number->string (+ col 1)) out)−390
(display "H" out))−391
;; Emit style if changed−392
(when (or (not (= fg last-fg))−393
(not (= bg last-bg))−394
(not (= attrs last-attrs)))−395
(emit-sgr out fg bg attrs)−396
(set! last-fg fg)−397
(set! last-bg bg)−398
(set! last-attrs attrs))−399
;; Emit character−400
(display ch out)−401
(set! cursor-col (+ col 1))−402
(set! cursor-row row))))−403
(col-loop (+ col 1))))−404
(row-loop (+ row 1))))−405
;; Reset style at end−406
(let ((result (get-output-string out)))−407
(if (= (string-length result) 0)−408
result−409
(begin−410
(display esc out)−411
(display "[0m" out)−412
(get-output-string out))))))))+196
(define-native (grid-diff prev curr)+197
(: grid? grid? -> string?))+198
+199
;;; Create a deep copy of a grid (copies cell contents, not references).+200
(define-native (grid-copy grid)+201
(: grid? -> grid?))))test/test-grid.sglmodified
@@ -1,6 +1,11 @@
1
(import (sigil test) 2
(sigil tui grid)) 3
+4
;; Skip grid tests when native code isn't linked (make-grid will be #f)+5
(when (not (procedure? make-grid))+6
(display "Skipping grid tests (native code not linked)\n")+7
(exit 0))+8
9
(test-group "grid" 10
11
(test-group "creation"