colored/
color.rs

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