Skip to main content

rusty_rich/
color.rs

1//! Color system — equivalent to Rich's `color.py`.
2//!
3//! Supports ANSI standard colors, 8-bit (256) colors, and 24-bit true color.
4//! Includes named color constants and color blending.
5
6use std::fmt;
7
8/// An RGB color triplet — equivalent to Rich's `ColorTriplet`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
10pub struct ColorTriplet {
11    pub red: u8,
12    pub green: u8,
13    pub blue: u8,
14}
15
16impl ColorTriplet {
17    pub const fn new(red: u8, green: u8, blue: u8) -> Self {
18        Self { red, green, blue }
19    }
20}
21
22impl fmt::Display for ColorTriplet {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        write!(f, "#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
25    }
26}
27use std::hash::Hash;
28
29
30// ---------------------------------------------------------------------------
31// ColorSystem — what the terminal supports
32// ---------------------------------------------------------------------------
33
34/// The color system supported by the terminal.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum ColorSystem {
37    /// 3-bit / 4-bit ANSI standard (8/16 colors)
38    Standard = 1,
39    /// 8-bit (256 colors)
40    EightBit = 2,
41    /// 24-bit true color
42    TrueColor = 3,
43}
44
45impl fmt::Display for ColorSystem {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Standard => write!(f, "standard"),
49            Self::EightBit => write!(f, "256"),
50            Self::TrueColor => write!(f, "truecolor"),
51        }
52    }
53}
54
55// ---------------------------------------------------------------------------
56// ColorType
57// ---------------------------------------------------------------------------
58
59/// How the color value is stored internally.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum ColorType {
62    /// No color / inherit
63    Default,
64    /// One of the 16 ANSI named colors
65    Standard,
66    /// 8-bit palette entry (0–255)
67    EightBit,
68    /// 24-bit true color
69    TrueColor,
70}
71
72// ---------------------------------------------------------------------------
73// ANSI_COLOR_NAMES — maps name → index in the 256-color table
74// ---------------------------------------------------------------------------
75
76/// Full set of named ANSI colors. Use `Color::name_to_index()` to look up.
77
78// ---------------------------------------------------------------------------
79// Color
80// ---------------------------------------------------------------------------
81
82/// A terminal color.
83///
84/// Can be one of: default (inherit), a standard ANSI name, an 8-bit palette
85/// index, or a 24-bit true-color RGB triple.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub struct Color {
88    pub(crate) color_type: ColorType,
89    /// For Standard: the ANSI index (0–15).
90    /// For EightBit: the palette index (0–255).
91    pub(crate) number: Option<u8>,
92    /// For TrueColor: the RGB triple.
93    pub(crate) triplet: Option<(u8, u8, u8)>,
94    /// Optional name string (kept for round-tripping).
95    pub(crate) name: Option<&'static str>,
96}
97
98impl Color {
99    // -- constructors -------------------------------------------------------
100
101    /// Create a "default" color (inherit from parent).
102    pub const fn default() -> Self {
103        Self {
104            color_type: ColorType::Default,
105            number: None,
106            triplet: None,
107            name: None,
108        }
109    }
110
111    /// Create from an ANSI standard name (e.g. "red", "bright_blue").
112    pub fn from_ansi_name(name: &str) -> Option<Self> {
113        let n = ANSI_NAME_MAP.get(name).copied()?;
114        Some(Self {
115            color_type: if n < 16 {
116                ColorType::Standard
117            } else {
118                ColorType::EightBit
119            },
120            number: Some(n),
121            triplet: None,
122            name: None,
123        })
124    }
125
126    /// Create from an 8-bit (256) palette index.
127    pub fn from_8bit(n: u8) -> Self {
128        Self {
129            color_type: ColorType::EightBit,
130            number: Some(n),
131            triplet: None,
132            name: None,
133        }
134    }
135
136    /// Create from 24-bit RGB components.
137    pub fn from_rgb(r: u8, g: u8, b: u8) -> Self {
138        Self {
139            color_type: ColorType::TrueColor,
140            number: None,
141            triplet: Some((r, g, b)),
142            name: None,
143        }
144    }
145
146    /// Create from a hex string like "#ff0000" or "ff0000".
147    pub fn from_hex(hex: &str) -> Result<Self, ColorParseError> {
148        let hex = hex.trim_start_matches('#');
149        if hex.len() != 6 {
150            return Err(ColorParseError::InvalidHex(hex.to_string()));
151        }
152        let r = u8::from_str_radix(&hex[0..2], 16)
153            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
154        let g = u8::from_str_radix(&hex[2..4], 16)
155            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
156        let b = u8::from_str_radix(&hex[4..6], 16)
157            .map_err(|_| ColorParseError::InvalidHex(hex.to_string()))?;
158        Ok(Self::from_rgb(r, g, b))
159    }
160
161    /// Parse a color from a string (name, hex, or "default").
162    pub fn parse(s: &str) -> Result<Self, ColorParseError> {
163        let lower = s.to_lowercase();
164        if lower == "default" || lower.is_empty() {
165            return Ok(Self::default());
166        }
167        if let Some(c) = Self::from_ansi_name(&lower) {
168            return Ok(c);
169        }
170        if lower.starts_with('#') || lower.len() == 6 {
171            return Self::from_hex(&lower);
172        }
173        // Try "color<N>" format for 8-bit
174        if lower.starts_with("color") {
175            if let Ok(n) = lower[5..].parse::<u8>() {
176                return Ok(Self::from_8bit(n));
177            }
178        }
179        Err(ColorParseError::UnknownName(lower))
180    }
181
182    // -- queries ------------------------------------------------------------
183
184    /// Is this the default/inherit color?
185    pub fn is_default(&self) -> bool {
186        matches!(self.color_type, ColorType::Default)
187    }
188
189    /// Get the RGB triplet if available (computes it for named/8-bit colors
190    /// by looking up the palette).
191    pub fn get_truecolor(&self, theme: &TerminalTheme) -> (u8, u8, u8) {
192        match self.color_type {
193            ColorType::TrueColor => self.triplet.unwrap(),
194            ColorType::Default => theme.foreground_color,
195            _ => {
196                if let Some(n) = self.number {
197                    if let Some(&[r, g, b]) = EIGHT_BIT_PALETTE.get(n as usize) {
198                        return (r, g, b);
199                    }
200                }
201                theme.foreground_color
202            }
203        }
204    }
205
206    /// Downgrade this color to the given color system.
207    pub fn downgrade(&self, system: ColorSystem) -> Self {
208        match system {
209            ColorSystem::TrueColor => *self,
210            ColorSystem::EightBit => {
211                if matches!(self.color_type, ColorType::TrueColor) {
212                    let (r, g, b) = self.triplet.unwrap();
213                    let idx = rgb_to_8bit(r, g, b);
214                    Self::from_8bit(idx)
215                } else {
216                    *self
217                }
218            }
219            ColorSystem::Standard => {
220                if matches!(self.color_type, ColorType::TrueColor) {
221                    let (r, g, b) = self.triplet.unwrap();
222                    let idx = rgb_to_standard(r, g, b);
223                    Self {
224                        color_type: ColorType::Standard,
225                        number: Some(idx),
226                        triplet: None,
227                        name: None,
228                    }
229                } else if let Some(n) = self.number {
230                    if n >= 16 {
231                        let idx = n % 16;
232                        Self {
233                            color_type: ColorType::Standard,
234                            number: Some(idx),
235                            triplet: None,
236                            name: None,
237                        }
238                    } else {
239                        *self
240                    }
241                } else {
242                    *self
243                }
244            }
245        }
246    }
247}
248
249impl fmt::Display for Color {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        match self.color_type {
252            ColorType::Default => write!(f, "default"),
253            ColorType::Standard => write!(f, "{}", STANDARD_COLOR_NAMES[self.number.unwrap() as usize]),
254            ColorType::EightBit => write!(f, "color({})", self.number.unwrap()),
255            ColorType::TrueColor => {
256                let (r, g, b) = self.triplet.unwrap();
257                write!(f, "#{:02x}{:02x}{:02x}", r, g, b)
258            }
259        }
260    }
261}
262
263// ---------------------------------------------------------------------------
264// ColorParseError
265// ---------------------------------------------------------------------------
266
267#[derive(Debug, Clone)]
268pub enum ColorParseError {
269    UnknownName(String),
270    InvalidHex(String),
271}
272
273impl fmt::Display for ColorParseError {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self {
276            Self::UnknownName(n) => write!(f, "unknown color name: {n}"),
277            Self::InvalidHex(h) => write!(f, "invalid hex color: {h}"),
278        }
279    }
280}
281
282impl std::error::Error for ColorParseError {}
283
284// ---------------------------------------------------------------------------
285// TerminalTheme
286// ---------------------------------------------------------------------------
287
288/// Describes the terminal's default theme colors (for blending / downgrade).
289#[derive(Debug, Clone, Copy)]
290pub struct TerminalTheme {
291    pub foreground_color: (u8, u8, u8),
292    pub background_color: (u8, u8, u8),
293}
294
295impl Default for TerminalTheme {
296    fn default() -> Self {
297        Self {
298            foreground_color: (255, 255, 255),
299            background_color: (0, 0, 0),
300        }
301    }
302}
303
304// ---------------------------------------------------------------------------
305// Built-in palettes
306// ---------------------------------------------------------------------------
307
308/// Standard 16-color ANSI palette.
309pub static STANDARD_PALETTE: &[(u8, u8, u8)] = &[
310    (0, 0, 0),       // 0: black
311    (128, 0, 0),     // 1: red
312    (0, 128, 0),     // 2: green
313    (128, 128, 0),   // 3: yellow
314    (0, 0, 128),     // 4: blue
315    (128, 0, 128),   // 5: magenta
316    (0, 128, 128),   // 6: cyan
317    (192, 192, 192), // 7: white
318    (128, 128, 128), // 8: bright black
319    (255, 0, 0),     // 9: bright red
320    (0, 255, 0),     // 10: bright green
321    (255, 255, 0),   // 11: bright yellow
322    (0, 0, 255),     // 12: bright blue
323    (255, 0, 255),   // 13: bright magenta
324    (0, 255, 255),   // 14: bright cyan
325    (255, 255, 255), // 15: bright white
326];
327
328pub static STANDARD_COLOR_NAMES: &[&str] = &[
329    "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
330    "bright_black", "bright_red", "bright_green", "bright_yellow",
331    "bright_blue", "bright_magenta", "bright_cyan", "bright_white",
332];
333
334/// The 6×6×6 color cube + greyscale ramp = 256-color palette.
335pub static EIGHT_BIT_PALETTE: Lazy<[[u8; 3]; 256]> = Lazy::new(|| {
336    let mut table = [[0u8; 3]; 256];
337    let std: [[u8; 3]; 16] = [
338        [0, 0, 0],
339        [128, 0, 0],
340        [0, 128, 0],
341        [128, 128, 0],
342        [0, 0, 128],
343        [128, 0, 128],
344        [0, 128, 128],
345        [192, 192, 192],
346        [128, 128, 128],
347        [255, 0, 0],
348        [0, 255, 0],
349        [255, 255, 0],
350        [0, 0, 255],
351        [255, 0, 255],
352        [0, 255, 255],
353        [255, 255, 255],
354    ];
355    for i in 0..16 {
356        table[i] = std[i];
357    }
358    let levels = [0u8, 95, 135, 175, 215, 255];
359    for r in 0..6 {
360        for g in 0..6 {
361            for b in 0..6 {
362                let idx = 16 + 36 * r + 6 * g + b;
363                table[idx] = [levels[r], levels[g], levels[b]];
364            }
365        }
366    }
367    for gr in 0..24 {
368        let val = (gr * 10 + 8) as u8;
369        table[232 + gr] = [val, val, val];
370    }
371    table
372});
373
374// ---------------------------------------------------------------------------
375// Color utilities
376// ---------------------------------------------------------------------------
377
378/// Convert RGB to the nearest 8-bit palette index.
379pub fn rgb_to_8bit(r: u8, g: u8, b: u8) -> u8 {
380    // Check if it's close to a greyscale value
381    let grey = ((r as u32 + g as u32 + b as u32) / 3) as u8;
382    if r == g && g == b {
383        if grey < 8 {
384            return 16; // black
385        }
386        if grey > 248 {
387            return 231; // white
388        }
389        return 232 + ((grey - 8) / 10) as u8;
390    }
391
392    // Find nearest cube color
393    let r6 = ((r as f64 / 255.0) * 5.0).round() as u8;
394    let g6 = ((g as f64 / 255.0) * 5.0).round() as u8;
395    let b6 = ((b as f64 / 255.0) * 5.0).round() as u8;
396    16 + 36 * r6 + 6 * g6 + b6
397}
398
399/// Convert RGB to nearest standard (16) ANSI color.
400pub fn rgb_to_standard(_r: u8, _g: u8, _b: u8) -> u8 {
401    // Simplified: just use the nearest by Euclidean distance
402    let mut best_idx = 0u8;
403    let mut best_dist = u32::MAX;
404    for (i, &(pr, pg, pb)) in STANDARD_PALETTE.iter().enumerate() {
405        let dr = _r as i32 - pr as i32;
406        let dg = _g as i32 - pg as i32;
407        let db = _b as i32 - pb as i32;
408        let dist = (dr * dr + dg * dg + db * db) as u32;
409        if dist < best_dist {
410            best_dist = dist;
411            best_idx = i as u8;
412        }
413    }
414    best_idx
415}
416
417/// Blend two RGB colors (like Rich's `blend_rgb`).
418pub fn blend_rgb(
419    color1: (u8, u8, u8),
420    color2: (u8, u8, u8),
421    cross_fade: f64,
422) -> (u8, u8, u8) {
423    let r = (color1.0 as f64 + (color2.0 as f64 - color1.0 as f64) * cross_fade) as u8;
424    let g = (color1.1 as f64 + (color2.1 as f64 - color1.1 as f64) * cross_fade) as u8;
425    let b = (color1.2 as f64 + (color2.2 as f64 - color1.2 as f64) * cross_fade) as u8;
426    (r, g, b)
427}
428
429/// Blend two Colors (downgrading to the supported system).
430pub fn blend_colors(
431    color1: &Color,
432    color2: &Color,
433    cross_fade: f64,
434    theme: &TerminalTheme,
435) -> Color {
436    let rgb1 = color1.get_truecolor(theme);
437    let rgb2 = color2.get_truecolor(theme);
438    let blended = blend_rgb(rgb1, rgb2, cross_fade);
439    Color::from_rgb(blended.0, blended.1, blended.2)
440}
441
442// ---------------------------------------------------------------------------
443// Phf map workaround — since we can't use phf easily, use a lazy static
444// ---------------------------------------------------------------------------
445
446// We use a simple linear scan backed by a slice — fast enough for the small
447// set of named colors.
448
449use once_cell::sync::Lazy;
450use std::collections::HashMap;
451
452static ANSI_NAME_MAP: Lazy<HashMap<&'static str, u8>> = Lazy::new(|| {
453    let mut m = HashMap::new();
454    m.insert("black", 0u8);
455    m.insert("red", 1u8);
456    m.insert("green", 2u8);
457    m.insert("yellow", 3u8);
458    m.insert("blue", 4u8);
459    m.insert("magenta", 5u8);
460    m.insert("cyan", 6u8);
461    m.insert("white", 7u8);
462    m.insert("bright_black", 8u8);
463    m.insert("bright_red", 9u8);
464    m.insert("bright_green", 10u8);
465    m.insert("bright_yellow", 11u8);
466    m.insert("bright_blue", 12u8);
467    m.insert("bright_magenta", 13u8);
468    m.insert("bright_cyan", 14u8);
469    m.insert("bright_white", 15u8);
470    m.insert("grey0", 16u8);
471    m.insert("gray0", 16u8);
472    m.insert("navy_blue", 17u8);
473    m.insert("dark_blue", 18u8);
474    m.insert("blue3", 20u8);
475    m.insert("blue1", 21u8);
476    m.insert("dark_green", 22u8);
477    m.insert("deep_sky_blue4", 25u8);
478    m.insert("dodger_blue3", 26u8);
479    m.insert("dodger_blue2", 27u8);
480    m.insert("green4", 28u8);
481    m.insert("spring_green4", 29u8);
482    m.insert("turquoise4", 30u8);
483    m.insert("deep_sky_blue3", 32u8);
484    m.insert("dodger_blue1", 33u8);
485    m.insert("green3", 40u8);
486    m.insert("spring_green3", 41u8);
487    m.insert("dark_cyan", 36u8);
488    m.insert("light_sea_green", 37u8);
489    m.insert("deep_sky_blue2", 38u8);
490    m.insert("deep_sky_blue1", 39u8);
491    m.insert("spring_green2", 47u8);
492    m.insert("cyan3", 43u8);
493    m.insert("dark_turquoise", 44u8);
494    m.insert("turquoise2", 45u8);
495    m.insert("green1", 46u8);
496    m.insert("spring_green1", 48u8);
497    m.insert("medium_spring_green", 49u8);
498    m.insert("cyan2", 50u8);
499    m.insert("cyan1", 51u8);
500    m.insert("dark_red", 88u8);
501    m.insert("deep_pink4", 125u8);
502    m.insert("purple4", 55u8);
503    m.insert("purple3", 56u8);
504    m.insert("blue_violet", 57u8);
505    m.insert("orange4", 94u8);
506    m.insert("grey37", 59u8);
507    m.insert("gray37", 59u8);
508    m.insert("medium_purple4", 60u8);
509    m.insert("slate_blue3", 62u8);
510    m.insert("royal_blue1", 63u8);
511    m.insert("chartreuse4", 64u8);
512    m.insert("dark_sea_green4", 71u8);
513    m.insert("pale_turquoise4", 66u8);
514    m.insert("steel_blue", 67u8);
515    m.insert("steel_blue3", 68u8);
516    m.insert("cornflower_blue", 69u8);
517    m.insert("dark_sea_green", 71u8);
518    m.insert("cadet_blue", 73u8);
519    m.insert("dark_slate_gray2", 87u8);
520    m.insert("dark_grey", 248u8);
521    m.insert("dark_gray", 248u8);
522    m.insert("light_grey", 250u8);
523    m.insert("light_gray", 250u8);
524    m.insert("grey", 8u8);
525    m.insert("gray", 8u8);
526    m
527});
528
529impl Color {
530    /// Look up a named color index.
531    pub fn name_to_index(name: &str) -> Option<u8> {
532        ANSI_NAME_MAP.get(name).copied()
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539
540    #[test]
541    fn test_default_color() {
542        let c = Color::default();
543        assert!(c.is_default());
544    }
545
546    #[test]
547    fn test_parse_red() {
548        let c = Color::parse("red").unwrap();
549        assert_eq!(c.number, Some(1));
550    }
551
552    #[test]
553    fn test_parse_hex() {
554        let c = Color::parse("#ff0000").unwrap();
555        assert_eq!(c.triplet, Some((255, 0, 0)));
556    }
557
558    #[test]
559    fn test_rgb_to_8bit_black() {
560        assert_eq!(rgb_to_8bit(0, 0, 0), 16);
561    }
562}