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