diff --git a/lisp/gateway-tui-main.lisp b/lisp/gateway-tui-main.lisp index ba60753..609ef85 100644 --- a/lisp/gateway-tui-main.lisp +++ b/lisp/gateway-tui-main.lisp @@ -44,11 +44,19 @@ ;; /theme command ((string-equal text "/theme") (add-msg :system - (format nil "Theme: user=~a agent=~a system=~a input=~a" + (format nil "Theme: ~a — user=~a agent=~a system=~a input=~a" + *tui-theme-current-name* (getf *tui-theme* :user) (getf *tui-theme* :agent) (getf *tui-theme* :system) - (getf *tui-theme* :input)))) + (getf *tui-theme* :input)) + (format nil "Presets: /theme dark | light | solarized | gruvbox"))) + ((and (>= (length text) 7) + (string-equal (subseq text 0 7) "/theme ")) + (let ((name (string-trim '(#\Space) (subseq text 7)))) + (if (theme-switch name) + (add-msg :system (format nil "Theme switched to ~a" name)) + (add-msg :system (format nil "Unknown theme '~a'. Try: dark light solarized gruvbox" name))))) ;; /eval command ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/eval ")) @@ -113,23 +121,32 @@ (setf (st :input-buffer) nil) (setf (st :cursor-pos) 0) (setf (st :dirty) (list t t t)))))) - ;; Tab — command completion - ((or (eql ch 9) (eq ch :tab)) - (let ((text (input-string))) - (when (and (> (length text) 1) (eql (char text 0) #\/)) - (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme")) - (match (find text cmds :test - (lambda (in cmd) - (and (>= (length cmd) (length in)) - (string-equal cmd in :end1 (length in))))))) - (when match - (setf (st :input-buffer) (reverse (coerce match 'list))) - (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (st :input-buffer))) - (setf (st :dirty) (list nil nil t))))))) - ;; Backspace - ((or (eq ch :backspace) (eql ch 127) (eql ch 8) - (eql ch #\Backspace)) + ;; Tab — command completion + ((or (eql ch 9) (eq ch :tab)) + (let ((text (input-string))) + (cond + ((and (>= (length text) 8) + (string-equal (subseq text 0 7) "/theme ")) + (let* ((partial (subseq text 7)) + (names '("dark" "light" "solarized" "gruvbox")) + (match (find partial names :test #'string-equal))) + (when match + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (st :dirty) (list nil nil t))))) + ((and (> (length text) 1) (eql (char text 0) #\/)) + (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme" "/reconnect" "/quit")) + (match (find text cmds :test + (lambda (in cmd) + (and (>= (length cmd) (length in)) + (string-equal cmd in :end1 (length in))))))) + (when match + (setf (st :input-buffer) (reverse (coerce match 'list))) + (when (member match '("/eval" "/focus" "/scope") :test #'string=) + (push #\Space (st :input-buffer))) + (setf (st :dirty) (list nil nil t)))))))) + ;; Backspace + ((or (eq ch :backspace) (eql ch 127) (eql ch 8) + (eql ch #\Backspace)) (input-delete-char) (setf (st :dirty) (list nil nil t))) ;; Left arrow @@ -279,6 +296,7 @@ (defun tui-main () (init-state) (load-history) + (theme-load) (with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil) (let* ((h (or (height scr) 24)) (w (or (width scr) 80)) diff --git a/lisp/gateway-tui-model.lisp b/lisp/gateway-tui-model.lisp index 0d81d17..5e2cdaa 100644 --- a/lisp/gateway-tui-model.lisp +++ b/lisp/gateway-tui-model.lisp @@ -13,9 +13,91 @@ (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.") + ;; Roles + '(:user :green :agent :white :system :yellow + ;; Content + :input :cyan :timestamp :yellow :help :cyan :error :red :warning :yellow + ;; Status + :connected :green :disconnected :red :busy :magenta :idle :white + ;; Gate trace + :gate-passed :green :gate-blocked :red :gate-approval :yellow + ;; Tools (future use) + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white + ;; Display + :scroll-indicator :cyan :border :white :background :black + ;; Differentiator (v0.4.0) + :rule-count :cyan :focus-map :yellow + ;; UI + :dim :white :highlight :cyan :accent :green) + "Color theme plist. 27 semantic keys → Croatoan color values. +See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") + +(defvar *tui-theme-presets* + '(:dark (:user :green :agent :white :system :yellow + :input :cyan :timestamp :yellow :help :cyan :error :red :warning :yellow + :connected :green :disconnected :red :busy :magenta :idle :white + :gate-passed :green :gate-blocked :red :gate-approval :yellow + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white + :scroll-indicator :cyan :border :white :background :black + :rule-count :cyan :focus-map :yellow + :dim :white :highlight :cyan :accent :green) + :light (:user :blue :agent :black :system :red + :input :black :timestamp :yellow :help :blue :error :red :warning :yellow + :connected :green :disconnected :red :busy :magenta :idle :black + :gate-passed :green :gate-blocked :red :gate-approval :yellow + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black + :scroll-indicator :blue :border :black :background :white + :rule-count :blue :focus-map :red + :dim :white :highlight :blue :accent :green) + :gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f" + :input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f" + :connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984" + :gate-passed "#b8bb26" :gate-blocked "#fb4934" :gate-approval "#fabd2f" + :tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2" + :scroll-indicator "#83a598" :border "#a89984" :background "#282828" + :rule-count "#83a598" :focus-map "#fabd2f" + :dim "#928374" :highlight "#83a598" :accent "#b8bb26") + :solarized (:user "#268bd2" :agent "#839496" :system "#b58900" + :input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900" + :connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83" + :gate-passed "#859900" :gate-blocked "#dc322f" :gate-approval "#b58900" + :tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496" + :scroll-indicator "#2aa198" :border "#657b83" :background "#002b36" + :rule-count "#2aa198" :focus-map "#b58900" + :dim "#586e75" :highlight "#2aa198" :accent "#859900")) + "Named theme presets. /theme loads one into *tui-theme*.") + +(defvar *tui-theme-current-name* :dark + "Name of the currently active theme preset.") + +(defun theme-save () + "Persist current theme to disk." + (let ((path (merge-pathnames ".cache/passepartout/theme.lisp" + (user-homedir-pathname)))) + (uiop:ensure-all-directories-exist (list path)) + (with-open-file (out path :direction :output :if-exists :supersede :if-does-not-exist :create) + (format out ";; Passepartout TUI theme — auto-generated~%") + (format out "(setf passepartout.gateway-tui::*tui-theme* '~s)~%" *tui-theme*) + (format out "(setf passepartout.gateway-tui::*tui-theme-current-name* ~s)~%" *tui-theme-current-name*)) + t)) + +(defun theme-load () + "Load persisted theme from disk. Called at startup." + (let ((path (merge-pathnames ".cache/passepartout/theme.lisp" + (user-homedir-pathname)))) + (when (uiop:file-exists-p path) + (ignore-errors (load path))))) + +(defun theme-switch (name) + "Switch to a named theme preset. Returns the preset name or nil if not found." + (let* ((key (intern (string-upcase (string name)) :keyword)) + (preset (getf *tui-theme-presets* key))) + (when preset + (setf *tui-theme* (copy-list preset) + *tui-theme-current-name* key) + (theme-save) + (setf (st :dirty) (list t t t)) + key))) (defun theme-color (role) "Returns the Croatoan color for a semantic role." diff --git a/org/gateway-tui-main.org b/org/gateway-tui-main.org index 05a9497..de81361 100644 --- a/org/gateway-tui-main.org +++ b/org/gateway-tui-main.org @@ -75,11 +75,19 @@ Event handlers + daemon I/O + main loop. ;; /theme command ((string-equal text "/theme") (add-msg :system - (format nil "Theme: user=~a agent=~a system=~a input=~a" + (format nil "Theme: ~a — user=~a agent=~a system=~a input=~a" + *tui-theme-current-name* (getf *tui-theme* :user) (getf *tui-theme* :agent) (getf *tui-theme* :system) - (getf *tui-theme* :input)))) + (getf *tui-theme* :input)) + (format nil "Presets: /theme dark | light | solarized | gruvbox"))) + ((and (>= (length text) 7) + (string-equal (subseq text 0 7) "/theme ")) + (let ((name (string-trim '(#\Space) (subseq text 7)))) + (if (theme-switch name) + (add-msg :system (format nil "Theme switched to ~a" name)) + (add-msg :system (format nil "Unknown theme '~a'. Try: dark light solarized gruvbox" name))))) ;; /eval command ((and (>= (length text) 6) (string-equal (subseq text 0 6) "/eval ")) @@ -144,23 +152,32 @@ Event handlers + daemon I/O + main loop. (setf (st :input-buffer) nil) (setf (st :cursor-pos) 0) (setf (st :dirty) (list t t t)))))) - ;; Tab — command completion - ((or (eql ch 9) (eq ch :tab)) - (let ((text (input-string))) - (when (and (> (length text) 1) (eql (char text 0) #\/)) - (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme")) - (match (find text cmds :test - (lambda (in cmd) - (and (>= (length cmd) (length in)) - (string-equal cmd in :end1 (length in))))))) - (when match - (setf (st :input-buffer) (reverse (coerce match 'list))) - (when (member match '("/eval" "/focus" "/scope") :test #'string=) - (push #\Space (st :input-buffer))) - (setf (st :dirty) (list nil nil t))))))) - ;; Backspace - ((or (eq ch :backspace) (eql ch 127) (eql ch 8) - (eql ch #\Backspace)) + ;; Tab — command completion + ((or (eql ch 9) (eq ch :tab)) + (let ((text (input-string))) + (cond + ((and (>= (length text) 8) + (string-equal (subseq text 0 7) "/theme ")) + (let* ((partial (subseq text 7)) + (names '("dark" "light" "solarized" "gruvbox")) + (match (find partial names :test #'string-equal))) + (when match + (setf (st :input-buffer) (reverse (coerce (concatenate 'string "/theme " match) 'list))) + (setf (st :dirty) (list nil nil t))))) + ((and (> (length text) 1) (eql (char text 0) #\/)) + (let* ((cmds '("/eval" "/focus" "/scope" "/unfocus" "/help" "/theme" "/reconnect" "/quit")) + (match (find text cmds :test + (lambda (in cmd) + (and (>= (length cmd) (length in)) + (string-equal cmd in :end1 (length in))))))) + (when match + (setf (st :input-buffer) (reverse (coerce match 'list))) + (when (member match '("/eval" "/focus" "/scope") :test #'string=) + (push #\Space (st :input-buffer))) + (setf (st :dirty) (list nil nil t)))))))) + ;; Backspace + ((or (eq ch :backspace) (eql ch 127) (eql ch 8) + (eql ch #\Backspace)) (input-delete-char) (setf (st :dirty) (list nil nil t))) ;; Left arrow @@ -319,6 +336,7 @@ Event handlers + daemon I/O + main loop. (defun tui-main () (init-state) (load-history) + (theme-load) (with-screen (scr :input-blocking nil :input-echoing nil :cursor-visible nil) (let* ((h (or (height scr) 24)) (w (or (width scr) 80)) diff --git a/org/gateway-tui-model.org b/org/gateway-tui-model.org index a186cac..763908d 100644 --- a/org/gateway-tui-model.org +++ b/org/gateway-tui-model.org @@ -33,9 +33,91 @@ All state mutation flows through event handlers in the controller. (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.") + ;; Roles + '(:user :green :agent :white :system :yellow + ;; Content + :input :cyan :timestamp :yellow :help :cyan :error :red :warning :yellow + ;; Status + :connected :green :disconnected :red :busy :magenta :idle :white + ;; Gate trace + :gate-passed :green :gate-blocked :red :gate-approval :yellow + ;; Tools (future use) + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white + ;; Display + :scroll-indicator :cyan :border :white :background :black + ;; Differentiator (v0.4.0) + :rule-count :cyan :focus-map :yellow + ;; UI + :dim :white :highlight :cyan :accent :green) + "Color theme plist. 27 semantic keys → Croatoan color values. +See *tui-theme-presets* for named presets (dark, light, solarized, gruvbox).") + +(defvar *tui-theme-presets* + '(:dark (:user :green :agent :white :system :yellow + :input :cyan :timestamp :yellow :help :cyan :error :red :warning :yellow + :connected :green :disconnected :red :busy :magenta :idle :white + :gate-passed :green :gate-blocked :red :gate-approval :yellow + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :white + :scroll-indicator :cyan :border :white :background :black + :rule-count :cyan :focus-map :yellow + :dim :white :highlight :cyan :accent :green) + :light (:user :blue :agent :black :system :red + :input :black :timestamp :yellow :help :blue :error :red :warning :yellow + :connected :green :disconnected :red :busy :magenta :idle :black + :gate-passed :green :gate-blocked :red :gate-approval :yellow + :tool-running :magenta :tool-success :green :tool-failure :red :tool-output :black + :scroll-indicator :blue :border :black :background :white + :rule-count :blue :focus-map :red + :dim :white :highlight :blue :accent :green) + :gruvbox (:user "#458588" :agent "#ebdbb2" :system "#fabd2f" + :input "#ebdbb2" :timestamp "#928374" :help "#83a598" :error "#fb4934" :warning "#fabd2f" + :connected "#b8bb26" :disconnected "#fb4934" :busy "#d3869b" :idle "#a89984" + :gate-passed "#b8bb26" :gate-blocked "#fb4934" :gate-approval "#fabd2f" + :tool-running "#d3869b" :tool-success "#b8bb26" :tool-failure "#fb4934" :tool-output "#ebdbb2" + :scroll-indicator "#83a598" :border "#a89984" :background "#282828" + :rule-count "#83a598" :focus-map "#fabd2f" + :dim "#928374" :highlight "#83a598" :accent "#b8bb26") + :solarized (:user "#268bd2" :agent "#839496" :system "#b58900" + :input "#839496" :timestamp "#93a1a1" :help "#2aa198" :error "#dc322f" :warning "#b58900" + :connected "#859900" :disconnected "#dc322f" :busy "#d33682" :idle "#657b83" + :gate-passed "#859900" :gate-blocked "#dc322f" :gate-approval "#b58900" + :tool-running "#d33682" :tool-success "#859900" :tool-failure "#dc322f" :tool-output "#839496" + :scroll-indicator "#2aa198" :border "#657b83" :background "#002b36" + :rule-count "#2aa198" :focus-map "#b58900" + :dim "#586e75" :highlight "#2aa198" :accent "#859900")) + "Named theme presets. /theme loads one into *tui-theme*.") + +(defvar *tui-theme-current-name* :dark + "Name of the currently active theme preset.") + +(defun theme-save () + "Persist current theme to disk." + (let ((path (merge-pathnames ".cache/passepartout/theme.lisp" + (user-homedir-pathname)))) + (uiop:ensure-all-directories-exist (list path)) + (with-open-file (out path :direction :output :if-exists :supersede :if-does-not-exist :create) + (format out ";; Passepartout TUI theme — auto-generated~%") + (format out "(setf passepartout.gateway-tui::*tui-theme* '~s)~%" *tui-theme*) + (format out "(setf passepartout.gateway-tui::*tui-theme-current-name* ~s)~%" *tui-theme-current-name*)) + t)) + +(defun theme-load () + "Load persisted theme from disk. Called at startup." + (let ((path (merge-pathnames ".cache/passepartout/theme.lisp" + (user-homedir-pathname)))) + (when (uiop:file-exists-p path) + (ignore-errors (load path))))) + +(defun theme-switch (name) + "Switch to a named theme preset. Returns the preset name or nil if not found." + (let* ((key (intern (string-upcase (string name)) :keyword)) + (preset (getf *tui-theme-presets* key))) + (when preset + (setf *tui-theme* (copy-list preset) + *tui-theme-current-name* key) + (theme-save) + (setf (st :dirty) (list t t t)) + key))) (defun theme-color (role) "Returns the Croatoan color for a semantic role."