@@ -39,73 +39,60 @@ def wrap_ansi_text(text, max_width)
3939 [ text ]
4040 else
4141 chunks = [ ]
42- remaining = text
43- active_colors = [ ]
4442
45- # Extract initial color state
46- text . scan ( /\e \[ [0-9;]*m/ ) do |ansi_code |
47- if /\e \[ 0m/ . match? ( ansi_code )
48- active_colors . clear
49- else
50- active_colors << ansi_code
51- end
52- end
43+ # Parse the text to extract segments with their colors
44+ segments = parse_ansi_segments ( text )
5345
54- while remaining . length > 0
55- clean_remaining = remaining . gsub ( /\e \[ [0-9;]*m/ , "" )
46+ current_chunk = ""
47+ current_chunk_length = 0
48+ active_color_state = ""
5649
57- if clean_remaining . length <= max_width
58- # Last chunk
59- chunks << if active_colors . any? && !remaining . start_with? ( *active_colors )
60- active_colors . join ( "" ) + remaining
50+ segments . each do |segment |
51+ if segment [ :type ] == :ansi
52+ # Track color state
53+ active_color_state = if segment [ :text ] == "\e [0m"
54+ ""
6155 else
62- remaining
56+ segment [ :text ]
6357 end
64- break
58+ current_chunk += segment [ :text ]
6559 else
66- # Find break point and preserve color state
67- break_point = max_width
68- original_pos = 0
69- clean_pos = 0
70- chunk_colors = active_colors . dup
71-
72- remaining . each_char . with_index do |char , idx |
73- if /^\e \[ [0-9;]*m/ . match? ( remaining [ idx ..] )
74- # Found ANSI sequence
75- ansi_match = remaining [ idx ..] . match ( /^(\e \[ [0-9;]*m)/ )
76- ansi_code = ansi_match [ 1 ]
77-
78- if /\e \[ 0m/ . match? ( ansi_code )
79- chunk_colors . clear
80- active_colors . clear
81- else
82- chunk_colors << ansi_code unless chunk_colors . include? ( ansi_code )
83- active_colors << ansi_code unless active_colors . include? ( ansi_code )
84- end
60+ # Text segment - check if it fits
61+ text_content = segment [ :text ]
8562
86- original_pos += ansi_code . length
87- idx + ansi_code . length - 1
88- else
89- clean_pos += 1
90- original_pos += 1
63+ while text_content . length > 0
64+ remaining_space = max_width - current_chunk_length
9165
92- if clean_pos >= break_point
93- break
66+ if text_content . length <= remaining_space
67+ # Entire text fits in current chunk
68+ current_chunk += text_content
69+ current_chunk_length += text_content . length
70+ break
71+ else
72+ # Need to split the text
73+ if remaining_space > 0
74+ # Take what fits in current chunk
75+ chunk_part = text_content [ 0 ...remaining_space ]
76+ current_chunk += chunk_part
77+ text_content = text_content [ remaining_space ..]
9478 end
95- end
96- end
9779
98- chunk_text = remaining [ 0 ...original_pos ]
99- chunks << if active_colors . any? && !chunk_text . start_with? ( *active_colors )
100- active_colors . join ( "" ) + chunk_text
101- else
102- chunk_text
103- end
80+ # Finish current chunk
81+ chunks << current_chunk
10482
105- remaining = remaining [ original_pos ..]
83+ # Start new chunk with color state
84+ current_chunk = active_color_state
85+ current_chunk_length = 0
86+ end
87+ end
10688 end
10789 end
10890
91+ # Add final chunk if it has content
92+ if current_chunk . length > 0
93+ chunks << current_chunk
94+ end
95+
10996 chunks
11097 end
11198 end
@@ -143,6 +130,40 @@ def wrap_plain_text(text, max_width)
143130
144131 attr_accessor :screen
145132
133+ def parse_ansi_segments ( text )
134+ segments = [ ]
135+ remaining = text
136+
137+ while remaining . length > 0
138+ # Look for next ANSI sequence
139+ ansi_match = remaining . match ( /^(\e \[ [0-9;]*m)/ )
140+
141+ if ansi_match
142+ # Found ANSI sequence at start
143+ segments << { type : :ansi , text : ansi_match [ 1 ] }
144+ remaining = remaining [ ansi_match [ 1 ] . length ..]
145+ else
146+ # Look for ANSI sequence anywhere in remaining text
147+ next_ansi = remaining . match ( /(\e \[ [0-9;]*m)/ )
148+
149+ if next_ansi
150+ # Text before ANSI sequence
151+ text_before = remaining [ 0 ...next_ansi . begin ( 1 ) ]
152+ if text_before . length > 0
153+ segments << { type : :text , text : text_before }
154+ end
155+ remaining = remaining [ next_ansi . begin ( 1 ) ..]
156+ else
157+ # No more ANSI sequences, rest is text
158+ segments << { type : :text , text : remaining }
159+ break
160+ end
161+ end
162+ end
163+
164+ segments
165+ end
166+
146167 def ansi_to_curses_color ( codes )
147168 # Convert ANSI color codes to curses color pairs
148169 return nil if codes . empty? || codes == [ 0 ]
0 commit comments