v0.3.3: word wrap in view-chat — break at word boundaries
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 3s
Some checks failed
Deploy (Gitea) / deploy (push) Failing after 3s
Adds word-wrap(text width) — splits strings into lines at word boundaries respecting terminal width. Rewrites view-chat to: - Wrap each message with word-wrap before rendering - Render each wrapped line as a separate add-string call - Account for wrapped line count in visible-message calculation RED proof: tmux capture shows messages split mid-word at terminal edge. GREEN proof: tmux capture shows clean word-boundary wrapping: The quick brown fox jumps over the lazy dog while the cat naps peacefully in the sunny garden
This commit is contained in:
@@ -14,33 +14,74 @@
|
|||||||
(add-string win (format nil " ~a" (now)) :y 2 :x 1 :fgcolor (theme-color :timestamp))
|
(add-string win (format nil " ~a" (now)) :y 2 :x 1 :fgcolor (theme-color :timestamp))
|
||||||
(refresh win))
|
(refresh win))
|
||||||
|
|
||||||
|
(defun word-wrap (text width)
|
||||||
|
"Break text into lines at word boundaries, each <= width chars.
|
||||||
|
Returns list of trimmed strings. Single words wider than width are split."
|
||||||
|
(let ((lines '())
|
||||||
|
(pos 0)
|
||||||
|
(len (length text)))
|
||||||
|
(loop while (< pos len)
|
||||||
|
do (let ((end (min len (+ pos width))))
|
||||||
|
(cond
|
||||||
|
((>= end len)
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos len)) lines)
|
||||||
|
(setf pos len))
|
||||||
|
((char= (char text (1- end)) #\Space)
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos end)) lines)
|
||||||
|
(setf pos end))
|
||||||
|
(t
|
||||||
|
(let ((last-space (position #\Space text :from-end t :end (1+ end) :start pos)))
|
||||||
|
(if (and last-space (> last-space pos))
|
||||||
|
(progn
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos last-space)) lines)
|
||||||
|
(setf pos (1+ last-space)))
|
||||||
|
(progn
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos end)) lines)
|
||||||
|
(setf pos end))))))))
|
||||||
|
(nreverse lines)))
|
||||||
|
|
||||||
(defun view-chat (win h)
|
(defun view-chat (win h)
|
||||||
(clear win)
|
(clear win)
|
||||||
(box win 0 0)
|
(box win 0 0)
|
||||||
(let* ((w (or (width win) 78))
|
(let* ((w (or (width win) 78))
|
||||||
(msgs (reverse (st :messages)))
|
(msgs (reverse (st :messages)))
|
||||||
(max-lines (- h 2))
|
(max-lines (- h 2))
|
||||||
(total (length msgs))
|
|
||||||
(start (max 0 (- total max-lines (st :scroll-offset))))
|
|
||||||
(y 1))
|
(y 1))
|
||||||
(loop for i from start below total
|
;; Count visible messages from end, accounting for word wrap
|
||||||
while (< y (1- h))
|
(let* ((msg-count 0)
|
||||||
do (let ((msg (nth i msgs)))
|
(lines-remaining max-lines))
|
||||||
(let* ((role (getf msg :role))
|
;; Walk from most recent backwards, counting wrapped lines
|
||||||
|
(let ((visible-msgs (reverse msgs)))
|
||||||
|
(loop for msg in visible-msgs
|
||||||
|
while (> lines-remaining 0)
|
||||||
|
do (let* ((role (getf msg :role))
|
||||||
(content (getf msg :content))
|
(content (getf msg :content))
|
||||||
(time (or (getf msg :time) ""))
|
(time (or (getf msg :time) ""))
|
||||||
(label (case role
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
(:user (format nil "⬆ [~a] ~a" time content))
|
(line-text (format nil "~a [~a] ~a" prefix time content))
|
||||||
(:agent (format nil "⬇ [~a] ~a" time content))
|
(wrapped (word-wrap line-text (- w 2)))
|
||||||
(:system (format nil " [~a] ~a" time content))
|
(nlines (length wrapped)))
|
||||||
(t (format nil " [~a] ~a" time content))))
|
(if (<= nlines lines-remaining)
|
||||||
(color (theme-color (case role
|
(progn (decf lines-remaining nlines) (incf msg-count))
|
||||||
(:user :user)
|
(setf lines-remaining 0))))
|
||||||
(:agent :agent)
|
;; Render from the correct starting message
|
||||||
(:system :system)
|
(let* ((total (length msgs))
|
||||||
(t :agent)))))
|
(scroll-skip (st :scroll-offset))
|
||||||
(add-string win label :y y :x 1 :n (1- w) :fgcolor color)
|
(start (max 0 (- total msg-count scroll-skip))))
|
||||||
(incf y)))))
|
(loop for i from start below total
|
||||||
|
while (< y (1- h))
|
||||||
|
do (let* ((msg (nth i msgs))
|
||||||
|
(role (getf msg :role))
|
||||||
|
(content (getf msg :content))
|
||||||
|
(time (or (getf msg :time) ""))
|
||||||
|
(color (theme-color (case role (:user :user) (:agent :agent) (:system :system) (t :agent))))
|
||||||
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
|
(line-text (format nil "~a [~a] ~a" prefix time content))
|
||||||
|
(wrapped (word-wrap line-text (- w 2))))
|
||||||
|
(dolist (line wrapped)
|
||||||
|
(when (< y (1- h))
|
||||||
|
(add-string win line :y y :x 1 :n (1- w) :fgcolor color)
|
||||||
|
(incf y)))))))))
|
||||||
(refresh win))
|
(refresh win))
|
||||||
|
|
||||||
(defun view-input (win)
|
(defun view-input (win)
|
||||||
|
|||||||
@@ -39,33 +39,74 @@ State is read via ~(st :key)~ — no mutation here.
|
|||||||
|
|
||||||
** Chat Area
|
** Chat Area
|
||||||
#+begin_src lisp
|
#+begin_src lisp
|
||||||
|
(defun word-wrap (text width)
|
||||||
|
"Break text into lines at word boundaries, each <= width chars.
|
||||||
|
Returns list of trimmed strings. Single words wider than width are split."
|
||||||
|
(let ((lines '())
|
||||||
|
(pos 0)
|
||||||
|
(len (length text)))
|
||||||
|
(loop while (< pos len)
|
||||||
|
do (let ((end (min len (+ pos width))))
|
||||||
|
(cond
|
||||||
|
((>= end len)
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos len)) lines)
|
||||||
|
(setf pos len))
|
||||||
|
((char= (char text (1- end)) #\Space)
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos end)) lines)
|
||||||
|
(setf pos end))
|
||||||
|
(t
|
||||||
|
(let ((last-space (position #\Space text :from-end t :end (1+ end) :start pos)))
|
||||||
|
(if (and last-space (> last-space pos))
|
||||||
|
(progn
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos last-space)) lines)
|
||||||
|
(setf pos (1+ last-space)))
|
||||||
|
(progn
|
||||||
|
(push (string-trim '(#\Space) (subseq text pos end)) lines)
|
||||||
|
(setf pos end))))))))
|
||||||
|
(nreverse lines)))
|
||||||
|
|
||||||
(defun view-chat (win h)
|
(defun view-chat (win h)
|
||||||
(clear win)
|
(clear win)
|
||||||
(box win 0 0)
|
(box win 0 0)
|
||||||
(let* ((w (or (width win) 78))
|
(let* ((w (or (width win) 78))
|
||||||
(msgs (reverse (st :messages)))
|
(msgs (reverse (st :messages)))
|
||||||
(max-lines (- h 2))
|
(max-lines (- h 2))
|
||||||
(total (length msgs))
|
|
||||||
(start (max 0 (- total max-lines (st :scroll-offset))))
|
|
||||||
(y 1))
|
(y 1))
|
||||||
(loop for i from start below total
|
;; Count visible messages from end, accounting for word wrap
|
||||||
while (< y (1- h))
|
(let* ((msg-count 0)
|
||||||
do (let ((msg (nth i msgs)))
|
(lines-remaining max-lines))
|
||||||
(let* ((role (getf msg :role))
|
;; Walk from most recent backwards, counting wrapped lines
|
||||||
|
(let ((visible-msgs (reverse msgs)))
|
||||||
|
(loop for msg in visible-msgs
|
||||||
|
while (> lines-remaining 0)
|
||||||
|
do (let* ((role (getf msg :role))
|
||||||
(content (getf msg :content))
|
(content (getf msg :content))
|
||||||
(time (or (getf msg :time) ""))
|
(time (or (getf msg :time) ""))
|
||||||
(label (case role
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
(:user (format nil "⬆ [~a] ~a" time content))
|
(line-text (format nil "~a [~a] ~a" prefix time content))
|
||||||
(:agent (format nil "⬇ [~a] ~a" time content))
|
(wrapped (word-wrap line-text (- w 2)))
|
||||||
(:system (format nil " [~a] ~a" time content))
|
(nlines (length wrapped)))
|
||||||
(t (format nil " [~a] ~a" time content))))
|
(if (<= nlines lines-remaining)
|
||||||
(color (theme-color (case role
|
(progn (decf lines-remaining nlines) (incf msg-count))
|
||||||
(:user :user)
|
(setf lines-remaining 0))))
|
||||||
(:agent :agent)
|
;; Render from the correct starting message
|
||||||
(:system :system)
|
(let* ((total (length msgs))
|
||||||
(t :agent)))))
|
(scroll-skip (st :scroll-offset))
|
||||||
(add-string win label :y y :x 1 :n (1- w) :fgcolor color)
|
(start (max 0 (- total msg-count scroll-skip))))
|
||||||
(incf y)))))
|
(loop for i from start below total
|
||||||
|
while (< y (1- h))
|
||||||
|
do (let* ((msg (nth i msgs))
|
||||||
|
(role (getf msg :role))
|
||||||
|
(content (getf msg :content))
|
||||||
|
(time (or (getf msg :time) ""))
|
||||||
|
(color (theme-color (case role (:user :user) (:agent :agent) (:system :system) (t :agent))))
|
||||||
|
(prefix (case role (:user "⬆") (:agent "⬇") (t " ")))
|
||||||
|
(line-text (format nil "~a [~a] ~a" prefix time content))
|
||||||
|
(wrapped (word-wrap line-text (- w 2))))
|
||||||
|
(dolist (line wrapped)
|
||||||
|
(when (< y (1- h))
|
||||||
|
(add-string win line :y y :x 1 :n (1- w) :fgcolor color)
|
||||||
|
(incf y)))))))))
|
||||||
(refresh win))
|
(refresh win))
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user