Fix ANSI style preservation in visible-subs and modal rendering

- Track active styles before substring range and prepend them when entering range
- Add reset codes around modal lines to prevent style bleeding between layers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-22 16:44:17 -05:00
parent 0e40fe01d7
commit 8cb4c82daa
2 changed files with 32 additions and 15 deletions
+31 -14
View File
@@ -159,11 +159,13 @@
(defn visible-subs
"Substring based on visible character positions (ignoring ANSI escape codes).
Returns substring from visible position start to end (or end of string).
Preserves ANSI escape sequences that affect the visible portion."
Preserves ANSI escape sequences that affect the visible portion, including
styles that were set before the start position but still active."
([s start] (visible-subs s start nil))
([s start end]
(when s
(let [ansi-pattern #"\u001b\[[0-9;]*m"
reset-pattern #"\u001b\[0m"
;; Split string into segments: ANSI codes and regular text
segments (loop [remaining s
result []]
@@ -180,22 +182,32 @@
(conj result {:type :text :text (subs remaining 0 idx)}))))
;; No more ANSI codes, rest is text
(conj result {:type :text :text remaining}))))
;; Build result by tracking visible position
;; Build result by tracking visible position and active styles
result (loop [segs segments
visible-pos 0
output []
in-range? false]
in-range? false
active-styles []] ;; Track styles set before range
(if (empty? segs)
output
(let [{:keys [type text]} (first segs)]
(if (= type :ansi)
;; Always include ANSI codes that appear in or after range start
(recur (rest segs)
visible-pos
(if (or in-range? (>= visible-pos start))
(if (or in-range? (>= visible-pos start))
;; In range - include ANSI codes directly
(recur (rest segs)
visible-pos
(conj output text)
output)
in-range?)
in-range?
active-styles)
;; Before range - track active styles
(let [new-styles (if (re-matches reset-pattern text)
[] ;; Reset clears all active styles
(conj active-styles text))]
(recur (rest segs)
visible-pos
output
in-range?
new-styles)))
;; Text segment
(let [seg-len (count text)
seg-end (+ visible-pos seg-len)
@@ -203,19 +215,24 @@
(cond
;; Segment entirely before range - skip
(<= seg-end start)
(recur (rest segs) seg-end output false)
(recur (rest segs) seg-end output false active-styles)
;; Segment entirely within or after range end - take partial or stop
(>= visible-pos effective-end)
output
;; Segment overlaps range
;; Segment overlaps range - entering range, prepend active styles
:else
(let [take-start (max 0 (- start visible-pos))
take-end (min seg-len (- effective-end visible-pos))
portion (subs text take-start take-end)]
portion (subs text take-start take-end)
;; Prepend active styles when first entering range
output-with-styles (if in-range?
output
(into output active-styles))]
(recur (rest segs)
seg-end
(conj output portion)
true))))))))]
(conj output-with-styles portion)
true
active-styles))))))))]
(apply str result)))))
+1 -1
View File
@@ -282,7 +282,7 @@
bg-after (if (< bg-after-start bg-width)
(ansi/visible-subs padded-bg-line bg-after-start)
"")]
(str bg-before modal-line bg-after))
(str bg-before ansi/reset modal-line ansi/reset bg-after))
;; No modal on this row, just background
(ansi/pad-right bg-line bg-width))))))