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 (defn visible-subs
"Substring based on visible character positions (ignoring ANSI escape codes). "Substring based on visible character positions (ignoring ANSI escape codes).
Returns substring from visible position start to end (or end of string). 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] (visible-subs s start nil))
([s start end] ([s start end]
(when s (when s
(let [ansi-pattern #"\u001b\[[0-9;]*m" (let [ansi-pattern #"\u001b\[[0-9;]*m"
reset-pattern #"\u001b\[0m"
;; Split string into segments: ANSI codes and regular text ;; Split string into segments: ANSI codes and regular text
segments (loop [remaining s segments (loop [remaining s
result []] result []]
@@ -180,22 +182,32 @@
(conj result {:type :text :text (subs remaining 0 idx)})))) (conj result {:type :text :text (subs remaining 0 idx)}))))
;; No more ANSI codes, rest is text ;; No more ANSI codes, rest is text
(conj result {:type :text :text remaining})))) (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 result (loop [segs segments
visible-pos 0 visible-pos 0
output [] output []
in-range? false] in-range? false
active-styles []] ;; Track styles set before range
(if (empty? segs) (if (empty? segs)
output output
(let [{:keys [type text]} (first segs)] (let [{:keys [type text]} (first segs)]
(if (= type :ansi) (if (= type :ansi)
;; Always include ANSI codes that appear in or after range start (if (or in-range? (>= visible-pos start))
(recur (rest segs) ;; In range - include ANSI codes directly
visible-pos (recur (rest segs)
(if (or in-range? (>= visible-pos start)) visible-pos
(conj output text) (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 ;; Text segment
(let [seg-len (count text) (let [seg-len (count text)
seg-end (+ visible-pos seg-len) seg-end (+ visible-pos seg-len)
@@ -203,19 +215,24 @@
(cond (cond
;; Segment entirely before range - skip ;; Segment entirely before range - skip
(<= seg-end start) (<= 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 ;; Segment entirely within or after range end - take partial or stop
(>= visible-pos effective-end) (>= visible-pos effective-end)
output output
;; Segment overlaps range ;; Segment overlaps range - entering range, prepend active styles
:else :else
(let [take-start (max 0 (- start visible-pos)) (let [take-start (max 0 (- start visible-pos))
take-end (min seg-len (- effective-end 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) (recur (rest segs)
seg-end seg-end
(conj output portion) (conj output-with-styles portion)
true))))))))] true
active-styles))))))))]
(apply str result))))) (apply str result)))))
+1 -1
View File
@@ -282,7 +282,7 @@
bg-after (if (< bg-after-start bg-width) bg-after (if (< bg-after-start bg-width)
(ansi/visible-subs padded-bg-line bg-after-start) (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 ;; No modal on this row, just background
(ansi/pad-right bg-line bg-width)))))) (ansi/pad-right bg-line bg-width))))))