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