From 7e9da0f867423d36ac5c67cd87ed96a58548a455 Mon Sep 17 00:00:00 2001 From: Amr Gharbeia Date: Sat, 16 May 2026 11:01:05 -0400 Subject: [PATCH] v0.10.5: multi-line expanding input box with software blinking cursor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- org/channel-tui-main.org | 7 ++-- org/channel-tui-state.org | 2 +- org/channel-tui-view.org | 86 +++++++++++++++++++++++---------------- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/org/channel-tui-main.org b/org/channel-tui-main.org index 2c0868e..d69a3a6 100644 --- a/org/channel-tui-main.org +++ b/org/channel-tui-main.org @@ -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 diff --git a/org/channel-tui-state.org b/org/channel-tui-state.org index 254b56a..a5a3585 100644 --- a/org/channel-tui-state.org +++ b/org/channel-tui-state.org @@ -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)) diff --git a/org/channel-tui-view.org b/org/channel-tui-view.org index 4e93b3c..a4bcb07 100644 --- a/org/channel-tui-view.org +++ b/org/channel-tui-view.org @@ -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