fast_rich/
style.rs

1//! Style and color types for terminal output.
2//!
3//! This module provides the core styling primitives used throughout fast-rich.
4//!
5//! # Examples
6//!
7//! ```
8//! use fast_rich::style::{Color, Style};
9//!
10//! let style = Style::new()
11//!     .foreground(Color::Red)
12//!     .bold()
13//!     .underline();
14//! ```
15
16use std::fmt;
17
18/// Represents a terminal color.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum Color {
21    /// Default terminal color
22    Default,
23    /// Black color
24    Black,
25    /// Red color
26    Red,
27    /// Green color
28    Green,
29    /// Yellow color
30    Yellow,
31    /// Blue color
32    Blue,
33    /// Magenta color
34    Magenta,
35    /// Cyan color
36    Cyan,
37    /// White color
38    White,
39    /// Bright black (gray)
40    BrightBlack,
41    /// Bright red
42    BrightRed,
43    /// Bright green
44    BrightGreen,
45    /// Bright yellow
46    BrightYellow,
47    /// Bright blue
48    BrightBlue,
49    /// Bright magenta
50    BrightMagenta,
51    /// Bright cyan
52    BrightCyan,
53    /// Bright white
54    BrightWhite,
55    /// 256-color palette (0-255)
56    Ansi256(u8),
57    /// True color RGB
58    Rgb { r: u8, g: u8, b: u8 },
59}
60
61impl Color {
62    /// Create a new RGB color.
63    pub fn rgb(r: u8, g: u8, b: u8) -> Self {
64        Color::Rgb { r, g, b }
65    }
66
67    /// Create a new 256-color palette color.
68    pub fn ansi256(code: u8) -> Self {
69        Color::Ansi256(code)
70    }
71
72    /// Parse a color from a string.
73    ///
74    /// Supports:
75    /// - Named colors: "red", "blue", "bright_red", etc.
76    /// - Hex colors: "#ff0000", "#f00"
77    /// - RGB: "rgb(255, 0, 0)"
78    /// - 256-color: "color(196)"
79    pub fn parse(s: &str) -> Option<Self> {
80        let s = s.trim().to_lowercase();
81
82        // Named colors
83        match s.as_str() {
84            "default" => return Some(Color::Default),
85            "black" => return Some(Color::Black),
86            "red" => return Some(Color::Red),
87            "green" => return Some(Color::Green),
88            "yellow" => return Some(Color::Yellow),
89            "blue" => return Some(Color::Blue),
90            "magenta" => return Some(Color::Magenta),
91            "cyan" => return Some(Color::Cyan),
92            "white" => return Some(Color::White),
93            "bright_black" | "brightblack" | "grey" | "gray" => return Some(Color::BrightBlack),
94            "bright_red" | "brightred" => return Some(Color::BrightRed),
95            "bright_green" | "brightgreen" => return Some(Color::BrightGreen),
96            "bright_yellow" | "brightyellow" => return Some(Color::BrightYellow),
97            "bright_blue" | "brightblue" => return Some(Color::BrightBlue),
98            "bright_magenta" | "brightmagenta" => return Some(Color::BrightMagenta),
99            "bright_cyan" | "brightcyan" => return Some(Color::BrightCyan),
100            "bright_white" | "brightwhite" => return Some(Color::BrightWhite),
101            _ => {}
102        }
103
104        // Hex colors: #rgb or #rrggbb
105        if let Some(hex) = s.strip_prefix('#') {
106            return Self::parse_hex(hex);
107        }
108
109        // RGB: rgb(r, g, b)
110        if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
111            let parts: Vec<&str> = inner.split(',').collect();
112            if parts.len() == 3 {
113                let r = parts[0].trim().parse().ok()?;
114                let g = parts[1].trim().parse().ok()?;
115                let b = parts[2].trim().parse().ok()?;
116                return Some(Color::Rgb { r, g, b });
117            }
118        }
119
120        // 256-color: color(n)
121        if let Some(inner) = s.strip_prefix("color(").and_then(|s| s.strip_suffix(')')) {
122            let code: u8 = inner.trim().parse().ok()?;
123            return Some(Color::Ansi256(code));
124        }
125
126        None
127    }
128
129    fn parse_hex(hex: &str) -> Option<Self> {
130        match hex.len() {
131            3 => {
132                // #rgb -> #rrggbb
133                let mut chars = hex.chars();
134                let r = chars.next()?;
135                let g = chars.next()?;
136                let b = chars.next()?;
137                let r = u8::from_str_radix(&format!("{r}{r}"), 16).ok()?;
138                let g = u8::from_str_radix(&format!("{g}{g}"), 16).ok()?;
139                let b = u8::from_str_radix(&format!("{b}{b}"), 16).ok()?;
140                Some(Color::Rgb { r, g, b })
141            }
142            6 => {
143                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
144                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
145                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
146                Some(Color::Rgb { r, g, b })
147            }
148            _ => None,
149        }
150    }
151
152    /// Convert to crossterm color.
153    pub fn to_crossterm(&self) -> crossterm::style::Color {
154        match self {
155            Color::Default => crossterm::style::Color::Reset,
156            Color::Black => crossterm::style::Color::Black,
157            Color::Red => crossterm::style::Color::DarkRed,
158            Color::Green => crossterm::style::Color::DarkGreen,
159            Color::Yellow => crossterm::style::Color::DarkYellow,
160            Color::Blue => crossterm::style::Color::DarkBlue,
161            Color::Magenta => crossterm::style::Color::DarkMagenta,
162            Color::Cyan => crossterm::style::Color::DarkCyan,
163            Color::White => crossterm::style::Color::Grey,
164            Color::BrightBlack => crossterm::style::Color::DarkGrey,
165            Color::BrightRed => crossterm::style::Color::Red,
166            Color::BrightGreen => crossterm::style::Color::Green,
167            Color::BrightYellow => crossterm::style::Color::Yellow,
168            Color::BrightBlue => crossterm::style::Color::Blue,
169            Color::BrightMagenta => crossterm::style::Color::Magenta,
170            Color::BrightCyan => crossterm::style::Color::Cyan,
171            Color::BrightWhite => crossterm::style::Color::White,
172            Color::Ansi256(code) => crossterm::style::Color::AnsiValue(*code),
173            Color::Rgb { r, g, b } => crossterm::style::Color::Rgb {
174                r: *r,
175                g: *g,
176                b: *b,
177            },
178        }
179    }
180
181    /// Convert color to CSS color string.
182    pub fn to_css(&self) -> String {
183        match self {
184            Color::Default => "inherit".to_string(),
185            Color::Black => "#000000".to_string(),
186            Color::Red => "#cd0000".to_string(),
187            Color::Green => "#00cd00".to_string(),
188            Color::Yellow => "#cdcd00".to_string(),
189            Color::Blue => "#0000cd".to_string(),
190            Color::Magenta => "#cd00cd".to_string(),
191            Color::Cyan => "#00cdcd".to_string(),
192            Color::White => "#e5e5e5".to_string(),
193            Color::BrightBlack => "#7f7f7f".to_string(),
194            Color::BrightRed => "#ff0000".to_string(),
195            Color::BrightGreen => "#00ff00".to_string(),
196            Color::BrightYellow => "#ffff00".to_string(),
197            Color::BrightBlue => "#5c5cff".to_string(),
198            Color::BrightMagenta => "#ff00ff".to_string(),
199            Color::BrightCyan => "#00ffff".to_string(),
200            Color::BrightWhite => "#ffffff".to_string(),
201            Color::Ansi256(code) => format!("var(--ansi-{})", code),
202            Color::Rgb { r, g, b } => format!("#{:02x}{:02x}{:02x}", r, g, b),
203        }
204    }
205
206    /// Convert to 256-color palette.
207    pub fn to_ansi256(&self) -> Self {
208        match self {
209            Color::Default => Color::Default,
210            Color::Ansi256(_) => *self,
211            Color::Rgb { r, g, b } => {
212                // Find nearest color in the 256-color palette using Euclidean distance
213                let mut min_dist = u32::MAX;
214                let mut best_idx = 0;
215
216                // Standard colors (0-15)
217                // 6x6x6 Color Cube (16-231)
218                // Grayscale (232-255)
219                // We'll iterate through all generated RGB values for 0-255
220                for i in 0..=255 {
221                    let (pr, pg, pb) = Self::ansi256_to_rgb_values(i);
222                    let dr = i32::from(*r) - i32::from(pr);
223                    let dg = i32::from(*g) - i32::from(pg);
224                    let db = i32::from(*b) - i32::from(pb);
225                    let dist = (dr * dr + dg * dg + db * db) as u32;
226
227                    if dist < min_dist {
228                        min_dist = dist;
229                        best_idx = i;
230                        if dist == 0 {
231                            break;
232                        } // Exact match
233                    }
234                }
235                Color::Ansi256(best_idx)
236            }
237            // Map named colors to their specific ANSI codes
238            Color::Black => Color::Ansi256(0),
239            Color::Red => Color::Ansi256(1),
240            Color::Green => Color::Ansi256(2),
241            Color::Yellow => Color::Ansi256(3),
242            Color::Blue => Color::Ansi256(4),
243            Color::Magenta => Color::Ansi256(5),
244            Color::Cyan => Color::Ansi256(6),
245            Color::White => Color::Ansi256(7),
246            Color::BrightBlack => Color::Ansi256(8),
247            Color::BrightRed => Color::Ansi256(9),
248            Color::BrightGreen => Color::Ansi256(10),
249            Color::BrightYellow => Color::Ansi256(11),
250            Color::BrightBlue => Color::Ansi256(12),
251            Color::BrightMagenta => Color::Ansi256(13),
252            Color::BrightCyan => Color::Ansi256(14),
253            Color::BrightWhite => Color::Ansi256(15),
254        }
255    }
256
257    /// Convert to standard 8/16-color ANSI.
258    pub fn to_standard(&self) -> Self {
259        match self {
260            Color::Default
261            | Color::Black
262            | Color::Red
263            | Color::Green
264            | Color::Yellow
265            | Color::Blue
266            | Color::Magenta
267            | Color::Cyan
268            | Color::White
269            | Color::BrightBlack
270            | Color::BrightRed
271            | Color::BrightGreen
272            | Color::BrightYellow
273            | Color::BrightBlue
274            | Color::BrightMagenta
275            | Color::BrightCyan
276            | Color::BrightWhite => *self,
277
278            Color::Ansi256(code) => {
279                if *code < 16 {
280                    // It's already in the standard range
281                    Self::from_ansi_standard_code(*code)
282                } else {
283                    // Convert 256-color to RGB, then find nearest standard color
284                    let (r, g, b) = Self::ansi256_to_rgb_values(*code);
285                    Color::Rgb { r, g, b }.to_standard()
286                }
287            }
288            Color::Rgb { r, g, b } => {
289                // Find nearest standard color
290                let palette = [
291                    (0, 0, 0),       // Black
292                    (128, 0, 0),     // Red
293                    (0, 128, 0),     // Green
294                    (128, 128, 0),   // Yellow
295                    (0, 0, 128),     // Blue
296                    (128, 0, 128),   // Magenta
297                    (0, 128, 128),   // Cyan
298                    (192, 192, 192), // White
299                    (128, 128, 128), // BrightBlack
300                    (255, 0, 0),     // BrightRed
301                    (0, 255, 0),     // BrightGreen
302                    (255, 255, 0),   // BrightYellow
303                    (0, 0, 255),     // BrightBlue
304                    (255, 0, 255),   // BrightMagenta
305                    (0, 255, 255),   // BrightCyan
306                    (255, 255, 255), // BrightWhite
307                ];
308
309                let mut min_dist = u32::MAX;
310                let mut best_idx = 0;
311
312                for (i, (pr, pg, pb)) in palette.iter().enumerate() {
313                    let dr = i32::from(*r) - pr;
314                    let dg = i32::from(*g) - pg;
315                    let db = i32::from(*b) - pb;
316                    let dist = (dr * dr + dg * dg + db * db) as u32;
317                    if dist < min_dist {
318                        min_dist = dist;
319                        best_idx = i;
320                    }
321                }
322
323                Self::from_ansi_standard_code(best_idx as u8)
324            }
325        }
326    }
327    /// Get the SGR foreground sequence for this color (Standard system only).
328    ///
329    /// Returns the ANSI sequence strings (e.g., "\x1b[31m") for standard colors.
330    /// Used when ColorSystem::Standard is enforced.
331    pub fn to_sgr_fg(&self) -> String {
332        match self {
333            Color::Black => "\x1b[30m".to_string(),
334            Color::Red => "\x1b[31m".to_string(),
335            Color::Green => "\x1b[32m".to_string(),
336            Color::Yellow => "\x1b[33m".to_string(),
337            Color::Blue => "\x1b[34m".to_string(),
338            Color::Magenta => "\x1b[35m".to_string(),
339            Color::Cyan => "\x1b[36m".to_string(),
340            Color::White => "\x1b[37m".to_string(),
341            Color::BrightBlack => "\x1b[90m".to_string(),
342            Color::BrightRed => "\x1b[91m".to_string(),
343            Color::BrightGreen => "\x1b[92m".to_string(),
344            Color::BrightYellow => "\x1b[93m".to_string(),
345            Color::BrightBlue => "\x1b[94m".to_string(),
346            Color::BrightMagenta => "\x1b[95m".to_string(),
347            Color::BrightCyan => "\x1b[96m".to_string(),
348            Color::BrightWhite => "\x1b[97m".to_string(),
349            Color::Default => "\x1b[39m".to_string(),
350            // For others, fall back to csi-wrapper (should be handled by downsampling first)
351            _ => String::new(),
352        }
353    }
354
355    /// Get the SGR background sequence for this color (Standard system only).
356    pub fn to_sgr_bg(&self) -> String {
357        match self {
358            Color::Black => "\x1b[40m".to_string(),
359            Color::Red => "\x1b[41m".to_string(),
360            Color::Green => "\x1b[42m".to_string(),
361            Color::Yellow => "\x1b[43m".to_string(),
362            Color::Blue => "\x1b[44m".to_string(),
363            Color::Magenta => "\x1b[45m".to_string(),
364            Color::Cyan => "\x1b[46m".to_string(),
365            Color::White => "\x1b[47m".to_string(),
366            Color::BrightBlack => "\x1b[100m".to_string(),
367            Color::BrightRed => "\x1b[101m".to_string(),
368            Color::BrightGreen => "\x1b[102m".to_string(),
369            Color::BrightYellow => "\x1b[103m".to_string(),
370            Color::BrightBlue => "\x1b[104m".to_string(),
371            Color::BrightMagenta => "\x1b[105m".to_string(),
372            Color::BrightCyan => "\x1b[106m".to_string(),
373            Color::BrightWhite => "\x1b[107m".to_string(),
374            Color::Default => "\x1b[49m".to_string(),
375            _ => String::new(),
376        }
377    }
378    /// Helper to convert standard ANSI code (0-15) to Color.
379    fn from_ansi_standard_code(code: u8) -> Self {
380        match code {
381            0 => Color::Black,
382            1 => Color::Red,
383            2 => Color::Green,
384            3 => Color::Yellow,
385            4 => Color::Blue,
386            5 => Color::Magenta,
387            6 => Color::Cyan,
388            7 => Color::White,
389            8 => Color::BrightBlack,
390            9 => Color::BrightRed,
391            10 => Color::BrightGreen,
392            11 => Color::BrightYellow,
393            12 => Color::BrightBlue,
394            13 => Color::BrightMagenta,
395            14 => Color::BrightCyan,
396            15 => Color::BrightWhite,
397            _ => Color::Default,
398        }
399    }
400
401    /// Helper to get RGB values for an ANSI 256 code.
402    fn ansi256_to_rgb_values(code: u8) -> (u8, u8, u8) {
403        if code < 16 {
404            // Standard colors
405            match code {
406                0 => (0, 0, 0),        // Black
407                1 => (128, 0, 0),      // Red
408                2 => (0, 128, 0),      // Green
409                3 => (128, 128, 0),    // Yellow
410                4 => (0, 0, 128),      // Blue
411                5 => (128, 0, 128),    // Magenta
412                6 => (0, 128, 128),    // Cyan
413                7 => (192, 192, 192),  // White
414                8 => (128, 128, 128),  // BrightBlack
415                9 => (255, 0, 0),      // BrightRed
416                10 => (0, 255, 0),     // BrightGreen
417                11 => (255, 255, 0),   // BrightYellow
418                12 => (0, 0, 255),     // BrightBlue
419                13 => (255, 0, 255),   // BrightMagenta
420                14 => (0, 255, 255),   // BrightCyan
421                15 => (255, 255, 255), // BrightWhite
422                _ => (0, 0, 0),
423            }
424        } else if code < 232 {
425            // 6x6x6 Color Cube
426            // Code = 16 + 36*r + 6*g + b
427            let index = code - 16;
428            let r_idx = index / 36;
429            let g_idx = (index % 36) / 6;
430            let b_idx = index % 6;
431
432            let val = |x| if x == 0 { 0 } else { x * 40 + 55 };
433            (val(r_idx), val(g_idx), val(b_idx))
434        } else {
435            // Grayscale (232-255)
436            // Code = 232 + i
437            let index = code - 232;
438            let val = index * 10 + 8;
439            (val, val, val)
440        }
441    }
442}
443
444/// Style attributes for text.
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
446pub struct Style {
447    /// Foreground color
448    pub foreground: Option<Color>,
449    /// Background color
450    pub background: Option<Color>,
451    /// Bold text
452    pub bold: bool,
453    /// Dim/faint text
454    pub dim: bool,
455    /// Italic text
456    pub italic: bool,
457    /// Underlined text
458    pub underline: bool,
459    /// Blinking text
460    pub blink: bool,
461    /// Reversed colors (fg/bg swapped)
462    pub reverse: bool,
463    /// Hidden/invisible text
464    pub hidden: bool,
465    /// Strikethrough text
466    pub strikethrough: bool,
467}
468
469impl Style {
470    /// Create a new empty style.
471    pub const fn new() -> Self {
472        Style {
473            foreground: None,
474            background: None,
475            bold: false,
476            dim: false,
477            italic: false,
478            underline: false,
479            blink: false,
480            reverse: false,
481            hidden: false,
482            strikethrough: false,
483        }
484    }
485
486    /// Set the foreground color.
487    pub fn foreground(mut self, color: Color) -> Self {
488        self.foreground = Some(color);
489        self
490    }
491
492    /// Set the background color.
493    pub fn background(mut self, color: Color) -> Self {
494        self.background = Some(color);
495        self
496    }
497
498    /// Set the foreground color (alias for consistency with Rich).
499    pub fn fg(self, color: Color) -> Self {
500        self.foreground(color)
501    }
502
503    /// Set the background color (alias for consistency with Rich).
504    pub fn bg(self, color: Color) -> Self {
505        self.background(color)
506    }
507
508    /// Enable bold.
509    pub fn bold(mut self) -> Self {
510        self.bold = true;
511        self
512    }
513
514    /// Enable dim/faint.
515    pub fn dim(mut self) -> Self {
516        self.dim = true;
517        self
518    }
519
520    /// Enable italic.
521    pub fn italic(mut self) -> Self {
522        self.italic = true;
523        self
524    }
525
526    /// Enable underline.
527    pub fn underline(mut self) -> Self {
528        self.underline = true;
529        self
530    }
531
532    /// Enable blink.
533    pub fn blink(mut self) -> Self {
534        self.blink = true;
535        self
536    }
537
538    /// Enable reverse (swap fg/bg).
539    pub fn reverse(mut self) -> Self {
540        self.reverse = true;
541        self
542    }
543
544    /// Enable hidden/invisible.
545    pub fn hidden(mut self) -> Self {
546        self.hidden = true;
547        self
548    }
549
550    /// Enable strikethrough.
551    pub fn strikethrough(mut self) -> Self {
552        self.strikethrough = true;
553        self
554    }
555
556    /// Combine this style with another, with `other` taking precedence.
557    pub fn combine(&self, other: &Style) -> Style {
558        Style {
559            foreground: other.foreground.or(self.foreground),
560            background: other.background.or(self.background),
561            bold: self.bold || other.bold,
562            dim: self.dim || other.dim,
563            italic: self.italic || other.italic,
564            underline: self.underline || other.underline,
565            blink: self.blink || other.blink,
566            reverse: self.reverse || other.reverse,
567            hidden: self.hidden || other.hidden,
568            strikethrough: self.strikethrough || other.strikethrough,
569        }
570    }
571
572    /// Check if this style has any attributes set.
573    pub fn is_empty(&self) -> bool {
574        self.foreground.is_none()
575            && self.background.is_none()
576            && !self.bold
577            && !self.dim
578            && !self.italic
579            && !self.underline
580            && !self.blink
581            && !self.reverse
582            && !self.hidden
583            && !self.strikethrough
584    }
585
586    /// Parse a style from a string.
587    ///
588    /// Supports space-separated attributes: "bold red on blue"
589    pub fn parse(s: &str) -> Self {
590        let mut style = Style::new();
591        let mut on_background = false;
592
593        for part in s.split_whitespace() {
594            let part_lower = part.to_lowercase();
595
596            if part_lower == "on" {
597                on_background = true;
598                continue;
599            }
600
601            // Check for attributes
602            match part_lower.as_str() {
603                "bold" | "b" => style.bold = true,
604                "dim" => style.dim = true,
605                "italic" | "i" => style.italic = true,
606                "underline" | "u" => style.underline = true,
607                "blink" => style.blink = true,
608                "reverse" => style.reverse = true,
609                "hidden" => style.hidden = true,
610                "strike" | "strikethrough" | "s" => style.strikethrough = true,
611                "not" => {
612                    // "not bold" etc. - skip for now, just consume
613                    continue;
614                }
615                _ => {
616                    // Try to parse as color
617                    if let Some(color) = Color::parse(&part_lower) {
618                        if on_background {
619                            style.background = Some(color);
620                            on_background = false;
621                        } else {
622                            style.foreground = Some(color);
623                        }
624                    }
625                }
626            }
627        }
628
629        style
630    }
631
632    /// Apply this style to crossterm for rendering.
633    pub fn to_crossterm_attributes(&self) -> crossterm::style::Attributes {
634        use crossterm::style::Attribute;
635        let mut attrs = crossterm::style::Attributes::default();
636
637        if self.bold {
638            attrs.set(Attribute::Bold);
639        }
640        if self.dim {
641            attrs.set(Attribute::Dim);
642        }
643        if self.italic {
644            attrs.set(Attribute::Italic);
645        }
646        if self.underline {
647            attrs.set(Attribute::Underlined);
648        }
649        if self.blink {
650            attrs.set(Attribute::SlowBlink);
651        }
652        if self.reverse {
653            attrs.set(Attribute::Reverse);
654        }
655        if self.hidden {
656            attrs.set(Attribute::Hidden);
657        }
658        if self.strikethrough {
659            attrs.set(Attribute::CrossedOut);
660        }
661
662        attrs
663    }
664
665    /// Convert this style to CSS inline style string.
666    ///
667    /// Returns a string suitable for use in HTML style attributes.
668    pub fn to_css(&self) -> String {
669        let mut parts = Vec::new();
670
671        if let Some(ref fg) = self.foreground {
672            parts.push(format!("color: {}", fg.to_css()));
673        }
674        if let Some(ref bg) = self.background {
675            parts.push(format!("background-color: {}", bg.to_css()));
676        }
677        if self.bold {
678            parts.push("font-weight: bold".to_string());
679        }
680        if self.italic {
681            parts.push("font-style: italic".to_string());
682        }
683        if self.underline {
684            parts.push("text-decoration: underline".to_string());
685        }
686        if self.strikethrough {
687            parts.push("text-decoration: line-through".to_string());
688        }
689        if self.dim {
690            parts.push("opacity: 0.5".to_string());
691        }
692
693        parts.join("; ")
694    }
695}
696
697impl fmt::Display for Style {
698    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699        let mut parts = Vec::new();
700
701        if self.bold {
702            parts.push("bold");
703        }
704        if self.dim {
705            parts.push("dim");
706        }
707        if self.italic {
708            parts.push("italic");
709        }
710        if self.underline {
711            parts.push("underline");
712        }
713        if self.strikethrough {
714            parts.push("strikethrough");
715        }
716
717        write!(f, "{}", parts.join(" "))
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724
725    #[test]
726    fn test_color_parse_named() {
727        assert_eq!(Color::parse("red"), Some(Color::Red));
728        assert_eq!(Color::parse("Blue"), Some(Color::Blue));
729        assert_eq!(Color::parse("BRIGHT_RED"), Some(Color::BrightRed));
730        assert_eq!(Color::parse("grey"), Some(Color::BrightBlack));
731    }
732
733    #[test]
734    fn test_color_parse_hex() {
735        assert_eq!(
736            Color::parse("#ff0000"),
737            Some(Color::Rgb { r: 255, g: 0, b: 0 })
738        );
739        assert_eq!(
740            Color::parse("#f00"),
741            Some(Color::Rgb { r: 255, g: 0, b: 0 })
742        );
743        assert_eq!(
744            Color::parse("#abc"),
745            Some(Color::Rgb {
746                r: 170,
747                g: 187,
748                b: 204
749            })
750        );
751    }
752
753    #[test]
754    fn test_color_parse_rgb() {
755        assert_eq!(
756            Color::parse("rgb(255, 128, 64)"),
757            Some(Color::Rgb {
758                r: 255,
759                g: 128,
760                b: 64
761            })
762        );
763    }
764
765    #[test]
766    fn test_color_parse_ansi256() {
767        assert_eq!(Color::parse("color(196)"), Some(Color::Ansi256(196)));
768    }
769
770    #[test]
771    fn test_style_parse() {
772        let style = Style::parse("bold red on blue");
773        assert!(style.bold);
774        assert_eq!(style.foreground, Some(Color::Red));
775        assert_eq!(style.background, Some(Color::Blue));
776    }
777
778    #[test]
779    fn test_style_builder() {
780        let style = Style::new().foreground(Color::Green).bold().underline();
781
782        assert!(style.bold);
783        assert!(style.underline);
784        assert_eq!(style.foreground, Some(Color::Green));
785        assert!(!style.italic);
786    }
787
788    #[test]
789    fn test_style_combine() {
790        let base = Style::new().foreground(Color::Red).bold();
791        let overlay = Style::new().foreground(Color::Blue).italic();
792        let combined = base.combine(&overlay);
793
794        assert_eq!(combined.foreground, Some(Color::Blue)); // overlay wins
795        assert!(combined.bold); // kept from base
796        assert!(combined.italic); // added from overlay
797    }
798
799    #[test]
800    fn test_style_is_empty() {
801        assert!(Style::new().is_empty());
802        assert!(!Style::new().bold().is_empty());
803    }
804}