AtlatestRepositorysigil-tui

sigil-tui / tree / exampleschat-demo.sgl

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))
12;; ============================================================
13;; Data model
14;; ============================================================
16(define-struct contact
17 (name default: "")
18 (online? default: #f)
19 (group? default: #f))
21(define-struct message
22 (sender default: "")
23 (text default: "")
24 (timestamp default: 0))
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))
37;; ============================================================
38;; Helpers
39;; ============================================================
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)))
53(define (get-conversation state name)
54 (let ((entry (assoc name (app-state-conversations state))))
55 (if entry (cdr entry) '())))
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)))))
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)))
77;; ============================================================
78;; Simulated incoming messages
79;; ============================================================
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")))
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)))))))
111;; ============================================================
112;; UI rendering
113;; ============================================================
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))))
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))))
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))))
175;; ============================================================
176;; Event handling
177;; ============================================================
179(define (handle-event state event)
180 (maybe-simulate-message! state)
182 (cond
183 ;; Quit
184 ((and (key-event? event)
185 (equal? event '(key #\q ctrl)))
186 'quit)
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)
195 ;; Roster mode keys
196 ((eq? (app-state-mode state) 'roster)
197 (handle-roster-event state event))
199 ;; Chat mode keys
200 ((eq? (app-state-mode state) 'chat)
201 (handle-chat-event state event))
203 (else state)))
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)
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)
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)
229 (else state)))
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)
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)
250 (else state)))
252;; ============================================================
253;; Main
254;; ============================================================
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))
271(tui-run
272 render-app
273 initial-state: (make-initial-state)
274 on-event: handle-event
275 fps: 30)