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 let name = name.to_lowercase();
98 let name = match name.strip_prefix("bright") {
100 Some(base) => format!("light{base}"),
101 None => name,
102 };
103 Some(match name.as_str() {
104 "default" => Color::Reset,
105 "black" => Color::Black,
106 "red" => Color::Red,
107 "green" => Color::Green,
108 "yellow" => Color::Yellow,
109 "blue" => Color::Blue,
110 "magenta" => Color::Magenta,
111 "cyan" => Color::Cyan,
112 "white" => Color::White,
113 "gray" | "grey" | "darkgray" | "darkgrey" | "lightblack" => Color::DarkGray,
114 "lightwhite" => Color::White,
115 "lightred" => Color::LightRed,
116 "lightgreen" => Color::LightGreen,
117 "lightyellow" => Color::LightYellow,
118 "lightblue" => Color::LightBlue,
119 "lightmagenta" => Color::LightMagenta,
120 "lightcyan" => Color::LightCyan,
121 _ => return None,
122 })
123}
124
125pub fn rule_style(
129 rule: &rmut_core::config::ColorRule,
130 what: &str,
131 warnings: &mut Vec<String>,
132) -> Style {
133 let mut style = Style::new();
134 for (name, is_fg) in [(&rule.fg, true), (&rule.bg, false)] {
135 let Some(name) = name else { continue };
136 match name.as_str() {
137 "bold" => style = style.bold(),
138 "underline" => style = style.underline(),
139 "reverse" | "standout" => style = style.reversed(),
140 "none" => {}
141 _ => match parse_color(name) {
142 Some(color) if is_fg => style = style.fg(color),
143 Some(color) => style = style.bg(color),
144 None => warnings.push(format!("unknown {what} color {name:?}")),
145 },
146 }
147 }
148 style
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn mutt_bright_names_parse_as_written_and_as_imported() {
157 assert_eq!(parse_color("brightblack"), Some(Color::DarkGray));
158 assert_eq!(parse_color("lightblack"), Some(Color::DarkGray));
159 assert_eq!(parse_color("brightwhite"), Some(Color::White));
160 assert_eq!(parse_color("lightwhite"), Some(Color::White));
161 assert_eq!(parse_color("BrightRed"), Some(Color::LightRed));
162 assert_eq!(parse_color("brightbogus"), None);
163 }
164
165 #[test]
166 fn hex_colors_parse_and_bad_ones_do_not() {
167 assert_eq!(parse_color("#ff8000"), Some(Color::Rgb(255, 128, 0)));
168 assert_eq!(parse_color("#FF8000"), Some(Color::Rgb(255, 128, 0)));
169 assert_eq!(parse_color("#f80"), None, "three digits stay unknown");
170 assert_eq!(parse_color("#zzzzzz"), None);
171 }
172
173 #[test]
174 fn patch_overlays_colors_and_adds_attributes() {
175 let base = Style::new().fg(Color::Red).bold();
176 let over = Style::new().bg(Color::Blue).reversed();
177 let got = base.patch(over);
178 assert_eq!(
179 got,
180 Style::new()
181 .fg(Color::Red)
182 .bg(Color::Blue)
183 .bold()
184 .reversed()
185 );
186 let got = got.patch(Style::new().fg(Color::Green));
187 assert_eq!(got.fg, Some(Color::Green));
188 assert!(got.bold && got.reversed);
189 }
190
191 #[test]
192 fn rules_read_attributes_and_colors_in_either_slot() {
193 let rule = rmut_core::config::ColorRule {
194 pattern: String::new(),
195 fg: Some("bold".into()),
196 bg: Some("blue".into()),
197 };
198 let mut warnings = Vec::new();
199 let style = rule_style(&rule, "color_index", &mut warnings);
200 assert_eq!(style, Style::new().bold().bg(Color::Blue));
201 assert!(warnings.is_empty());
202 let rule = rmut_core::config::ColorRule {
203 pattern: String::new(),
204 fg: Some("chartreuse".into()),
205 bg: None,
206 };
207 assert_eq!(rule_style(&rule, "color_body", &mut warnings), Style::new());
208 assert_eq!(warnings.len(), 1);
209 }
210}