From 8cb4c82daa2425d8af638dbb8d2102590a68c81b Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Thu, 22 Jan 2026 16:44:17 -0500 Subject: [PATCH] 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 --- src/tui/ansi.clj | 45 +++++++++++++++++++++++++++++++-------------- src/tui/render.clj | 2 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/tui/ansi.clj b/src/tui/ansi.clj index aa5ca02..7fa02e0 100644 --- a/src/tui/ansi.clj +++ b/src/tui/ansi.clj @@ -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))))) diff --git a/src/tui/render.clj b/src/tui/render.clj index 38ec0cf..86185d1 100644 --- a/src/tui/render.clj +++ b/src/tui/render.clj @@ -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))))))