1use std::fmt;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum Color {
21 Default,
23 Black,
25 Red,
27 Green,
29 Yellow,
31 Blue,
33 Magenta,
35 Cyan,
37 White,
39 BrightBlack,
41 BrightRed,
43 BrightGreen,
45 BrightYellow,
47 BrightBlue,
49 BrightMagenta,
51 BrightCyan,
53 BrightWhite,
55 Ansi256(u8),
57 Rgb { r: u8, g: u8, b: u8 },
59}
60
61impl Color {
62 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
64 Color::Rgb { r, g, b }
65 }
66
67 pub fn ansi256(code: u8) -> Self {
69 Color::Ansi256(code)
70 }
71
72 pub fn parse(s: &str) -> Option<Self> {
80 let s = s.trim().to_lowercase();
81
82 match s.as_str() {
84 "default" => return Some(Color::Default),
85 "black" => return Some(Color::Black),
86 "red" => return Some(Color::Red),
87 "green" => return Some(Color::Green),
88 "yellow" => return Some(Color::Yellow),
89 "blue" => return Some(Color::Blue),
90 "magenta" => return Some(Color::Magenta),
91 "cyan" => return Some(Color::Cyan),
92 "white" => return Some(Color::White),
93 "bright_black" | "brightblack" | "grey" | "gray" => return Some(Color::BrightBlack),
94 "bright_red" | "brightred" => return Some(Color::BrightRed),
95 "bright_green" | "brightgreen" => return Some(Color::BrightGreen),
96 "bright_yellow" | "brightyellow" => return Some(Color::BrightYellow),
97 "bright_blue" | "brightblue" => return Some(Color::BrightBlue),
98 "bright_magenta" | "brightmagenta" => return Some(Color::BrightMagenta),
99 "bright_cyan" | "brightcyan" => return Some(Color::BrightCyan),
100 "bright_white" | "brightwhite" => return Some(Color::BrightWhite),
101 _ => {}
102 }
103
104 if let Some(hex) = s.strip_prefix('#') {
106 return Self::parse_hex(hex);
107 }
108
109 if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
111 let parts: Vec<&str> = inner.split(',').collect();
112 if parts.len() == 3 {
113 let r = parts[0].trim().parse().ok()?;
114 let g = parts[1].trim().parse().ok()?;
115 let b = parts[2].trim().parse().ok()?;
116 return Some(Color::Rgb { r, g, b });
117 }
118 }
119
120 if let Some(inner) = s.strip_prefix("color(").and_then(|s| s.strip_suffix(')')) {
122 let code: u8 = inner.trim().parse().ok()?;
123 return Some(Color::Ansi256(code));
124 }
125
126 None
127 }
128
129 fn parse_hex(hex: &str) -> Option<Self> {
130 match hex.len() {
131 3 => {
132 let mut chars = hex.chars();
134 let r = chars.next()?;
135 let g = chars.next()?;
136 let b = chars.next()?;
137 let r = u8::from_str_radix(&format!("{r}{r}"), 16).ok()?;
138 let g = u8::from_str_radix(&format!("{g}{g}"), 16).ok()?;
139 let b = u8::from_str_radix(&format!("{b}{b}"), 16).ok()?;
140 Some(Color::Rgb { r, g, b })
141 }
142 6 => {
143 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
144 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
145 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
146 Some(Color::Rgb { r, g, b })
147 }
148 _ => None,
149 }
150 }
151
152 pub fn to_crossterm(&self) -> crossterm::style::Color {
154 match self {
155 Color::Default => crossterm::style::Color::Reset,
156 Color::Black => crossterm::style::Color::Black,
157 Color::Red => crossterm::style::Color::DarkRed,
158 Color::Green => crossterm::style::Color::DarkGreen,
159 Color::Yellow => crossterm::style::Color::DarkYellow,
160 Color::Blue => crossterm::style::Color::DarkBlue,
161 Color::Magenta => crossterm::style::Color::DarkMagenta,
162 Color::Cyan => crossterm::style::Color::DarkCyan,
163 Color::White => crossterm::style::Color::Grey,
164 Color::BrightBlack => crossterm::style::Color::DarkGrey,
165 Color::BrightRed => crossterm::style::Color::Red,
166 Color::BrightGreen => crossterm::style::Color::Green,
167 Color::BrightYellow => crossterm::style::Color::Yellow,
168 Color::BrightBlue => crossterm::style::Color::Blue,
169 Color::BrightMagenta => crossterm::style::Color::Magenta,
170 Color::BrightCyan => crossterm::style::Color::Cyan,
171 Color::BrightWhite => crossterm::style::Color::White,
172 Color::Ansi256(code) => crossterm::style::Color::AnsiValue(*code),
173 Color::Rgb { r, g, b } => crossterm::style::Color::Rgb {
174 r: *r,
175 g: *g,
176 b: *b,
177 },
178 }
179 }
180
181 pub fn to_css(&self) -> String {
183 match self {
184 Color::Default => "inherit".to_string(),
185 Color::Black => "#000000".to_string(),
186 Color::Red => "#cd0000".to_string(),
187 Color::Green => "#00cd00".to_string(),
188 Color::Yellow => "#cdcd00".to_string(),
189 Color::Blue => "#0000cd".to_string(),
190 Color::Magenta => "#cd00cd".to_string(),
191 Color::Cyan => "#00cdcd".to_string(),
192 Color::White => "#e5e5e5".to_string(),
193 Color::BrightBlack => "#7f7f7f".to_string(),
194 Color::BrightRed => "#ff0000".to_string(),
195 Color::BrightGreen => "#00ff00".to_string(),
196 Color::BrightYellow => "#ffff00".to_string(),
197 Color::BrightBlue => "#5c5cff".to_string(),
198 Color::BrightMagenta => "#ff00ff".to_string(),
199 Color::BrightCyan => "#00ffff".to_string(),
200 Color::BrightWhite => "#ffffff".to_string(),
201 Color::Ansi256(code) => format!("var(--ansi-{})", code),
202 Color::Rgb { r, g, b } => format!("#{:02x}{:02x}{:02x}", r, g, b),
203 }
204 }
205
206 pub fn to_ansi256(&self) -> Self {
208 match self {
209 Color::Default => Color::Default,
210 Color::Ansi256(_) => *self,
211 Color::Rgb { r, g, b } => {
212 let mut min_dist = u32::MAX;
214 let mut best_idx = 0;
215
216 for i in 0..=255 {
221 let (pr, pg, pb) = Self::ansi256_to_rgb_values(i);
222 let dr = i32::from(*r) - i32::from(pr);
223 let dg = i32::from(*g) - i32::from(pg);
224 let db = i32::from(*b) - i32::from(pb);
225 let dist = (dr * dr + dg * dg + db * db) as u32;
226
227 if dist < min_dist {
228 min_dist = dist;
229 best_idx = i;
230 if dist == 0 {
231 break;
232 } }
234 }
235 Color::Ansi256(best_idx)
236 }
237 Color::Black => Color::Ansi256(0),
239 Color::Red => Color::Ansi256(1),
240 Color::Green => Color::Ansi256(2),
241 Color::Yellow => Color::Ansi256(3),
242 Color::Blue => Color::Ansi256(4),
243 Color::Magenta => Color::Ansi256(5),
244 Color::Cyan => Color::Ansi256(6),
245 Color::White => Color::Ansi256(7),
246 Color::BrightBlack => Color::Ansi256(8),
247 Color::BrightRed => Color::Ansi256(9),
248 Color::BrightGreen => Color::Ansi256(10),
249 Color::BrightYellow => Color::Ansi256(11),
250 Color::BrightBlue => Color::Ansi256(12),
251 Color::BrightMagenta => Color::Ansi256(13),
252 Color::BrightCyan => Color::Ansi256(14),
253 Color::BrightWhite => Color::Ansi256(15),
254 }
255 }
256
257 pub fn to_standard(&self) -> Self {
259 match self {
260 Color::Default
261 | Color::Black
262 | Color::Red
263 | Color::Green
264 | Color::Yellow
265 | Color::Blue
266 | Color::Magenta
267 | Color::Cyan
268 | Color::White
269 | Color::BrightBlack
270 | Color::BrightRed
271 | Color::BrightGreen
272 | Color::BrightYellow
273 | Color::BrightBlue
274 | Color::BrightMagenta
275 | Color::BrightCyan
276 | Color::BrightWhite => *self,
277
278 Color::Ansi256(code) => {
279 if *code < 16 {
280 Self::from_ansi_standard_code(*code)
282 } else {
283 let (r, g, b) = Self::ansi256_to_rgb_values(*code);
285 Color::Rgb { r, g, b }.to_standard()
286 }
287 }
288 Color::Rgb { r, g, b } => {
289 let palette = [
291 (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), (0, 128, 128), (192, 192, 192), (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), ];
308
309 let mut min_dist = u32::MAX;
310 let mut best_idx = 0;
311
312 for (i, (pr, pg, pb)) in palette.iter().enumerate() {
313 let dr = i32::from(*r) - pr;
314 let dg = i32::from(*g) - pg;
315 let db = i32::from(*b) - pb;
316 let dist = (dr * dr + dg * dg + db * db) as u32;
317 if dist < min_dist {
318 min_dist = dist;
319 best_idx = i;
320 }
321 }
322
323 Self::from_ansi_standard_code(best_idx as u8)
324 }
325 }
326 }
327 pub fn to_sgr_fg(&self) -> String {
332 match self {
333 Color::Black => "\x1b[30m".to_string(),
334 Color::Red => "\x1b[31m".to_string(),
335 Color::Green => "\x1b[32m".to_string(),
336 Color::Yellow => "\x1b[33m".to_string(),
337 Color::Blue => "\x1b[34m".to_string(),
338 Color::Magenta => "\x1b[35m".to_string(),
339 Color::Cyan => "\x1b[36m".to_string(),
340 Color::White => "\x1b[37m".to_string(),
341 Color::BrightBlack => "\x1b[90m".to_string(),
342 Color::BrightRed => "\x1b[91m".to_string(),
343 Color::BrightGreen => "\x1b[92m".to_string(),
344 Color::BrightYellow => "\x1b[93m".to_string(),
345 Color::BrightBlue => "\x1b[94m".to_string(),
346 Color::BrightMagenta => "\x1b[95m".to_string(),
347 Color::BrightCyan => "\x1b[96m".to_string(),
348 Color::BrightWhite => "\x1b[97m".to_string(),
349 Color::Default => "\x1b[39m".to_string(),
350 _ => String::new(),
352 }
353 }
354
355 pub fn to_sgr_bg(&self) -> String {
357 match self {
358 Color::Black => "\x1b[40m".to_string(),
359 Color::Red => "\x1b[41m".to_string(),
360 Color::Green => "\x1b[42m".to_string(),
361 Color::Yellow => "\x1b[43m".to_string(),
362 Color::Blue => "\x1b[44m".to_string(),
363 Color::Magenta => "\x1b[45m".to_string(),
364 Color::Cyan => "\x1b[46m".to_string(),
365 Color::White => "\x1b[47m".to_string(),
366 Color::BrightBlack => "\x1b[100m".to_string(),
367 Color::BrightRed => "\x1b[101m".to_string(),
368 Color::BrightGreen => "\x1b[102m".to_string(),
369 Color::BrightYellow => "\x1b[103m".to_string(),
370 Color::BrightBlue => "\x1b[104m".to_string(),
371 Color::BrightMagenta => "\x1b[105m".to_string(),
372 Color::BrightCyan => "\x1b[106m".to_string(),
373 Color::BrightWhite => "\x1b[107m".to_string(),
374 Color::Default => "\x1b[49m".to_string(),
375 _ => String::new(),
376 }
377 }
378 fn from_ansi_standard_code(code: u8) -> Self {
380 match code {
381 0 => Color::Black,
382 1 => Color::Red,
383 2 => Color::Green,
384 3 => Color::Yellow,
385 4 => Color::Blue,
386 5 => Color::Magenta,
387 6 => Color::Cyan,
388 7 => Color::White,
389 8 => Color::BrightBlack,
390 9 => Color::BrightRed,
391 10 => Color::BrightGreen,
392 11 => Color::BrightYellow,
393 12 => Color::BrightBlue,
394 13 => Color::BrightMagenta,
395 14 => Color::BrightCyan,
396 15 => Color::BrightWhite,
397 _ => Color::Default,
398 }
399 }
400
401 fn ansi256_to_rgb_values(code: u8) -> (u8, u8, u8) {
403 if code < 16 {
404 match code {
406 0 => (0, 0, 0), 1 => (128, 0, 0), 2 => (0, 128, 0), 3 => (128, 128, 0), 4 => (0, 0, 128), 5 => (128, 0, 128), 6 => (0, 128, 128), 7 => (192, 192, 192), 8 => (128, 128, 128), 9 => (255, 0, 0), 10 => (0, 255, 0), 11 => (255, 255, 0), 12 => (0, 0, 255), 13 => (255, 0, 255), 14 => (0, 255, 255), 15 => (255, 255, 255), _ => (0, 0, 0),
423 }
424 } else if code < 232 {
425 let index = code - 16;
428 let r_idx = index / 36;
429 let g_idx = (index % 36) / 6;
430 let b_idx = index % 6;
431
432 let val = |x| if x == 0 { 0 } else { x * 40 + 55 };
433 (val(r_idx), val(g_idx), val(b_idx))
434 } else {
435 let index = code - 232;
438 let val = index * 10 + 8;
439 (val, val, val)
440 }
441 }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
446pub struct Style {
447 pub foreground: Option<Color>,
449 pub background: Option<Color>,
451 pub bold: bool,
453 pub dim: bool,
455 pub italic: bool,
457 pub underline: bool,
459 pub blink: bool,
461 pub reverse: bool,
463 pub hidden: bool,
465 pub strikethrough: bool,
467}
468
469impl Style {
470 pub const fn new() -> Self {
472 Style {
473 foreground: None,
474 background: None,
475 bold: false,
476 dim: false,
477 italic: false,
478 underline: false,
479 blink: false,
480 reverse: false,
481 hidden: false,
482 strikethrough: false,
483 }
484 }
485
486 pub fn foreground(mut self, color: Color) -> Self {
488 self.foreground = Some(color);
489 self
490 }
491
492 pub fn background(mut self, color: Color) -> Self {
494 self.background = Some(color);
495 self
496 }
497
498 pub fn fg(self, color: Color) -> Self {
500 self.foreground(color)
501 }
502
503 pub fn bg(self, color: Color) -> Self {
505 self.background(color)
506 }
507
508 pub fn bold(mut self) -> Self {
510 self.bold = true;
511 self
512 }
513
514 pub fn dim(mut self) -> Self {
516 self.dim = true;
517 self
518 }
519
520 pub fn italic(mut self) -> Self {
522 self.italic = true;
523 self
524 }
525
526 pub fn underline(mut self) -> Self {
528 self.underline = true;
529 self
530 }
531
532 pub fn blink(mut self) -> Self {
534 self.blink = true;
535 self
536 }
537
538 pub fn reverse(mut self) -> Self {
540 self.reverse = true;
541 self
542 }
543
544 pub fn hidden(mut self) -> Self {
546 self.hidden = true;
547 self
548 }
549
550 pub fn strikethrough(mut self) -> Self {
552 self.strikethrough = true;
553 self
554 }
555
556 pub fn combine(&self, other: &Style) -> Style {
558 Style {
559 foreground: other.foreground.or(self.foreground),
560 background: other.background.or(self.background),
561 bold: self.bold || other.bold,
562 dim: self.dim || other.dim,
563 italic: self.italic || other.italic,
564 underline: self.underline || other.underline,
565 blink: self.blink || other.blink,
566 reverse: self.reverse || other.reverse,
567 hidden: self.hidden || other.hidden,
568 strikethrough: self.strikethrough || other.strikethrough,
569 }
570 }
571
572 pub fn is_empty(&self) -> bool {
574 self.foreground.is_none()
575 && self.background.is_none()
576 && !self.bold
577 && !self.dim
578 && !self.italic
579 && !self.underline
580 && !self.blink
581 && !self.reverse
582 && !self.hidden
583 && !self.strikethrough
584 }
585
586 pub fn parse(s: &str) -> Self {
590 let mut style = Style::new();
591 let mut on_background = false;
592
593 for part in s.split_whitespace() {
594 let part_lower = part.to_lowercase();
595
596 if part_lower == "on" {
597 on_background = true;
598 continue;
599 }
600
601 match part_lower.as_str() {
603 "bold" | "b" => style.bold = true,
604 "dim" => style.dim = true,
605 "italic" | "i" => style.italic = true,
606 "underline" | "u" => style.underline = true,
607 "blink" => style.blink = true,
608 "reverse" => style.reverse = true,
609 "hidden" => style.hidden = true,
610 "strike" | "strikethrough" | "s" => style.strikethrough = true,
611 "not" => {
612 continue;
614 }
615 _ => {
616 if let Some(color) = Color::parse(&part_lower) {
618 if on_background {
619 style.background = Some(color);
620 on_background = false;
621 } else {
622 style.foreground = Some(color);
623 }
624 }
625 }
626 }
627 }
628
629 style
630 }
631
632 pub fn to_crossterm_attributes(&self) -> crossterm::style::Attributes {
634 use crossterm::style::Attribute;
635 let mut attrs = crossterm::style::Attributes::default();
636
637 if self.bold {
638 attrs.set(Attribute::Bold);
639 }
640 if self.dim {
641 attrs.set(Attribute::Dim);
642 }
643 if self.italic {
644 attrs.set(Attribute::Italic);
645 }
646 if self.underline {
647 attrs.set(Attribute::Underlined);
648 }
649 if self.blink {
650 attrs.set(Attribute::SlowBlink);
651 }
652 if self.reverse {
653 attrs.set(Attribute::Reverse);
654 }
655 if self.hidden {
656 attrs.set(Attribute::Hidden);
657 }
658 if self.strikethrough {
659 attrs.set(Attribute::CrossedOut);
660 }
661
662 attrs
663 }
664
665 pub fn to_css(&self) -> String {
669 let mut parts = Vec::new();
670
671 if let Some(ref fg) = self.foreground {
672 parts.push(format!("color: {}", fg.to_css()));
673 }
674 if let Some(ref bg) = self.background {
675 parts.push(format!("background-color: {}", bg.to_css()));
676 }
677 if self.bold {
678 parts.push("font-weight: bold".to_string());
679 }
680 if self.italic {
681 parts.push("font-style: italic".to_string());
682 }
683 if self.underline {
684 parts.push("text-decoration: underline".to_string());
685 }
686 if self.strikethrough {
687 parts.push("text-decoration: line-through".to_string());
688 }
689 if self.dim {
690 parts.push("opacity: 0.5".to_string());
691 }
692
693 parts.join("; ")
694 }
695}
696
697impl fmt::Display for Style {
698 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
699 let mut parts = Vec::new();
700
701 if self.bold {
702 parts.push("bold");
703 }
704 if self.dim {
705 parts.push("dim");
706 }
707 if self.italic {
708 parts.push("italic");
709 }
710 if self.underline {
711 parts.push("underline");
712 }
713 if self.strikethrough {
714 parts.push("strikethrough");
715 }
716
717 write!(f, "{}", parts.join(" "))
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn test_color_parse_named() {
727 assert_eq!(Color::parse("red"), Some(Color::Red));
728 assert_eq!(Color::parse("Blue"), Some(Color::Blue));
729 assert_eq!(Color::parse("BRIGHT_RED"), Some(Color::BrightRed));
730 assert_eq!(Color::parse("grey"), Some(Color::BrightBlack));
731 }
732
733 #[test]
734 fn test_color_parse_hex() {
735 assert_eq!(
736 Color::parse("#ff0000"),
737 Some(Color::Rgb { r: 255, g: 0, b: 0 })
738 );
739 assert_eq!(
740 Color::parse("#f00"),
741 Some(Color::Rgb { r: 255, g: 0, b: 0 })
742 );
743 assert_eq!(
744 Color::parse("#abc"),
745 Some(Color::Rgb {
746 r: 170,
747 g: 187,
748 b: 204
749 })
750 );
751 }
752
753 #[test]
754 fn test_color_parse_rgb() {
755 assert_eq!(
756 Color::parse("rgb(255, 128, 64)"),
757 Some(Color::Rgb {
758 r: 255,
759 g: 128,
760 b: 64
761 })
762 );
763 }
764
765 #[test]
766 fn test_color_parse_ansi256() {
767 assert_eq!(Color::parse("color(196)"), Some(Color::Ansi256(196)));
768 }
769
770 #[test]
771 fn test_style_parse() {
772 let style = Style::parse("bold red on blue");
773 assert!(style.bold);
774 assert_eq!(style.foreground, Some(Color::Red));
775 assert_eq!(style.background, Some(Color::Blue));
776 }
777
778 #[test]
779 fn test_style_builder() {
780 let style = Style::new().foreground(Color::Green).bold().underline();
781
782 assert!(style.bold);
783 assert!(style.underline);
784 assert_eq!(style.foreground, Some(Color::Green));
785 assert!(!style.italic);
786 }
787
788 #[test]
789 fn test_style_combine() {
790 let base = Style::new().foreground(Color::Red).bold();
791 let overlay = Style::new().foreground(Color::Blue).italic();
792 let combined = base.combine(&overlay);
793
794 assert_eq!(combined.foreground, Some(Color::Blue)); assert!(combined.bold); assert!(combined.italic); }
798
799 #[test]
800 fn test_style_is_empty() {
801 assert!(Style::new().is_empty());
802 assert!(!Style::new().bold().is_empty());
803 }
804}