fancy_table/
ansi.rs

1use std::cmp::min;
2
3pub const RST_CODE: &str = "\x1b[0m";
4
5// open SGRs, text slice, total length (excluding ANSI codes), has reset code?, needs reset?
6type 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        // codes collected become codes to continue in case of line breaks
59        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
98/// Processes input text with ANSI codes into formatted lines that fit within specified dimensions.
99///
100/// Takes raw text containing ANSI escape sequences and breaks it into lines that respect
101/// both horizontal (character) and vertical (line) constraints while preserving ANSI formatting.
102///
103/// # Arguments
104/// * `input` - Raw input text that may contain ANSI escape sequences
105/// * `hspace` - Maximum characters per line (excluding ANSI codes)
106/// * `vspace` - Maximum number of lines to generate
107/// * `overflow` - Strategy for handling text that exceeds horizontal space
108///
109/// # Returns
110/// Vector of `AnsiString` objects, each representing a formatted line with:
111/// - Original text slice (may include ANSI codes)
112/// - Continuation codes needed to maintain formatting across line breaks
113/// - Actual display length (excluding ANSI codes)
114/// - Whether the line needs a reset code for proper formatting
115pub 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    // stack of collected ANSI codes
125    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    // Tracks whether the current line needs a reset code to properly close ANSI formatting
132    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        // Separator length: 0 for first segment in line, 1 otherwise.
147        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 there is no more space for a segment then wrap-or-truncate the line
156        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            // Constituate current segment as initial in the new line
164            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 segment contains reset code at any position wipe out
173        // all ANSI codes collected up to the reset code so far...
174        if has_rst {
175            queue.clear();
176            line_reset = needs_rst;
177        } else {
178            line_reset = line_reset || needs_rst;
179        }
180
181        // ...and collect all the codes coming right after the reset code
182        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        // Bail out early if there is no more vertical space available
195        if result.len() == vspace {
196            return result;
197        }
198    }
199    result
200}
201
202/// Splits input string into segments for parsing based on overflow strategy.
203///
204/// - `Overflow::Truncate`: Splits only on newlines, creating one segment per line
205/// - `Overflow::WordWrap`: Splits on both newlines and spaces for word-based wrapping
206fn 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
234/// Parses a single text segment, extracting ANSI codes and enforcing character limits.
235///
236/// Returns a tuple containing:
237/// - Vector of ANSI escape sequences found in the segment
238/// - Text slice (including ANSI codes) truncated to fit the character limit
239/// - Actual text length (excluding ANSI codes)  
240/// - Whether a reset code was found in the segment
241/// - Whether the segment needs a reset code (has unclosed ANSI styling)
242fn 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                // Valid SGR sequence terminator
264                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                // consume, do nothing
297            }
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        // First call moves collected to continue
353        assert_eq!(queue.codes_to_continue(), None);
354        assert!(queue.has_codes_to_continue());
355
356        // Clear should mark for reset
357        queue.clear();
358
359        // Next call should clear and return existing codes
360        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        // First collected code.
372        // Nothing to be applied at the beginning of current line.
373        queue.collect(vec!["\x1b[31m"]);
374        assert_eq!(queue.codes_to_continue(), None);
375
376        // Second collected code should append to queue of codes to continue
377        // but current line should be prepended with previously collected code.
378        queue.collect(vec!["\x1b[1m"]);
379        assert_eq!(queue.codes_to_continue(), Some(String::from("\x1b[31m")));
380
381        // Finally, next call of `codes_to_continue` should generate a sequence
382        // of all codes collected so far.
383        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        // Set up some continuing codes
394        queue.collect(vec!["\x1b[31m", "\x1b[1m"]);
395        queue.codes_to_continue();
396
397        // Collect new codes then clear
398        queue.collect(vec!["\x1b[32m"]);
399        queue.clear();
400
401        // Should get the old continuing codes (before clear) and new codes should be cleared
402        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        // Empty queue
414        assert!(!queue.has_codes_to_continue());
415        assert_eq!(queue.codes_to_continue(), None);
416
417        // Empty collection
418        queue.collect(vec![]);
419        assert_eq!(queue.codes_to_continue(), None);
420        assert!(!queue.has_codes_to_continue());
421
422        // Clear empty queue
423        queue.clear();
424        assert_eq!(queue.codes_to_continue(), None);
425        assert!(!queue.has_codes_to_continue());
426    }
427}