#+TITLE: Passepartout TUI — View #+PROPERTY: header-args:lisp :tangle ../lisp/gateway-tui-view.lisp * View Pure render functions. Each takes a Croatoan window and current state. State is read via ~(st :key)~ — no mutation here. ** Contract 1. (view-status win): renders the status bar with connection info, version, and timestamp. 2. (view-chat win): renders the scrolled chat message list. Messages are color-coded: green (user), white (agent), yellow (system). 3. (view-input win): renders the input line with cursor and typing indicator. 4. (redraw scr chat-win status-win input-win): dispatches redraws based on ~(st :dirty)~ flags. Minimizes terminal writes. ** Status Bar #+begin_src lisp (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)) #+end_src ** Chat Area #+begin_src lisp (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)) #+end_src ** Input Line #+begin_src lisp (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)) #+end_src ** Redraw (dirty-flag dispatch) #+begin_src lisp (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)))) #+end_src