1use ansi_to_tui::IntoText;
11use ratatui::text::Span;
12
13pub(crate) fn parse_ansi_spans(line: &str) -> Vec<Span<'static>> {
19 if !line.contains('\x1b') {
21 return vec![Span::raw(line.to_string())];
22 }
23
24 match line.as_bytes().into_text() {
26 Ok(text) => {
27 text.lines
30 .into_iter()
31 .flat_map(|l| l.spans)
32 .map(|s| Span::styled(s.content.into_owned(), s.style))
33 .collect()
34 }
35 Err(_) => {
36 let stripped = strip_ansi_escapes(line);
38 vec![Span::raw(stripped)]
39 }
40 }
41}
42
43pub(crate) fn strip_ansi_escapes(text: &str) -> String {
46 let mut result = String::with_capacity(text.len());
47 let mut chars = text.chars().peekable();
48 while let Some(c) = chars.next() {
49 if c == '\x1b' {
50 while let Some(&next) = chars.peek() {
52 chars.next();
53 if next == 'm' {
54 break;
55 }
56 }
57 } else {
58 result.push(c);
59 }
60 }
61 result
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use ratatui::style::Color;
68
69 #[test]
70 fn test_parse_ansi_plain_text() {
71 let spans = parse_ansi_spans("hello world");
72 assert_eq!(spans.len(), 1);
73 assert_eq!(spans[0].content.as_ref(), "hello world");
74 }
75
76 #[test]
77 fn test_parse_ansi_colored_text() {
78 let input = "\x1b[31mERROR\x1b[0m: something failed";
80 let spans = parse_ansi_spans(input);
81 assert!(spans.len() >= 2, "should produce multiple spans: {spans:?}");
82 assert_eq!(spans[0].content.as_ref(), "ERROR");
84 assert_eq!(spans[0].style.fg, Some(Color::Red));
85 }
86
87 #[test]
88 fn test_parse_ansi_no_escape_fast_path() {
89 let spans = parse_ansi_spans("just plain text");
91 assert_eq!(spans.len(), 1);
92 assert_eq!(spans[0].content.as_ref(), "just plain text");
93 }
94
95 #[test]
96 fn test_strip_ansi_escapes_fallback() {
97 let input = "\x1b[1;32mOK\x1b[0m done";
98 let stripped = strip_ansi_escapes(input);
99 assert_eq!(stripped, "OK done");
100 }
101
102 #[test]
103 fn test_strip_ansi_empty() {
104 assert_eq!(strip_ansi_escapes(""), "");
105 }
106}