1 | use std::{borrow::Cow, env, str::FromStr}; |
2 | |
3 | /// The 8 standard colors. |
4 | #[derive (Clone, Copy, Debug, PartialEq, Eq)] |
5 | #[allow (missing_docs)] |
6 | pub 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 | |
26 | fn truecolor_support() -> bool { |
27 | let truecolor: Result = env::var(key:"COLORTERM" ); |
28 | if let Ok(truecolor: String) = truecolor { |
29 | truecolor == "truecolor" || truecolor == "24bit" |
30 | } else { |
31 | false |
32 | } |
33 | } |
34 | |
35 | #[allow (missing_docs)] |
36 | impl 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 | |
202 | impl From<&str> for Color { |
203 | fn from(src: &str) -> Self { |
204 | src.parse().unwrap_or(default:Color::White) |
205 | } |
206 | } |
207 | |
208 | impl From<String> for Color { |
209 | fn from(src: String) -> Self { |
210 | src.parse().unwrap_or(default:Color::White) |
211 | } |
212 | } |
213 | |
214 | impl 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)] |
244 | mod 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 | } |
382 | |