Skip to main content

rmut_front/
style.rs

1//! How text is colored, said without a toolkit: the sixteen named
2//! colors a muttrc can name, and a style that is a color pair plus
3//! the three attributes mutt's `color` lines carry. Each front end
4//! maps this onto its own kind of style.
5
6/// A terminal palette color. `Reset` is the front end's default.
7#[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    /// A truecolor value, from `#rrggbb` in the config. Terminals
26    /// carry it as 24-bit color; the window uses it directly.
27    Rgb(u8, u8, u8),
28}
29
30/// A style patch: what a color rule sets, leaving the rest alone.
31#[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    /// This style with `other` laid over it: `other`'s colors where
77    /// it has them, its attributes added.
78    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
89/// A color as a muttrc or the config names it.
90pub 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    // mutt's bright* names, which the muttrc importer writes as light*.
99    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
125/// A `color_index` / `color_body` rule's look: a color, or an
126/// attribute name (bold, underline, reverse, standout; none clears
127/// nothing, as in the TUI it always did), in either slot.
128pub 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}