1use crate::errors::{Result, RichError};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum ColorSystem {
19 Standard,
21 EightBit,
23 Truecolor,
25 Windows,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum ColorType {
32 Default,
34 Standard,
36 EightBit,
38 Truecolor,
40 Windows,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct ColorTriplet {
47 pub red: u8,
48 pub green: u8,
49 pub blue: u8,
50}
51
52impl ColorTriplet {
53 pub const fn new(red: u8, green: u8, blue: u8) -> Self {
54 Self { red, green, blue }
55 }
56
57 pub fn hex(&self) -> String {
59 format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct Color {
66 pub name: String,
68 pub kind: ColorType,
69 pub number: Option<u8>,
71 pub triplet: Option<ColorTriplet>,
73}
74
75impl Color {
76 pub fn is_default(&self) -> bool {
78 self.kind == ColorType::Default
79 }
80
81 pub fn default_color() -> Self {
83 Color {
84 name: "default".to_string(),
85 kind: ColorType::Default,
86 number: None,
87 triplet: None,
88 }
89 }
90
91 fn named(name: &str, number: u8) -> Self {
94 let kind = if number < 16 {
95 ColorType::Standard
96 } else {
97 ColorType::EightBit
98 };
99 Color {
100 name: name.to_string(),
101 kind,
102 number: Some(number),
103 triplet: None,
104 }
105 }
106
107 pub fn from_ansi(number: u8) -> Self {
110 Color {
111 name: format!("color({number})"),
112 kind: if number < 16 {
113 ColorType::Standard
114 } else {
115 ColorType::EightBit
116 },
117 number: Some(number),
118 triplet: None,
119 }
120 }
121
122 pub fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
125 let triplet = ColorTriplet::new(red, green, blue);
126 Color {
127 name: triplet.hex(),
128 kind: ColorType::Truecolor,
129 number: None,
130 triplet: Some(triplet),
131 }
132 }
133
134 pub fn parse(color: &str) -> Result<Self> {
139 let original = color.trim();
140 let lower = original.to_ascii_lowercase();
141
142 if lower == "default" {
143 return Ok(Color::default_color());
144 }
145 if let Some(number) = crate::color_names::ansi_color_number(&lower) {
146 return Ok(Color::named(&lower, number));
147 }
148 if let Some(hex) = lower.strip_prefix('#') {
149 let triplet = parse_hex(hex)
150 .ok_or_else(|| RichError::ColorParse(format!("invalid hex color {original:?}")))?;
151 return Ok(Color {
152 name: lower.clone(),
158 kind: ColorType::Truecolor,
159 number: None,
160 triplet: Some(triplet),
161 });
162 }
163 if let Some(inner) = lower.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
164 let triplet = parse_rgb(inner)
165 .ok_or_else(|| RichError::ColorParse(format!("invalid rgb color {original:?}")))?;
166 return Ok(Color {
167 name: lower.clone(),
173 kind: ColorType::Truecolor,
174 number: None,
175 triplet: Some(triplet),
176 });
177 }
178 if let Some(inner) = lower
179 .strip_prefix("color(")
180 .and_then(|s| s.strip_suffix(')'))
181 {
182 let n: u16 = inner
183 .trim()
184 .parse()
185 .map_err(|_| RichError::ColorParse(format!("invalid color number {original:?}")))?;
186 if n > 255 {
187 return Err(RichError::ColorParse(format!(
188 "color number must be <= 255, not {n}"
189 )));
190 }
191 return Ok(Color {
194 name: lower.clone(),
200 kind: if n < 16 {
201 ColorType::Standard
202 } else {
203 ColorType::EightBit
204 },
205 number: Some(n as u8),
206 triplet: None,
207 });
208 }
209 Err(RichError::ColorParse(format!(
210 "{original:?} is not a valid color"
211 )))
212 }
213
214 pub fn get_truecolor(&self) -> Option<ColorTriplet> {
218 match self.kind {
219 ColorType::Default => None,
220 ColorType::Truecolor => self.triplet,
221 ColorType::Standard => self.number.map(|n| ANSI_BASE_PALETTE[n as usize]),
222 ColorType::Windows => self.number.map(|n| WINDOWS_PALETTE[(n & 15) as usize]),
224 ColorType::EightBit => self.number.map(eight_bit_triplet),
225 }
226 }
227
228 pub fn ansi_codes(&self, foreground: bool) -> Vec<String> {
232 match self.kind {
233 ColorType::Default => vec![if foreground { "39" } else { "49" }.to_string()],
234 ColorType::Windows | ColorType::Standard => {
235 let number = self.number.unwrap_or(0);
236 let (fore, back) = if number < 8 { (30, 40) } else { (82, 92) };
237 vec![(if foreground { fore } else { back } + number as u32).to_string()]
238 }
239 ColorType::EightBit => {
240 let number = self.number.unwrap_or(0);
241 vec![
242 if foreground { "38" } else { "48" }.to_string(),
243 "5".to_string(),
244 number.to_string(),
245 ]
246 }
247 ColorType::Truecolor => {
248 let t = self.triplet.unwrap_or(ColorTriplet::new(0, 0, 0));
249 vec![
250 if foreground { "38" } else { "48" }.to_string(),
251 "2".to_string(),
252 t.red.to_string(),
253 t.green.to_string(),
254 t.blue.to_string(),
255 ]
256 }
257 }
258 }
259
260 pub fn downgrade(&self, system: ColorSystem) -> Color {
266 let same_system = matches!(
267 (self.kind, system),
268 (ColorType::Standard, ColorSystem::Standard)
269 | (ColorType::EightBit, ColorSystem::EightBit)
270 | (ColorType::Truecolor, ColorSystem::Truecolor)
271 | (ColorType::Windows, ColorSystem::Windows)
272 );
273 if self.kind == ColorType::Default || same_system {
274 return self.clone();
275 }
276 let is_truecolor = self.kind == ColorType::Truecolor;
277 let source_triplet = || {
280 if is_truecolor {
281 self.triplet
282 } else {
283 self.number.map(eight_bit_triplet)
284 }
285 };
286 match system {
287 ColorSystem::EightBit if is_truecolor => {
288 let Some(triplet) = self.triplet else {
289 return self.clone();
290 };
291 Color {
292 name: self.name.clone(),
293 kind: ColorType::EightBit,
294 number: Some(truecolor_to_eight_bit(triplet)),
295 triplet: None,
296 }
297 }
298 ColorSystem::Standard => {
299 let Some(triplet) = source_triplet() else {
300 return self.clone();
301 };
302 Color {
303 name: self.name.clone(),
304 kind: ColorType::Standard,
305 number: Some(match_color(&STANDARD_PALETTE, triplet)),
306 triplet: None,
307 }
308 }
309 ColorSystem::Windows => {
310 if !is_truecolor {
311 if let Some(number) = self.number.filter(|&number| number < 16) {
312 return Color {
313 name: self.name.clone(),
314 kind: ColorType::Windows,
315 number: Some(number),
316 triplet: None,
317 };
318 }
319 }
320 let Some(triplet) = source_triplet() else {
321 return self.clone();
322 };
323 Color {
324 name: self.name.clone(),
325 kind: ColorType::Windows,
326 number: Some(match_color(&WINDOWS_PALETTE, triplet)),
327 triplet: None,
328 }
329 }
330 _ => self.clone(),
331 }
332 }
333}
334
335fn parse_hex(hex: &str) -> Option<ColorTriplet> {
336 if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
337 return None;
338 }
339 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
340 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
341 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
342 Some(ColorTriplet::new(r, g, b))
343}
344
345fn parse_rgb(inner: &str) -> Option<ColorTriplet> {
346 let parts: Vec<&str> = inner.split(',').map(str::trim).collect();
347 if parts.len() != 3 {
348 return None;
349 }
350 let r = parts[0].parse().ok()?;
351 let g = parts[1].parse().ok()?;
352 let b = parts[2].parse().ok()?;
353 Some(ColorTriplet::new(r, g, b))
354}
355
356pub const ANSI_BASE_PALETTE: [ColorTriplet; 16] = [
365 ColorTriplet::new(0, 0, 0),
366 ColorTriplet::new(128, 0, 0),
367 ColorTriplet::new(0, 128, 0),
368 ColorTriplet::new(128, 128, 0),
369 ColorTriplet::new(0, 0, 128),
370 ColorTriplet::new(128, 0, 128),
371 ColorTriplet::new(0, 128, 128),
372 ColorTriplet::new(192, 192, 192),
373 ColorTriplet::new(128, 128, 128),
374 ColorTriplet::new(255, 0, 0),
375 ColorTriplet::new(0, 255, 0),
376 ColorTriplet::new(255, 255, 0),
377 ColorTriplet::new(0, 0, 255),
378 ColorTriplet::new(255, 0, 255),
379 ColorTriplet::new(0, 255, 255),
380 ColorTriplet::new(255, 255, 255),
381];
382
383pub const STANDARD_PALETTE: [ColorTriplet; 16] = [
390 ColorTriplet::new(0, 0, 0),
391 ColorTriplet::new(170, 0, 0),
392 ColorTriplet::new(0, 170, 0),
393 ColorTriplet::new(170, 85, 0),
394 ColorTriplet::new(0, 0, 170),
395 ColorTriplet::new(170, 0, 170),
396 ColorTriplet::new(0, 170, 170),
397 ColorTriplet::new(170, 170, 170),
398 ColorTriplet::new(85, 85, 85),
399 ColorTriplet::new(255, 85, 85),
400 ColorTriplet::new(85, 255, 85),
401 ColorTriplet::new(255, 255, 85),
402 ColorTriplet::new(85, 85, 255),
403 ColorTriplet::new(255, 85, 255),
404 ColorTriplet::new(85, 255, 255),
405 ColorTriplet::new(255, 255, 255),
406];
407
408pub const WINDOWS_PALETTE: [ColorTriplet; 16] = [
412 ColorTriplet::new(12, 12, 12),
413 ColorTriplet::new(197, 15, 31),
414 ColorTriplet::new(19, 161, 14),
415 ColorTriplet::new(193, 156, 0),
416 ColorTriplet::new(0, 55, 218),
417 ColorTriplet::new(136, 23, 152),
418 ColorTriplet::new(58, 150, 221),
419 ColorTriplet::new(204, 204, 204),
420 ColorTriplet::new(118, 118, 118),
421 ColorTriplet::new(231, 72, 86),
422 ColorTriplet::new(22, 198, 12),
423 ColorTriplet::new(249, 241, 165),
424 ColorTriplet::new(59, 120, 255),
425 ColorTriplet::new(180, 0, 158),
426 ColorTriplet::new(97, 214, 214),
427 ColorTriplet::new(242, 242, 242),
428];
429
430static EIGHT_BIT_PALETTE: [ColorTriplet; 256] = build_eight_bit_palette();
433
434const fn build_eight_bit_palette() -> [ColorTriplet; 256] {
435 let mut palette = [ColorTriplet::new(0, 0, 0); 256];
436 let mut i = 0;
438 while i < 16 {
439 palette[i] = ANSI_BASE_PALETTE[i];
440 i += 1;
441 }
442 let levels = [0u8, 95, 135, 175, 215, 255];
444 let mut r = 0;
445 while r < 6 {
446 let mut g = 0;
447 while g < 6 {
448 let mut b = 0;
449 while b < 6 {
450 let index = 16 + 36 * r + 6 * g + b;
451 palette[index] = ColorTriplet::new(levels[r], levels[g], levels[b]);
452 b += 1;
453 }
454 g += 1;
455 }
456 r += 1;
457 }
458 let mut n = 0;
460 while n < 24 {
461 let value = 8 + 10 * n as u8;
462 palette[232 + n] = ColorTriplet::new(value, value, value);
463 n += 1;
464 }
465 palette
466}
467
468fn eight_bit_triplet(number: u8) -> ColorTriplet {
469 EIGHT_BIT_PALETTE[number as usize]
470}
471
472fn rgb_to_ls(red: f64, green: f64, blue: f64) -> (f64, f64) {
478 let max = red.max(green).max(blue);
479 let min = red.min(green).min(blue);
480 let lightness = (max + min) / 2.0;
481 if max == min {
482 return (lightness, 0.0);
483 }
484 let saturation = if lightness <= 0.5 {
485 (max - min) / (max + min)
486 } else {
487 (max - min) / (2.0 - max - min)
488 };
489 (lightness, saturation)
490}
491
492fn truecolor_to_eight_bit(color: ColorTriplet) -> u8 {
501 let (red, green, blue) = (
502 color.red as f64 / 255.0,
503 color.green as f64 / 255.0,
504 color.blue as f64 / 255.0,
505 );
506 let (lightness, saturation) = rgb_to_ls(red, green, blue);
507
508 if saturation < 0.15 {
510 let gray = (lightness * 25.0).round_ties_even() as i64;
513 return match gray {
514 0 => 16,
515 25 => 231,
516 other => (231 + other) as u8,
517 };
518 }
519
520 let axis = |component: u8| -> f64 {
522 let value = component as f64;
523 if value < 95.0 {
524 value / 95.0
525 } else {
526 1.0 + (value - 95.0) / 40.0
527 }
528 };
529 let six_red = axis(color.red).round_ties_even();
530 let six_green = axis(color.green).round_ties_even();
531 let six_blue = axis(color.blue).round_ties_even();
532 (16.0 + 36.0 * six_red + 6.0 * six_green + six_blue) as u8
533}
534
535fn match_color(palette: &[ColorTriplet], color: ColorTriplet) -> u8 {
536 let (red1, green1, blue1) = (color.red as i64, color.green as i64, color.blue as i64);
537 let mut best_index = 0usize;
538 let mut best_distance = i64::MAX;
539 for (index, candidate) in palette.iter().enumerate() {
540 let (red2, green2, blue2) = (
541 candidate.red as i64,
542 candidate.green as i64,
543 candidate.blue as i64,
544 );
545 let red_mean = (red1 + red2) / 2;
546 let red = red1 - red2;
547 let green = green1 - green2;
548 let blue = blue1 - blue2;
549 let distance = (((512 + red_mean) * red * red) >> 8)
552 + 4 * green * green
553 + (((767 - red_mean) * blue * blue) >> 8);
554 if distance < best_distance {
555 best_distance = distance;
556 best_index = index;
557 }
558 }
559 best_index as u8
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
570 fn windows_downgrade_uses_the_windows_palette() {
571 let grey = Color::parse("#808080")
572 .unwrap()
573 .downgrade(ColorSystem::Windows);
574 assert_eq!((grey.kind, grey.number), (ColorType::Windows, Some(8)));
575 let low = Color::parse("color(9)")
576 .unwrap()
577 .downgrade(ColorSystem::Windows);
578 assert_eq!((low.kind, low.number), (ColorType::Windows, Some(9)));
579 assert!(Color::parse("grey").is_err());
580 assert!(Color::parse("gray").is_err());
581 }
582
583 #[test]
584 fn parses_standard_name() {
585 let c = Color::parse("red").unwrap();
586 assert_eq!(c.kind, ColorType::Standard);
587 assert_eq!(c.number, Some(1));
588 assert_eq!(c.ansi_codes(true), vec!["31"]);
589 assert_eq!(c.ansi_codes(false), vec!["41"]);
590 }
591
592 #[test]
593 fn extended_name_is_eight_bit() {
594 let c = Color::parse("orange1").unwrap();
596 assert_eq!(c.kind, ColorType::EightBit);
597 assert_eq!(c.number, Some(214));
598 assert_eq!(c.ansi_codes(true), vec!["38", "5", "214"]);
599 }
600
601 #[test]
602 fn bright_color_uses_high_intensity_sgr() {
603 let c = Color::parse("bright_red").unwrap();
604 assert_eq!(c.number, Some(9));
605 assert_eq!(c.ansi_codes(true), vec!["91"]);
607 }
608
609 #[test]
610 fn parses_hex_truecolor() {
611 let c = Color::parse("#ff8800").unwrap();
612 assert_eq!(c.kind, ColorType::Truecolor);
613 assert_eq!(c.triplet, Some(ColorTriplet::new(0xff, 0x88, 0x00)));
614 assert_eq!(c.ansi_codes(true), vec!["38", "2", "255", "136", "0"]);
615 }
616
617 #[test]
624 fn downgrade_to_standard_matches_upstream() {
625 let standard = |hex: &str| {
626 let down = Color::parse(hex).unwrap().downgrade(ColorSystem::Standard);
627 assert_eq!(down.kind, ColorType::Standard);
628 down.number
629 };
630 assert_eq!(standard("#ff0000"), Some(1));
631 assert_eq!(standard("#00ff00"), Some(2));
632 assert_eq!(standard("#0000ff"), Some(4));
633 assert_eq!(standard("#ffffff"), Some(15));
634 assert_eq!(standard("#808080"), Some(7));
635 assert_eq!(standard("#ff8800"), Some(9));
636 }
637
638 #[test]
642 fn downgrade_to_eight_bit_matches_upstream() {
643 let eight_bit = |hex: &str| {
644 let down = Color::parse(hex).unwrap().downgrade(ColorSystem::EightBit);
645 assert_eq!(down.kind, ColorType::EightBit);
646 down.number
647 };
648 assert_eq!(eight_bit("#ff0000"), Some(196));
649 assert_eq!(eight_bit("#00ff00"), Some(46));
650 assert_eq!(eight_bit("#0000ff"), Some(21));
651 assert_eq!(eight_bit("#ff8800"), Some(208));
652 assert_eq!(eight_bit("#ffffff"), Some(231));
654 assert_eq!(eight_bit("#808080"), Some(244));
655 }
656
657 #[test]
658 fn eight_bit_palette_cube_is_correct() {
659 assert_eq!(eight_bit_triplet(196), ColorTriplet::new(255, 0, 0));
661 }
662}