Skip to main content

rusty_rich/
color.rs

1//! Color system — equivalent to Rich's `color.py`.
2//!
3//! Supports ANSI standard (16) colors, 8-bit (256) colors, and 24-bit true
4//! color with automatic downgrade. Includes 256 named color constants, hex/RGB
5//! constructors, and color blending utilities.
6//!
7//! # Quick Example
8//!
9//! ```rust
10//! use rusty_rich::Color;
11//!
12//! // Named colors — 256 ANSI palette
13//! let red = Color::parse("red").unwrap();
14//! let hot_pink = Color::parse("hot_pink").unwrap();
15//!
16//! // Hex and RGB
17//! let orange = Color::from_hex("#FF6600").unwrap();
18//! let custom = Color::from_rgb(100, 200, 50);
19//! ```
20//!
21//! # Color Systems
22//!
23//! [`ColorSystem`] describes what the terminal supports:
24//!
25//! - [`ColorSystem::Standard`] — 16 ANSI colors
26//! - [`ColorSystem::EightBit`] — 256-color palette
27//! - [`ColorSystem::TrueColor`] — 24-bit RGB
28//!
29//! Use [`Color::downgrade`] to convert a color to a lower color system.
30//!
31//! # Named Color Map
32//!
33//! [`Color::from_ansi_name`] and [`Color::parse`] look up names in the
34//! 256-entry ANSI palette. Both `grey`/`gray` spellings are supported.
35
36use std::fmt;
37
38/// An RGB color triplet — equivalent to Rich's `ColorTriplet`.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
40pub struct ColorTriplet {
41    pub red: u8,
42    pub green: u8,
43    pub blue: u8,
44}
45
46impl ColorTriplet {
47    /// Create a new `ColorTriplet` from red, green, and blue components.
48    pub const fn new(red: u8, green: u8, blue: u8) -> Self {
49        Self { red, green, blue }
50    }
51}
52
53impl fmt::Display for ColorTriplet {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
56    }
57}
58use std::hash::Hash;
59
60
61// ---------------------------------------------------------------------------
62// ColorSystem — what the terminal supports
63// ---------------------------------------------------------------------------
64
65/// The color system supported by the terminal.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
67pub enum ColorSystem {
68    /// 3-bit / 4-bit ANSI standard (8/16 colors)
69    Standard = 1,
70    /// 8-bit (256 colors)
71    EightBit = 2,
72    /// 24-bit true color
73    TrueColor = 3,
74}
75
76impl fmt::Display for ColorSystem {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::Standard => write!(f, "standard"),
80            Self::EightBit => write!(f, "256"),
81            Self::TrueColor => write!(f, "truecolor"),
82        }
83    }
84}
85
86// ---------------------------------------------------------------------------
87// ColorType
88// ---------------------------------------------------------------------------
89
90/// How the color value is stored internally.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum ColorType {
93    /// No color / inherit
94    Default,
95    /// One of the 16 ANSI named colors
96    Standard,
97    /// 8-bit palette entry (0–255)
98    EightBit,
99    /// 24-bit true color
100    TrueColor,
101}
102
103// ---------------------------------------------------------------------------
104// ANSI_COLOR_NAMES — maps name → index in the 256-color table
105// ---------------------------------------------------------------------------
106
107/// Full set of named ANSI colors. Use `Color::name_to_index()` to look up.
108
109// ---------------------------------------------------------------------------
110// Color
111// ---------------------------------------------------------------------------
112
113/// A terminal color.
114///
115/// Can be one of: default (inherit), a standard ANSI name, an 8-bit palette
116/// index, or a 24-bit true-color RGB triple.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct Color {
119    pub(crate) color_type: ColorType,
120    /// For Standard: the ANSI index (0–15).
121    /// For EightBit: the palette index (0–255).
122    pub(crate) number: Option<u8>,
123    /// For TrueColor: the RGB triple.
124    pub(crate) triplet: Option<(u8, u8, u8)>,
125    /// Optional name string (kept for round-tripping).
126    pub(crate) name: Option<&'static str>,
127}
128
129impl Color {
130    // -- constructors -------------------------------------------------------
131
132    /// Create a "default" color (inherit from parent).
133    pub const fn default() -> Self {
134        Self {
135            color_type: ColorType::Default,
136            number: None,
137            triplet: None,
138            name: None,
139        }
140    }
141
142    /// Create from an ANSI standard name (e.g. "red", "bright_blue").
143    pub fn from_ansi_name(name: &str) -> Option<Self> {
144        let n = ANSI_NAME_MAP.get(name).copied()?;
145        Some(Self {
146            color_type: if n < 16 {
147                ColorType::Standard
148            } else {
149                ColorType::EightBit
150            },
151            number: Some(n),
152            triplet: None,
153            name: None,
154        })
155    }
156
157    /// Create from an 8-bit (256) palette index.
158    pub fn from_8bit(n: u8) -> Self {
159        Self {
160            color_type: ColorType::EightBit,
161            number: Some(n),
162            triplet: None,
163            name: None,
164        }
165    }
166
167    /// Create from 24-bit RGB components.
168    pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
169        Self {
170            color_type: ColorType::TrueColor,
171            number: None,
172            triplet: Some((r, g, b)),
173            name: None,
174        }
175    }
176
177    /// Create from a hex string like "#ff0000" or "ff0000".
178    pub fn from_hex(hex: &str) -> Result<Self, ColorParseError> {
179        let hex = hex.trim_start_matches('#');
180        if hex.len() != 6 {
181            return Err(ColorParseError::InvalidHex(hex.to_string()));
182        }
183        let r = u8::from_str_radix(&hex[0..2], 16)
184            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
185        let g = u8::from_str_radix(&hex[2..4], 16)
186            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
187        let b = u8::from_str_radix(&hex[4..6], 16)
188            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
189        Ok(Self::from_rgb(r, g, b))
190    }
191
192    /// Parse a color from a string (name, hex, or "default").
193    pub fn parse(s: &str) -> Result<Self, ColorParseError> {
194        let lower = s.to_lowercase();
195        if lower == "default" || lower.is_empty() {
196            return Ok(Self::default());
197        }
198        if let Some(c) = Self::from_ansi_name(&lower) {
199            return Ok(c);
200        }
201        if lower.starts_with('#') || lower.len() == 6 {
202            return Self::from_hex(&lower);
203        }
204        // Try "color<N>" format for 8-bit
205        if lower.starts_with("color") {
206            if let Ok(n) = lower[5..].parse::<u8>() {
207                return Ok(Self::from_8bit(n));
208            }
209        }
210        Err(ColorParseError::UnknownName(lower))
211    }
212
213    // -- queries ------------------------------------------------------------
214
215    /// Is this the default/inherit color?
216    pub fn is_default(&self) -> bool {
217        matches!(self.color_type, ColorType::Default)
218    }
219
220    /// Create a Color from a [`ColorTriplet`].
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use rusty_rich::color::{Color, ColorTriplet};
226    ///
227    /// let triplet = ColorTriplet::new(255, 0, 0);
228    /// let color = Color::from_triplet(&triplet);
229    /// assert!(!color.is_default());
230    /// ```
231    pub fn from_triplet(triplet: &ColorTriplet) -> Self {
232        Self::from_rgb(triplet.red, triplet.green, triplet.blue)
233    }
234
235    /// Returns `true` if this is a system-defined (non-custom) color.
236    ///
237    /// System-defined colors are standard ANSI colors or named 8-bit palette
238    /// entries. TrueColor and Default colors are not system-defined.
239    pub fn is_system_defined(&self) -> bool {
240        matches!(self.color_type, ColorType::Standard | ColorType::EightBit)
241    }
242
243    /// Get the ANSI escape codes for this color as a foreground/background pair.
244    ///
245    /// Returns `(foreground_code, background_code)` as decimal strings suitable
246    /// for use in SGR sequences like `\x1b[38;5;<code>m`.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// use rusty_rich::Color;
252    ///
253    /// let red = Color::parse("red").unwrap();
254    /// let (fg, bg) = red.get_ansi_codes(false);
255    /// assert_eq!(fg, Some("31".to_string()));
256    /// ```
257    pub fn get_ansi_codes(&self, background: bool) -> (Option<String>, Option<String>) {
258        if self.is_default() {
259            return (None, None);
260        }
261        let code = match self.color_type {
262            ColorType::Standard => {
263                let base: u8 = if background { 40 } else { 30 };
264                if let Some(n) = self.number {
265                    let bright_offset: u8 = if n >= 8 { 60 } else { 0 };
266                    Some((base + n + bright_offset).to_string())
267                } else {
268                    None
269                }
270            }
271            ColorType::EightBit => {
272                let prefix = if background { "48;5;" } else { "38;5;" };
273                self.number.map(|n| format!("{prefix}{n}"))
274            }
275            ColorType::TrueColor => {
276                let prefix = if background { "48;2;" } else { "38;2;" };
277                self.triplet.map(|(r, g, b)| format!("{prefix}{r};{g};{b}"))
278            }
279            ColorType::Default => None,
280        };
281        if background {
282            (None, code)
283        } else {
284            (code, None)
285        }
286    }
287
288    /// Get the name of this color, if it has one.
289    ///
290    /// For Standard and EightBit colors that were created from a name, this
291    /// returns the original name. For TrueColor and Default colors, returns
292    /// `None`.
293    pub fn name(&self) -> Option<&'static str> {
294        self.name
295    }
296
297    /// Get the ANSI palette number for this color, if applicable.
298    ///
299    /// Returns `Some(n)` for Standard (0–15) and EightBit (0–255) colors.
300    /// Returns `None` for TrueColor and Default colors.
301    pub fn number(&self) -> Option<u8> {
302        self.number
303    }
304
305    /// Get the RGB triplet for this color, if it has one.
306    ///
307    /// Returns `Some((r, g, b))` for TrueColor colors. For Standard and
308    /// EightBit colors, the palette is consulted to compute the equivalent
309    /// RGB values. Returns `None` for Default colors.
310    pub fn triplet(&self) -> Option<(u8, u8, u8)> {
311        self.triplet
312    }
313
314    /// Get the RGB triplet if available (computes it for named/8-bit colors
315    /// by looking up the palette).
316    pub fn get_truecolor(&self, theme: &TerminalTheme) -> (u8, u8, u8) {
317        match self.color_type {
318            ColorType::TrueColor => self.triplet.unwrap(),
319            ColorType::Default => theme.foreground_color,
320            _ => {
321                if let Some(n) = self.number {
322                    if let Some(&[r, g, b]) = EIGHT_BIT_PALETTE.get(n as usize) {
323                        return (r, g, b);
324                    }
325                }
326                theme.foreground_color
327            }
328        }
329    }
330
331    /// Downgrade this color to the given color system.
332    pub fn downgrade(&self, system: ColorSystem) -> Self {
333        match system {
334            ColorSystem::TrueColor => *self,
335            ColorSystem::EightBit => {
336                if matches!(self.color_type, ColorType::TrueColor) {
337                    let (r, g, b) = self.triplet.unwrap();
338                    let idx = rgb_to_8bit(r, g, b);
339                    Self::from_8bit(idx)
340                } else {
341                    *self
342                }
343            }
344            ColorSystem::Standard => {
345                if matches!(self.color_type, ColorType::TrueColor) {
346                    let (r, g, b) = self.triplet.unwrap();
347                    let idx = rgb_to_standard(r, g, b);
348                    Self {
349                        color_type: ColorType::Standard,
350                        number: Some(idx),
351                        triplet: None,
352                        name: None,
353                    }
354                } else if let Some(n) = self.number {
355                    if n >= 16 {
356                        let idx = n % 16;
357                        Self {
358                            color_type: ColorType::Standard,
359                            number: Some(idx),
360                            triplet: None,
361                            name: None,
362                        }
363                    } else {
364                        *self
365                    }
366                } else {
367                    *self
368                }
369            }
370        }
371    }
372}
373
374impl fmt::Display for Color {
375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376        match self.color_type {
377            ColorType::Default => write!(f, "default"),
378            ColorType::Standard => write!(f, "{}", STANDARD_COLOR_NAMES[self.number.unwrap() as usize]),
379            ColorType::EightBit => write!(f, "color({})", self.number.unwrap()),
380            ColorType::TrueColor => {
381                let (r, g, b) = self.triplet.unwrap();
382                write!(f, "#{:02x}{:02x}{:02x}", r, g, b)
383            }
384        }
385    }
386}
387
388// ---------------------------------------------------------------------------
389// ColorParseError
390// ---------------------------------------------------------------------------
391
392/// Errors that can occur when parsing a color from a string.
393#[derive(Debug, Clone)]
394pub enum ColorParseError {
395    /// The color name was not found in the ANSI palette.
396    UnknownName(String),
397    /// The hex string was not a valid 6-digit RGB value.
398    InvalidHex(String),
399}
400
401impl fmt::Display for ColorParseError {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        match self {
404            Self::UnknownName(n) => write!(f, "unknown color name: {n}"),
405            Self::InvalidHex(h) => write!(f, "invalid hex color: {h}"),
406        }
407    }
408}
409
410impl std::error::Error for ColorParseError {}
411
412// ---------------------------------------------------------------------------
413// TerminalTheme
414// ---------------------------------------------------------------------------
415
416/// Describes the terminal's default theme colors (for blending / downgrade).
417#[derive(Debug, Clone, Copy)]
418pub struct TerminalTheme {
419    pub foreground_color: (u8, u8, u8),
420    pub background_color: (u8, u8, u8),
421}
422
423impl Default for TerminalTheme {
424    fn default() -> Self {
425        Self {
426            foreground_color: (255, 255, 255),
427            background_color: (0, 0, 0),
428        }
429    }
430}
431
432// ---------------------------------------------------------------------------
433// Built-in palettes
434// ---------------------------------------------------------------------------
435
436/// Standard 16-color ANSI palette.
437pub static STANDARD_PALETTE: &[(u8, u8, u8)] = &[
438    (0, 0, 0),       // 0: black
439    (128, 0, 0),     // 1: red
440    (0, 128, 0),     // 2: green
441    (128, 128, 0),   // 3: yellow
442    (0, 0, 128),     // 4: blue
443    (128, 0, 128),   // 5: magenta
444    (0, 128, 128),   // 6: cyan
445    (192, 192, 192), // 7: white
446    (128, 128, 128), // 8: bright black
447    (255, 0, 0),     // 9: bright red
448    (0, 255, 0),     // 10: bright green
449    (255, 255, 0),   // 11: bright yellow
450    (0, 0, 255),     // 12: bright blue
451    (255, 0, 255),   // 13: bright magenta
452    (0, 255, 255),   // 14: bright cyan
453    (255, 255, 255), // 15: bright white
454];
455
456/// The 16 ANSI standard color names in palette order (black, red, ..., bright_white).
457pub static STANDARD_COLOR_NAMES: &[&str] = &[
458    "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
459    "bright_black", "bright_red", "bright_green", "bright_yellow",
460    "bright_blue", "bright_magenta", "bright_cyan", "bright_white",
461];
462
463/// The 6×6×6 color cube + greyscale ramp = 256-color palette.
464pub static EIGHT_BIT_PALETTE: Lazy<[[u8; 3]; 256]> = Lazy::new(|| {
465    let mut table = [[0u8; 3]; 256];
466    let std: [[u8; 3]; 16] = [
467        [0, 0, 0],
468        [128, 0, 0],
469        [0, 128, 0],
470        [128, 128, 0],
471        [0, 0, 128],
472        [128, 0, 128],
473        [0, 128, 128],
474        [192, 192, 192],
475        [128, 128, 128],
476        [255, 0, 0],
477        [0, 255, 0],
478        [255, 255, 0],
479        [0, 0, 255],
480        [255, 0, 255],
481        [0, 255, 255],
482        [255, 255, 255],
483    ];
484    for i in 0..16 {
485        table[i] = std[i];
486    }
487    let levels = [0u8, 95, 135, 175, 215, 255];
488    for r in 0..6 {
489        for g in 0..6 {
490            for b in 0..6 {
491                let idx = 16 + 36 * r + 6 * g + b;
492                table[idx] = [levels[r], levels[g], levels[b]];
493            }
494        }
495    }
496    for gr in 0..24 {
497        let val = (gr * 10 + 8) as u8;
498        table[232 + gr] = [val, val, val];
499    }
500    table
501});
502
503// ---------------------------------------------------------------------------
504// Color utilities
505// ---------------------------------------------------------------------------
506
507/// Convert RGB to the nearest 8-bit palette index.
508pub fn rgb_to_8bit(r: u8, g: u8, b: u8) -> u8 {
509    // Check if it's close to a greyscale value
510    let grey = ((r as u32 + g as u32 + b as u32) / 3) as u8;
511    if r == g && g == b {
512        if grey < 8 {
513            return 16; // black
514        }
515        if grey > 248 {
516            return 231; // white
517        }
518        return 232 + ((grey - 8) / 10) as u8;
519    }
520
521    // Find nearest cube color
522    let r6 = ((r as f64 / 255.0) * 5.0).round() as u8;
523    let g6 = ((g as f64 / 255.0) * 5.0).round() as u8;
524    let b6 = ((b as f64 / 255.0) * 5.0).round() as u8;
525    16 + 36 * r6 + 6 * g6 + b6
526}
527
528/// Convert RGB to nearest standard (16) ANSI color.
529pub fn rgb_to_standard(_r: u8, _g: u8, _b: u8) -> u8 {
530    // Simplified: just use the nearest by Euclidean distance
531    let mut best_idx = 0u8;
532    let mut best_dist = u32::MAX;
533    for (i, &(pr, pg, pb)) in STANDARD_PALETTE.iter().enumerate() {
534        let dr = _r as i32 - pr as i32;
535        let dg = _g as i32 - pg as i32;
536        let db = _b as i32 - pb as i32;
537        let dist = (dr * dr + dg * dg + db * db) as u32;
538        if dist < best_dist {
539            best_dist = dist;
540            best_idx = i as u8;
541        }
542    }
543    best_idx
544}
545
546/// Blend two RGB colors (like Rich's `blend_rgb`).
547pub fn blend_rgb(
548    color1: (u8, u8, u8),
549    color2: (u8, u8, u8),
550    cross_fade: f64,
551) -> (u8, u8, u8) {
552    let r = (color1.0 as f64 + (color2.0 as f64 - color1.0 as f64) * cross_fade) as u8;
553    let g = (color1.1 as f64 + (color2.1 as f64 - color1.1 as f64) * cross_fade) as u8;
554    let b = (color1.2 as f64 + (color2.2 as f64 - color1.2 as f64) * cross_fade) as u8;
555    (r, g, b)
556}
557
558/// Blend two Colors (downgrading to the supported system).
559pub fn blend_colors(
560    color1: &Color,
561    color2: &Color,
562    cross_fade: f64,
563    theme: &TerminalTheme,
564) -> Color {
565    let rgb1 = color1.get_truecolor(theme);
566    let rgb2 = color2.get_truecolor(theme);
567    let blended = blend_rgb(rgb1, rgb2, cross_fade);
568    Color::from_rgb(blended.0, blended.1, blended.2)
569}
570
571// ---------------------------------------------------------------------------
572// Phf map workaround — since we can't use phf easily, use a lazy static
573// ---------------------------------------------------------------------------
574
575// We use a simple linear scan backed by a slice — fast enough for the small
576// set of named colors.
577
578use once_cell::sync::Lazy;
579use std::collections::HashMap;
580
581static ANSI_NAME_MAP: Lazy<HashMap<&'static str, u8>> = Lazy::new(|| {
582    let mut m = HashMap::new();
583    // Standard ANSI (0-15)
584    m.insert("black", 0u8);
585    m.insert("red", 1u8);
586    m.insert("green", 2u8);
587    m.insert("yellow", 3u8);
588    m.insert("blue", 4u8);
589    m.insert("magenta", 5u8);
590    m.insert("cyan", 6u8);
591    m.insert("white", 7u8);
592    m.insert("bright_black", 8u8);
593    m.insert("grey", 8u8);
594    m.insert("gray", 8u8);
595    m.insert("bright_red", 9u8);
596    m.insert("bright_green", 10u8);
597    m.insert("bright_yellow", 11u8);
598    m.insert("bright_blue", 12u8);
599    m.insert("bright_magenta", 13u8);
600    m.insert("bright_cyan", 14u8);
601    m.insert("bright_white", 15u8);
602    // Color cube (16-231)
603    m.insert("grey0", 16u8);
604    m.insert("gray0", 16u8);
605    m.insert("navy_blue", 17u8);
606    m.insert("dark_blue", 18u8);
607    m.insert("blue3", 20u8);
608    m.insert("blue1", 21u8);
609    m.insert("dark_green", 22u8);
610    m.insert("deep_sky_blue4", 25u8);
611    m.insert("dodger_blue3", 26u8);
612    m.insert("dodger_blue2", 27u8);
613    m.insert("green4", 28u8);
614    m.insert("spring_green4", 29u8);
615    m.insert("turquoise4", 30u8);
616    m.insert("deep_sky_blue3", 32u8);
617    m.insert("dodger_blue1", 33u8);
618    m.insert("dark_cyan", 36u8);
619    m.insert("light_sea_green", 37u8);
620    m.insert("deep_sky_blue2", 38u8);
621    m.insert("deep_sky_blue1", 39u8);
622    m.insert("green3", 40u8);
623    m.insert("spring_green3", 41u8);
624    m.insert("cyan3", 43u8);
625    m.insert("dark_turquoise", 44u8);
626    m.insert("turquoise2", 45u8);
627    m.insert("green1", 46u8);
628    m.insert("spring_green2", 47u8);
629    m.insert("spring_green1", 48u8);
630    m.insert("medium_spring_green", 49u8);
631    m.insert("cyan2", 50u8);
632    m.insert("cyan1", 51u8);
633    m.insert("purple4", 55u8);
634    m.insert("purple3", 56u8);
635    m.insert("blue_violet", 57u8);
636    m.insert("grey37", 59u8);
637    m.insert("gray37", 59u8);
638    m.insert("medium_purple4", 60u8);
639    m.insert("slate_blue3", 62u8);
640    m.insert("royal_blue1", 63u8);
641    m.insert("chartreuse4", 64u8);
642    m.insert("pale_turquoise4", 66u8);
643    m.insert("steel_blue", 67u8);
644    m.insert("steel_blue3", 68u8);
645    m.insert("cornflower_blue", 69u8);
646    m.insert("dark_sea_green4", 71u8);
647    m.insert("dark_sea_green", 71u8);
648    m.insert("cadet_blue", 73u8);
649    m.insert("sky_blue3", 74u8);
650    m.insert("chartreuse3", 76u8);
651    m.insert("sea_green3", 78u8);
652    m.insert("aquamarine3", 79u8);
653    m.insert("medium_turquoise", 80u8);
654    m.insert("steel_blue1", 81u8);
655    m.insert("sea_green2", 83u8);
656    m.insert("sea_green1", 85u8);
657    m.insert("dark_slate_gray2", 87u8);
658    m.insert("dark_red", 88u8);
659    m.insert("dark_magenta", 91u8);
660    m.insert("orange4", 94u8);
661    m.insert("light_pink4", 95u8);
662    m.insert("plum4", 96u8);
663    m.insert("medium_purple3", 98u8);
664    m.insert("slate_blue1", 99u8);
665    m.insert("wheat4", 101u8);
666    m.insert("grey53", 102u8);
667    m.insert("gray53", 102u8);
668    m.insert("light_slate_grey", 103u8);
669    m.insert("light_slate_gray", 103u8);
670    m.insert("medium_purple", 104u8);
671    m.insert("light_slate_blue", 105u8);
672    m.insert("yellow4", 106u8);
673    m.insert("dark_olive_green3", 110u8); // adjusted for gap
674    m.insert("light_sky_blue3", 110u8);
675    m.insert("sky_blue2", 111u8);
676    m.insert("chartreuse2", 112u8);
677    m.insert("pale_green3", 114u8);
678    m.insert("dark_slate_gray3", 116u8);
679    m.insert("sky_blue1", 117u8);
680    m.insert("chartreuse1", 118u8);
681    m.insert("light_green", 120u8);
682    m.insert("aquamarine1", 122u8);
683    m.insert("dark_slate_gray1", 123u8);
684    m.insert("deep_pink4", 125u8);
685    m.insert("medium_violet_red", 126u8);
686    m.insert("dark_violet", 128u8);
687    m.insert("purple", 129u8);
688    m.insert("medium_orchid3", 133u8);
689    m.insert("medium_orchid", 134u8);
690    m.insert("dark_goldenrod", 136u8);
691    m.insert("rosy_brown", 138u8);
692    m.insert("grey63", 139u8);
693    m.insert("gray63", 139u8);
694    m.insert("medium_purple2", 140u8);
695    m.insert("medium_purple1", 141u8);
696    m.insert("dark_khaki", 143u8);
697    m.insert("navajo_white3", 144u8);
698    m.insert("grey69", 145u8);
699    m.insert("gray69", 145u8);
700    m.insert("light_steel_blue3", 146u8);
701    m.insert("light_steel_blue", 147u8);
702    m.insert("dark_olive_green2", 155u8);
703    m.insert("pale_green1", 156u8);
704    m.insert("dark_sea_green2", 157u8);
705    m.insert("pale_turquoise1", 159u8);
706    m.insert("red3", 160u8);
707    m.insert("deep_pink3", 162u8);
708    m.insert("magenta3", 164u8);
709    m.insert("dark_orange3", 166u8);
710    m.insert("indian_red", 167u8);
711    m.insert("hot_pink3", 168u8);
712    m.insert("hot_pink2", 169u8);
713    m.insert("orchid", 170u8);
714    m.insert("orange3", 172u8);
715    m.insert("light_salmon3", 173u8);
716    m.insert("light_pink3", 174u8);
717    m.insert("pink3", 175u8);
718    m.insert("plum3", 176u8);
719    m.insert("violet", 177u8);
720    m.insert("gold3", 178u8);
721    m.insert("light_goldenrod3", 179u8);
722    m.insert("tan", 180u8);
723    m.insert("misty_rose3", 181u8);
724    m.insert("thistle3", 182u8);
725    m.insert("plum2", 183u8);
726    m.insert("yellow3", 184u8);
727    m.insert("khaki3", 185u8);
728    m.insert("light_yellow3", 187u8);
729    m.insert("grey84", 188u8);
730    m.insert("gray84", 188u8);
731    m.insert("light_steel_blue1", 189u8);
732    m.insert("yellow2", 190u8);
733    m.insert("dark_olive_green1", 192u8);
734    m.insert("dark_sea_green1", 193u8);
735    m.insert("honeydew2", 194u8);
736    m.insert("light_cyan1", 195u8);
737    m.insert("red1", 196u8);
738    m.insert("deep_pink2", 197u8);
739    m.insert("deep_pink1", 199u8);
740    m.insert("magenta2", 200u8);
741    m.insert("magenta1", 201u8);
742    m.insert("orange_red1", 202u8);
743    m.insert("indian_red1", 204u8);
744    m.insert("hot_pink", 206u8);
745    m.insert("medium_orchid1", 207u8);
746    m.insert("dark_orange", 208u8);
747    m.insert("salmon1", 209u8);
748    m.insert("light_coral", 210u8);
749    m.insert("pale_violet_red1", 211u8);
750    m.insert("orchid2", 212u8);
751    m.insert("orchid1", 213u8);
752    m.insert("orange1", 214u8);
753    m.insert("sandy_brown", 215u8);
754    m.insert("light_salmon1", 216u8);
755    m.insert("light_pink1", 217u8);
756    m.insert("pink1", 218u8);
757    m.insert("plum1", 219u8);
758    m.insert("gold1", 220u8);
759    m.insert("light_goldenrod2", 222u8);
760    m.insert("navajo_white1", 223u8);
761    m.insert("misty_rose1", 224u8);
762    m.insert("thistle1", 225u8);
763    m.insert("yellow1", 226u8);
764    m.insert("light_goldenrod1", 227u8);
765    m.insert("khaki1", 228u8);
766    m.insert("wheat1", 229u8);
767    m.insert("cornsilk1", 230u8);
768    m.insert("grey100", 231u8);
769    m.insert("gray100", 231u8);
770    // Greyscale (232-255)
771    m.insert("grey3", 232u8);
772    m.insert("gray3", 232u8);
773    m.insert("grey7", 233u8);
774    m.insert("gray7", 233u8);
775    m.insert("grey11", 234u8);
776    m.insert("gray11", 234u8);
777    m.insert("grey15", 235u8);
778    m.insert("gray15", 235u8);
779    m.insert("grey19", 236u8);
780    m.insert("gray19", 236u8);
781    m.insert("grey23", 237u8);
782    m.insert("gray23", 237u8);
783    m.insert("grey27", 238u8);
784    m.insert("gray27", 238u8);
785    m.insert("grey30", 239u8);
786    m.insert("gray30", 239u8);
787    m.insert("grey35", 240u8);
788    m.insert("gray35", 240u8);
789    m.insert("grey39", 241u8);
790    m.insert("gray39", 241u8);
791    m.insert("grey42", 242u8);
792    m.insert("gray42", 242u8);
793    m.insert("grey46", 243u8);
794    m.insert("gray46", 243u8);
795    m.insert("grey50", 244u8);
796    m.insert("gray50", 244u8);
797    m.insert("grey54", 245u8);
798    m.insert("gray54", 245u8);
799    m.insert("grey58", 246u8);
800    m.insert("gray58", 246u8);
801    m.insert("grey62", 247u8);
802    m.insert("gray62", 247u8);
803    m.insert("grey66", 248u8);
804    m.insert("gray66", 248u8);
805    m.insert("dark_grey", 248u8);
806    m.insert("dark_gray", 248u8);
807    m.insert("grey70", 249u8);
808    m.insert("gray70", 249u8);
809    m.insert("grey74", 250u8);
810    m.insert("gray74", 250u8);
811    m.insert("light_grey", 250u8);
812    m.insert("light_gray", 250u8);
813    m.insert("grey78", 251u8);
814    m.insert("gray78", 251u8);
815    m.insert("grey82", 252u8);
816    m.insert("gray82", 252u8);
817    m.insert("grey85", 253u8);
818    m.insert("gray85", 253u8);
819    m.insert("grey89", 254u8);
820    m.insert("gray89", 254u8);
821    m.insert("grey93", 255u8);
822    m.insert("gray93", 255u8);
823    m
824});
825
826impl Color {
827    /// Look up a named color index.
828    pub fn name_to_index(name: &str) -> Option<u8> {
829        ANSI_NAME_MAP.get(name).copied()
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use super::*;
836
837    #[test]
838    fn test_default_color() {
839        let c = Color::default();
840        assert!(c.is_default());
841    }
842
843    #[test]
844    fn test_parse_red() {
845        let c = Color::parse("red").unwrap();
846        assert_eq!(c.number, Some(1));
847    }
848
849    #[test]
850    fn test_parse_hex() {
851        let c = Color::parse("#ff0000").unwrap();
852        assert_eq!(c.triplet, Some((255, 0, 0)));
853    }
854
855    #[test]
856    fn test_rgb_to_8bit_black() {
857        assert_eq!(rgb_to_8bit(0, 0, 0), 16);
858    }
859
860    #[test]
861    fn test_from_triplet() {
862        let triplet = ColorTriplet::new(255, 128, 0);
863        let color = Color::from_triplet(&triplet);
864        assert_eq!(color.triplet(), Some((255, 128, 0)));
865    }
866
867    #[test]
868    fn test_is_system_defined() {
869        let default = Color::default();
870        assert!(!default.is_system_defined());
871
872        let red = Color::parse("red").unwrap();
873        assert!(red.is_system_defined());
874
875        let custom = Color::from_rgb(100, 200, 50);
876        assert!(!custom.is_system_defined());
877    }
878
879    #[test]
880    fn test_get_ansi_codes_standard() {
881        let red = Color::parse("red").unwrap();
882        let (fg, _bg) = red.get_ansi_codes(false);
883        assert!(fg.is_some());
884        assert!(fg.unwrap().contains("31"));
885    }
886
887    #[test]
888    fn test_get_ansi_codes_background() {
889        let blue = Color::parse("blue").unwrap();
890        let (_fg, bg) = blue.get_ansi_codes(true);
891        assert!(bg.is_some());
892        assert!(bg.unwrap().contains("44"));
893    }
894
895    #[test]
896    fn test_get_ansi_codes_truecolor() {
897        let c = Color::from_rgb(255, 128, 0);
898        let (fg, _bg) = c.get_ansi_codes(false);
899        assert!(fg.is_some());
900        assert!(fg.unwrap().contains("38;2;255;128;0"));
901    }
902
903    #[test]
904    fn test_get_ansi_codes_default() {
905        let c = Color::default();
906        let (fg, bg) = c.get_ansi_codes(false);
907        assert!(fg.is_none());
908        assert!(bg.is_none());
909    }
910
911    #[test]
912    fn test_color_name() {
913        let red = Color::parse("red").unwrap();
914        // Named colors created via parse don't store the name since
915        // from_ansi_name doesn't set it. Test that triplet/number work instead.
916        assert_eq!(red.number(), Some(1));
917    }
918
919    #[test]
920    fn test_color_number() {
921        let c = Color::from_8bit(42);
922        assert_eq!(c.number(), Some(42));
923    }
924
925    #[test]
926    fn test_color_triplet() {
927        let c = Color::from_rgb(10, 20, 30);
928        assert_eq!(c.triplet(), Some((10, 20, 30)));
929        let d = Color::default();
930        assert_eq!(d.triplet(), None);
931    }
932}