TUI: 3-file split (model/view/controller)

- tui-model.lisp: defpackage, *state*, st/init-state, add-msg, event queue
- tui-view.lisp: view-status, view-chat, view-input, redraw (pure renders)
- tui-main.lisp: on-key, on-daemon-msg, daemon I/O, connect, tui-main
- ASDF updated to serial 3-file dependency
- Removed monolithic org/gateway-tui.org and lisp/gateway-tui.lisp
- Pre-commit hook: added 3 split files to croatoan exclusion
- core-skills: added 3 split files to skill loader exclusion
- Verified: LLM response arrives, /eval works, colors render
[no-verify: pre-commit hook SKIPped for TUI files]
This commit is contained in:
2026-05-04 17:01:02 -04:00
parent 3c1ed77c85
commit cbbf409059
12 changed files with 342 additions and 283 deletions

View File

@@ -99,8 +99,11 @@
(string= n "security-dispatcher")
(string= n "system-model-router")
(string= n "system-model-embedding")
(string= n "system-model-explorer")
(string= n "gateway-tui"))))
(string= n "system-model-explorer")
(string= n "gateway-tui")
(string= n "gateway-tui-model")
(string= n "gateway-tui-view")
(string= n "gateway-tui-main"))))
all-files))
(adj (make-hash-table :test 'equal))
(name-to-file (make-hash-table :test 'equal))

0
lisp/file-list.txt Normal file
View File

View File

@@ -1,94 +1,9 @@
(defpackage :passepartout.gateway-tui
(:use :cl :croatoan :passepartout :usocket :bordeaux-threads)
(:export :tui-main))
(in-package :passepartout.gateway-tui)
(defvar *state* nil)
(defvar *event-queue* nil)
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
(defun st (key) (getf *state* key))
(defun (setf st) (val key) (setf (getf *state* key) val))
(defun init-state ()
(setf *state*
(list :running t :mode :chat :connected nil :stream nil
:input-buffer nil :input-history nil :input-hpos 0
:messages nil :scroll-offset 0 :dirty (list nil nil nil))))
(defun now ()
(multiple-value-bind (h m) (get-decoded-time)
(format nil "~2,'0d:~2,'0d" h m)))
(defun input-string ()
(coerce (reverse (st :input-buffer)) 'string))
(defun add-msg (role content)
(push (list :role role :content content :time (now)) (st :messages))
(setf (st :dirty) (list t t nil)))
(defun view-status (win)
(clear win)
(box win 0 0)
(add-string win
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a"
(if (st :connected) "● Connected" "○ Disconnected")
(string-upcase (string (st :mode)))
(length (st :messages))
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0"))
:y 1 :x 1 :fgcolor (if (st :connected) :green :red))
(add-string win (format nil " ~a" (now)) :y 2 :x 1 :fgcolor :yellow)
(refresh win))
(defun view-chat (win h)
(clear win)
(box win 0 0)
(let* ((w (or (width win) 78))
(msgs (reverse (st :messages)))
(max-lines (- h 2))
(total (length msgs))
(start (max 0 (- total max-lines (st :scroll-offset))))
(y 1))
(loop for i from start below total
while (< y (1- h))
do (let ((msg (nth i msgs)))
(let* ((role (getf msg :role))
(content (getf msg :content))
(time (or (getf msg :time) ""))
(label (case role
(:user (format nil "⬆ [~a] ~a" time content))
(:agent (format nil "⬇ [~a] ~a" time content))
(:system (format nil " [~a] ~a" time content))
(t (format nil " [~a] ~a" time content))))
(color (case role
(:user :green)
(:agent :white)
(:system :yellow)
(t :white))))
(add-string win label :y y :x 1 :n (1- w) :fgcolor color)
(incf y)))))
(refresh win))
(defun view-input (win)
(let* ((text (input-string))
(w (or (width win) 78))
(clip (min (length text) (1- w))))
(clear win)
(add-string win (format nil "~a " text) :y 0 :x 0 :n (1- w) :fgcolor :cyan)
(setf (cursor-position win) (list 0 clip)))
(refresh win))
(defun queue-event (ev)
(bt:with-lock-held (*event-lock*) (push ev *event-queue*)))
(defun drain-queue ()
(bt:with-lock-held (*event-lock*)
(let ((evs (nreverse *event-queue*)))
(setf *event-queue* nil) evs)))
(defun on-key (&rest args)
(let ((ch (car args)))
(cond
;; Enter
((or (eql ch 10) (eql ch 13) (eq ch :enter)
(eql ch #\Newline) (eql ch #\Return))
(let ((text (string-trim '(#\Space #\Tab) (input-string))))
@@ -97,7 +12,7 @@
(setf (st :input-hpos) 0)
(setf (st :scroll-offset) 0)
(cond
;; /eval command: evaluate Lisp form
;; /eval command
((and (>= (length text) 6)
(string-equal (subseq text 0 6) "/eval "))
(handler-case
@@ -106,16 +21,18 @@
(r (eval (read-from-string (subseq text 6)))))
(add-msg :system (format nil "=> ~s" r)))
(error (c) (add-msg :system (format nil "=> ✗ ~a" c)))))
;; Normal message: send to daemon
;; Normal message
(t
(add-msg :user text)
(send-daemon (list :type :event
:payload (list :sensor :user-input :text text)))))
(setf (st :input-buffer) nil)
(setf (st :dirty) (list t t t)))))
;; Backspace
((or (eql ch 127) (eql ch 8) (eq ch :backspace) (eql ch #\Backspace))
(when (st :input-buffer) (pop (st :input-buffer)))
(setf (st :dirty) (list nil nil t)))
;; Up arrow
((or (eq ch :up) (eql ch 259))
(let* ((h (st :input-history)) (p (st :input-hpos)))
(when (and h (< p (1- (length h))))
@@ -123,6 +40,7 @@
(setf (st :input-buffer)
(reverse (coerce (nth (st :input-hpos) h) 'list)))
(setf (st :dirty) (list nil nil t)))))
;; Down arrow
((or (eq ch :down) (eql ch 258))
(when (> (st :input-hpos) 0)
(decf (st :input-hpos))
@@ -132,12 +50,15 @@
(reverse (coerce (nth (st :input-hpos) h) 'list))
nil))
(setf (st :dirty) (list nil nil t)))))
;; PageUp
((or (eq ch :ppage) (eql ch 339))
(incf (st :scroll-offset) 5)
(setf (st :dirty) (list nil t nil)))
;; PageDown
((or (eq ch :npage) (eql ch 338))
(setf (st :scroll-offset) (max 0 (- (st :scroll-offset) 5)))
(setf (st :dirty) (list nil t nil)))
;; Printable
(t
(let ((chr (typecase ch
(character ch)
@@ -206,13 +127,6 @@
(setf (st :stream) nil (st :connected) nil)
(add-msg :system "* Disconnected *")))
(defun redraw (sw cw ch iw)
(destructuring-bind (sd cd id) (st :dirty)
(when sd (view-status sw))
(when cd (view-chat cw ch))
(when id (view-input iw))
(setf (st :dirty) (list nil nil nil))))
(defun tui-main ()
(init-state)
(with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil)
@@ -228,7 +142,6 @@
(setf (function-keys-enabled-p iw) t
(st :dirty) (list t t t))
(connect-daemon)
;; Start Swank REPL (optional - set TUI_SWANK_PORT=0 to disable)
(when (> swank-port 0)
(handler-case
(progn
@@ -237,9 +150,8 @@
:port swank-port :dont-close t)
(add-msg :system
(format nil "* Swank ~d M-x slime-connect *" swank-port)))
(error (c)
(error ()
(add-msg :system "* Swank unavailable *"))))
;; Main loop
(loop while (st :running) do
(dolist (ev (drain-queue))
(when (eq (getf ev :type) :daemon)

View File

@@ -0,0 +1,38 @@
(defpackage :passepartout.gateway-tui
(:use :cl :croatoan :passepartout :usocket :bordeaux-threads)
(:export :tui-main :st :add-msg :now :input-string
:queue-event :drain-queue :init-state
:view-status :view-chat :view-input :redraw))
(in-package :passepartout.gateway-tui)
(defvar *state* nil)
(defvar *event-queue* nil)
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
(defun st (key) (getf *state* key))
(defun (setf st) (val key) (setf (getf *state* key) val))
(defun init-state ()
(setf *state*
(list :running t :mode :chat :connected nil :stream nil
:input-buffer nil :input-history nil :input-hpos 0
:messages nil :scroll-offset 0 :dirty (list nil nil nil))))
(defun now ()
(multiple-value-bind (h m) (get-decoded-time)
(format nil "~2,'0d:~2,'0d" h m)))
(defun input-string ()
(coerce (reverse (st :input-buffer)) 'string))
(defun add-msg (role content)
(push (list :role role :content content :time (now)) (st :messages))
(setf (st :dirty) (list t t nil)))
(defun queue-event (ev)
(bt:with-lock-held (*event-lock*) (push ev *event-queue*)))
(defun drain-queue ()
(bt:with-lock-held (*event-lock*)
(let ((evs (nreverse *event-queue*)))
(setf *event-queue* nil) evs)))

View File

@@ -0,0 +1,59 @@
(in-package :passepartout.gateway-tui)
(defun view-status (win)
(clear win)
(box win 0 0)
(add-string win
(format nil " Passepartout ~a [~a] msgs:~a scroll:~a"
(if (st :connected) "● Connected" "○ Disconnected")
(string-upcase (string (st :mode)))
(length (st :messages))
(if (> (st :scroll-offset) 0) (format nil "~a↑" (st :scroll-offset)) "0"))
:y 1 :x 1 :fgcolor (if (st :connected) :green :red))
(add-string win (format nil " ~a" (now)) :y 2 :x 1 :fgcolor :yellow)
(refresh win))
(defun view-chat (win h)
(clear win)
(box win 0 0)
(let* ((w (or (width win) 78))
(msgs (reverse (st :messages)))
(max-lines (- h 2))
(total (length msgs))
(start (max 0 (- total max-lines (st :scroll-offset))))
(y 1))
(loop for i from start below total
while (< y (1- h))
do (let ((msg (nth i msgs)))
(let* ((role (getf msg :role))
(content (getf msg :content))
(time (or (getf msg :time) ""))
(label (case role
(:user (format nil "⬆ [~a] ~a" time content))
(:agent (format nil "⬇ [~a] ~a" time content))
(:system (format nil " [~a] ~a" time content))
(t (format nil " [~a] ~a" time content))))
(color (case role
(:user :green)
(:agent :white)
(:system :yellow)
(t :white))))
(add-string win label :y y :x 1 :n (1- w) :fgcolor color)
(incf y)))))
(refresh win))
(defun view-input (win)
(let* ((text (input-string))
(w (or (width win) 78))
(clip (min (length text) (1- w))))
(clear win)
(add-string win (format nil "~a " text) :y 0 :x 0 :n (1- w) :fgcolor :cyan)
(setf (cursor-position win) (list 0 clip)))
(refresh win))
(defun redraw (sw cw ch iw)
(destructuring-bind (sd cd id) (st :dirty)
(when sd (view-status sw))
(when cd (view-chat cw ch))
(when id (view-input iw))
(setf (st :dirty) (list nil nil nil))))