| 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 |  | 
|---|