1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
8pub enum Color {
9 Reset,
10 Black,
11 Red,
12 Green,
13 Yellow,
14 Blue,
15 Magenta,
16 Cyan,
17 White,
18 DarkGray,
19 LightRed,
20 LightGreen,
21 LightYellow,
22 LightBlue,
23 LightMagenta,
24 LightCyan,
25 Rgb(u8, u8, u8),
28}
29
30#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
32pub struct Style {
33 pub fg: Option<Color>,
34 pub bg: Option<Color>,
35 pub bold: bool,
36 pub underline: bool,
37 pub reversed: bool,
38}
39
40impl Style {
41 pub const fn new() -> Style {
42 Style {
43 fg: None,
44 bg: None,
45 bold: false,
46 underline: false,
47 reversed: false,
48 }
49 }
50
51 pub const fn fg(mut self, color: Color) -> Style {
52 self.fg = Some(color);
53 self
54 }
55
56 pub const fn bg(mut self, color: Color) -> Style {
57 self.bg = Some(color);
58 self
59 }
60
61 pub const fn bold(mut self) -> Style {
62 self.bold = true;
63 self
64 }
65
66 pub const fn underline(mut self) -> Style {
67 self.underline = true;
68 self
69 }
70
71 pub const fn reversed(mut self) -> Style {
72 self.reversed = true;
73 self
74 }
75
76 pub fn patch(self, other: Style) -> Style {
79 Style {
80 fg: other.fg.or(self.fg),
81 bg: other.bg.or(self.bg),
82 bold: self.bold || other.bold,
83 underline: self.underline || other.underline,
84 reversed: self.reversed || other.reversed,
85 }
86 }
87}
88
89pub fn parse_color(name: &str) -> Option<Color> {
91 if let Some(hex) = name.strip_prefix('#')
92 && hex.len() == 6
93 && let Ok(v) = u32::from_str_radix(hex, 16)
94 {
95 return Some(Color::Rgb((v >> 16) as u8, (v >> 8) as u8, v as u8));
96 }
97 Some(match name.to_lowercase().as_str() {
98 "default" => Color::Reset,
99 "black" => Color::Black,
100 "red" => Color::Red,
101 "green" => Color::Green,
102 "yellow" => Color::Yellow,
103 "blue" => Color::Blue,
104 "magenta" => Color::Magenta,
105 "cyan" => Color::Cyan,
106 "white" => Color::White,
107 "gray" | "grey" | "darkgray" | "darkgrey" => Color::DarkGray,
108 "lightred" => Color::LightRed,
109 "lightgreen" => Color::LightGreen,
110 "lightyellow" => Color::LightYellow,
111 "lightblue" => Color::LightBlue,
112 "lightmagenta" => Color::LightMagenta,
113 "lightcyan" => Color::LightCyan,
114 _ => return None,
115 })
116}
117
118pub fn rule_style(
122 rule: &rmut_core::config::ColorRule,
123 what: &str,
124 warnings: &mut Vec<String>,
125) -> Style {
126 let mut style = Style::new();
127 for (name, is_fg) in [(&rule.fg, true), (&rule.bg, false)] {
128 let Some(name) = name else { continue };
129 match name.as_str() {
130 "bold" => style = style.bold(),
131 "underline" => style = style.underline(),
132 "reverse" | "standout" => style = style.reversed(),
133 "none" => {}
134 _ => match parse_color(name) {
135 Some(color) if is_fg => style = style.fg(color),
136 Some(color) => style = style.bg(color),
137 None => warnings.push(format!("unknown {what} color {name:?}")),
138 },
139 }
140 }
141 style
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn hex_colors_parse_and_bad_ones_do_not() {
150 assert_eq!(parse_color("#ff8000"), Some(Color::Rgb(255, 128, 0)));
151 assert_eq!(parse_color("#FF8000"), Some(Color::Rgb(255, 128, 0)));
152 assert_eq!(parse_color("#f80"), None, "three digits stay unknown");
153 assert_eq!(parse_color("#zzzzzz"), None);
154 }
155
156 #[test]
157 fn patch_overlays_colors_and_adds_attributes() {
158 let base = Style::new().fg(Color::Red).bold();
159 let over = Style::new().bg(Color::Blue).reversed();
160 let got = base.patch(over);
161 assert_eq!(
162 got,
163 Style::new()
164 .fg(Color::Red)
165 .bg(Color::Blue)
166 .bold()
167 .reversed()
168 );
169 let got = got.patch(Style::new().fg(Color::Green));
170 assert_eq!(got.fg, Some(Color::Green));
171 assert!(got.bold && got.reversed);
172 }
173
174 #[test]
175 fn rules_read_attributes_and_colors_in_either_slot() {
176 let rule = rmut_core::config::ColorRule {
177 pattern: String::new(),
178 fg: Some("bold".into()),
179 bg: Some("blue".into()),
180 };
181 let mut warnings = Vec::new();
182 let style = rule_style(&rule, "color_index", &mut warnings);
183 assert_eq!(style, Style::new().bold().bg(Color::Blue));
184 assert!(warnings.is_empty());
185 let rule = rmut_core::config::ColorRule {
186 pattern: String::new(),
187 fg: Some("chartreuse".into()),
188 bg: None,
189 };
190 assert_eq!(rule_style(&rule, "color_body", &mut warnings), Style::new());
191 assert_eq!(warnings.len(), 1);
192 }
193}