Skip to main content

egui_sgr/
lib.rs

1//! # egui_sgr
2//!
3//! A library for converting ASCII/ANSI escape sequence color models to colored text in egui.
4//!
5//! ## Features
6//!
7//! - Supports 4-bit color (16 colors) model
8//! - Supports 8-bit color (256 colors) model
9//! - Supports 24-bit true color model
10//! - Automatically detects and converts mixed color sequences
11//! - Supports simultaneous setting of foreground and background colors
12//! - Handles malformed ANSI sequences gracefully
13//! - Strips non-SGR control sequences (OSC, etc.)
14//!
15//! ## Usage Example
16//!
17//! ```rust
18//! use egui_sgr::ansi_to_rich_text;
19//!
20//! // Convert ANSI color sequences to egui RichText
21//! let red_text = ansi_to_rich_text("\x1b[31mRed Text\x1b[0m");
22//! let orange_text = ansi_to_rich_text("\x1b[38;5;208mOrange Text\x1b[0m");
23//! let pink_text = ansi_to_rich_text("\x1b[38;2;255;105;180mPink Text\x1b[0m");
24//! let colored_bg = ansi_to_rich_text("\x1b[41;33mYellow on Red\x1b[0m");
25//! // Empty reset sequence (equivalent to [0m)
26//! let reset_text = ansi_to_rich_text("\x1b[31mRed\x1b[mDefault");
27//! ```
28
29use egui::{Color32, RichText};
30use regex::Regex;
31use std::sync::LazyLock;
32
33mod color_models;
34
35/// Pre-compiled regex for matching CSI (Control Sequence Introducer) ANSI escape sequences
36/// Matches: ESC [ <params> <final byte>
37/// This includes SGR (Select Graphic Rendition) and other CSI sequences
38static CSI_REGEX: LazyLock<Regex> = LazyLock::new(|| {
39    // CSI sequences: ESC [ followed by optional parameter bytes
40    // Parameters can include: digits, semicolon, question mark (private mode), etc.
41    // ending with a final byte (letter or @)
42    // CSI parameter bytes: 0x30-0x3F (0-9, ;, :, <, =, >, ?)
43    Regex::new(r"\x1b\[([\x30-\x3F]*)([\x40-\x7E])")
44        .expect("Invalid CSI regex pattern")
45});
46
47/// Pre-compiled regex for matching OSC (Operating System Command) sequences
48/// OSC sequences: ESC ] ... ST (String Terminator = ESC \ or BEL)
49static OSC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
50    // Match OSC sequences: ESC ] ... (BEL or ESC \)
51    Regex::new(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?")
52        .expect("Invalid OSC regex pattern")
53});
54
55/// Pre-compiled regex for matching other common escape sequences
56/// Includes: ESC followed by a single character (0x40-0x5A), excluding [ (CSI) and ] (OSC)
57static OTHER_ESC_REGEX: LazyLock<Regex> = LazyLock::new(|| {
58    // Match other escape sequences (non-CSI, non-OSC)
59    // ESC followed by single char: @, A-Z, ^, _, but NOT [ (CSI) or ] (OSC)
60    // Hex ranges: 0x40-0x5A (@, A-Z), 0x5E (^), 0x5F (_)
61    // Excluding 0x5B ([) and 0x5D (])
62    Regex::new(r"\x1b[\x40-\x5A\x5E\x5F]")
63        .expect("Invalid escape regex pattern")
64});
65
66// Re-export color model modules
67pub use color_models::*;
68
69/// Represents a text segment with optional foreground and background color information.
70///
71/// This struct is the output of parsing ANSI escape sequences, where each segment
72/// represents a continuous piece of text with consistent color attributes.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ColoredText {
75    /// The text content of this segment
76    pub text: String,
77    /// Optional foreground (text) color
78    pub foreground_color: Option<Color32>,
79    /// Optional background color
80    pub background_color: Option<Color32>,
81}
82
83impl ColoredText {
84    /// Creates a new ColoredText with no colors applied.
85    #[must_use]
86    pub fn new(text: impl Into<String>) -> Self {
87        Self {
88            text: text.into(),
89            foreground_color: None,
90            background_color: None,
91        }
92    }
93
94    /// Creates a new ColoredText with the specified foreground color.
95    #[must_use]
96    pub fn with_foreground(text: impl Into<String>, color: Color32) -> Self {
97        Self {
98            text: text.into(),
99            foreground_color: Some(color),
100            background_color: None,
101        }
102    }
103
104    /// Creates a new ColoredText with the specified background color.
105    #[must_use]
106    pub fn with_background(text: impl Into<String>, color: Color32) -> Self {
107        Self {
108            text: text.into(),
109            foreground_color: None,
110            background_color: Some(color),
111        }
112    }
113
114    /// Creates a new ColoredText with both foreground and background colors.
115    #[must_use]
116    pub fn with_colors(
117        text: impl Into<String>,
118        foreground: Option<Color32>,
119        background: Option<Color32>,
120    ) -> Self {
121        Self {
122            text: text.into(),
123            foreground_color: foreground,
124            background_color: background,
125        }
126    }
127}
128
129/// ANSI escape sequence parser that converts ANSI color codes to egui colors.
130///
131/// This parser maintains color state between escape sequences, allowing for
132/// proper handling of sequential color changes and nested formatting.
133#[derive(Debug, Clone)]
134pub struct AnsiParser {
135    /// Currently cached foreground color
136    current_fg: Option<Color32>,
137    /// Currently cached background color
138    current_bg: Option<Color32>,
139}
140
141impl Default for AnsiParser {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147impl AnsiParser {
148    /// Creates a new ANSI parser with no active colors.
149    pub fn new() -> Self {
150        Self {
151            current_fg: None,
152            current_bg: None,
153        }
154    }
155
156    /// Parses text containing ANSI escape sequences
157    ///
158    /// # Arguments
159    /// - `input`: Text containing ANSI escape sequences
160    ///
161    /// # Returns
162    /// A list of text segments with color information
163    pub fn parse(&mut self, input: &str) -> Vec<ColoredText> {
164        // First, strip non-SGR sequences (OSC, other escape sequences)
165        let cleaned = self.strip_non_sgr_sequences(input);
166        // Then parse SGR sequences
167        self.parse_sgr(&cleaned)
168    }
169
170    /// Strip non-SGR ANSI sequences that shouldn't appear as visible text
171    fn strip_non_sgr_sequences(&self, input: &str) -> String {
172        let mut result = input.to_string();
173
174        // Remove OSC sequences (Operating System Command)
175        result = OSC_REGEX.replace_all(&result, "").to_string();
176
177        // Remove other escape sequences (non-CSI)
178        result = OTHER_ESC_REGEX.replace_all(&result, "").to_string();
179
180        result
181    }
182
183    /// Parse SGR (Select Graphic Rendition) sequences
184    fn parse_sgr(&mut self, input: &str) -> Vec<ColoredText> {
185        let mut result = Vec::new();
186
187        // Reset current colors at start of new input
188        self.reset_colors();
189
190        let mut last_end = 0;
191
192        // Iterate over all matched CSI sequences using pre-compiled regex
193        for cap in CSI_REGEX.captures_iter(input) {
194            let params = cap.get(1).unwrap().as_str();
195            let final_byte = cap.get(2).unwrap().as_str();
196            let start = cap.get(0).unwrap().start();
197            let end = cap.get(0).unwrap().end();
198
199            // Add the text before the escape sequence (if any)
200            if start > last_end {
201                let plain_text = &input[last_end..start];
202                if !plain_text.is_empty() {
203                    result.push(ColoredText {
204                        text: plain_text.to_string(),
205                        foreground_color: self.current_fg,
206                        background_color: self.current_bg,
207                    });
208                }
209            }
210
211            // Only process SGR sequences (ending with 'm')
212            if final_byte == "m" {
213                // Empty params means reset (equivalent to [0m)
214                if params.is_empty() {
215                    self.reset_colors();
216                } else {
217                    self.process_sgr_sequence(params);
218                }
219            }
220            // Other CSI sequences are silently ignored (cursor movement, etc.)
221
222            last_end = end;
223        }
224
225        // Add the remaining text (if any)
226        if last_end < input.len() {
227            let plain_text = &input[last_end..];
228            if !plain_text.is_empty() {
229                result.push(ColoredText {
230                    text: plain_text.to_string(),
231                    foreground_color: self.current_fg,
232                    background_color: self.current_bg,
233                });
234            }
235        }
236
237        // If no escape sequences were found, return the entire text
238        if result.is_empty() {
239            return vec![ColoredText {
240                text: input.to_string(),
241                foreground_color: None,
242                background_color: None,
243            }];
244        }
245
246        result
247    }
248
249    /// Resets the current colors
250    fn reset_colors(&mut self) {
251        self.current_fg = None;
252        self.current_bg = None;
253    }
254
255    /// Processes a single SGR sequence and updates the current color cache
256    ///
257    /// # Arguments
258    /// - `sequence`: The SGR parameter string (e.g., "31", "38;5;208", "0")
259    fn process_sgr_sequence(&mut self, sequence: &str) {
260        let codes: Vec<&str> = sequence.split(';').collect();
261        let mut i = 0;
262
263        while i < codes.len() {
264            // Handle empty code (e.g., from leading/trailing semicolons)
265            if codes[i].is_empty() {
266                i += 1;
267                continue;
268            }
269
270            match codes[i] {
271                "0" | "" => {
272                    // Reset (0 or empty means reset)
273                    self.reset_colors();
274                    i += 1;
275                }
276                "1" => {
277                    // Bold - we don't support this in egui, skip
278                    i += 1;
279                }
280                "4" => {
281                    // Underline - we don't support this in egui, skip
282                    i += 1;
283                }
284                "7" => {
285                    // Reverse video - swap fg and bg
286                    std::mem::swap(&mut self.current_fg, &mut self.current_bg);
287                    i += 1;
288                }
289                "22" => {
290                    // Normal intensity (not bold)
291                    i += 1;
292                }
293                "24" => {
294                    // Not underlined
295                    i += 1;
296                }
297                "27" => {
298                    // Not reversed
299                    i += 1;
300                }
301                "38" => {
302                    // Set foreground color
303                    if i + 2 < codes.len() {
304                        match codes[i + 1] {
305                            "5" => {
306                                // 256-color mode: 38;5;n
307                                if let Ok(color_code) = codes[i + 2].parse::<u8>() {
308                                    self.current_fg =
309                                        Some(color_models::eight_bit::ansi_256_to_egui(color_code));
310                                }
311                                i += 3; // Skip 38, 5, and the color code
312                            }
313                            "2" => {
314                                // 24-bit true color mode: 38;2;r;g;b
315                                if i + 4 < codes.len() {
316                                    if let (Ok(r), Ok(g), Ok(b)) = (
317                                        codes[i + 2].parse::<u8>(),
318                                        codes[i + 3].parse::<u8>(),
319                                        codes[i + 4].parse::<u8>(),
320                                    ) {
321                                        self.current_fg = Some(Color32::from_rgb(r, g, b));
322                                    }
323                                    i += 5; // Skip 38, 2, r, g, b
324                                } else {
325                                    i += 1;
326                                }
327                            }
328                            _ => {
329                                i += 1;
330                            }
331                        }
332                    } else {
333                        i += 1;
334                    }
335                }
336                "48" => {
337                    // Set background color
338                    if i + 2 < codes.len() {
339                        match codes[i + 1] {
340                            "5" => {
341                                // 256-color mode: 48;5;n
342                                if let Ok(color_code) = codes[i + 2].parse::<u8>() {
343                                    self.current_bg =
344                                        Some(color_models::eight_bit::ansi_256_to_egui(color_code));
345                                }
346                                i += 3; // Skip 48, 5, and the color code
347                            }
348                            "2" => {
349                                // 24-bit true color mode: 48;2;r;g;b
350                                if i + 4 < codes.len() {
351                                    if let (Ok(r), Ok(g), Ok(b)) = (
352                                        codes[i + 2].parse::<u8>(),
353                                        codes[i + 3].parse::<u8>(),
354                                        codes[i + 4].parse::<u8>(),
355                                    ) {
356                                        self.current_bg = Some(Color32::from_rgb(r, g, b));
357                                    }
358                                    i += 5; // Skip 48, 2, r, g, b
359                                } else {
360                                    i += 1;
361                                }
362                            }
363                            _ => {
364                                i += 1;
365                            }
366                        }
367                    } else {
368                        i += 1;
369                    }
370                }
371                "39" => {
372                    // Default foreground color
373                    self.current_fg = None;
374                    i += 1;
375                }
376                "49" => {
377                    // Default background color
378                    self.current_bg = None;
379                    i += 1;
380                }
381                code => {
382                    // Handle 4-bit color codes
383                    if let Ok(color_code) = code.parse::<u8>() {
384                        match color_code {
385                            30..=37 => {
386                                let color_index = color_code - 30;
387                                self.current_fg =
388                                    Some(color_models::four_bit::ansi_color_to_egui(color_index));
389                            }
390                            40..=47 => {
391                                let color_index = color_code - 40;
392                                self.current_bg =
393                                    Some(color_models::four_bit::ansi_color_to_egui(color_index));
394                            }
395                            90..=97 => {
396                                let color_index = color_code - 90 + 8;
397                                self.current_fg =
398                                    Some(color_models::four_bit::ansi_color_to_egui(color_index));
399                            }
400                            100..=107 => {
401                                let color_index = color_code - 100 + 8;
402                                self.current_bg =
403                                    Some(color_models::four_bit::ansi_color_to_egui(color_index));
404                            }
405                            _ => {}
406                        }
407                    }
408                    i += 1;
409                }
410            }
411        }
412    }
413}
414
415/// Converts a list of ColoredText to a list of RichText that can be displayed in egui
416///
417/// # Arguments
418/// - `colored_texts`: A list of text segments with color information
419///
420/// # Returns
421/// A list of RichText that can be displayed in egui
422pub fn convert_to_rich_text(colored_texts: &[ColoredText]) -> Vec<RichText> {
423    colored_texts
424        .iter()
425        .map(|colored_text| {
426            let mut rich_text = RichText::new(&colored_text.text);
427
428            if let Some(fg) = colored_text.foreground_color {
429                rich_text = rich_text.color(fg);
430            }
431
432            if let Some(bg) = colored_text.background_color {
433                rich_text = rich_text.background_color(bg);
434            }
435
436            rich_text
437        })
438        .collect()
439}
440
441/// Convenience function: directly converts text with ANSI escape sequences to a list of RichText
442///
443/// # Arguments
444/// - `input`: Text containing ANSI escape sequences
445///
446/// # Returns
447/// A list of RichText that can be displayed in egui
448pub fn ansi_to_rich_text(input: &str) -> Vec<RichText> {
449    // Directly parse input - don't preprocess escape sequences
450    // Only handle actual control characters for egui display compatibility
451    let mut parser = AnsiParser::new();
452    let colored_texts = parser.parse(input);
453    convert_to_rich_text(&colored_texts)
454}
455
456/// Creates an example function to demonstrate how to use this library
457pub fn example_usage() {
458    // 4-bit color example
459    let example_4bit =
460        "This is \x1b[31mred\x1b[0m, \x1b[34mblue\x1b[0m and \x1b[33myellow\x1b[0m text";
461    let _rich_text_4bit = ansi_to_rich_text(example_4bit);
462
463    // 8-bit color example
464    let example_8bit = "This is \x1b[38;5;208morange\x1b[0m, \x1b[38;5;51msky blue\x1b[0m and \x1b[38;5;120mgreen\x1b[0m text";
465    let _rich_text_8bit = ansi_to_rich_text(example_8bit);
466
467    // 24-bit color example
468    let example_24bit = "This is \x1b[38;2;255;105;180mhot pink\x1b[0m and \x1b[38;2;128;0;128mdeep purple\x1b[0m text";
469    let _rich_text_24bit = ansi_to_rich_text(example_24bit);
470
471    // Mixed example
472    let example_mixed = "Normal text \x1b[31mred\x1b[0m normal \x1b[38;5;208morange\x1b[0m normal \x1b[38;2;255;105;180mpink\x1b[0m normal";
473    let _rich_text_mixed = ansi_to_rich_text(example_mixed);
474
475    // Foreground and background color combination example
476    let example_fg_bg = "Default text \x1b[41;33mYellow on red\x1b[0m default text";
477    let _rich_text_fg_bg = ansi_to_rich_text(example_fg_bg);
478
479    // Use these RichText lists in an egui application
480    // ui.horizontal(|ui| {
481    //     for text in rich_text_4bit {
482    //         ui.label(text);
483    //     }
484    // });
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    #[test]
492    fn test_4bit_color_parsing() {
493        let input = "\x1b[31mRed Text\x1b[0m";
494        let result = ansi_to_rich_text(input);
495        assert_eq!(result.len(), 1);
496        // Verify that the color is correct
497        assert_eq!(result[0].text(), "Red Text");
498        // Note: We don't verify the specific color value here as our implementation may not be perfectly consistent
499    }
500
501    #[test]
502    fn test_8bit_color_parsing() {
503        let input = "\x1b[38;5;208mOrange Text\x1b[0m";
504        let result = ansi_to_rich_text(input);
505        assert_eq!(result.len(), 1);
506        assert_eq!(result[0].text(), "Orange Text");
507    }
508
509    #[test]
510    fn test_24bit_color_parsing() {
511        let input = "\x1b[38;2;255;105;180mHot Pink Text\x1b[0m";
512        let result = ansi_to_rich_text(input);
513        assert_eq!(result.len(), 1);
514        assert_eq!(result[0].text(), "Hot Pink Text");
515    }
516
517    #[test]
518    fn test_mixed_colors() {
519        let input =
520            "Normal text \x1b[31mred\x1b[0m normal text \x1b[38;5;208morange\x1b[0m normal text";
521        let result = ansi_to_rich_text(input);
522        assert_eq!(result.len(), 5); // Normal text + red + normal text + orange + normal text
523        assert_eq!(result[0].text(), "Normal text ");
524        assert_eq!(result[1].text(), "red");
525        assert_eq!(result[2].text(), " normal text ");
526        assert_eq!(result[3].text(), "orange");
527        assert_eq!(result[4].text(), " normal text");
528    }
529
530    #[test]
531    fn test_background_color() {
532        let input = "\x1b[41mWhite on red\x1b[0m";
533        let result = ansi_to_rich_text(input);
534        assert_eq!(result.len(), 1);
535        assert_eq!(result[0].text(), "White on red");
536    }
537
538    #[test]
539    fn test_foreground_and_background_color() {
540        let input = "\x1b[31;43mYellow on red\x1b[0m";
541        let mut parser = AnsiParser::new();
542        let colored_segments = parser.parse(input);
543
544        // There should be only one text segment with both foreground and background colors
545        assert_eq!(colored_segments.len(), 1);
546        assert_eq!(colored_segments[0].text, "Yellow on red");
547        assert!(colored_segments[0].foreground_color.is_some());
548        assert!(colored_segments[0].background_color.is_some());
549    }
550
551    #[test]
552    fn test_sequential_color_changes() {
553        let input = "Default\x1b[31mRed\x1b[43mRed on yellow\x1b[32mGreen on yellow\x1b[0mDefault";
554        let mut parser = AnsiParser::new();
555        let colored_segments = parser.parse(input);
556
557        // There should be 5 text segments: Default, Red, Red on yellow, Green on yellow, Default
558        assert_eq!(colored_segments.len(), 5);
559        assert_eq!(colored_segments[0].text, "Default");
560        assert_eq!(colored_segments[1].text, "Red");
561        assert_eq!(colored_segments[2].text, "Red on yellow");
562        assert_eq!(colored_segments[3].text, "Green on yellow");
563        assert_eq!(colored_segments[4].text, "Default");
564
565        // Check colors
566        assert!(colored_segments[0].foreground_color.is_none());
567        assert!(colored_segments[0].background_color.is_none());
568
569        assert!(colored_segments[1].foreground_color.is_some());
570        assert!(colored_segments[1].background_color.is_none());
571
572        assert!(colored_segments[2].foreground_color.is_some());
573        assert!(colored_segments[2].background_color.is_some());
574
575        assert!(colored_segments[3].foreground_color.is_some());
576        assert!(colored_segments[3].background_color.is_some());
577
578        assert!(colored_segments[4].foreground_color.is_none());
579        assert!(colored_segments[4].background_color.is_none());
580    }
581
582    #[test]
583    fn test_escape_sequence_variations() {
584        // Test that escaped sequences (with double backslashes) are NOT processed
585        // Only actual control characters should be processed
586        let inputs = [
587            "\\x1b[31mRed\\x1b[0m",
588            "\\x1B[31mRed\\x1B[0m",
589            "\\X1b[31mRed\\X1b[0m",
590            "\\X1B[31mRed\\X1B[0m",
591            "\\033[31mRed\\033[0m",
592        ];
593
594        for input in inputs {
595            let mut parser = AnsiParser::new();
596            let colored_segments = parser.parse(input);
597
598            // All should return the raw text unchanged (no ANSI processing)
599            assert_eq!(colored_segments.len(), 1);
600            assert_eq!(colored_segments[0].text, input);
601            assert!(colored_segments[0].foreground_color.is_none());
602            assert!(colored_segments[0].background_color.is_none());
603        }
604    }
605
606    #[test]
607    fn test_mixed_escape_sequence_variations() {
608        // Test mixing different escape sequence representations in the same string
609        // These should NOT be processed since they are escaped sequences
610        let input = "\\x1b[31mRed\\033[32mGreen\\X1B[33mYellow\\x1B[0m";
611        let mut parser = AnsiParser::new();
612        let colored_segments = parser.parse(input);
613
614        // Should have 1 segment: the entire text unchanged
615        assert_eq!(colored_segments.len(), 1);
616        assert_eq!(colored_segments[0].text, input);
617
618        // Should have no colors
619        assert!(colored_segments[0].foreground_color.is_none());
620        assert!(colored_segments[0].background_color.is_none());
621    }
622
623    #[test]
624    fn test_ansi_to_rich_text_with_escape_variations() {
625        // Test the convenience function with escaped sequences - should NOT be processed
626        let inputs = [
627            "\\x1b[31mRed\\x1b[0m",
628            "\\x1B[31mRed\\x1B[0m",
629            "\\X1b[31mRed\\X1b[0m",
630            "\\X1B[31mRed\\X1B[0m",
631            "\\033[31mRed\\033[0m",
632        ];
633
634        for input in inputs {
635            let rich_text = ansi_to_rich_text(input);
636            assert_eq!(rich_text.len(), 1);
637            assert_eq!(rich_text[0].text(), input);
638            // Should not have any color processing
639        }
640    }
641
642    // Additional comprehensive tests for edge cases and stability
643
644    #[test]
645    fn test_empty_input() {
646        let result = ansi_to_rich_text("");
647        assert_eq!(result.len(), 1);
648        assert_eq!(result[0].text(), "");
649    }
650
651    #[test]
652    fn test_plain_text_no_escape() {
653        let input = "Hello, World!";
654        let result = ansi_to_rich_text(input);
655        assert_eq!(result.len(), 1);
656        assert_eq!(result[0].text(), "Hello, World!");
657    }
658
659    #[test]
660    fn test_reset_foreground_color() {
661        let input = "\x1b[31mRed\x1b[39mDefault";
662        let mut parser = AnsiParser::new();
663        let colored_segments = parser.parse(input);
664
665        assert_eq!(colored_segments.len(), 2);
666        assert_eq!(colored_segments[0].text, "Red");
667        assert!(colored_segments[0].foreground_color.is_some());
668        assert_eq!(colored_segments[1].text, "Default");
669        assert!(colored_segments[1].foreground_color.is_none());
670    }
671
672    #[test]
673    fn test_reset_background_color() {
674        let input = "\x1b[41mRed BG\x1b[49mDefault BG";
675        let mut parser = AnsiParser::new();
676        let colored_segments = parser.parse(input);
677
678        assert_eq!(colored_segments.len(), 2);
679        assert_eq!(colored_segments[0].text, "Red BG");
680        assert!(colored_segments[0].background_color.is_some());
681        assert_eq!(colored_segments[1].text, "Default BG");
682        assert!(colored_segments[1].background_color.is_none());
683    }
684
685    #[test]
686    fn test_bright_foreground_colors() {
687        let input = "\x1b[90mBright Black\x1b[91mBright Red\x1b[97mBright White\x1b[0m";
688        let mut parser = AnsiParser::new();
689        let colored_segments = parser.parse(input);
690
691        assert_eq!(colored_segments.len(), 3);
692        assert_eq!(colored_segments[0].text, "Bright Black");
693        assert!(colored_segments[0].foreground_color.is_some());
694        assert_eq!(colored_segments[1].text, "Bright Red");
695        assert!(colored_segments[1].foreground_color.is_some());
696        assert_eq!(colored_segments[2].text, "Bright White");
697        assert!(colored_segments[2].foreground_color.is_some());
698    }
699
700    #[test]
701    fn test_bright_background_colors() {
702        let input = "\x1b[100mBright Black BG\x1b[101mBright Red BG\x1b[107mBright White BG\x1b[0m";
703        let mut parser = AnsiParser::new();
704        let colored_segments = parser.parse(input);
705
706        assert_eq!(colored_segments.len(), 3);
707        assert_eq!(colored_segments[0].text, "Bright Black BG");
708        assert!(colored_segments[0].background_color.is_some());
709        assert_eq!(colored_segments[1].text, "Bright Red BG");
710        assert!(colored_segments[1].background_color.is_some());
711        assert_eq!(colored_segments[2].text, "Bright White BG");
712        assert!(colored_segments[2].background_color.is_some());
713    }
714
715    #[test]
716    fn test_8bit_background_color() {
717        let input = "\x1b[48;5;196mRed BG\x1b[0m";
718        let mut parser = AnsiParser::new();
719        let colored_segments = parser.parse(input);
720
721        assert_eq!(colored_segments.len(), 1);
722        assert_eq!(colored_segments[0].text, "Red BG");
723        assert!(colored_segments[0].background_color.is_some());
724    }
725
726    #[test]
727    fn test_24bit_foreground_color_value() {
728        let input = "\x1b[38;2;255;0;0mRed\x1b[0m";
729        let mut parser = AnsiParser::new();
730        let colored_segments = parser.parse(input);
731
732        assert_eq!(colored_segments.len(), 1);
733        assert_eq!(colored_segments[0].text, "Red");
734        assert_eq!(
735            colored_segments[0].foreground_color,
736            Some(Color32::from_rgb(255, 0, 0))
737        );
738    }
739
740    #[test]
741    fn test_24bit_background_color_value() {
742        let input = "\x1b[48;2;0;255;0mGreen BG\x1b[0m";
743        let mut parser = AnsiParser::new();
744        let colored_segments = parser.parse(input);
745
746        assert_eq!(colored_segments.len(), 1);
747        assert_eq!(colored_segments[0].text, "Green BG");
748        assert_eq!(
749            colored_segments[0].background_color,
750            Some(Color32::from_rgb(0, 255, 0))
751        );
752    }
753
754    #[test]
755    fn test_256_color_boundary_values() {
756        // Test boundary values for 256-color mode (standard colors, RGB cube, grayscale)
757        let input = "\x1b[38;5;0mColor0\x1b[38;5;15mColor15\x1b[38;5;16mColor16\x1b[38;5;231mColor231\x1b[38;5;232mColor232\x1b[38;5;255mColor255\x1b[0m";
758        let mut parser = AnsiParser::new();
759        let colored_segments = parser.parse(input);
760
761        assert_eq!(colored_segments.len(), 6);
762        for segment in &colored_segments {
763            assert!(segment.foreground_color.is_some());
764        }
765    }
766
767    #[test]
768    fn test_consecutive_resets() {
769        let input = "\x1b[0m\x1b[0m\x1b[0mText\x1b[0m";
770        let mut parser = AnsiParser::new();
771        let colored_segments = parser.parse(input);
772
773        assert_eq!(colored_segments.len(), 1);
774        assert_eq!(colored_segments[0].text, "Text");
775        assert!(colored_segments[0].foreground_color.is_none());
776        assert!(colored_segments[0].background_color.is_none());
777    }
778
779    #[test]
780    fn test_default_parser() {
781        let parser: AnsiParser = Default::default();
782        let mut parser = parser;
783        let result = parser.parse("\x1b[31mRed\x1b[0m");
784        assert_eq!(result.len(), 1);
785        assert_eq!(result[0].text, "Red");
786    }
787
788    #[test]
789    fn test_colored_text_constructors() {
790        let plain = ColoredText::new("Hello");
791        assert_eq!(plain.text, "Hello");
792        assert!(plain.foreground_color.is_none());
793        assert!(plain.background_color.is_none());
794
795        let fg = ColoredText::with_foreground("Hello", Color32::RED);
796        assert_eq!(fg.text, "Hello");
797        assert_eq!(fg.foreground_color, Some(Color32::RED));
798        assert!(fg.background_color.is_none());
799
800        let bg = ColoredText::with_background("Hello", Color32::BLUE);
801        assert_eq!(bg.text, "Hello");
802        assert!(bg.foreground_color.is_none());
803        assert_eq!(bg.background_color, Some(Color32::BLUE));
804
805        let both = ColoredText::with_colors("Hello", Some(Color32::RED), Some(Color32::BLUE));
806        assert_eq!(both.text, "Hello");
807        assert_eq!(both.foreground_color, Some(Color32::RED));
808        assert_eq!(both.background_color, Some(Color32::BLUE));
809    }
810
811    #[test]
812    fn test_colored_text_equality() {
813        let a = ColoredText::new("Hello");
814        let b = ColoredText::new("Hello");
815        assert_eq!(a, b);
816
817        let c = ColoredText::with_foreground("Hello", Color32::RED);
818        let d = ColoredText::with_foreground("Hello", Color32::RED);
819        assert_eq!(c, d);
820
821        let e = ColoredText::with_foreground("Hello", Color32::BLUE);
822        assert_ne!(c, e);
823    }
824
825    #[test]
826    fn test_multiline_text() {
827        let input = "\x1b[31mLine1\nLine2\x1b[0m";
828        let mut parser = AnsiParser::new();
829        let colored_segments = parser.parse(input);
830
831        assert_eq!(colored_segments.len(), 1);
832        assert_eq!(colored_segments[0].text, "Line1\nLine2");
833        assert!(colored_segments[0].foreground_color.is_some());
834    }
835
836    #[test]
837    fn test_unicode_text() {
838        let input = "\x1b[31m你好世界🎉\x1b[0m";
839        let mut parser = AnsiParser::new();
840        let colored_segments = parser.parse(input);
841
842        assert_eq!(colored_segments.len(), 1);
843        assert_eq!(colored_segments[0].text, "你好世界🎉");
844        assert!(colored_segments[0].foreground_color.is_some());
845    }
846
847    #[test]
848    fn test_4bit_color_values() {
849        use color_models::four_bit::ansi_color_to_egui;
850
851        assert_eq!(ansi_color_to_egui(0), Color32::BLACK);
852        assert_eq!(ansi_color_to_egui(1), Color32::RED);
853        assert_eq!(ansi_color_to_egui(2), Color32::GREEN);
854        assert_eq!(ansi_color_to_egui(3), Color32::YELLOW);
855        assert_eq!(ansi_color_to_egui(4), Color32::BLUE);
856    }
857
858    #[test]
859    fn test_8bit_standard_colors() {
860        use color_models::eight_bit::ansi_256_to_egui;
861
862        assert_eq!(ansi_256_to_egui(0), Color32::BLACK);
863        assert_eq!(ansi_256_to_egui(1), Color32::RED);
864        assert_eq!(ansi_256_to_egui(15), Color32::WHITE);
865    }
866
867    #[test]
868    fn test_8bit_rgb_cube() {
869        use color_models::eight_bit::ansi_256_to_egui;
870
871        // 16 = (0,0,0) = black in RGB cube
872        assert_eq!(ansi_256_to_egui(16), Color32::from_rgb(0, 0, 0));
873        // 21 = (0,0,5) = pure blue
874        assert_eq!(ansi_256_to_egui(21), Color32::from_rgb(0, 0, 255));
875        // 196 = (5,0,0) = pure red
876        assert_eq!(ansi_256_to_egui(196), Color32::from_rgb(255, 0, 0));
877    }
878
879    #[test]
880    fn test_8bit_grayscale() {
881        use color_models::eight_bit::ansi_256_to_egui;
882
883        assert_eq!(ansi_256_to_egui(232), Color32::from_rgb(8, 8, 8));
884        assert_eq!(ansi_256_to_egui(255), Color32::from_rgb(248, 248, 248));
885    }
886
887    // Tests for empty parameter reset sequence
888    #[test]
889    fn test_empty_reset_sequence() {
890        // \x1b[m should be treated as \x1b[0m (reset)
891        let input = "\x1b[31mRed\x1b[mDefault";
892        let mut parser = AnsiParser::new();
893        let colored_segments = parser.parse(input);
894
895        assert_eq!(colored_segments.len(), 2);
896        assert_eq!(colored_segments[0].text, "Red");
897        assert!(colored_segments[0].foreground_color.is_some());
898        assert_eq!(colored_segments[1].text, "Default");
899        assert!(colored_segments[1].foreground_color.is_none());
900    }
901
902    #[test]
903    fn test_empty_reset_at_start() {
904        let input = "\x1b[mPlain text";
905        let mut parser = AnsiParser::new();
906        let colored_segments = parser.parse(input);
907
908        assert_eq!(colored_segments.len(), 1);
909        assert_eq!(colored_segments[0].text, "Plain text");
910        assert!(colored_segments[0].foreground_color.is_none());
911    }
912
913    // Tests for Linux terminal prompt format
914    #[test]
915    fn test_linux_prompt_format() {
916        // Typical Linux prompt with colors: \x1b[1;31mroot\x1b[m@\x1b[1;34mhostname\x1b[m:#
917        let input = "\x1b[1;31mroot\x1b[m@\x1b[1;34mhost\x1b[m:#";
918        let mut parser = AnsiParser::new();
919        let colored_segments = parser.parse(input);
920
921        // Should have: root, @, host, :#
922        assert!(!colored_segments.is_empty());
923        // Verify text is properly separated
924        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
925        assert_eq!(combined, "root@host:#");
926    }
927
928    #[test]
929    fn test_bash_prompt_colors() {
930        // Test typical bash PS1 colors
931        let input = "\x1b[1;34muser@host\x1b[m:\x1b[1;32m~\x1b[m$ ";
932        let mut parser = AnsiParser::new();
933        let colored_segments = parser.parse(input);
934
935        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
936        assert_eq!(combined, "user@host:~$ ");
937    }
938
939    // Tests for non-SGR sequence stripping
940    #[test]
941    fn test_osc_sequence_stripped() {
942        // OSC sequence for setting window title: \x1b]0;Title\x07
943        let input = "Before\x1b]0;Window Title\x07After";
944        let mut parser = AnsiParser::new();
945        let colored_segments = parser.parse(input);
946
947        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
948        assert_eq!(combined, "BeforeAfter");
949    }
950
951    #[test]
952    fn test_osc_with_bel_terminator() {
953        let input = "\x1b]2;Title\x07Text";
954        let mut parser = AnsiParser::new();
955        let colored_segments = parser.parse(input);
956
957        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
958        assert_eq!(combined, "Text");
959    }
960
961    #[test]
962    fn test_csi_cursor_movement_ignored() {
963        // Cursor movement sequences should be ignored but not appear in output
964        let input = "\x1b[2J\x1b[H\x1b[31mRed\x1b[0m";
965        let mut parser = AnsiParser::new();
966        let colored_segments = parser.parse(input);
967
968        assert_eq!(colored_segments.len(), 1);
969        assert_eq!(colored_segments[0].text, "Red");
970    }
971
972    #[test]
973    fn test_complex_terminal_output() {
974        // Complex output with multiple sequences
975        let input = "\x1b[1;31mError:\x1b[m \x1b[33mFile not found\x1b[m\n\x1b[32mDone\x1b[m";
976        let mut parser = AnsiParser::new();
977        let colored_segments = parser.parse(input);
978
979        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
980        assert!(combined.contains("Error:"));
981        assert!(combined.contains("File not found"));
982        assert!(combined.contains("Done"));
983    }
984
985    #[test]
986    fn test_osc_without_terminator() {
987        // OSC sequence without proper terminator - should still be stripped
988        let input = "Start\x1b]0;No terminatorText";
989        let mut parser = AnsiParser::new();
990        let colored_segments = parser.parse(input);
991
992        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
993        // Should not contain the OSC sequence
994        assert!(!combined.contains("\x1b]"));
995    }
996
997    #[test]
998    fn test_reverse_video() {
999        // Test reverse video (swap foreground and background)
1000        let input = "\x1b[31;42mRed on Green\x1b[7mSwapped\x1b[0m";
1001        let mut parser = AnsiParser::new();
1002        let colored_segments = parser.parse(input);
1003
1004        assert_eq!(colored_segments.len(), 2);
1005        assert_eq!(colored_segments[0].text, "Red on Green");
1006        assert_eq!(colored_segments[1].text, "Swapped");
1007        // After reverse, foreground should be previous background
1008        assert!(colored_segments[1].foreground_color.is_some());
1009    }
1010
1011    #[test]
1012    fn test_text_attributes() {
1013        // Test that text attributes (bold, underline) are handled gracefully
1014        let input = "\x1b[1mBold\x1b[22m\x1b[4mUnderline\x1b[24mNormal";
1015        let mut parser = AnsiParser::new();
1016        let colored_segments = parser.parse(input);
1017
1018        let combined: String = colored_segments.iter().map(|s| s.text.as_str()).collect();
1019        assert_eq!(combined, "BoldUnderlineNormal");
1020    }
1021}