colored/
color.rs

1use std::{borrow::Cow, cmp, env, str::FromStr};
2use Color::{
3    AnsiColor, Black, Blue, BrightBlack, BrightBlue, BrightCyan, BrightGreen, BrightMagenta,
4    BrightRed, BrightWhite, BrightYellow, Cyan, Green, Magenta, Red, TrueColor, White, Yellow,
5};
6
7/// The 8 standard colors.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[allow(missing_docs)]
10pub enum Color {
11    Black,
12    Red,
13    Green,
14    Yellow,
15    Blue,
16    Magenta,
17    Cyan,
18    White,
19    BrightBlack,
20    BrightRed,
21    BrightGreen,
22    BrightYellow,
23    BrightBlue,
24    BrightMagenta,
25    BrightCyan,
26    BrightWhite,
27    AnsiColor(u8),
28    TrueColor { r: u8, g: u8, b: u8 },
29}
30
31fn truecolor_support() -> bool {
32    let truecolor = env::var("COLORTERM");
33    truecolor.is_ok_and(|truecolor| truecolor == "truecolor" || truecolor == "24bit")
34}
35
36#[allow(missing_docs)]
37impl Color {
38    #[must_use]
39    pub fn to_fg_str(&self) -> Cow<'static, str> {
40        match *self {
41            Self::Black => "30".into(),
42            Self::Red => "31".into(),
43            Self::Green => "32".into(),
44            Self::Yellow => "33".into(),
45            Self::Blue => "34".into(),
46            Self::Magenta => "35".into(),
47            Self::Cyan => "36".into(),
48            Self::White => "37".into(),
49            Self::BrightBlack => "90".into(),
50            Self::BrightRed => "91".into(),
51            Self::BrightGreen => "92".into(),
52            Self::BrightYellow => "93".into(),
53            Self::BrightBlue => "94".into(),
54            Self::BrightMagenta => "95".into(),
55            Self::BrightCyan => "96".into(),
56            Self::BrightWhite => "97".into(),
57            Self::TrueColor { .. } if !truecolor_support() => {
58                self.closest_color_euclidean().to_fg_str()
59            }
60            Self::AnsiColor(code) => format!("38;5;{code}").into(),
61            Self::TrueColor { r, g, b } => format!("38;2;{r};{g};{b}").into(),
62        }
63    }
64
65    #[must_use]
66    pub fn to_bg_str(&self) -> Cow<'static, str> {
67        match *self {
68            Self::Black => "40".into(),
69            Self::Red => "41".into(),
70            Self::Green => "42".into(),
71            Self::Yellow => "43".into(),
72            Self::Blue => "44".into(),
73            Self::Magenta => "45".into(),
74            Self::Cyan => "46".into(),
75            Self::White => "47".into(),
76            Self::BrightBlack => "100".into(),
77            Self::BrightRed => "101".into(),
78            Self::BrightGreen => "102".into(),
79            Self::BrightYellow => "103".into(),
80            Self::BrightBlue => "104".into(),
81            Self::BrightMagenta => "105".into(),
82            Self::BrightCyan => "106".into(),
83            Self::BrightWhite => "107".into(),
84            Self::AnsiColor(code) => format!("48;5;{code}").into(),
85            Self::TrueColor { .. } if !truecolor_support() => {
86                self.closest_color_euclidean().to_bg_str()
87            }
88            Self::TrueColor { r, g, b } => format!("48;2;{r};{g};{b}").into(),
89        }
90    }
91
92    /// Gets the closest plain color to the `TrueColor`
93    fn closest_color_euclidean(self) -> Self {
94        match self {
95            TrueColor {
96                r: r1,
97                g: g1,
98                b: b1,
99            } => {
100                let colors = vec![
101                    Black,
102                    Red,
103                    Green,
104                    Yellow,
105                    Blue,
106                    Magenta,
107                    Cyan,
108                    White,
109                    BrightBlack,
110                    BrightRed,
111                    BrightGreen,
112                    BrightYellow,
113                    BrightBlue,
114                    BrightMagenta,
115                    BrightCyan,
116                    BrightWhite,
117                ]
118                .into_iter()
119                .map(|c| (c, c.into_truecolor()));
120                let distances = colors.map(|(c_original, c)| {
121                    if let TrueColor { r, g, b } = c {
122                        let rd = cmp::max(r, r1) - cmp::min(r, r1);
123                        let gd = cmp::max(g, g1) - cmp::min(g, g1);
124                        let bd = cmp::max(b, b1) - cmp::min(b, b1);
125                        let rd: u32 = rd.into();
126                        let gd: u32 = gd.into();
127                        let bd: u32 = bd.into();
128                        let distance = rd.pow(2) + gd.pow(2) + bd.pow(2);
129                        (c_original, distance)
130                    } else {
131                        unimplemented!("{:?} not a TrueColor", c)
132                    }
133                });
134                distances.min_by(|(_, d1), (_, d2)| d1.cmp(d2)).unwrap().0
135            }
136            c => c,
137        }
138    }
139
140    const fn into_truecolor(self) -> Self {
141        match self {
142            Black => TrueColor { r: 0, g: 0, b: 0 },
143            Red => TrueColor { r: 205, g: 0, b: 0 },
144            Green => TrueColor { r: 0, g: 205, b: 0 },
145            Yellow => TrueColor {
146                r: 205,
147                g: 205,
148                b: 0,
149            },
150            Blue => TrueColor { r: 0, g: 0, b: 238 },
151            Magenta => TrueColor {
152                r: 205,
153                g: 0,
154                b: 205,
155            },
156            Cyan => TrueColor {
157                r: 0,
158                g: 205,
159                b: 205,
160            },
161            White => TrueColor {
162                r: 229,
163                g: 229,
164                b: 229,
165            },
166            BrightBlack => TrueColor {
167                r: 127,
168                g: 127,
169                b: 127,
170            },
171            BrightRed => TrueColor { r: 255, g: 0, b: 0 },
172            BrightGreen => TrueColor { r: 0, g: 255, b: 0 },
173            BrightYellow => TrueColor {
174                r: 255,
175                g: 255,
176                b: 0,
177            },
178            BrightBlue => TrueColor {
179                r: 92,
180                g: 92,
181                b: 255,
182            },
183            BrightMagenta => TrueColor {
184                r: 255,
185                g: 0,
186                b: 255,
187            },
188            BrightCyan => TrueColor {
189                r: 0,
190                g: 255,
191                b: 255,
192            },
193            BrightWhite => TrueColor {
194                r: 255,
195                g: 255,
196                b: 255,
197            },
198            AnsiColor(color) => AnsiColor(color),
199            TrueColor { r, g, b } => TrueColor { r, g, b },
200        }
201    }
202}
203
204impl From<&str> for Color {
205    fn from(src: &str) -> Self {
206        src.parse().unwrap_or(Self::White)
207    }
208}
209
210impl From<String> for Color {
211    fn from(src: String) -> Self {
212        src.parse().unwrap_or(Self::White)
213    }
214}
215
216impl FromStr for Color {
217    type Err = ();
218
219    fn from_str(src: &str) -> Result<Self, Self::Err> {
220        let src = src.to_lowercase();
221
222        match src.as_ref() {
223            "black" => Ok(Self::Black),
224            "red" => Ok(Self::Red),
225            "green" => Ok(Self::Green),
226            "yellow" => Ok(Self::Yellow),
227            "blue" => Ok(Self::Blue),
228            "magenta" | "purple" => Ok(Self::Magenta),
229            "cyan" => Ok(Self::Cyan),
230            "white" => Ok(Self::White),
231            "bright black" => Ok(Self::BrightBlack),
232            "bright red" => Ok(Self::BrightRed),
233            "bright green" => Ok(Self::BrightGreen),
234            "bright yellow" => Ok(Self::BrightYellow),
235            "bright blue" => Ok(Self::BrightBlue),
236            "bright magenta" => Ok(Self::BrightMagenta),
237            "bright cyan" => Ok(Self::BrightCyan),
238            "bright white" => Ok(Self::BrightWhite),
239            s if s.starts_with('#') => parse_hex(&s[1..]).ok_or(()),
240            _ => Err(()),
241        }
242    }
243}
244
245fn parse_hex(s: &str) -> Option<Color> {
246    if s.len() == 6 {
247        let r = u8::from_str_radix(&s[0..2], 16).ok()?;
248        let g = u8::from_str_radix(&s[2..4], 16).ok()?;
249        let b = u8::from_str_radix(&s[4..6], 16).ok()?;
250        Some(Color::TrueColor { r, g, b })
251    } else if s.len() == 3 {
252        let r = u8::from_str_radix(&s[0..1], 16).ok()?;
253        let r = r | (r << 4);
254        let g = u8::from_str_radix(&s[1..2], 16).ok()?;
255        let g = g | (g << 4);
256        let b = u8::from_str_radix(&s[2..3], 16).ok()?;
257        let b = b | (b << 4);
258        Some(Color::TrueColor { r, g, b })
259    } else {
260        None
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    pub use super::*;
267
268    mod from_str {
269        pub use super::*;
270
271        macro_rules! make_test {
272            ( $( $name:ident: $src:expr => $dst:expr),* ) => {
273
274                $(
275                    #[test]
276                    fn $name() {
277                        let color : Color = $src.into();
278                        assert_eq!($dst, color)
279                    }
280                )*
281            }
282        }
283
284        make_test!(
285            black: "black" => Color::Black,
286            red: "red" => Color::Red,
287            green: "green" => Color::Green,
288            yellow: "yellow" => Color::Yellow,
289            blue: "blue" => Color::Blue,
290            magenta: "magenta" => Color::Magenta,
291            purple: "purple" => Color::Magenta,
292            cyan: "cyan" => Color::Cyan,
293            white: "white" => Color::White,
294            brightblack: "bright black" => Color::BrightBlack,
295            brightred: "bright red" => Color::BrightRed,
296            brightgreen: "bright green" => Color::BrightGreen,
297            brightyellow: "bright yellow" => Color::BrightYellow,
298            brightblue: "bright blue" => Color::BrightBlue,
299            brightmagenta: "bright magenta" => Color::BrightMagenta,
300            brightcyan: "bright cyan" => Color::BrightCyan,
301            brightwhite: "bright white" => Color::BrightWhite,
302
303            invalid: "invalid" => Color::White,
304            capitalized: "BLUE" => Color::Blue,
305            mixed_case: "bLuE" => Color::Blue,
306
307            hex3_lower: "#abc" => Color::TrueColor { r: 170, g: 187, b: 204 },
308            hex3_upper: "#ABC" => Color::TrueColor { r: 170, g: 187, b: 204 },
309            hex3_mixed: "#aBc" => Color::TrueColor { r: 170, g: 187, b: 204 },
310            hex6_lower: "#abcdef" => Color::TrueColor { r: 171, g: 205, b: 239 },
311            hex6_upper: "#ABCDEF" => Color::TrueColor { r: 171, g: 205, b: 239 },
312            hex6_mixed: "#aBcDeF" => Color::TrueColor { r: 171, g: 205, b: 239 },
313            hex_too_short: "#aa" => Color::White,
314            hex_too_long: "#aaabbbccc" => Color::White,
315            hex_invalid: "#abcxyz" => Color::White
316        );
317    }
318
319    mod from_string {
320        pub use super::*;
321
322        macro_rules! make_test {
323            ( $( $name:ident: $src:expr => $dst:expr),* ) => {
324
325                $(
326                    #[test]
327                    fn $name() {
328                        let src = String::from($src);
329                        let color : Color = src.into();
330                        assert_eq!($dst, color)
331                    }
332                )*
333            }
334        }
335
336        make_test!(
337            black: "black" => Color::Black,
338            red: "red" => Color::Red,
339            green: "green" => Color::Green,
340            yellow: "yellow" => Color::Yellow,
341            blue: "blue" => Color::Blue,
342            magenta: "magenta" => Color::Magenta,
343            cyan: "cyan" => Color::Cyan,
344            white: "white" => Color::White,
345            brightblack: "bright black" => Color::BrightBlack,
346            brightred: "bright red" => Color::BrightRed,
347            brightgreen: "bright green" => Color::BrightGreen,
348            brightyellow: "bright yellow" => Color::BrightYellow,
349            brightblue: "bright blue" => Color::BrightBlue,
350            brightmagenta: "bright magenta" => Color::BrightMagenta,
351            brightcyan: "bright cyan" => Color::BrightCyan,
352            brightwhite: "bright white" => Color::BrightWhite,
353
354            invalid: "invalid" => Color::White,
355            capitalized: "BLUE" => Color::Blue,
356            mixed_case: "bLuE" => Color::Blue,
357
358            hex3_lower: "#abc" => Color::TrueColor { r: 170, g: 187, b: 204 },
359            hex3_upper: "#ABC" => Color::TrueColor { r: 170, g: 187, b: 204 },
360            hex3_mixed: "#aBc" => Color::TrueColor { r: 170, g: 187, b: 204 },
361            hex6_lower: "#abcdef" => Color::TrueColor { r: 171, g: 205, b: 239 },
362            hex6_upper: "#ABCDEF" => Color::TrueColor { r: 171, g: 205, b: 239 },
363            hex6_mixed: "#aBcDeF" => Color::TrueColor { r: 171, g: 205, b: 239 },
364            hex_too_short: "#aa" => Color::White,
365            hex_too_long: "#aaabbbccc" => Color::White,
366            hex_invalid: "#abcxyz" => Color::White
367        );
368    }
369
370    mod fromstr {
371        pub use super::*;
372
373        #[test]
374        fn parse() {
375            let color: Result<Color, _> = "blue".parse();
376            assert_eq!(Ok(Color::Blue), color);
377        }
378
379        #[test]
380        fn error() {
381            let color: Result<Color, ()> = "bloublou".parse();
382            assert_eq!(Err(()), color);
383        }
384    }
385
386    mod closest_euclidean {
387        use super::*;
388
389        macro_rules! make_euclidean_distance_test {
390            ( $test:ident : ( $r:literal, $g: literal, $b:literal ), $expected:expr ) => {
391                #[test]
392                fn $test() {
393                    let true_color = Color::TrueColor {
394                        r: $r,
395                        g: $g,
396                        b: $b,
397                    };
398                    let actual = true_color.closest_color_euclidean();
399                    assert_eq!(actual, $expected);
400                }
401            };
402        }
403
404        make_euclidean_distance_test! { exact_black: (0, 0, 0), Color::Black }
405        make_euclidean_distance_test! { exact_red: (205, 0, 0), Color::Red }
406        make_euclidean_distance_test! { exact_green: (0, 205, 0), Color::Green }
407        make_euclidean_distance_test! { exact_yellow: (205, 205, 0), Color::Yellow }
408        make_euclidean_distance_test! { exact_blue: (0, 0, 238), Color::Blue }
409        make_euclidean_distance_test! { exact_magenta: (205, 0, 205), Color::Magenta }
410        make_euclidean_distance_test! { exact_cyan: (0, 205, 205), Color::Cyan }
411        make_euclidean_distance_test! { exact_white: (229, 229, 229), Color::White }
412
413        make_euclidean_distance_test! { almost_black: (10, 15, 10), Color::Black }
414        make_euclidean_distance_test! { almost_red: (215, 10, 10), Color::Red }
415        make_euclidean_distance_test! { almost_green: (10, 195, 10), Color::Green }
416        make_euclidean_distance_test! { almost_yellow: (195, 215, 10), Color::Yellow }
417        make_euclidean_distance_test! { almost_blue: (0, 0, 200), Color::Blue }
418        make_euclidean_distance_test! { almost_magenta: (215, 0, 195), Color::Magenta }
419        make_euclidean_distance_test! { almost_cyan: (10, 215, 215), Color::Cyan }
420        make_euclidean_distance_test! { almost_white: (209, 209, 229), Color::White }
421    }
422}