1use std::cmp::min;
2
3pub const RST_CODE: &str = "\x1b[0m";
4
5type AnsiSegment<'a> = (Vec<&'a str>, &'a str, usize, bool, bool);
7
8#[derive(Default)]
9pub struct AnsiString<'a> {
10 pub slice: &'a str,
11 pub c2c: Option<String>,
12 pub len: usize,
13 pub needs_rst: bool,
14}
15
16#[derive(PartialEq)]
17enum AnsiToken {
18 Escape,
19 Opening,
20 Code,
21}
22
23#[derive(PartialEq)]
24pub enum Overflow {
25 WordWrap,
26 Truncate,
27}
28
29#[derive(Debug)]
30enum Segment<'a> {
31 Word(&'a str, usize),
32 Term(&'a str, usize),
33}
34
35#[derive(Debug, Default)]
36struct CodeQueue<'a> {
37 codes_to_continue: Vec<&'a str>,
38 codes_to_collect: Vec<&'a str>,
39 reset_after_get: bool,
40}
41
42impl<'a> CodeQueue<'a> {
43 pub fn codes_to_continue(&mut self) -> Option<String> {
44 let mut seq = None;
45 let count = self.codes_to_continue.len();
46
47 if count > 0 {
48 let mut codes = String::with_capacity(count);
49 for s in self.codes_to_continue.iter() {
50 codes.push_str(s);
51 }
52 seq = Some(codes)
53 }
54 if self.reset_after_get {
55 self.codes_to_continue.clear();
56 self.reset_after_get = false;
57 }
58 self.codes_to_continue.append(&mut self.codes_to_collect);
60 seq
61 }
62 pub fn collect(&mut self, codes: Vec<&'a str>) {
63 self.codes_to_collect.extend(codes);
64 }
65 pub fn clear(&mut self) {
66 self.reset_after_get = true;
67 self.codes_to_collect.clear();
68 }
69 pub fn has_codes_to_continue(&self) -> bool {
70 !self.codes_to_continue.is_empty()
71 }
72}
73
74impl<'a> Segment<'a> {
75 fn text(&self) -> &'a str {
76 match self {
77 Segment::Word(txt, _) | Segment::Term(txt, _) => txt,
78 }
79 }
80 fn pos(&self) -> usize {
81 match self {
82 Segment::Word(_, pos) | Segment::Term(_, pos) => *pos,
83 }
84 }
85}
86
87impl<'a> AnsiString<'a> {
88 pub fn build(c2c: Option<String>, slice: &'a str, len: usize, needs_rst: bool) -> Self {
89 Self {
90 slice,
91 len,
92 c2c,
93 needs_rst,
94 }
95 }
96}
97
98pub fn build_string<'a>(
116 input: &'a str,
117 hspace: usize,
118 vspace: usize,
119 overflow: &Overflow,
120) -> Vec<AnsiString<'a>> {
121 if hspace == 0 {
122 return vec![];
123 }
124 let mut queue = CodeQueue::default();
126 let mut result = Vec::with_capacity(vspace);
127 let mut str_pos = 0;
128 let mut end_pos = 0;
129 let mut txt_len = 0;
130
131 let mut line_reset = false;
133
134 let segments = build_segments(input, overflow);
135 let count = segments.len();
136
137 for (i, seg) in segments.iter().enumerate() {
138 let is_last_str = i == count - 1;
139 let is_init_str = txt_len == 0;
140 let is_term_str = matches!(seg, Segment::Term(_, _));
141
142 let (new_codes, txt, total_len, has_rst, needs_rst) = parse_segment(seg, hspace);
143 let len = min(total_len, hspace);
144 let eol = is_last_str || is_term_str;
145
146 let sep_len = (txt_len > 0) as usize;
148
149 if txt_len == 0 {
150 str_pos = seg.pos();
151 end_pos = str_pos;
152 line_reset = queue.has_codes_to_continue();
153 }
154
155 if !is_init_str && txt_len + total_len + sep_len > hspace {
157 result.push(AnsiString::build(
158 queue.codes_to_continue(),
159 &input[str_pos..end_pos],
160 txt_len,
161 line_reset,
162 ));
163 str_pos = seg.pos();
165 end_pos = str_pos + txt.len();
166 txt_len = len;
167 } else {
168 end_pos += txt.len() + sep_len;
169 txt_len += len + sep_len;
170 }
171
172 if has_rst {
175 queue.clear();
176 line_reset = needs_rst;
177 } else {
178 line_reset = line_reset || needs_rst;
179 }
180
181 queue.collect(new_codes);
183
184 if result.len() < vspace && (txt_len == hspace || eol) {
185 result.push(AnsiString::build(
186 queue.codes_to_continue(),
187 &input[str_pos..end_pos],
188 txt_len,
189 line_reset,
190 ));
191 txt_len = 0;
192 }
193
194 if result.len() == vspace {
196 return result;
197 }
198 }
199 result
200}
201
202fn build_segments<'a>(input: &'a str, overflow: &Overflow) -> Vec<Segment<'a>> {
207 let input_ptr = input.as_ptr();
208 match overflow {
209 Overflow::Truncate => input
210 .lines()
211 .map(|s| Segment::Term(s, s.as_ptr() as usize - input_ptr as usize))
212 .collect::<Vec<_>>(),
213 Overflow::WordWrap => input
214 .lines()
215 .flat_map(|s| {
216 let mut iter = s.split(' ').peekable();
217
218 std::iter::from_fn(move || {
219 iter.next().map(|slice| {
220 let pos = slice.as_ptr() as usize - input_ptr as usize;
221
222 if iter.peek().is_none() {
223 Segment::Term(slice, pos)
224 } else {
225 Segment::Word(slice, pos)
226 }
227 })
228 })
229 })
230 .collect::<Vec<_>>(),
231 }
232}
233
234fn parse_segment<'a>(segment: &'a Segment, len: usize) -> AnsiSegment<'a> {
243 let mut codes = Vec::new();
244 let mut expected = AnsiToken::Escape;
245 let mut current_code_start = 0;
246
247 let mut txt_len: usize = 0;
248 let mut end_pos = None;
249 let mut has_rst = false;
250 let mut needs_rst = false;
251 let mut stop_collecting = false;
252
253 let input = segment.text();
254
255 for (pos, ch) in input.char_indices() {
256 match ch {
257 '\x1b' if expected == AnsiToken::Escape => {
258 expected = AnsiToken::Opening;
259 current_code_start = pos;
260 }
261 '[' if expected == AnsiToken::Opening => expected = AnsiToken::Code,
262 'm' if expected == AnsiToken::Code => {
263 let sequence = &input[current_code_start..pos + 1];
265 let seq_rst = sequence == RST_CODE;
266
267 has_rst = seq_rst;
268
269 if seq_rst {
270 codes.clear();
271 } else {
272 codes.push(sequence);
273 }
274 if !stop_collecting {
275 needs_rst = !has_rst;
276 if end_pos.is_some() {
277 end_pos = Some(pos + 1);
278 }
279 }
280 expected = AnsiToken::Escape
281 }
282 '0'..='9' | ';' | ':' if expected == AnsiToken::Code => {
283 continue;
284 }
285 _ if end_pos.is_none() => {
286 txt_len += 1;
287
288 if txt_len == len {
289 end_pos = Some(pos + ch.len_utf8());
290 }
291 expected = AnsiToken::Escape;
292 }
293 _ => {
294 stop_collecting = true;
295 expected = AnsiToken::Escape;
296 }
298 }
299 }
300 let slice = &input[0..end_pos.unwrap_or(input.len())];
301 (codes, slice, txt_len, has_rst, needs_rst)
302}
303
304#[macro_export]
305macro_rules! assert_ansi_string {
306 ($string:expr, $hspace:expr, $vspace:expr, $overflow:expr, []) => {
307 let str = format!($string);
308 let segments = $crate::ansi::build_string(&str, $hspace, $vspace, &$overflow);
309
310 assert!(segments.is_empty());
311 };
312 ($string:expr, $hspace:expr, $vspace:expr, $overflow:expr, [$($segment:tt),*]) => {
313 {
314 let str = format!($string);
315 let segments = $crate::ansi::build_string(&str, $hspace, $vspace, &$overflow);
316 let mut segment_index = 0;
317
318 $(
319 assert_ansi_string!(@verify_segment segments[segment_index], $segment);
320 segment_index += 1;
321 )+
322 assert_eq!(segments.len(), segment_index, "Expected {} segments, found {}", segment_index, segments.len());
323 }
324 };
325 (@verify_segment $seg:expr, { $($field:ident => $value:tt),* }) => {
326 let seg = &$seg;
327 $(
328 assert_ansi_string!(@check_field seg, $field, $value);
329 )*
330 };
331 (@check_field $seg:expr, len, $expected:expr) => {
332 assert_eq!($seg.len, $expected);
333 };
334 (@check_field $seg:expr, txt, $expected:literal) => {
335 let formatted = format!($expected);
336 assert_eq!($seg.slice.as_ref(), formatted);
337 };
338 (@check_field $seg:expr, rst, $expected:expr) => {
339 assert_eq!($seg.needs_rst, $expected);
340 };
341 }
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_codes_queue_clear() {
349 let mut queue = CodeQueue::default();
350 queue.collect(vec!["\x1b[31m", "\x1b[1m"]);
351
352 assert_eq!(queue.codes_to_continue(), None);
354 assert!(queue.has_codes_to_continue());
355
356 queue.clear();
358
359 assert_eq!(
361 queue.codes_to_continue(),
362 Some(String::from("\x1b[31m\x1b[1m"))
363 );
364 assert!(!queue.has_codes_to_continue());
365 }
366
367 #[test]
368 fn test_codes_queue_multiple_codes_collected() {
369 let mut queue = CodeQueue::default();
370
371 queue.collect(vec!["\x1b[31m"]);
374 assert_eq!(queue.codes_to_continue(), None);
375
376 queue.collect(vec!["\x1b[1m"]);
379 assert_eq!(queue.codes_to_continue(), Some(String::from("\x1b[31m")));
380
381 assert_eq!(
384 queue.codes_to_continue(),
385 Some(String::from("\x1b[31m\x1b[1m"))
386 );
387 }
388
389 #[test]
390 fn test_codes_queue_clear_with_new_codes() {
391 let mut queue = CodeQueue::default();
392
393 queue.collect(vec!["\x1b[31m", "\x1b[1m"]);
395 queue.codes_to_continue();
396
397 queue.collect(vec!["\x1b[32m"]);
399 queue.clear();
400
401 assert_eq!(
403 queue.codes_to_continue(),
404 Some(String::from("\x1b[31m\x1b[1m"))
405 );
406 assert!(!queue.has_codes_to_continue());
407 }
408
409 #[test]
410 fn test_codes_queue_empty_states() {
411 let mut queue = CodeQueue::default();
412
413 assert!(!queue.has_codes_to_continue());
415 assert_eq!(queue.codes_to_continue(), None);
416
417 queue.collect(vec![]);
419 assert_eq!(queue.codes_to_continue(), None);
420 assert!(!queue.has_codes_to_continue());
421
422 queue.clear();
424 assert_eq!(queue.codes_to_continue(), None);
425 assert!(!queue.has_codes_to_continue());
426 }
427}