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    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
118/// A `color_index` / `color_body` rule's look: a color, or an
119/// attribute name (bold, underline, reverse, standout; none clears
120/// nothing, as in the TUI it always did), in either slot.
121pub 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}