1use std::{borrow::Cow, env, str::FromStr};
2
3#[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 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}