v0.10.5: multi-line expanding input box with software blinking cursor

view-input word-wraps input at prompt-w, expanding the grey panel
upward as needed. Uses software cursor (█) in :input-fg blinking
at 2Hz via get-internal-real-time.
view-chat max-lines adapts to variable panel height via input-panel-top.
Removed terminal cursor (position-cursor, cursor-show, cursor-style).
Dialog minibuffer top now computed from input-panel-top.
This commit is contained in:
2026-05-16 11:01:05 -04:00
parent bb98b486e4
commit 7e9da0f867
3 changed files with 56 additions and 39 deletions

View File

@@ -1029,7 +1029,8 @@ 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 7 mh)))
(panel-top (passepartout.channel-tui:input-panel-top chat-w h))
(top (max 0 (- panel-top mh)))
(bg-p (theme-color :bg-panel))
(sep-c (theme-color :separator)))
;; Fill minibuffer area with panel bg
@@ -1066,9 +1067,7 @@ Returns T on success, nil on failure. Does NOT wait or retry."
(format nil "> ~a" (or filter ""))
(theme-color :input-prompt) bg-p))
(cl-tty.backend:end-sync be))
(sleep 0.1)
;; Show cursor at input position every frame
(passepartout.channel-tui:position-cursor be w h)))
(sleep 0.1)))
(progn (disconnect-daemon)))))
#+END_SRC

View File

@@ -23,7 +23,7 @@ All state mutation flows through event handlers in the controller.
(:export :tui-main :st :add-msg :now :input-string
:queue-event :drain-queue :init-state
:view-status :view-chat :view-input :redraw
:position-cursor
:input-panel-top
:on-key :on-daemon-msg :send-daemon
:connect-daemon :disconnect-daemon
:*tui-theme* :theme-color))

View File

@@ -12,8 +12,9 @@
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.
3. (view-input fb w h): renders expanding light grey input box,
multi-line word-wrapped prompt, software blinking cursor (█),
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.
@@ -69,6 +70,22 @@ Returns a list of strings, one per line."
;; No clock, no dot, no text. Everything clean.
)
(defun cursor-visible-p ()
"Returns T if the blinking cursor should be visible this frame (2Hz)."
(evenp (floor (get-internal-real-time)
(floor internal-time-units-per-second 2))))
(defun input-panel-top (chat-w h)
"Compute the top row of the input panel based on current input buffer."
(let* ((hpad 2)
(inner-w (- chat-w (* 2 hpad)))
(prompt-w (- inner-w 2))
(text (input-string))
(lines (word-wrap text prompt-w))
(n-lines (max 1 (length lines)))
(panel-rows (max 4 (+ n-lines 2))))
(- h 4 panel-rows -1)))
;; v0.7.2: search-highlight — wrap matching text in **bold** for markdown
(defun search-highlight (content query)
@@ -94,7 +111,8 @@ Returns a list of strings, one per line."
(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))
(panel-top (input-panel-top chat-w h))
(max-lines (max 0 panel-top)) (is-search (st :search-mode))
(bordered-w (- chat-w (* 2 hpad) 2))
(unbordered-w (- chat-w (* 2 hpad)))
(y 0))
@@ -177,10 +195,10 @@ Returns a list of strings, one per line."
(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 7))
(loop for i from start below total while (< y panel-top)
do (let ((pairs (aref msg-lines i)))
(dolist (pair pairs)
(when (>= y (- h 7)) (return))
(dolist (pair pairs)
(when (>= y panel-top) (return))
(destructuring-bind (bstr bcolor tstr tcolor &optional rect-bg) pair
(when rect-bg
(cl-tty.backend:draw-rect fb 0 y 1 1 :bg rect-bg))
@@ -206,18 +224,34 @@ Returns a list of strings, one per line."
(prompt-w (- inner-w 2))
(text (input-string))
(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-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)
;; Speaker line for all 4 input rows (at hpad, matching history)
(dolist (r (list (- h 7) (- h 6) (- h 5) (- h 4)))
(cl-tty.backend:draw-text fb hpad r "│" (theme-color :input-prompt) nil))
;; Prompt at h-6, second row at h-5 — text at hpad+2 matching history
(cl-tty.backend:draw-text fb (+ hpad 2) (- h 6) visible input-fg nil)
(lines (word-wrap text prompt-w))
(n-lines (max 1 (length lines)))
(panel-rows (max 4 (+ n-lines 2)))
(panel-top (input-panel-top chat-w h))
(bg-i (theme-color :bg-input))
(input-fg (theme-color :input-fg))
(hint-fg (theme-color :hint)))
;; Fill input panel: panel-top to h-4, indented by hpad
(cl-tty.backend:draw-rect fb hpad panel-top inner-w panel-rows :bg bg-i)
;; Speaker lines for all input rows
(dotimes (r panel-rows)
(cl-tty.backend:draw-text fb hpad (+ panel-top r) "│" (theme-color :input-prompt) nil))
;; Draw each wrapped input line
(let ((accum 0) (cursor-line 0) (cursor-col 0))
(dotimes (i n-lines)
(let* ((line (nth i lines))
(row (+ panel-top 1 i))
(len (length line)))
(when (>= row (- h 4)) (return))
(cl-tty.backend:draw-text fb (+ hpad 2) row line input-fg nil)
(when (and (>= pos accum) (<= pos (+ accum len)))
(setf cursor-line i
cursor-col (- pos accum)))
(incf accum (1+ len))))
;; Draw software blinking cursor at insertion point
(when (cursor-visible-p)
(let ((cursor-row (+ panel-top 1 cursor-line)))
(cl-tty.backend:draw-text fb (+ hpad 2 cursor-col) cursor-row "█" 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)))))
@@ -291,23 +325,7 @@ Returns a list of strings, one per line."
(when (sidebar-visible-p w)
(view-sidebar fb w h))
(cl-tty.backend:end-sync fb)
(position-cursor fb w h)
(setf (st :dirty) (list nil nil nil))))
(defun position-cursor (fb w h)
"Move blinking block cursor to the input insertion point."
(let* ((sw (if (sidebar-visible-p w) (or (st :sidebar-width) 42) 0))
(cw (- w sw))
(hpad 2)
(text (input-string))
(pos (or (st :cursor-pos) 0))
(prompt-w (- cw (* 2 hpad) 2))
(display-start (max 0 (- pos (1- prompt-w))))
(cx (+ hpad 2 (- pos display-start)))
(cy (- h 6)))
(cl-tty.backend:cursor-move fb cx cy)
(cl-tty.backend:cursor-style fb :block :blink t)
(cl-tty.backend:cursor-show fb)))
#+END_SRC
* Implementation — v0.7.0 additions