fm/config/
colors.rs

1use ratatui::style::Color;
2
3use crate::config::{ARRAY_GRADIENT, COLORER};
4
5/// How many colors are possible in a gradient.
6/// ATM it's 254 which should be enough to distinguish every
7/// displayed extension.
8pub const MAX_GRADIENT_NORMAL: usize = 254;
9
10/// No style but a method to color give a color for any extension.
11/// Extension should be hashed to an usize first.
12pub struct NormalFileColorer {}
13
14impl NormalFileColorer {
15    #[inline]
16    pub fn colorer(hash: usize) -> Color {
17        let gradient = ARRAY_GRADIENT
18            .get()
19            .expect("Gradient normal file should be set");
20        gradient[hash % MAX_GRADIENT_NORMAL]
21    }
22}
23
24#[inline]
25fn sum_hash(string: &str) -> usize {
26    let hash: usize = string
27        .as_bytes()
28        .iter()
29        .map(|s| *s as usize)
30        .reduce(|acc, elt| acc.saturating_mul(MAX_GRADIENT_NORMAL).saturating_add(elt))
31        .unwrap_or_default();
32    hash & MAX_GRADIENT_NORMAL
33}
34
35/// Returns a color based on the extension.
36/// Those colors will always be the same, but a palette is defined from a yaml value.
37#[inline]
38pub fn extension_color(extension: &str) -> Color {
39    COLORER.get().expect("Colorer should be set")(sum_hash(extension))
40}
41
42/// Describe a 24 bits rgb color.
43/// it's mostly used in gradients, since [`ratatui`] or [`crossterm`] colors
44/// also includes many more variants like `Color::Red` which I can't always figure.
45///
46/// This strict type allows infaillible calculations and so, gradients.
47#[derive(Debug, Clone, Copy)]
48pub struct ColorG {
49    pub r: u8,
50    pub g: u8,
51    pub b: u8,
52}
53
54impl Default for ColorG {
55    fn default() -> Self {
56        Self {
57            r: 255,
58            g: 255,
59            b: 0,
60        }
61    }
62}
63
64impl ColorG {
65    pub fn new(r: u8, g: u8, b: u8) -> Self {
66        Self { r, g, b }
67    }
68    /// Parse a ratatui color into it's rgb values.
69    /// Non parsable colors returns None.
70    pub fn from_ratatui(color: Color) -> Option<Self> {
71        match color {
72            Color::Rgb(r, g, b) => Some(Self { r, g, b }),
73            _ => None,
74        }
75    }
76
77    pub fn as_ratatui(&self) -> Color {
78        Color::Rgb(self.r, self.g, self.b)
79    }
80
81    #[rustfmt::skip]
82    fn from_ansi_desc(color_name: &str) -> Option<Self> {
83        match color_name.to_lowercase().as_str() {
84            "black"         => Some(Self::new(0,     0,   0)),
85            "red"           => Some(Self::new(255,   0,   0)),
86            "green"         => Some(Self::new(0,   255,   0)),
87            "yellow"        => Some(Self::new(255, 255,   0)),
88            "blue"          => Some(Self::new(0,     0, 255)),
89            "magenta"       => Some(Self::new(255,   0, 255)),
90            "cyan"          => Some(Self::new(0,   255, 255)),
91            "white"         => Some(Self::new(255, 255, 255)),
92
93            "light_black"   => Some(Self::new(85,   85,  85)),
94            "light_red"     => Some(Self::new(255,  85,  85)),
95            "light_green"   => Some(Self::new(85,  255,  85)),
96            "light_yellow"  => Some(Self::new(255, 255,  85)),
97            "light_blue"    => Some(Self::new(85,   85, 255)),
98            "light_magenta" => Some(Self::new(255,  85, 255)),
99            "light_cyan"    => Some(Self::new(85,  255, 255)),
100            "light_white"   => Some(Self::new(255, 255, 255)),
101
102            _               => None,
103        }
104    }
105
106    /// Parse a color in any kind of format: ANSI text (red etc.), rgb or hex.
107    /// The parser for ANSI text colors recognize all common name whatever the capitalization.
108    /// It doesn't try to parse rgb or hex values
109    /// Only the default values are used. If the user changed "red" to be #ffff00 (which is yellow...)
110    /// in its terminal setup, we can't know. So, what the user will get on screen is red: #ff0000.
111    pub fn parse_any_color(text: &str) -> Option<Self> {
112        match parse_text_triplet(text) {
113            Some((r, g, b)) => Some(Self::new(r, g, b)),
114            None => Self::from_ansi_desc(text),
115        }
116    }
117}
118
119/// Tries to parse a string color into a [`ratatui::style::Color`].
120/// Ansi colors are converted to their corresponding version in ratatui.
121/// rgb and hexadecimal formats are parsed also.
122/// rgb( 123,   78,          0)     -> Color::Rgb(123, 78, 0)
123/// #FF00FF                         -> Color::Rgb(255, 0, 255)
124/// Other formats are unknown.
125/// Unreadable colors are replaced by `Color::default()` which is white.
126#[rustfmt::skip]
127pub fn str_to_ratatui<S>(color: S) -> Color
128where
129    S: AsRef<str>,
130{
131    match color.as_ref() {
132        "white"         => Color::White,
133        "red"           => Color::Red,
134        "green"         => Color::Green,
135        "blue"          => Color::Blue,
136        "yellow"        => Color::Yellow,
137        "cyan"          => Color::Cyan,
138        "magenta"       => Color::Magenta,
139        "black"         => Color::Black,
140        "light_white"   => Color::White,
141        "light_red"     => Color::LightRed,
142        "light_green"   => Color::LightGreen,
143        "light_blue"    => Color::LightBlue,
144        "light_yellow"  => Color::LightYellow,
145        "light_cyan"    => Color::LightCyan,
146        "light_magenta" => Color::LightMagenta,
147        "light_black"   => Color::Black,
148        color     => parse_text_triplet_unfaillible(color),
149    }
150}
151
152fn parse_text_triplet_unfaillible(color: &str) -> Color {
153    match parse_text_triplet(color) {
154        Some((r, g, b)) => Color::Rgb(r, g, b),
155        None => Color::Rgb(0, 0, 0),
156    }
157}
158
159fn parse_text_triplet(color: &str) -> Option<(u8, u8, u8)> {
160    let color = color.to_lowercase();
161    if color.starts_with("rgb(") && color.ends_with(')') {
162        return parse_rgb_triplet(&color);
163    } else if color.starts_with('#') && color.len() >= 7 {
164        return parse_hex_triplet(&color);
165    }
166    None
167}
168
169fn parse_rgb_triplet(color: &str) -> Option<(u8, u8, u8)> {
170    let triplet: Vec<u8> = color
171        .replace("rgb(", "")
172        .replace([')', ' '], "")
173        .trim()
174        .split(',')
175        .filter_map(|s| s.parse().ok())
176        .collect();
177    if triplet.len() == 3 {
178        return Some((triplet[0], triplet[1], triplet[2]));
179    }
180    None
181}
182
183fn parse_hex_triplet(color: &str) -> Option<(u8, u8, u8)> {
184    let r = parse_hex_byte(&color[1..3])?;
185    let g = parse_hex_byte(&color[3..5])?;
186    let b = parse_hex_byte(&color[5..7])?;
187    Some((r, g, b))
188}
189
190fn parse_hex_byte(byte: &str) -> Option<u8> {
191    u8::from_str_radix(byte, 16).ok()
192}