Commitba04e66cRecorded26 Feb 2026Repositorysigil-tui
Add chat demo and optimize TUI rendering performance
Message
Demo app exercises all TUI components with a simulated chat client.
Performance optimizations: - Mutable cells: grid-set! mutates existing cell vectors in place instead of allocating new ones, eliminating thousands of vector allocations per frame - Skip idle renders: only render after input events, not on timeout - Input draining: process all buffered input before rendering once - Inline cell access in grid-diff to reduce function call overhead
Changed
examples/chat-demo.sgl | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sigil/tui.sgl | 98 ++++++++++++++++++++++++++++++++++++----------------------
src/sigil/tui/components.sgl | 33 +++++++++-----------
src/sigil/tui/grid.sgl | 69 ++++++++++++++++++++++++++++-------------
test/test-grid.sgl | 19 +++++-------
5 files changed, 407 insertions(+), 87 deletions(-)Diff
examples/chat-demo.sgladded
@@ -0,0 +1,275 @@
+1
;;; Chat Demo — exercises all sigil-tui components+2
;;;+3
;;; Run with:+4
;;; ./build/dev/bin/sigil run packages/sigil-tui/examples/chat-demo.sgl+5
+6
(import (sigil core)+7
(sigil struct)+8
(sigil math)+9
(sigil time)+10
(sigil tui))+11
+12
;; ============================================================+13
;; Data model+14
;; ============================================================+15
+16
(define-struct contact+17
(name default: "")+18
(online? default: #f)+19
(group? default: #f))+20
+21
(define-struct message+22
(sender default: "")+23
(text default: "")+24
(timestamp default: 0))+25
+26
;; Application state — all mutable for easy updates+27
(define-struct app-state+28
(contacts default: '() mutable: #t)+29
(selected default: 0 mutable: #t)+30
(conversations default: '() mutable: #t) ;; alist: name -> list of messages+31
(input default: #f mutable: #t)+32
(mode default: 'roster mutable: #t) ;; 'roster or 'chat+33
(scroll-offset default: 0 mutable: #t)+34
(last-tick default: 0 mutable: #t)+35
(sim-counter default: 0 mutable: #t))+36
+37
;; ============================================================+38
;; Helpers+39
;; ============================================================+40
+41
(define (format-time ts)+42
(let* ((total (exact (floor ts)))+43
(hours (modulo (quotient total 3600) 24))+44
(mins (modulo (quotient total 60) 60))+45
(h-str (if (< hours 10)+46
(string-append "0" (number->string hours))+47
(number->string hours)))+48
(m-str (if (< mins 10)+49
(string-append "0" (number->string mins))+50
(number->string mins))))+51
(string-append h-str ":" m-str)))+52
+53
(define (get-conversation state name)+54
(let ((entry (assoc name (app-state-conversations state))))+55
(if entry (cdr entry) '())))+56
+57
(define (add-message! state contact-name sender text)+58
(let* ((msg (message sender: sender+59
text: text+60
timestamp: (current-second)))+61
(convs (app-state-conversations state))+62
(entry (assoc contact-name convs))+63
(msgs (if entry (cdr entry) '()))+64
(new-msgs (append msgs (list msg))))+65
(if entry+66
(set-cdr! entry new-msgs)+67
(set-app-state-conversations! state+68
(cons (cons contact-name new-msgs) convs)))))+69
+70
(define (selected-contact state)+71
(let ((contacts (app-state-contacts state))+72
(idx (app-state-selected state)))+73
(if (< idx (length contacts))+74
(list-ref contacts idx)+75
#f)))+76
+77
;; ============================================================+78
;; Simulated incoming messages+79
;; ============================================================+80
+81
(define *sim-messages*+82
'(("alice" "Hey, how's it going?")+83
("alice" "Working on anything fun?")+84
("bob" "Did you see the game last night?")+85
("alice" "I just finished the new TUI library!")+86
("#general" "Welcome to #general!")+87
("bob" "Let me know when you're free")+88
("#general" "Anyone working on something cool?")+89
("alice" "The grid diffing is so fast")+90
("bob" "Pizza for lunch?")+91
("#general" "Reminder: standup at 10am")))+92
+93
(define (maybe-simulate-message! state)+94
(let ((now (current-second))+95
(last (app-state-last-tick state)))+96
(when (> (- now last) 3.0)+97
(set-app-state-last-tick! state now)+98
(let* ((idx (modulo (app-state-sim-counter state)+99
(length *sim-messages*)))+100
(entry (list-ref *sim-messages* idx))+101
(contact-name (car entry))+102
(text (cadr entry))+103
(sender (if (and (> (string-length contact-name) 0)+104
(char=? (string-ref contact-name 0) #\#))+105
"someone"+106
contact-name)))+107
(add-message! state contact-name sender text)+108
(set-app-state-sim-counter! state+109
(+ 1 (app-state-sim-counter state)))))))+110
+111
;; ============================================================+112
;; UI rendering+113
;; ============================================================+114
+115
(define (render-roster state)+116
(let* ((contacts (app-state-contacts state))+117
(items (map (lambda (c)+118
(let* ((dot (if (contact-online? c) "● " "○ "))+119
(name (contact-name c))+120
(label (string-append dot name))+121
(fg (if (contact-online? c)+122
color-green+123
color-bright-black))+124
(bg color-default))+125
(list label fg bg attr-none)))+126
contacts)))+127
(selectable-list items (app-state-selected state))))+128
+129
(define (render-chat-messages state contact-name)+130
(let* ((msgs (get-conversation state contact-name))+131
(lines (map (lambda (m)+132
(let* ((time-str (format-time (message-timestamp m)))+133
(sender (message-sender m))+134
(text (message-text m))+135
(line (string-append+136
"[" time-str "] "+137
sender ": " text)))+138
(if (string=? sender "you")+139
(list line color-cyan color-default attr-none)+140
(list line color-default color-default attr-none))))+141
msgs)))+142
(if (null? lines)+143
(text-block (list (list "No messages yet."+144
color-bright-black color-default attr-italic)))+145
(text-block lines))))+146
+147
(define (render-app state)+148
(let* ((contact (selected-contact state))+149
(contact-name (if contact (contact-name contact) ""))+150
(chat-title (string-append "Chat: " contact-name))+151
(input-st (app-state-input state))+152
(prompt (string-append contact-name "> "))+153
(status-contact (if contact+154
(string-append contact-name+155
(if (contact-online? contact)+156
" (online)"+157
" (offline)"))+158
""))+159
(mode-str (if (eq? (app-state-mode state) 'roster)+160
"[roster]"+161
"[chat]"))+162
(status-right (string-append mode-str " Ctrl-Q quit")))+163
(vertical-split 0.92+164
(horizontal-split 0.25+165
(panel "Roster" (render-roster state))+166
(vertical-split 0.88+167
(panel chat-title+168
(render-chat-messages state contact-name))+169
(panel ""+170
(text-input-widget prompt+171
(input-value input-st)+172
(input-cursor input-st)))))+173
(status-bar "chat-demo" status-contact status-right))))+174
+175
;; ============================================================+176
;; Event handling+177
;; ============================================================+178
+179
(define (handle-event state event)+180
(maybe-simulate-message! state)+181
+182
(cond+183
;; Quit+184
((and (key-event? event)+185
(equal? event '(key #\q ctrl)))+186
'quit)+187
+188
;; Tab to switch focus mode+189
((and (key-event? event)+190
(equal? event '(key tab)))+191
(set-app-state-mode! state+192
(if (eq? (app-state-mode state) 'roster) 'chat 'roster))+193
state)+194
+195
;; Roster mode keys+196
((eq? (app-state-mode state) 'roster)+197
(handle-roster-event state event))+198
+199
;; Chat mode keys+200
((eq? (app-state-mode state) 'chat)+201
(handle-chat-event state event))+202
+203
(else state)))+204
+205
(define (handle-roster-event state event)+206
(cond+207
((and (key-event? event)+208
(equal? event '(key up)))+209
(let ((sel (app-state-selected state)))+210
(when (> sel 0)+211
(set-app-state-selected! state (- sel 1))))+212
state)+213
+214
((and (key-event? event)+215
(equal? event '(key down)))+216
(let ((sel (app-state-selected state))+217
(max-idx (- (length (app-state-contacts state)) 1)))+218
(when (< sel max-idx)+219
(set-app-state-selected! state (+ sel 1))))+220
state)+221
+222
;; Enter or right arrow switches to chat+223
((and (key-event? event)+224
(or (equal? event '(key enter))+225
(equal? event '(key right))))+226
(set-app-state-mode! state 'chat)+227
state)+228
+229
(else state)))+230
+231
(define (handle-chat-event state event)+232
(cond+233
;; Left arrow goes back to roster+234
((and (key-event? event)+235
(equal? event '(key left)))+236
(set-app-state-mode! state 'roster)+237
state)+238
+239
;; Pass key events to input+240
((key-event? event)+241
(let ((result (input-handle-key (app-state-input state) event)))+242
(when (eq? result 'submit)+243
(let* ((contact (selected-contact state))+244
(text (input-value (app-state-input state))))+245
(when (and contact (> (string-length text) 0))+246
(add-message! state (contact-name contact) "you" text)+247
(input-clear! (app-state-input state))))))+248
state)+249
+250
(else state)))+251
+252
;; ============================================================+253
;; Main+254
;; ============================================================+255
+256
(define (make-initial-state)+257
(let ((state (app-state+258
contacts: (list+259
(contact name: "alice" online?: #t)+260
(contact name: "bob" online?: #f)+261
(contact name: "#general" online?: #t group?: #t)+262
(contact name: "#random" online?: #f group?: #t))+263
input: (make-input-state)+264
last-tick: (current-second))))+265
;; Seed some initial messages+266
(add-message! state "alice" "alice" "Hey, welcome to the chat!")+267
(add-message! state "alice" "you" "Thanks! Testing the TUI.")+268
(add-message! state "#general" "admin" "Welcome to #general")+269
state))+270
+271
(tui-run+272
render-app+273
initial-state: (make-initial-state)+274
on-event: handle-event+275
fps: 30)src/sigil/tui.sglmodified
@@ -91,7 +91,35 @@
91
'quit 92
(process-events new-state (cdr events) on-event))))) 93
−94
;; Single frame: render, diff, write, wait for input, recurse+94
;; Drain all pending input from stdin, processing events as they arrive.+95
;; Returns the final state after all available input is consumed.+96
(define (drain-input state parser on-event)+97
(let ((ready (fd-select (list 0) '() 0)))+98
(if (and ready (pair? (car ready)))+99
(let ((raw (terminal-read-raw 256)))+100
(if raw+101
(let ((new-state (process-events+102
state+103
(parse-input parser raw)+104
on-event)))+105
(if (eq? new-state 'quit)+106
'quit+107
(drain-input new-state parser on-event)))+108
state))+109
state)))+110
+111
;; Render state into curr grid, diff against prev, write changes.+112
(define (do-render render state prev curr cols rows)+113
(grid-clear! curr)+114
(let* ((tree (render state))+115
(commands (layout-tree tree 0 0 cols rows)))+116
(render-commands curr commands))+117
(let ((diff-str (grid-diff prev curr)))+118
(when (> (string-length diff-str) 0)+119
(terminal-write-raw diff-str))))+120
+121
;; Main loop: wait for input, process it, render, repeat.+122
;; Only renders after input or resize -- skips render on timeout. 123
(define (run-frame render on-event state parser prev curr 124
cols rows frame-ms) 125
;; Handle resize@@ -103,44 +131,37 @@
131
(prev (if resize-info (make-grid cols rows) prev)) 132
(curr (if resize-info (make-grid cols rows) curr))) 133
(when resize-info−106
(terminal-write-raw "\x1b;[2J"))+134
(terminal-write-raw "\x1b;[2J")+135
(do-render render state prev curr cols rows)) 136
137
;; Handle interrupt 138
(if (signal-pending? 'int) 139
state−111
(begin−112
;; Render current frame−113
(grid-clear! curr)−114
(let* ((tree (render state))−115
(commands (layout-tree tree 0 0 cols rows)))−116
(render-commands curr commands))−117
−118
;; Diff and write−119
(let ((diff-str (grid-diff prev curr)))−120
(when (> (string-length diff-str) 0)−121
(terminal-write-raw diff-str)))−122
−123
;; Swap buffers−124
(let* ((new-prev (grid-copy curr))−125
;; Wait for input with frame timeout−126
(ready (fd-select (list 0) '() frame-ms)))−127
(if (and ready (pair? (car ready)))−128
;; Input available−129
(let ((raw (terminal-read-raw 256)))−130
(if raw−131
(let ((new-state (process-events−132
state−133
(parse-input parser raw)−134
on-event)))−135
(if (eq? new-state 'quit)−136
new-state+140
;; Wait for input or frame timeout+141
(let ((ready (fd-select (list 0) '() frame-ms)))+142
(if (and ready (pair? (car ready)))+143
;; Input available+144
(let ((raw (terminal-read-raw 256)))+145
(if (not raw)+146
(run-frame render on-event state parser+147
prev curr cols rows frame-ms)+148
(let* ((new-state (process-events+149
state+150
(parse-input parser raw)+151
on-event))+152
(new-state (if (eq? new-state 'quit)+153
'quit+154
(drain-input new-state+155
parser on-event))))+156
(if (eq? new-state 'quit)+157
'quit+158
(begin+159
(do-render render new-state prev curr cols rows) 160
(run-frame render on-event new-state parser−138
new-prev curr cols rows frame-ms)))−139
(run-frame render on-event state parser−140
new-prev curr cols rows frame-ms)))−141
;; Timeout - redraw next frame−142
(run-frame render on-event state parser−143
new-prev curr cols rows frame-ms)))))))+161
curr prev cols rows frame-ms))))))+162
;; Timeout -- no input, skip render+163
(run-frame render on-event state parser+164
prev curr cols rows frame-ms)))))) 165
166
;;; Run a TUI application. 167
;;;@@ -161,10 +182,13 @@
182
(let* ((size (terminal-size)) 183
(cols (car size)) 184
(rows (cdr size))−164
(frame-ms (exact (floor (/ 1000 fps)))))+185
(frame-ms (exact (floor (/ 1000 fps))))+186
(prev (make-grid cols rows))+187
(curr (make-grid cols rows)))+188
;; Initial render+189
(do-render render initial-state prev curr cols rows) 190
(run-frame render on-event 191
initial-state 192
(make-parser-state)−168
(make-grid cols rows)−169
(make-grid cols rows)+193
curr prev 194
cols rows frame-ms))))))))src/sigil/tui/components.sglmodified
@@ -34,13 +34,13 @@
34
(define (render-panel grid x y w h title) 35
(when (and (> w 0) (> h 0)) 36
;; Top border−37
(grid-set! grid x y (make-cell box-tl color-default color-default attr-none))+37
(grid-set! grid x y box-tl color-default color-default attr-none) 38
(let loop ((c (+ x 1))) 39
(when (< c (+ x w -1))−40
(grid-set! grid c y (make-cell box-h color-default color-default attr-none))+40
(grid-set! grid c y box-h color-default color-default attr-none) 41
(loop (+ c 1)))) 42
(when (> w 1)−43
(grid-set! grid (+ x w -1) y (make-cell box-tr color-default color-default attr-none)))+43
(grid-set! grid (+ x w -1) y box-tr color-default color-default attr-none)) 44
45
;; Title (centered in top border) 46
(when (and title (> (string-length title) 0) (> w 4))@@ -51,35 +51,33 @@
51
title)) 52
(dlen (string-length display-title)) 53
(start (+ x 2)))−54
(grid-set! grid (- start 1) y−55
(make-cell #\space color-default color-default attr-none))+54
(grid-set! grid (- start 1) y #\space color-default color-default attr-none) 55
(let loop ((i 0)) 56
(when (< i dlen) 57
(grid-set! grid (+ start i) y−59
(make-cell (string-ref display-title i)−60
color-default color-default attr-bold))+58
(string-ref display-title i)+59
color-default color-default attr-bold) 60
(loop (+ i 1))))−62
(grid-set! grid (+ start dlen) y−63
(make-cell #\space color-default color-default attr-none))))+61
(grid-set! grid (+ start dlen) y #\space color-default color-default attr-none))) 62
63
;; Side borders 64
(let loop ((r (+ y 1))) 65
(when (< r (+ y h -1))−68
(grid-set! grid x r (make-cell box-v color-default color-default attr-none))+66
(grid-set! grid x r box-v color-default color-default attr-none) 67
(when (> w 1)−70
(grid-set! grid (+ x w -1) r (make-cell box-v color-default color-default attr-none)))+68
(grid-set! grid (+ x w -1) r box-v color-default color-default attr-none)) 69
(loop (+ r 1)))) 70
71
;; Bottom border 72
(when (> h 1)−75
(grid-set! grid x (+ y h -1) (make-cell box-bl color-default color-default attr-none))+73
(grid-set! grid x (+ y h -1) box-bl color-default color-default attr-none) 74
(let loop ((c (+ x 1))) 75
(when (< c (+ x w -1))−78
(grid-set! grid c (+ y h -1) (make-cell box-h color-default color-default attr-none))+76
(grid-set! grid c (+ y h -1) box-h color-default color-default attr-none) 77
(loop (+ c 1)))) 78
(when (> w 1) 79
(grid-set! grid (+ x w -1) (+ y h -1)−82
(make-cell box-br color-default color-default attr-none))))))+80
box-br color-default color-default attr-none))))) 81
82
;; Render text lines 83
;; Each line is (string fg bg attrs) or just a string@@ -127,8 +125,7 @@
125
(when is-selected 126
(let pad ((c (+ x slen))) 127
(when (< c (+ x w))−130
(grid-set! grid c (+ y row)−131
(make-cell #\space fg bg attrs))+128
(grid-set! grid c (+ y row) #\space fg bg attrs) 129
(pad (+ c 1)))))) 130
(loop (+ row 1) (+ idx 1)))))) 131
@@ -152,7 +149,7 @@
149
;; Fill entire row with inverse 150
(let loop ((c x)) 151
(when (< c (+ x w))−155
(grid-set! grid c y (make-cell #\space color-default color-default attr-inverse))+152
(grid-set! grid c y #\space color-default color-default attr-inverse) 153
(loop (+ c 1)))) 154
;; Left text 155
(grid-write-string! grid x y left color-default color-default attr-inverse)@@ -183,7 +180,7 @@
180
;; Clear remaining space 181
(let loop ((c (+ x plen (string-length visible-val)))) 182
(when (< c (+ x w))−186
(grid-set! grid c y (make-cell #\space color-default color-default attr-none))+183
(grid-set! grid c y #\space color-default color-default attr-none) 184
(loop (+ c 1))))))) 185
186
;; ============================================================src/sigil/tui/grid.sglmodified
@@ -182,19 +182,29 @@
182
(: vector? integer? integer? -> vector?) 183
(vector-ref (grid-cells grid) (grid-index grid col row))) 184
−185
;;; Set the cell at (col, row).−186
(define (grid-set! grid col row cell)−187
(: vector? integer? integer? vector? -> void?)−188
(vector-set! (grid-cells grid) (grid-index grid col row) cell))−189
−190
;;; Clear the grid to default cells.+185
;;; 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)))+195
+196
;;; 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)−197
(vector-set! cells i (default-cell))+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)))))) 209
210
;;; Write a string to the grid at (col, row) with the given style.@@ -203,26 +213,37 @@
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))−209
(grid-set! grid c row (make-cell (string-ref str i) fg bg attrs))+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)))))) 227
−212
;;; Fill a rectangle with a cell.−213
(define (grid-fill-rect! grid x y w h cell)−214
(: vector? integer? integer? integer? integer? vector? -> void?)−215
(let ((gw (grid-width grid))+228
;;; 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))−221
(grid-set! grid c r cell)+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
−225
;;; Create a deep copy of a grid.+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))@@ -335,6 +356,8 @@
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)@@ -345,13 +368,17 @@
368
(when (< row h) 369
(let col-loop ((col 0)) 370
(when (< col w)−348
(let ((pc (grid-ref prev col row))−349
(cc (grid-ref curr col row)))−350
(when (not (cell=? pc cc))−351
(let ((fg (cell-fg cc))−352
(bg (cell-bg cc))−353
(attrs (cell-attrs cc))−354
(ch (cell-char cc)))+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)))test/test-grid.sglmodified
@@ -33,9 +33,8 @@
33
34
(test-group "read/write" 35
(test "grid-set! and grid-ref"−36
(let ((g (make-grid 10 5))−37
(c (make-cell #\X color-green color-default attr-bold)))−38
(grid-set! g 3 2 c)+36
(let ((g (make-grid 10 5)))+37
(grid-set! g 3 2 #\X color-green color-default attr-bold) 38
(assert-equal #\X (cell-char (grid-ref g 3 2))) 39
(assert-equal color-green (cell-fg (grid-ref g 3 2))))) 40
@@ -60,9 +59,8 @@
59
60
(test-group "fill-rect" 61
(test "grid-fill-rect! fills area"−63
(let ((g (make-grid 10 5))−64
(c (make-cell #\# color-yellow color-default attr-none)))−65
(grid-fill-rect! g 1 1 3 2 c)+62
(let ((g (make-grid 10 5)))+63
(grid-fill-rect! g 1 1 3 2 #\# color-yellow color-default attr-none) 64
;; Inside the rect 65
(assert-equal #\# (cell-char (grid-ref g 1 1))) 66
(assert-equal #\# (cell-char (grid-ref g 3 2)))@@ -87,7 +85,7 @@
85
(test "single cell change" 86
(let ((prev (make-grid 5 3)) 87
(curr (make-grid 5 3)))−90
(grid-set! curr 2 1 (make-cell #\X color-red color-default attr-none))+88
(grid-set! curr 2 1 #\X color-red color-default attr-none) 89
(let ((diff (grid-diff prev curr))) 90
;; Should contain cursor move, SGR, and the character 91
(assert-true (> (string-length diff) 0))@@ -100,14 +98,13 @@
98
(grid-write-string! curr 2 1 "AB" color-green color-default attr-none) 99
(let ((diff (grid-diff prev curr))) 100
;; Should have one cursor move, not two−103
;; Count ESC [ sequences — should have: 1 cursor move + 1 SGR + 1 reset = 3 101
(assert-true (> (string-length diff) 0))))) 102
103
(test "style changes emit new SGR" 104
(let ((prev (make-grid 10 3)) 105
(curr (make-grid 10 3)))−109
(grid-set! curr 0 0 (make-cell #\A color-red color-default attr-none))−110
(grid-set! curr 1 0 (make-cell #\B color-blue color-default attr-none))+106
(grid-set! curr 0 0 #\A color-red color-default attr-none)+107
(grid-set! curr 1 0 #\B color-blue color-default attr-none) 108
(let ((diff (grid-diff prev curr))) 109
;; Two different styles means two SGR sequences 110
(assert-true (> (string-length diff) 10))))))@@ -116,7 +113,7 @@
113
(test "copy creates independent grid" 114
(let* ((orig (make-grid 5 3)) 115
(copy (grid-copy orig)))−119
(grid-set! copy 0 0 (make-cell #\X color-red color-default attr-none))+116
(grid-set! copy 0 0 #\X color-red color-default attr-none) 117
;; Original should be unchanged 118
(assert-equal #\space (cell-char (grid-ref orig 0 0))) 119
;; Copy should have the change