fix: TUI flicker, bottom-anchored minibuffer, 13 color presets
Issue 1 — flickering during typing/updating: - Wrap every frame render in DECICM sync (begin-sync/end-sync) so the terminal defers rendering until the entire frame is written - Replace backend-clear (ESC[2J full clear) with draw-rect background fill — eliminates visible blank frame between redraws - These two changes together eliminate all visible tearing/flicker Issue 2 — bottom-anchored minibuffer (Emacs-style): - Replace centered overlay dialog with bottom-anchored minibuffer that expands upward from the input line - Unified command menu: Ctrl+P and / both open the same menu with all 35+ commands (slash + daemon), dispatch by value type - Filter prompt at h-3 (same position as normal input), options listed above, grows up to 15 lines - No full-screen dim backdrop — just clear the minibuffer area Issue 3 — color schemes: - Add 5 new presets: catppuccin, tokyonight, dracula, gemini, mono - Total: 13 presets (up from 8) - Update /theme completion list and help text Also fixed: pre-existing unbalanced paren in tui-main (missing close)
This commit is contained in:
@@ -380,13 +380,13 @@ Event handlers + daemon I/O + main loop.
|
||||
(getf *tui-theme* :agent-fg)
|
||||
(getf *tui-theme* :system)
|
||||
(getf *tui-theme* :input-fg)))
|
||||
(add-msg :system "Presets: /theme amber | gold | terracotta | sepia | nord-warm | monokai-warm | gruvbox-warm | light-amber"))
|
||||
(add-msg :system "Presets: /theme amber | gold | terracotta | sepia | nord-warm | monokai-warm | gruvbox-warm | light-amber | catppuccin | tokyonight | dracula | gemini | mono"))
|
||||
((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: amber gold terracotta sepia nord-warm monokai-warm gruvbox-warm light-amber" name)))))
|
||||
(add-msg :system (format nil "Unknown theme '~a'. Try: amber gold terracotta sepia nord-warm monokai-warm gruvbox-warm light-amber catppuccin tokyonight dracula gemini mono" name)))))
|
||||
;; /eval command
|
||||
((and (>= (length text) 6)
|
||||
(string-equal (subseq text 0 6) "/eval "))
|
||||
@@ -474,7 +474,7 @@ Event handlers + daemon I/O + main loop.
|
||||
;; /theme subcommand
|
||||
((and (>= (length text) 7) (string-equal (subseq text 0 7) "/theme "))
|
||||
(let* ((partial (string-trim '(#\Space) (subseq text 7)))
|
||||
(names '("amber" "gold" "terracotta" "sepia" "nord-warm" "monokai-warm" "gruvbox-warm" "light-amber"))
|
||||
(names '("amber" "gold" "terracotta" "sepia" "nord-warm" "monokai-warm" "gruvbox-warm" "light-amber" "catppuccin" "tokyonight" "dracula" "gemini" "mono"))
|
||||
(match (if (string= partial "") (first names)
|
||||
(find partial names :test #'string-equal))))
|
||||
(when match
|
||||
@@ -562,40 +562,30 @@ Event handlers + daemon I/O + main loop.
|
||||
(setf (st :dirty) (list nil nil t))
|
||||
(when (and (char= chr #\/) (null (st :dialog-stack))
|
||||
(= (length (st :input-buffer)) 1))
|
||||
(minibuffer-show-commands)))))))
|
||||
(unified-menu-show "/")))))))
|
||||
|
||||
;; v0.8.0 — minibuffer dialog for slash commands
|
||||
(defun minibuffer-show-commands ()
|
||||
;; v0.9.0 — unified command minibuffer (replaces separate palette and slash menus)
|
||||
(defun unified-menu-show (&optional initial-filter)
|
||||
"Open the command minibuffer with ALL commands. If INITIAL-FILTER is
|
||||
supplied (e.g. \"/\"), pre-fill the select filter with it."
|
||||
(let* ((on-select (lambda (opt)
|
||||
(let ((cmd (getf opt :value)))
|
||||
(pop (st :dialog-stack))
|
||||
(setf (st :minibuffer-active) nil)
|
||||
(setf (st :input-buffer) (reverse (coerce cmd 'list)))
|
||||
(setf (st :cursor-pos) 0)
|
||||
(setf (st :dirty) (list nil nil t)))))
|
||||
(sel (cl-tty.select:make-select :options *slash-commands* :on-select on-select))
|
||||
(dlg (make-instance 'cl-tty.dialog:dialog
|
||||
:title "Commands"
|
||||
:content sel)))
|
||||
(push dlg (st :dialog-stack))
|
||||
(setf (st :minibuffer-active) t)))
|
||||
|
||||
;; v0.8.0 — command palette for daemon commands (Ctrl+P)
|
||||
(defun command-palette-show-commands ()
|
||||
(let* ((on-select (lambda (cmd)
|
||||
(pop (st :dialog-stack))
|
||||
(setf (st :command-palette-active) nil)
|
||||
(let ((action (getf cmd :value)))
|
||||
(send-daemon (list :type :event :payload action))
|
||||
(add-msg :system (format nil "Sent: ~a" action)))
|
||||
(setf (st :dirty) (list t t nil))))
|
||||
(sel (cl-tty.select:make-select :options *daemon-commands*
|
||||
:on-select on-select))
|
||||
(dlg (make-instance 'cl-tty.dialog:dialog
|
||||
:title "Command Palette"
|
||||
:content sel)))
|
||||
(push dlg (st :dialog-stack))
|
||||
(setf (st :command-palette-active) t)))
|
||||
(let ((val (getf opt :value)))
|
||||
(cond ((stringp val)
|
||||
;; Slash command — fill input buffer
|
||||
(setf (st :input-buffer) (reverse (coerce val 'list)))
|
||||
(setf (st :cursor-pos) 0)
|
||||
(setf (st :dirty) (list nil nil t)))
|
||||
((listp val)
|
||||
;; Daemon action — send immediately
|
||||
(send-daemon (list :type :event :payload val))
|
||||
(add-msg :system (format nil "Sent: ~a" (getf opt :title)))
|
||||
(setf (st :dirty) (list t t nil)))))))
|
||||
(sel (cl-tty.select:make-select :options (all-commands) :on-select on-select)))
|
||||
(when initial-filter
|
||||
(setf (cl-tty.select:select-filter sel) initial-filter))
|
||||
(let ((dlg (make-instance 'cl-tty.dialog:dialog :title "Commands" :content sel)))
|
||||
(push dlg (st :dialog-stack)))))
|
||||
|
||||
;; v0.7.2 — resolve-hitl-panel: marks panel as resolved after approve/deny
|
||||
(defun resolve-hitl-panel (decision)
|
||||
@@ -805,7 +795,7 @@ Event handlers + daemon I/O + main loop.
|
||||
(:ctrl+q (lambda (e) (declare (ignore e))
|
||||
(setf (st :running) nil)))
|
||||
(:ctrl+p (lambda (e) (declare (ignore e))
|
||||
(command-palette-show-commands)))
|
||||
(unified-menu-show)))
|
||||
(:ctrl+b (lambda (e) (declare (ignore e))
|
||||
(setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||
(setf (st :dirty) (list t t nil))))
|
||||
@@ -955,7 +945,7 @@ Event handlers + daemon I/O + main loop.
|
||||
(ch (getf payload :ch)))
|
||||
(case ch
|
||||
(:CTRL-Q (setf (st :running) nil))
|
||||
(:CTRL-P (command-palette-show-commands))
|
||||
(:CTRL-P (unified-menu-show))
|
||||
(:CTRL-B (setf (st :sidebar-visible) (not (st :sidebar-visible)))
|
||||
(setf (st :dirty) (list t t t)))
|
||||
(:CTRL-L (setf (st :dirty) (list t t t)))
|
||||
@@ -963,11 +953,9 @@ Event handlers + daemon I/O + main loop.
|
||||
(let* ((dlg (car (st :dialog-stack)))
|
||||
(sel (cl-tty.dialog:dialog-content dlg)))
|
||||
(cond
|
||||
((eql ch :escape)
|
||||
(pop (st :dialog-stack))
|
||||
(setf (st :minibuffer-active) nil)
|
||||
(setf (st :command-palette-active) nil)
|
||||
(setf (st :dirty) (list t t nil)))
|
||||
((eql ch :escape)
|
||||
(pop (st :dialog-stack))
|
||||
(setf (st :dirty) (list t t nil)))
|
||||
((member ch '(:up :down))
|
||||
(if (eql ch :up)
|
||||
(cl-tty.select:select-prev sel)
|
||||
@@ -1026,55 +1014,69 @@ Event handlers + daemon I/O + main loop.
|
||||
;; 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 (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
||||
(or (st :sidebar-width) 30) 0))
|
||||
(chat-w (- w sidebar-w)))
|
||||
(cl-tty.backend:backend-clear be)
|
||||
(view-status be w h)
|
||||
(view-chat be w h)
|
||||
;; Draw separator line above input
|
||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─)
|
||||
(theme-color :separator) nil)
|
||||
(view-input be w h)
|
||||
(when (and (st :sidebar-visible) (>= w 60))
|
||||
(view-sidebar be w h))
|
||||
(setf (st :dirty) (list nil nil nil))))
|
||||
(let ((ds (st :dialog-stack)))
|
||||
(when ds
|
||||
(let* ((dlg (car ds))
|
||||
(sel (cl-tty.dialog:dialog-content dlg))
|
||||
(filtered (cl-tty.select:select-filtered-options sel))
|
||||
(sel-idx (cl-tty.select:select-selected-index sel))
|
||||
(cnt (length filtered))
|
||||
(dw 60) (dh (min 20 (+ 4 cnt)))
|
||||
(mx (floor (- w dw) 2))
|
||||
(my 3))
|
||||
(dotimes (row h)
|
||||
(cl-tty.backend:draw-rect be 0 row w 1 :bg (theme-color :status-bg)))
|
||||
(cl-tty.backend:draw-border be mx my dw dh :style :single
|
||||
:title (cl-tty.dialog:dialog-title dlg)
|
||||
:fg (theme-color :user-border))
|
||||
(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" sel-p title))))
|
||||
(when (>= y-off (1- dh)) (return))
|
||||
(cl-tty.backend:draw-text be (1+ mx) (+ my y-off) text
|
||||
(cond (cat (theme-color :dim))
|
||||
(sel-p (theme-color :accent))
|
||||
(t (theme-color :agent-fg)))
|
||||
nil :bold sel-p)
|
||||
(incf y-off)))))))
|
||||
(sleep 0.1))
|
||||
(when (or (first (st :dirty)) (second (st :dirty)) (third (st :dirty)))
|
||||
(let* ((sidebar-w (if (and (st :sidebar-visible) (>= w 60))
|
||||
(or (st :sidebar-width) 30) 0))
|
||||
(chat-w (- w sidebar-w)))
|
||||
(cl-tty.backend:begin-sync be)
|
||||
(cl-tty.backend:draw-rect be 0 0 w h :bg (theme-color :status-bg))
|
||||
(view-status be w h)
|
||||
(view-chat be w h)
|
||||
;; Draw separator line above input
|
||||
(cl-tty.backend:draw-text be 0 (- h 4) (make-string chat-w :initial-element #\─)
|
||||
(theme-color :separator) nil)
|
||||
(view-input be w h)
|
||||
(when (and (st :sidebar-visible) (>= w 60))
|
||||
(view-sidebar be w h))
|
||||
(cl-tty.backend:end-sync be)
|
||||
(setf (st :dirty) (list nil nil nil))))
|
||||
(let ((ds (st :dialog-stack)))
|
||||
(when ds
|
||||
(cl-tty.backend:begin-sync be)
|
||||
(let* ((dlg (car ds))
|
||||
(sel (cl-tty.dialog:dialog-content dlg))
|
||||
(filtered (cl-tty.select:select-filtered-options sel))
|
||||
(sel-idx (cl-tty.select:select-selected-index sel))
|
||||
(cnt (length filtered))
|
||||
(filter (cl-tty.select:select-filter sel))
|
||||
;; Bottom-anchored: filter at h-3, options above
|
||||
(mh (min 15 (+ 1 cnt))) ;; +1 for title
|
||||
(top (max 0 (- h 3 mh))))
|
||||
;; Clear the full minibuffer area (top to h-2)
|
||||
(dotimes (r (min (- h 2 top) h))
|
||||
(cl-tty.backend:draw-rect be 0 (+ top r) w 1
|
||||
:bg (theme-color :status-bg)))
|
||||
;; Title line
|
||||
(cl-tty.backend:draw-text be 0 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" 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 :dim))
|
||||
(sel-p (theme-color :accent))
|
||||
(t (theme-color :agent-fg)))
|
||||
nil :bold sel-p)
|
||||
(incf y-off))))
|
||||
;; Filter prompt at h-3
|
||||
(cl-tty.backend:draw-text be 0 (- h 3)
|
||||
(format nil "> ~a" filter)
|
||||
(theme-color :input-prompt) nil))
|
||||
(cl-tty.backend:end-sync be))
|
||||
(sleep 0.1))
|
||||
(uiop:terminate-process *cat-proc*))
|
||||
(add-msg :system (format nil "* cat ~a ended *" (uiop:process-info-pid *cat-proc*))))
|
||||
(progn (disconnect-daemon)))
|
||||
(progn (disconnect-daemon))))
|
||||
#+END_SRC
|
||||
|
||||
* Test Suite
|
||||
|
||||
Reference in New Issue
Block a user