Skip to main content

winx_code_agent/state/
ansi_codes.rs

1//! ANSI terminal code definitions and handlers
2//!
3//! This module provides constants and utilities for handling ANSI escape codes
4//! commonly used in terminal output, focusing on rich text formatting including
5//! 24-bit true color and extended attribute support.
6
7use regex::Regex;
8use std::collections::HashMap;
9use std::str::FromStr;
10use std::sync::OnceLock;
11
12static ANSI_REGEX: OnceLock<std::result::Result<Regex, regex::Error>> = OnceLock::new();
13const BASIC_COLORS: &[(&str, u8)] = &[
14    ("black", 0),
15    ("red", 1),
16    ("green", 2),
17    ("yellow", 3),
18    ("blue", 4),
19    ("magenta", 5),
20    ("cyan", 6),
21    ("white", 7),
22    ("brightblack", 8),
23    ("brightred", 9),
24    ("brightgreen", 10),
25    ("brightyellow", 11),
26    ("brightblue", 12),
27    ("brightmagenta", 13),
28    ("brightcyan", 14),
29    ("brightwhite", 15),
30];
31
32fn ansi_regex() -> Option<&'static Regex> {
33    ANSI_REGEX.get_or_init(|| Regex::new(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")).as_ref().ok()
34}
35
36/// Basic ANSI control codes
37pub mod control {
38    /// Bell
39    pub const BEL: &str = "\x07";
40    /// Backspace
41    pub const BS: &str = "\x08";
42    /// Horizontal tab
43    pub const HT: &str = "\x09";
44    /// Line feed
45    pub const LF: &str = "\x0A";
46    /// Vertical tab
47    pub const VT: &str = "\x0B";
48    /// Form feed
49    pub const FF: &str = "\x0C";
50    /// Carriage return
51    pub const CR: &str = "\x0D";
52    /// Escape
53    pub const ESC: &str = "\x1B";
54    /// Delete
55    pub const DEL: &str = "\x7F";
56}
57
58/// CSI (Control Sequence Introducer) sequences
59pub mod csi {
60    /// CSI sequence start
61    pub const CSI: &str = "\x1B[";
62
63    /// Cursor Up
64    pub fn cursor_up(n: usize) -> String {
65        format!("\x1B[{n}A")
66    }
67
68    /// Cursor Down
69    pub fn cursor_down(n: usize) -> String {
70        format!("\x1B[{n}B")
71    }
72
73    /// Cursor Forward
74    pub fn cursor_forward(n: usize) -> String {
75        format!("\x1B[{n}C")
76    }
77
78    /// Cursor Back
79    pub fn cursor_back(n: usize) -> String {
80        format!("\x1B[{n}D")
81    }
82
83    /// Cursor Next Line
84    pub fn cursor_next_line(n: usize) -> String {
85        format!("\x1B[{n}E")
86    }
87
88    /// Cursor Previous Line
89    pub fn cursor_prev_line(n: usize) -> String {
90        format!("\x1B[{n}F")
91    }
92
93    /// Cursor Horizontal Absolute
94    pub fn cursor_horizontal(n: usize) -> String {
95        format!("\x1B[{n}G")
96    }
97
98    /// Cursor Position (row, column)
99    pub fn cursor_position(row: usize, col: usize) -> String {
100        format!("\x1B[{row};{col}H")
101    }
102
103    /// Erase in Display
104    pub fn erase_in_display(n: usize) -> String {
105        format!("\x1B[{n}J")
106    }
107
108    /// Erase in Line
109    pub fn erase_in_line(n: usize) -> String {
110        format!("\x1B[{n}K")
111    }
112
113    /// Scroll Up
114    pub fn scroll_up(n: usize) -> String {
115        format!("\x1B[{n}S")
116    }
117
118    /// Scroll Down
119    pub fn scroll_down(n: usize) -> String {
120        format!("\x1B[{n}T")
121    }
122
123    /// Request Cursor Position
124    pub const REQUEST_CURSOR_POSITION: &str = "\x1B[6n";
125
126    /// Save Cursor Position
127    pub const SAVE_CURSOR_POSITION: &str = "\x1B[s";
128
129    /// Restore Cursor Position
130    pub const RESTORE_CURSOR_POSITION: &str = "\x1B[u";
131
132    /// Hide Cursor
133    pub const HIDE_CURSOR: &str = "\x1B[?25l";
134
135    /// Show Cursor
136    pub const SHOW_CURSOR: &str = "\x1B[?25h";
137
138    /// Enable Alternative Screen Buffer
139    pub const ENABLE_ALT_SCREEN: &str = "\x1B[?1049h";
140
141    /// Disable Alternative Screen Buffer
142    pub const DISABLE_ALT_SCREEN: &str = "\x1B[?1049l";
143}
144
145/// SGR (Select Graphic Rendition) for text styling
146pub mod sgr {
147    /// Reset all attributes
148    pub const RESET: &str = "\x1B[0m";
149
150    /// Bold
151    pub const BOLD: &str = "\x1B[1m";
152
153    /// Faint/Dim
154    pub const DIM: &str = "\x1B[2m";
155
156    /// Italic
157    pub const ITALIC: &str = "\x1B[3m";
158
159    /// Underline
160    pub const UNDERLINE: &str = "\x1B[4m";
161
162    /// Slow Blink
163    pub const BLINK: &str = "\x1B[5m";
164
165    /// Rapid Blink
166    pub const RAPID_BLINK: &str = "\x1B[6m";
167
168    /// Reverse Video
169    pub const REVERSE: &str = "\x1B[7m";
170
171    /// Conceal/Hide
172    pub const CONCEAL: &str = "\x1B[8m";
173
174    /// Crossed-out/Strike
175    pub const STRIKE: &str = "\x1B[9m";
176
177    /// Primary/Default Font
178    pub const PRIMARY_FONT: &str = "\x1B[10m";
179
180    /// Alternative Font 1-9
181    pub fn alt_font(n: usize) -> String {
182        if !(1..=9).contains(&n) {
183            return String::new();
184        }
185        format!("\x1B[{}m", 10 + n)
186    }
187
188    /// Fraktur (Gothic)
189    pub const FRAKTUR: &str = "\x1B[20m";
190
191    /// Double Underline
192    pub const DOUBLE_UNDERLINE: &str = "\x1B[21m";
193
194    /// Normal Intensity (not bold and not faint)
195    pub const NORMAL_INTENSITY: &str = "\x1B[22m";
196
197    /// Not Italic, Not Fraktur
198    pub const NO_ITALIC: &str = "\x1B[23m";
199
200    /// Not Underlined
201    pub const NO_UNDERLINE: &str = "\x1B[24m";
202
203    /// Not Blinking
204    pub const NO_BLINK: &str = "\x1B[25m";
205
206    /// Proportional Spacing
207    pub const PROPORTIONAL_SPACING: &str = "\x1B[26m";
208
209    /// Not Reversed
210    pub const NO_REVERSE: &str = "\x1B[27m";
211
212    /// Reveal (Not Concealed)
213    pub const REVEAL: &str = "\x1B[28m";
214
215    /// Not Crossed Out
216    pub const NO_STRIKE: &str = "\x1B[29m";
217
218    /// Foreground Color (30-37 for basic colors, 90-97 for bright)
219    pub fn fg_color(n: usize) -> String {
220        format!("\x1B[{n}m")
221    }
222
223    /// Background Color (40-47 for basic colors, 100-107 for bright)
224    pub fn bg_color(n: usize) -> String {
225        format!("\x1B[{n}m")
226    }
227
228    /// 8-bit Foreground Color (0-255)
229    pub fn fg_color_256(n: u8) -> String {
230        format!("\x1B[38;5;{n}m")
231    }
232
233    /// 8-bit Background Color (0-255)
234    pub fn bg_color_256(n: u8) -> String {
235        format!("\x1B[48;5;{n}m")
236    }
237
238    /// 24-bit Foreground Color (RGB)
239    pub fn fg_color_rgb(r: u8, g: u8, b: u8) -> String {
240        format!("\x1B[38;2;{r};{g};{b}m")
241    }
242
243    /// 24-bit Background Color (RGB)
244    pub fn bg_color_rgb(r: u8, g: u8, b: u8) -> String {
245        format!("\x1B[48;2;{r};{g};{b}m")
246    }
247
248    /// Default Foreground Color
249    pub const DEFAULT_FG: &str = "\x1B[39m";
250
251    /// Default Background Color
252    pub const DEFAULT_BG: &str = "\x1B[49m";
253
254    /// Disable Proportional Spacing
255    pub const NO_PROPORTIONAL_SPACING: &str = "\x1B[50m";
256
257    /// Framed
258    pub const FRAMED: &str = "\x1B[51m";
259
260    /// Encircled
261    pub const ENCIRCLED: &str = "\x1B[52m";
262
263    /// Overlined
264    pub const OVERLINED: &str = "\x1B[53m";
265
266    /// Not Framed, Not Encircled
267    pub const NO_FRAMED: &str = "\x1B[54m";
268
269    /// Not Overlined
270    pub const NO_OVERLINED: &str = "\x1B[55m";
271
272    /// Ideogram Underline
273    pub const IDEOGRAM_UNDERLINE: &str = "\x1B[60m";
274
275    /// Ideogram Double Underline
276    pub const IDEOGRAM_DOUBLE_UNDERLINE: &str = "\x1B[61m";
277
278    /// Ideogram Overline
279    pub const IDEOGRAM_OVERLINE: &str = "\x1B[62m";
280
281    /// Ideogram Double Overline
282    pub const IDEOGRAM_DOUBLE_OVERLINE: &str = "\x1B[63m";
283
284    /// Ideogram Stress Marking
285    pub const IDEOGRAM_STRESS: &str = "\x1B[64m";
286
287    /// No Ideogram Attributes
288    pub const NO_IDEOGRAM: &str = "\x1B[65m";
289
290    /// Superscript
291    pub const SUPERSCRIPT: &str = "\x1B[73m";
292
293    /// Subscript
294    pub const SUBSCRIPT: &str = "\x1B[74m";
295
296    /// Neither Superscript nor Subscript
297    pub const NO_SCRIPT: &str = "\x1B[75m";
298}
299
300/// OSC (Operating System Command) sequences
301pub mod osc {
302    /// Set window title
303    pub fn set_title(title: &str) -> String {
304        format!("\x1B]0;{title}\x07")
305    }
306
307    /// Set window and icon title
308    pub fn set_window_icon_title(title: &str) -> String {
309        format!("\x1B]2;{title}\x07")
310    }
311
312    /// Set icon title
313    pub fn set_icon_title(title: &str) -> String {
314        format!("\x1B]1;{title}\x07")
315    }
316
317    /// Set color definition
318    pub fn set_color(num: u8, rgb: &str) -> String {
319        format!("\x1B]4;{num};{rgb}\x07")
320    }
321
322    /// Hyperlink
323    pub fn hyperlink(url: &str, text: &str) -> String {
324        format!("\x1B]8;;{url}\x07{text}\x1B]8;;\x07")
325    }
326}
327
328/// Mouse reporting modes
329pub mod mouse {
330    /// Enable normal mouse tracking
331    pub const NORMAL_TRACKING: &str = "\x1B[?1000h";
332
333    /// Disable normal mouse tracking
334    pub const NO_NORMAL_TRACKING: &str = "\x1B[?1000l";
335
336    /// Enable highlight mouse tracking
337    pub const HIGHLIGHT_TRACKING: &str = "\x1B[?1001h";
338
339    /// Disable highlight mouse tracking
340    pub const NO_HIGHLIGHT_TRACKING: &str = "\x1B[?1001l";
341
342    /// Enable button-event tracking
343    pub const BUTTON_EVENT_TRACKING: &str = "\x1B[?1002h";
344
345    /// Disable button-event tracking
346    pub const NO_BUTTON_EVENT_TRACKING: &str = "\x1B[?1002l";
347
348    /// Enable any-event tracking
349    pub const ANY_EVENT_TRACKING: &str = "\x1B[?1003h";
350
351    /// Disable any-event tracking
352    pub const NO_ANY_EVENT_TRACKING: &str = "\x1B[?1003l";
353
354    /// Enable focus tracking
355    pub const FOCUS_TRACKING: &str = "\x1B[?1004h";
356
357    /// Disable focus tracking
358    pub const NO_FOCUS_TRACKING: &str = "\x1B[?1004l";
359
360    /// Enable extended mouse coordinates
361    pub const EXTENDED_COORDINATES: &str = "\x1B[?1006h";
362
363    /// Disable extended mouse coordinates
364    pub const NO_EXTENDED_COORDINATES: &str = "\x1B[?1006l";
365
366    /// Enable SGR mouse coordinates
367    pub const SGR_COORDINATES: &str = "\x1B[?1016h";
368
369    /// Disable SGR mouse coordinates
370    pub const NO_SGR_COORDINATES: &str = "\x1B[?1016l";
371}
372
373/// Mode switching sequences
374pub mod modes {
375    /// Application Cursor Keys (DECCKM)
376    pub const APPLICATION_CURSOR_KEYS: &str = "\x1B[?1h";
377
378    /// Normal Cursor Keys
379    pub const NORMAL_CURSOR_KEYS: &str = "\x1B[?1l";
380
381    /// ANSI Mode (vs VT52)
382    pub const ANSI_MODE: &str = "\x1B[?2h";
383
384    /// VT52 Mode
385    pub const VT52_MODE: &str = "\x1B[?2l";
386
387    /// 132 Column Mode (DECCOLM)
388    pub const MODE_132_COLUMN: &str = "\x1B[?3h";
389
390    /// 80 Column Mode
391    pub const MODE_80_COLUMN: &str = "\x1B[?3l";
392
393    /// Smooth Scroll (DECSCLM)
394    pub const SMOOTH_SCROLL: &str = "\x1B[?4h";
395
396    /// Jump Scroll
397    pub const JUMP_SCROLL: &str = "\x1B[?4l";
398
399    /// Reverse Screen (DECSCNM)
400    pub const REVERSE_SCREEN: &str = "\x1B[?5h";
401
402    /// Normal Screen
403    pub const NORMAL_SCREEN: &str = "\x1B[?5l";
404
405    /// Application Keypad (DECNKM)
406    pub const APPLICATION_KEYPAD: &str = "\x1B[?66h";
407
408    /// Numeric Keypad
409    pub const NUMERIC_KEYPAD: &str = "\x1B[?66l";
410
411    /// Wraparound Mode (DECAWM)
412    pub const WRAPAROUND: &str = "\x1B[?7h";
413
414    /// No Wraparound
415    pub const NO_WRAPAROUND: &str = "\x1B[?7l";
416
417    /// Auto-repeat Keys (DECARM)
418    pub const AUTOREPEAT_KEYS: &str = "\x1B[?8h";
419
420    /// No Auto-repeat Keys
421    pub const NO_AUTOREPEAT_KEYS: &str = "\x1B[?8l";
422
423    /// Send Mouse X & Y on button press
424    pub const MOUSE_TRACKING: &str = "\x1B[?9h";
425
426    /// No Mouse Tracking
427    pub const NO_MOUSE_TRACKING: &str = "\x1B[?9l";
428
429    /// Show toolbar (rxvt)
430    pub const SHOW_TOOLBAR: &str = "\x1B[?10h";
431
432    /// Hide toolbar
433    pub const HIDE_TOOLBAR: &str = "\x1B[?10l";
434
435    /// Start Blinking Cursor
436    pub const BLINKING_CURSOR: &str = "\x1B[?12h";
437
438    /// Stop Blinking Cursor
439    pub const NO_BLINKING_CURSOR: &str = "\x1B[?12l";
440
441    /// Print Form Feed (DECPFF)
442    pub const PRINT_FORM_FEED: &str = "\x1B[?18h";
443
444    /// No Form Feed
445    pub const NO_PRINT_FORM_FEED: &str = "\x1B[?18l";
446
447    /// Set Print Screen (DECPEX)
448    pub const PRINT_SCREEN: &str = "\x1B[?19h";
449
450    /// No Print Screen
451    pub const NO_PRINT_SCREEN: &str = "\x1B[?19l";
452
453    /// Enable Linefeed/Newline Mode (LNM)
454    pub const NEWLINE_MODE: &str = "\x1B[20h";
455
456    /// Disable Linefeed/Newline Mode
457    pub const NO_NEWLINE_MODE: &str = "\x1B[20l";
458}
459
460/// Terminal color code definitions
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub enum TermColor {
463    /// Standard basic color (0-15)
464    Basic(u8),
465
466    /// 256-color mode (0-255)
467    Color256(u8),
468
469    /// 24-bit RGB color
470    TrueColor {
471        /// Red component (0-255)
472        r: u8,
473        /// Green component (0-255)
474        g: u8,
475        /// Blue component (0-255)
476        b: u8,
477    },
478}
479
480impl TermColor {
481    /// Get the ANSI code for foreground color
482    pub fn fg_code(&self) -> String {
483        match self {
484            TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 30 + n),
485            TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 82 + n),
486            TermColor::Basic(n) | TermColor::Color256(n) => sgr::fg_color_256(*n),
487            TermColor::TrueColor { r, g, b } => sgr::fg_color_rgb(*r, *g, *b),
488        }
489    }
490
491    /// Get the ANSI code for background color
492    pub fn bg_code(&self) -> String {
493        match self {
494            TermColor::Basic(n) if *n < 8 => format!("\x1B[{}m", 40 + n),
495            TermColor::Basic(n) if *n < 16 => format!("\x1B[{}m", 92 + n),
496            TermColor::Basic(n) | TermColor::Color256(n) => sgr::bg_color_256(*n),
497            TermColor::TrueColor { r, g, b } => sgr::bg_color_rgb(*r, *g, *b),
498        }
499    }
500}
501
502/// Parse ANSI escape sequences in text
503///
504/// This extracts all escape sequences based on their type and position
505///
506/// # Arguments
507///
508/// * `text` - The text containing ANSI escape sequences
509///
510/// # Returns
511///
512/// A vector of (position, sequence) tuples
513pub fn parse_ansi_sequences(text: &str) -> Vec<(usize, String)> {
514    ansi_regex().map_or_else(Vec::new, |regex| {
515        regex.find_iter(text).map(|m| (m.start(), m.as_str().to_string())).collect()
516    })
517}
518
519/// Color name to ANSI code mapping
520pub fn color_name_to_code(name: &str) -> Option<TermColor> {
521    // Try as a basic color name
522    if let Some((_, code)) = BASIC_COLORS.iter().find(|(color, _)| color.eq_ignore_ascii_case(name))
523    {
524        return Some(TermColor::Basic(*code));
525    }
526
527    // Try as a color number first (before hex parsing)
528    if let Ok(num) = u8::from_str(name) {
529        return Some(TermColor::Color256(num));
530    }
531
532    // Try as a hex color (only if it starts with # or is clearly hex)
533    if name.starts_with('#') || (name.len() == 6 && name.chars().all(|c| c.is_ascii_hexdigit())) {
534        if let Some(color) = parse_hex_color(name) {
535            return Some(color);
536        }
537    }
538
539    None
540}
541
542/// Parse a hex color into `TermColor`
543///
544/// Supports formats like #RGB, #RRGGBB
545fn parse_hex_color(hex: &str) -> Option<TermColor> {
546    let hex = hex.trim_start_matches('#');
547
548    match hex.len() {
549        3 => {
550            // #RGB format
551            let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
552            let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
553            let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
554
555            // Convert from 0-15 to 0-255 range
556            let r = r * 17;
557            let g = g * 17;
558            let b = b * 17;
559
560            Some(TermColor::TrueColor { r, g, b })
561        }
562        6 => {
563            // #RRGGBB format
564            let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
565            let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
566            let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
567
568            Some(TermColor::TrueColor { r, g, b })
569        }
570        _ => None,
571    }
572}
573
574/// Format text with ANSI styling
575///
576/// This is a convenience function to easily add common ANSI styles
577///
578/// # Arguments
579///
580/// * `text` - The text to format
581/// * `bold` - Whether to make the text bold
582/// * `italic` - Whether to make the text italic
583/// * `underline` - Whether to underline the text
584/// * `fg_color` - Optional foreground color
585/// * `bg_color` - Optional background color
586///
587/// # Returns
588///
589/// The formatted text with ANSI codes
590pub fn format_ansi_text(
591    text: &str,
592    bold: bool,
593    italic: bool,
594    underline: bool,
595    fg_color: Option<&TermColor>,
596    bg_color: Option<&TermColor>,
597) -> String {
598    let mut result = String::new();
599
600    // Add style codes
601    if bold {
602        result.push_str(sgr::BOLD);
603    }
604    if italic {
605        result.push_str(sgr::ITALIC);
606    }
607    if underline {
608        result.push_str(sgr::UNDERLINE);
609    }
610
611    // Add color codes
612    if let Some(color) = fg_color {
613        result.push_str(&color.fg_code());
614    }
615    if let Some(color) = bg_color {
616        result.push_str(&color.bg_code());
617    }
618
619    // Add text and reset
620    result.push_str(text);
621    result.push_str(sgr::RESET);
622
623    result
624}
625
626/// Strip all ANSI escape sequences from text
627///
628/// # Arguments
629///
630/// * `text` - The text containing ANSI escape sequences
631///
632/// # Returns
633///
634/// The text with all ANSI sequences removed
635pub fn strip_ansi_codes(text: &str) -> String {
636    ansi_regex().map_or_else(|| text.to_string(), |regex| regex.replace_all(text, "").to_string())
637}
638
639/// Extract structured style information from ANSI-formatted text
640///
641/// # Arguments
642///
643/// * `text` - The text with ANSI escape sequences
644///
645/// # Returns
646///
647/// A vector of (position, styles) where styles is a map of active styles
648pub fn extract_ansi_styles(text: &str) -> Vec<(usize, HashMap<String, String>)> {
649    let mut result = Vec::new();
650    let mut current_styles = HashMap::new();
651    let mut pos = 0;
652
653    for (idx, seq) in parse_ansi_sequences(text) {
654        // Adjust position for any actual text content
655        if idx > pos {
656            result.push((pos, current_styles.clone()));
657            // No need to update pos here as it's updated below
658        }
659
660        // Process the sequence and update styles
661        if seq == sgr::RESET {
662            current_styles.clear();
663        } else if seq == sgr::BOLD {
664            current_styles.insert("weight".to_string(), "bold".to_string());
665        } else if seq == sgr::ITALIC {
666            current_styles.insert("style".to_string(), "italic".to_string());
667        } else if seq == sgr::UNDERLINE {
668            current_styles.insert("text-decoration".to_string(), "underline".to_string());
669        } else if seq.starts_with("\x1B[38;") {
670            current_styles.insert("color".to_string(), extract_color_from_seq(&seq));
671        } else if seq.starts_with("\x1B[48;") {
672            current_styles.insert("background-color".to_string(), extract_color_from_seq(&seq));
673        }
674
675        // Move position past the sequence
676        pos = idx + seq.len();
677    }
678
679    // Add final segment if needed
680    if pos < text.len() {
681        result.push((pos, current_styles));
682    }
683
684    result
685}
686
687/// Extract color information from an SGR color sequence
688fn extract_color_from_seq(seq: &str) -> String {
689    if seq.starts_with("\x1B[38;5;") || seq.starts_with("\x1B[48;5;") {
690        // 8-bit color
691        let parts: Vec<&str> = seq.split(';').collect();
692        if parts.len() >= 3 {
693            if let Some(color_part) = parts[2].strip_suffix('m') {
694                return format!("color-{color_part}");
695            }
696        }
697    } else if seq.starts_with("\x1B[38;2;") || seq.starts_with("\x1B[48;2;") {
698        // 24-bit color
699        let parts: Vec<&str> = seq.split(';').collect();
700        if parts.len() >= 5 {
701            let r = parts[2];
702            let g = parts[3];
703            let b =
704                if let Some(stripped) = parts[4].strip_suffix('m') { stripped } else { parts[4] };
705            return format!("rgb({r},{g},{b})");
706        }
707    }
708
709    "unknown".to_string()
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn test_term_color() {
718        let red = TermColor::Basic(1);
719        assert_eq!(red.fg_code(), "\x1B[31m");
720        assert_eq!(red.bg_code(), "\x1B[41m");
721
722        let color256 = TermColor::Color256(128);
723        assert_eq!(color256.fg_code(), "\x1B[38;5;128m");
724        assert_eq!(color256.bg_code(), "\x1B[48;5;128m");
725
726        let true_color = TermColor::TrueColor { r: 255, g: 128, b: 64 };
727        assert_eq!(true_color.fg_code(), "\x1B[38;2;255;128;64m");
728        assert_eq!(true_color.bg_code(), "\x1B[48;2;255;128;64m");
729    }
730
731    #[test]
732    fn test_color_name_to_code() {
733        assert_eq!(color_name_to_code("red"), Some(TermColor::Basic(1)));
734        assert_eq!(color_name_to_code("brightblue"), Some(TermColor::Basic(12)));
735        assert_eq!(color_name_to_code("123"), Some(TermColor::Color256(123)));
736
737        assert_eq!(
738            color_name_to_code("#ff00ff"),
739            Some(TermColor::TrueColor { r: 255, g: 0, b: 255 })
740        );
741
742        assert_eq!(color_name_to_code("#f0f"), Some(TermColor::TrueColor { r: 255, g: 0, b: 255 }));
743    }
744
745    #[test]
746    fn test_format_ansi_text() {
747        let text = format_ansi_text("Hello", true, false, true, Some(&TermColor::Basic(1)), None);
748        assert_eq!(text, "\x1B[1m\x1B[4m\x1B[31mHello\x1B[0m");
749    }
750
751    #[test]
752    fn test_strip_ansi_codes() {
753        let input = "\x1B[1m\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m";
754        let output = strip_ansi_codes(input);
755        assert_eq!(output, "Hello World");
756    }
757
758    #[test]
759    fn test_parse_ansi_sequences() {
760        let input = "Normal \x1B[1mBold\x1B[0m Normal";
761        let sequences = parse_ansi_sequences(input);
762        assert_eq!(sequences.len(), 2);
763        assert_eq!(sequences[0], (7, "\x1B[1m".to_string()));
764        assert_eq!(sequences[1], (15, "\x1B[0m".to_string()));
765    }
766}