diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 7131461..02a85e8 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -1016,17 +1016,8 @@ Returns T on success, nil on failure. Does NOT wait or retry." ;; Guard w and h before render (resize or other code may have set them to nil) (setq w (or (and (numberp w) (> w 0) w) 80) h (or (and (numberp h) (> h 0) h) 24)) - (when (and (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) - (null (st :dialog-stack))) - (cl-tty.backend:begin-sync be) - (cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :bg)) - (view-status be w h) - (view-chat be w h) - (view-input be w h) - (when (sidebar-visible-p w) - (view-sidebar be w h)) - (cl-tty.backend:end-sync be) - (setf (st :dirty) (list nil nil nil))) + (unless (st :dialog-stack) + (redraw be w h)) (let ((ds (st :dialog-stack))) (when ds (cl-tty.backend:begin-sync be) @@ -1038,42 +1029,42 @@ Returns T on success, nil on failure. Does NOT wait or retry." (cnt (length filtered)) (filter (cl-tty.select:select-filter sel)) (mh (min 15 (+ 1 cnt))) - (top (max 0 (- h 4 mh))) + (top (max 0 (- h 7 mh))) (bg-p (theme-color :bg-panel)) (sep-c (theme-color :separator))) ;; Fill minibuffer area with panel bg (dotimes (r (min (- h 3 top) h)) (cl-tty.backend:draw-rect be 0 (+ top r) chat-w 1 :bg bg-p)) - ;; Top separator - (cl-tty.backend:draw-text be 0 top - (make-string chat-w :initial-element #\─) - sep-c nil) - (cl-tty.backend:draw-text be 1 top - (cl-tty.dialog:dialog-title dlg) - (theme-color :accent) nil) - ;; Options - (let ((y-off 1)) - (dolist (item filtered) - (let* ((display-idx (first item)) - (option (third item)) - (title (getf option :title)) - (cat (getf option :category)) - (sel-p (eql display-idx sel-idx)) - (text (if cat (format nil " ~a" title) - (format nil " ~a~a" (if sel-p "▸ " " ") title))) - (row (+ top y-off))) - (when (>= row (1- h)) (return)) - (cl-tty.backend:draw-text be 1 row text - (cond (cat (theme-color :text-muted)) - (sel-p (theme-color :accent)) - (t (theme-color :agent-fg))) - nil :bold sel-p) - (incf y-off)))) - ;; Filter prompt - (cl-tty.backend:draw-rect be 0 (- h 3) chat-w 1 :bg bg-p) - (cl-tty.backend:draw-text be 0 (- h 3) - (format nil "> ~a" (or filter "")) - (theme-color :input-prompt) nil)) + ;; Top separator + (cl-tty.backend:draw-text be 0 top + (make-string chat-w :initial-element #\─) + sep-c bg-p) + (cl-tty.backend:draw-text be 1 top + (cl-tty.dialog:dialog-title dlg) + (theme-color :accent) bg-p) + ;; Options + (let ((y-off 1)) + (dolist (item filtered) + (let* ((display-idx (first item)) + (option (third item)) + (title (getf option :title)) + (cat (getf option :category)) + (sel-p (eql display-idx sel-idx)) + (text (if cat (format nil " ~a" title) + (format nil " ~a~a" (if sel-p "▸ " " ") title))) + (row (+ top y-off))) + (when (>= row (1- h)) (return)) + (cl-tty.backend:draw-text be 1 row text + (cond (cat (theme-color :text-muted)) + (sel-p (theme-color :accent)) + (t (theme-color :agent-fg))) + bg-p :bold sel-p) + (incf y-off)))) + ;; Filter prompt + (cl-tty.backend:draw-rect be 0 (- h 3) chat-w 1 :bg bg-p) + (cl-tty.backend:draw-text be 0 (- h 3) + (format nil "> ~a" (or filter "")) + (theme-color :input-prompt) bg-p)) (cl-tty.backend:end-sync be)) (sleep 0.1))) (progn (disconnect-daemon))))) diff --git a/org/channel-tui-state.org b/org/channel-tui-state.org index f511a7f..e501abb 100644 --- a/org/channel-tui-state.org +++ b/org/channel-tui-state.org @@ -42,13 +42,14 @@ All state mutation flows through event handlers in the controller. :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" :text-muted "#808080" :dot-connected "#7fd88f" :dot-disconnected "#e06c75" + :bg-input "#2e2e2e" :error "#e06c75" :tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75" :separator "#3c3c3c" :accent "#fab283" :dim "#606060") "Dark-neutral color theme with warm amber accent. Backgrounds are dark grays, semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :text-muted, :user-fg/bg/border, :agent-header/fg, :system, :input-prompt/fg, -:hint, :status-bg/fg, :dot-connected/disconnected, :error, :tool-*, +:hint, :status-bg/fg, :bg-input, :dot-connected/disconnected, :error, :tool-*, :separator, :accent, :dim.") (defvar *tui-theme-presets* @@ -60,6 +61,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#e8e8e8" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7fd88f" :dot-disconnected "#e06c75" :error "#e06c75" @@ -73,6 +75,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#ffd700" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7fd88f" :dot-disconnected "#e06c75" :error "#e06c75" @@ -86,6 +89,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#d4956a" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#6cb85c" :dot-disconnected "#d94a3a" :error "#d94a3a" @@ -99,6 +103,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#b89870" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7aac5c" :dot-disconnected "#c84a3a" :error "#c84a3a" @@ -112,6 +117,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#c8a080" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7cb860" :dot-disconnected "#d06050" :error "#d06050" @@ -125,6 +131,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#cc9966" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7ab85c" :dot-disconnected "#d94a3a" :error "#d94a3a" @@ -138,6 +145,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :hint "#606060" :status-bg "#141414" :status-fg "#c8a070" :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#7ab85c" :dot-disconnected "#d94a3a" :error "#d94a3a" @@ -150,7 +158,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#cc6600" :input-fg "#3a2a1a" :hint "#a0a0a0" :status-bg "#ebebeb" :status-fg "#3a2a1a" - :bg "#ffffff" :bg-panel "#f5f5f5" :bg-element "#ebebeb" + :bg "#ffffff" :bg-panel "#f5f5f5" :bg-element "#ebebeb" + :bg-input "#d4d4d4" :text-muted "#808080" :dot-connected "#2e8b57" :dot-disconnected "#cc3300" :error "#cc3300" @@ -163,7 +172,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#fab387" :input-fg "#cdd6f4" :hint "#6c7086" :status-bg "#181825" :status-fg "#a6adc8" - :bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e" + :bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e" + :bg-input "#2e2e2e" :text-muted "#6c7086" :dot-connected "#a6e3a1" :dot-disconnected "#f38ba8" :error "#f38ba8" @@ -176,7 +186,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#ff9e64" :input-fg "#a9b1d6" :hint "#565f89" :status-bg "#16161e" :status-fg "#9aa5ce" - :bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26" + :bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26" + :bg-input "#2e2e2e" :text-muted "#565f89" :dot-connected "#9ece6a" :dot-disconnected "#db4b4b" :error "#db4b4b" @@ -189,7 +200,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#ff9580" :input-fg "#f8f8f2" :hint "#6272a4" :status-bg "#191a24" :status-fg "#e0e0e0" - :bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b" + :bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b" + :bg-input "#2e2e2e" :text-muted "#6272a4" :dot-connected "#50fa7b" :dot-disconnected "#ff5555" :error "#ff5555" @@ -202,7 +214,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#87afff" :input-fg "#ffffff" :hint "#606060" :status-bg "#141414" :status-fg "#afafaf" - :bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a" + :bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#d7ffd7" :dot-disconnected "#ff87af" :error "#ff87af" @@ -215,7 +228,8 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :input-prompt "#ffffff" :input-fg "#d0d0d0" :hint "#606060" :status-bg "#141414" :status-fg "#b0b0b0" - :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a" + :bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a" + :bg-input "#2e2e2e" :text-muted "#808080" :dot-connected "#a0a0a0" :dot-disconnected "#808080" :error "#808080" @@ -223,7 +237,7 @@ semantic text colors for context. Keys: :bg (deepest), :bg-panel, :bg-element, :separator "#303030" :accent "#ffffff" :dim "#505050")) "13 theme presets (amber, gold, terracotta, sepia, nord-warm, monokai-warm, gruvbox-warm, light-amber, catppuccin, tokyonight, dracula, -gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-element/:text-muted slots.") +gemini, mono). Keys: :bg/:bg-panel/:bg-element/:bg-input/:text-muted.") (defvar *tui-theme-current-name* :amber "Name of the currently active theme preset.") @@ -240,11 +254,19 @@ gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-eleme t)) (defun theme-load () - "Load persisted theme from disk. Called at startup." + "Load persisted theme from disk. Called at startup. +Adds any missing keys with defaults to handle saved themes from older versions." (let ((path (merge-pathnames ".cache/passepartout/theme.lisp" (user-homedir-pathname)))) (when (uiop:file-exists-p path) - (ignore-errors (load path))))) + (ignore-errors (load path))) + ;; Fill in any missing keys from the default preset + (let ((defaults (getf *tui-theme-presets* *tui-theme-current-name*))) + (when defaults + (dolist (key '(:bg-input :bg-element :text-muted)) + (unless (getf *tui-theme* key) + (let ((val (getf defaults key))) + (when val (setf (getf *tui-theme* key) val))))))))) (defun theme-switch (name) "Switch to a named theme preset. Returns the preset name or nil if not found." @@ -298,18 +320,6 @@ gemini, mono). Now with dark-neutral backgrounds and new :bg/:bg-panel/:bg-eleme :dirty (list nil nil nil)))) #+END_SRC -** Sidebar panel definitions -#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp -(defvar *sidebar-panels* - '((:id :gate-trace :title "Gate Trace" :width 28) - (:id :focus :title "Focus" :width 28) - (:id :rules :title "Rules" :width 28) - (:id :context :title "Context" :width 28) - (:id :cost :title "Cost" :width 28) - (:id :files :title "Files" :width 28)) - "Sidebar panel definitions for cl-tty slot registrations.") -#+END_SRC - ** Helpers #+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp (defun now () diff --git a/org/channel-tui-view.org b/org/channel-tui-view.org index 344decd..5f04317 100644 --- a/org/channel-tui-view.org +++ b/org/channel-tui-view.org @@ -8,21 +8,19 @@ ** Contract -1. (view-status win): renders the status bar with connection info, - msg count, scroll offset, rule counter, focus map (v0.4.0), and - timestamp. Two lines: line 1 (status + rules), line 2 (focus + time). -2. (view-chat win h): renders the scrolled chat message list. Takes - window and available height. 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 sw cw ch iw): dispatches redraws based on ~(st :dirty)~ - flags (status, chat, input). Minimizes terminal writes. -5. (char-width ch): returns the terminal column width of character CH. +1. (view-status fb w h): no-op. Status bar is a clean black line. +2. (view-chat fb w h): renders scrolled chat messages. User messages + get amber left border (│), agent messages no border, streaming + agent gets grey left border. Gate traces/tool calls use ╎ prefix. +3. (view-input fb w h): renders light grey input box (h-7 to h-4), + prompt at h-6, right-aligned lowercase hint at h-2. +4. (redraw fb w h): wraps view-status/chat/input in begin-sync/end-sync, + dispatches per dirty flags, fills global :bg first. +5. (char-width ch): returns terminal column width of character CH. ASCII < 128 = 1. CJK, fullwidth, emoji = 2. Combining marks = 0. Tab = 8. Used by word-wrap for accurate line counting (v0.7.0). -6. (view-status win): v0.7.0 — timestamp right-aligned at (- chat-w 12) - on line 2, focus info at :x 1. No overlap. +6. (sidebar-visible-p w): returns T if sidebar should show given width W + and current :sidebar-mode (:auto >120, :visible always, :hidden never). ** Status Bar @@ -92,11 +90,11 @@ Returns a list of strings, one per line." (defun view-chat (fb w h) (let* ((w (or (and (numberp w) (> w 0) w) 80)) (h (or (and (numberp h) (> h 0) h) 24)) - (hpad 2) - (sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0)) - (chat-w (- w sidebar-w)) - (msgs (st :messages)) (total (length msgs)) - (max-lines (- h 4)) (is-search (st :search-mode)) + (hpad 2) + (sidebar-w (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0)) + (chat-w (- w sidebar-w)) + (msgs (st :messages)) (total (length msgs)) + (max-lines (- h 7)) (is-search (st :search-mode)) (bordered-w (- chat-w (* 2 hpad) 2)) (unbordered-w (- chat-w (* 2 hpad))) (y 0)) @@ -113,7 +111,7 @@ Returns a list of strings, one per line." (content (getf msg :content)) (cs (if is-search (search-highlight content (st :search-query)) content)) (pairs nil) - (dim-bg (theme-color :dim)) + (dim-fg (theme-color :dim)) (user-bdr (theme-color :user-border)) (user-fg (theme-color :user-fg)) (agent-fg (theme-color :agent-fg)) @@ -124,7 +122,7 @@ Returns a list of strings, one per line." (push (list "│" user-bdr l user-fg) pairs))) (:agent (let* ((streaming (getf msg :streaming)) - (bdr-color (if streaming dim-bg nil)) + (bdr-color (if streaming dim-fg nil)) (bdr-str (if streaming "│" "")) (wrap-w (if streaming bordered-w unbordered-w)) (nodes (cl-tty.markdown:parse-blocks cs)) @@ -138,18 +136,18 @@ Returns a list of strings, one per line." (let ((gt (getf msg :gate-trace))) (when (and gt (eq role :agent)) (if (member i (st :collapsed-gates)) - (push (list "╎" dim-bg (format nil "Gate trace: ~a gates" (length gt)) dim-bg) pairs) + (push (list "╎" dim-fg (format nil "Gate trace: ~a gates" (length gt)) dim-fg) pairs) (dolist (entry (passepartout::gate-trace-lines gt)) (let ((ec (theme-color (getf (cdr entry) :fgcolor)))) (dolist (l (cl-tty.box:word-wrap (car entry) bordered-w)) - (push (list "╎" dim-bg l ec) pairs))))))) + (push (list "╎" dim-fg l ec) pairs))))))) ;; Tool calls (let ((tc (getf msg :tool-calls))) (when tc (if (member i (st :collapsed-tools)) (let* ((n (or (getf (first tc) :name) "tool")) (d (or (getf (first tc) :duration) 0.0))) - (push (list "╎" dim-bg (format nil "~a … ~,1fs" n d) (theme-color :tool-done)) pairs)) + (push (list "╎" dim-fg (format nil "~a … ~,1fs" n d) (theme-color :tool-done)) pairs)) (dolist (call tc) (let* ((name (or (getf call :name) "tool")) (dur (or (getf call :duration) 0.0)) @@ -169,16 +167,16 @@ Returns a list of strings, one per line." (let ((msg-count 0) (lines-remaining max-lines)) (loop for i from (1- total) downto 0 while (> lines-remaining 0) - do (let ((h (aref msg-heights i))) - (if (<= h lines-remaining) - (progn (decf lines-remaining h) (incf msg-count)) + do (let ((mh (aref msg-heights i))) + (if (<= mh lines-remaining) + (progn (decf lines-remaining mh) (incf msg-count)) (setf lines-remaining 0)))) (let* ((scroll-skip (st :scroll-offset)) (start (max 0 (- total msg-count scroll-skip)))) - (loop for i from start below total while (< y (- h 4)) - do (let ((pairs (aref msg-lines i))) - (dolist (pair pairs) - (when (>= y (- h 4)) (return)) + (loop for i from start below total while (< y (- h 7)) + do (let ((pairs (aref msg-lines i))) + (dolist (pair pairs) + (when (>= y (- h 7)) (return)) (destructuring-bind (bstr bcolor tstr tcolor) pair (let ((has-border (and bstr (> (length bstr) 0)))) (when has-border @@ -201,19 +199,16 @@ Returns a list of strings, one per line." (pos (or (st :cursor-pos) 0)) (display-start (max 0 (- pos (1- prompt-w)))) (visible (subseq text display-start (min (length text) (+ display-start prompt-w)))) - (bg-p (theme-color :bg-panel)) - (sep-c (theme-color :separator)) - (input-fg (theme-color :input-fg)) - (hint-fg (theme-color :hint))) - ;; Fill the 2-line input area (separator + prompt) with panel bg, indented by hpad - (cl-tty.backend:draw-rect fb hpad (- h 4) inner-w 2 :bg bg-p) - ;; Separator line within the panel - (cl-tty.backend:draw-text fb hpad (- h 4) (make-string inner-w :initial-element #\─) sep-c nil) - ;; Input line - (cl-tty.backend:draw-text fb hpad (- h 3) (format nil"> ~a" visible) input-fg nil) - ;; Hint line — right-aligned on black background at the very bottom - (let ((hint "Ctrl+P | /help")) - (cl-tty.backend:draw-text fb (- chat-w (length hint) 2) (- h 1) hint hint-fg (theme-color :bg))))) + (bg-i (theme-color :bg-input)) + (input-fg (theme-color :input-fg)) + (hint-fg (theme-color :hint))) + ;; Light grey input panel: h-7 to h-4 (4 rows), indented by hpad + (cl-tty.backend:draw-rect fb hpad (- h 7) inner-w 4 :bg bg-i) + ;; Prompt at h-6, second row at h-5 (placeholder for expansion) + (cl-tty.backend:draw-text fb hpad (- h 6) (format nil"> ~a" visible) input-fg nil) + ;; Hint — lowercase, right-aligned at h-2 + (let ((hint "ctrl+p | /help")) + (cl-tty.backend:draw-text fb (- chat-w (length hint) 2) (- h 2) hint hint-fg (theme-color :bg))))) #+end_src ** Sidebar @@ -275,14 +270,15 @@ Returns a list of strings, one per line." (defun redraw (fb w h) (setq w (or (and (numberp w) (> w 0) w) 80) h (or (and (numberp h) (> h 0) h) 24)) - (destructuring-bind (sd cd id) (st :dirty) - ;; Fill global background + (when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty))) + (cl-tty.backend:begin-sync fb) (cl-tty.backend:draw-rect fb 0 0 w h :bg (theme-color :bg)) - (when sd (view-status fb w h)) - (when cd (view-chat fb w h)) - (when id (view-input fb w h)) + (view-status fb w h) + (view-chat fb w h) + (view-input fb w h) (when (sidebar-visible-p w) (view-sidebar fb w h)) + (cl-tty.backend:end-sync fb) (setf (st :dirty) (list nil nil nil)))) #+END_SRC @@ -380,7 +376,7 @@ dead code. (cl-tty.backend:draw-text fb x y text (cond (url (passepartout.channel-tui:theme-color :accent)) (t (passepartout.channel-tui:theme-color (or (getf attrs :role) :agent-fg)))) - nil + (passepartout.channel-tui:theme-color :bg) :bold bold) (incf x (length text)))) y) @@ -628,9 +624,10 @@ and current sidebar mode." (is (getf passepartout.channel-tui::*tui-theme* :status-bg))) (test test-new-theme-keys - "v0.10.0: theme has new :bg, :bg-panel, :bg-element, :text-muted keys." + "v0.10.0: theme has new :bg, :bg-panel, :bg-element, :bg-input, :text-muted keys." (is (getf passepartout.channel-tui::*tui-theme* :bg)) (is (getf passepartout.channel-tui::*tui-theme* :bg-panel)) (is (getf passepartout.channel-tui::*tui-theme* :bg-element)) + (is (getf passepartout.channel-tui::*tui-theme* :bg-input)) (is (getf passepartout.channel-tui::*tui-theme* :text-muted))) #+END_SRC