Some checks failed
Deploy (Gitea) / deploy (push) Failing after 2s
SIGWINCH: handle KEY_RESIZE (410) in main loop — re-measure screen, re-create status/chat/input windows at new dimensions, force redraw. Scroll clamp: PageUp clamped to (max 0 (- total 1)), prevents scrolling past message list end. Status bar shows 'msgs:N scroll:0'. /quit: saves :input-history to ~/.cache/passepartout/history (one line per entry, most recent first), sends goodbye handshake, sets :running nil. /reconnect: closes stale socket via disconnect-daemon, re-runs connect-daemon with retry backoff. Connection-loss detection: reader-loop counts consecutive nils; after 10, queues :disconnected event. Handler clears :connected/:busy, shows red system message. Load-history: reads ~/.cache/passepartout/history on startup, populates :input-history for up-arrow recall. Message vector: :messages init as adjustable vector with fill pointer. add-msg uses vector-push-extend (O(1) append). view-chat uses aref (O(1) access) instead of nth (O(n) for lists).
73 lines
2.6 KiB
Common Lisp
73 lines
2.6 KiB
Common Lisp
(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
|
|
:on-key :on-daemon-msg :send-daemon
|
|
:connect-daemon :disconnect-daemon
|
|
:*tui-theme* :theme-color))
|
|
(in-package :passepartout.gateway-tui)
|
|
|
|
(defvar *state* nil)
|
|
(defvar *event-queue* nil)
|
|
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
|
|
|
|
(defvar *tui-theme*
|
|
'(:user :green :agent :white :system :yellow :input :cyan
|
|
:connected :green :disconnected :red :timestamp :yellow)
|
|
"Color theme plist. Keys are semantic roles, values are Croatoan colors.")
|
|
|
|
(defun theme-color (role)
|
|
"Returns the Croatoan color for a semantic role."
|
|
(or (getf *tui-theme* role) :white))
|
|
|
|
(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 (make-array 16 :adjustable t :fill-pointer 0)
|
|
:scroll-offset 0 :busy nil :cursor-pos 0
|
|
:dirty (list nil nil nil))))
|
|
|
|
(defun now ()
|
|
(multiple-value-bind (s m h) (get-decoded-time)
|
|
(declare (ignore s))
|
|
(format nil "~2,'0d:~2,'0d" h m)))
|
|
|
|
(defun input-string ()
|
|
(coerce (reverse (st :input-buffer)) 'string))
|
|
|
|
(defun input-insert-char (ch)
|
|
"Insert character at cursor position into the input buffer."
|
|
(let* ((buf (st :input-buffer))
|
|
(pos (or (st :cursor-pos) 0))
|
|
(s (coerce (reverse buf) 'string))
|
|
(new (concatenate 'string (subseq s 0 pos) (string ch) (subseq s pos))))
|
|
(setf (st :input-buffer) (reverse (coerce new 'list)))
|
|
(setf (st :cursor-pos) (1+ pos))))
|
|
|
|
(defun input-delete-char ()
|
|
"Delete character before cursor position (standard backspace)."
|
|
(let* ((buf (st :input-buffer))
|
|
(pos (or (st :cursor-pos) 0)))
|
|
(when (and buf (> pos 0))
|
|
(let* ((s (coerce (reverse buf) 'string))
|
|
(new (concatenate 'string (subseq s 0 (1- pos)) (subseq s pos))))
|
|
(setf (st :input-buffer) (reverse (coerce new 'list)))
|
|
(setf (st :cursor-pos) (1- pos))))))
|
|
|
|
(defun add-msg (role content)
|
|
(vector-push-extend (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)))
|