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#[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 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}