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}
26
27/// A style patch: what a color rule sets, leaving the rest alone.
28#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
29pub struct Style {
30    pub fg: Option<Color>,
31    pub bg: Option<Color>,
32    pub bold: bool,
33    pub underline: bool,
34    pub reversed: bool,
35}
36
37impl Style {
38    pub const fn new() -> Style {
39        Style {
40            fg: None,
41            bg: None,
42            bold: false,
43            underline: false,
44            reversed: false,
45        }
46    }
47
48    pub const fn fg(mut self, color: Color) -> Style {
49        self.fg = Some(color);
50        self
51    }
52
53    pub const fn bg(mut self, color: Color) -> Style {
54        self.bg = Some(color);
55        self
56    }
57
58    pub const fn bold(mut self) -> Style {
59        self.bold = true;
60        self
61    }
62
63    pub const fn underline(mut self) -> Style {
64        self.underline = true;
65        self
66    }
67
68    pub const fn reversed(mut self) -> Style {
69        self.reversed = true;
70        self
71    }
72
73    /// This style with `other` laid over it: `other`'s colors where
74    /// it has them, its attributes added.
75    pub fn patch(self, other: Style) -> Style {
76        Style {
77            fg: other.fg.or(self.fg),
78            bg: other.bg.or(self.bg),
79            bold: self.bold || other.bold,
80            underline: self.underline || other.underline,
81            reversed: self.reversed || other.reversed,
82        }
83    }
84}
85
86/// A color as a muttrc or the config names it.
87pub fn parse_color(name: &str) -> Option<Color> {
88    Some(match name.to_lowercase().as_str() {
89        "default" => Color::Reset,
90        "black" => Color::Black,
91        "red" => Color::Red,
92        "green" => Color::Green,
93        "yellow" => Color::Yellow,
94        "blue" => Color::Blue,
95        "magenta" => Color::Magenta,
96        "cyan" => Color::Cyan,
97        "white" => Color::White,
98        "gray" | "grey" | "darkgray" | "darkgrey" => Color::DarkGray,
99        "lightred" => Color::LightRed,
100        "lightgreen" => Color::LightGreen,
101        "lightyellow" => Color::LightYellow,
102        "lightblue" => Color::LightBlue,
103        "lightmagenta" => Color::LightMagenta,
104        "lightcyan" => Color::LightCyan,
105        _ => return None,
106    })
107}
108
109/// A `color_index` / `color_body` rule's look: a color, or an
110/// attribute name (bold, underline, reverse, standout; none clears
111/// nothing, as in the TUI it always did), in either slot.
112pub fn rule_style(
113    rule: &rmut_core::config::ColorRule,
114    what: &str,
115    warnings: &mut Vec<String>,
116) -> Style {
117    let mut style = Style::new();
118    for (name, is_fg) in [(&rule.fg, true), (&rule.bg, false)] {
119        let Some(name) = name else { continue };
120        match name.as_str() {
121            "bold" => style = style.bold(),
122            "underline" => style = style.underline(),
123            "reverse" | "standout" => style = style.reversed(),
124            "none" => {}
125            _ => match parse_color(name) {
126                Some(color) if is_fg => style = style.fg(color),
127                Some(color) => style = style.bg(color),
128                None => warnings.push(format!("unknown {what} color {name:?}")),
129            },
130        }
131    }
132    style
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn patch_overlays_colors_and_adds_attributes() {
141        let base = Style::new().fg(Color::Red).bold();
142        let over = Style::new().bg(Color::Blue).reversed();
143        let got = base.patch(over);
144        assert_eq!(
145            got,
146            Style::new()
147                .fg(Color::Red)
148                .bg(Color::Blue)
149                .bold()
150                .reversed()
151        );
152        let got = got.patch(Style::new().fg(Color::Green));
153        assert_eq!(got.fg, Some(Color::Green));
154        assert!(got.bold && got.reversed);
155    }
156
157    #[test]
158    fn rules_read_attributes_and_colors_in_either_slot() {
159        let rule = rmut_core::config::ColorRule {
160            pattern: String::new(),
161            fg: Some("bold".into()),
162            bg: Some("blue".into()),
163        };
164        let mut warnings = Vec::new();
165        let style = rule_style(&rule, "color_index", &mut warnings);
166        assert_eq!(style, Style::new().bold().bg(Color::Blue));
167        assert!(warnings.is_empty());
168        let rule = rmut_core::config::ColorRule {
169            pattern: String::new(),
170            fg: Some("chartreuse".into()),
171            bg: None,
172        };
173        assert_eq!(rule_style(&rule, "color_body", &mut warnings), Style::new());
174        assert_eq!(warnings.len(), 1);
175    }
176}