ex_cli/util/
colors.rs

1use crate::regex;
2use std::collections::HashMap;
3use termcolor::{Color, ColorSpec};
4
5pub struct ColorMap {
6    pub dir_color: ColorSpec,
7    pub exec_color: ColorSpec,
8    pub exec_other: ColorSpec,
9    pub link_color: ColorSpec,
10    pub bad_color: ColorSpec,
11    #[cfg(debug_assertions)]
12    pub debug_color: ColorSpec,
13    ext_colors: HashMap<String, ColorSpec>,
14}
15
16impl ColorMap {
17    pub fn new(colors: Option<String>) -> Self {
18        let (std_colors, ext_colors) = Self::load_colors(colors);
19        let dir_color = Self::default_color(&std_colors, "di", Color::Blue, None);
20        let exec_color = Self::default_color(&std_colors, "ex", Color::Green, None);
21        let exec_other = Self::darken_color(&exec_color);
22        let link_color = Self::default_color(&std_colors, "ln", Color::Cyan, None);
23        let bad_color = Self::default_color(&std_colors, "or", Color::Red, Some(Color::Black));
24        #[cfg(debug_assertions)]
25        let debug_color = Self::create_color(Some(Color::Yellow), None, true);
26        Self {
27            dir_color,
28            exec_color,
29            exec_other,
30            link_color,
31            bad_color,
32            #[cfg(debug_assertions)]
33            debug_color,
34            ext_colors,
35        }
36    }
37
38    pub fn find_color(&self, ext: &str) -> Option<&ColorSpec> {
39        let ext = ext.to_lowercase();
40        if ext.starts_with('.') {
41            self.ext_colors.get(&ext[1..])
42        } else {
43            self.ext_colors.get(&ext)
44        }
45    }
46
47    fn load_colors(colors: Option<String>) -> (HashMap<String, ColorSpec>, HashMap<String, ColorSpec>) {
48        let mut std_colors = HashMap::new();
49        let mut ext_colors = HashMap::new();
50        colors.unwrap_or_default()
51            .split(':')
52            .map(Self::parse_item)
53            .flat_map(std::convert::identity)
54            .for_each(|(key, color)| Self::load_into(&mut std_colors, &mut ext_colors, key, color));
55        (std_colors, ext_colors)
56    }
57
58    fn load_into(
59        std_colors: &mut HashMap<String, ColorSpec>,
60        ext_colors: &mut HashMap<String, ColorSpec>,
61        key: String,
62        color: ColorSpec,
63    ) {
64        let ext_regex = regex!(r"^\*\.(\w+)$");
65        let ext = ext_regex.captures(&key)
66            .and_then(|captures| captures.get(1))
67            .map(|matched| matched.as_str());
68        if let Some(ext) = ext {
69            ext_colors.insert(ext.to_lowercase(), color);
70        } else {
71            std_colors.insert(key.to_lowercase(), color);
72        }
73    }
74
75    fn parse_item(item: &str) -> Option<(String, ColorSpec)> {
76        let pair_regex = regex!(r"^(.+)=(.+)$");
77        pair_regex.captures(item)
78            .map(|captures| captures.extract())
79            .and_then(|(_, [key, value])| Self::parse_color(key, value))
80    }
81
82    fn parse_color(key: &str, value: &str) -> Option<(String, ColorSpec)> {
83        let mut color = ColorSpec::new();
84        for item in value.split(';') {
85            let item = item.parse::<usize>().ok()?;
86            match item {
87                1 => { color.set_bold(true); }
88                4 => { color.set_underline(true); }
89                30 => { color.set_fg(Some(Color::Black)); }
90                31 => { color.set_fg(Some(Color::Red)); }
91                32 => { color.set_fg(Some(Color::Green)); }
92                33 => { color.set_fg(Some(Color::Yellow)); }
93                34 => { color.set_fg(Some(Color::Blue)); }
94                35 => { color.set_fg(Some(Color::Magenta)); }
95                36 => { color.set_fg(Some(Color::Cyan)); }
96                37 => { color.set_fg(Some(Color::White)); }
97                40 => { color.set_bg(Some(Color::Black)); }
98                41 => { color.set_bg(Some(Color::Red)); }
99                42 => { color.set_bg(Some(Color::Green)); }
100                43 => { color.set_bg(Some(Color::Yellow)); }
101                44 => { color.set_bg(Some(Color::Blue)); }
102                45 => { color.set_bg(Some(Color::Magenta)); }
103                46 => { color.set_bg(Some(Color::Cyan)); }
104                47 => { color.set_bg(Some(Color::White)); }
105                _ => (),
106            }
107        }
108        Some((key.to_string(), color))
109    }
110
111    fn default_color(
112        colors: &HashMap<String, ColorSpec>,
113        key: &str,
114        fg_color: Color,
115        bg_color: Option<Color>,
116    ) -> ColorSpec {
117        colors.get(key)
118            .map(ColorSpec::clone)
119            .unwrap_or_else(|| Self::create_color(Some(fg_color), bg_color, true))
120    }
121
122    fn create_color(
123        fg_color: Option<Color>,
124        bg_color: Option<Color>,
125        bold: bool,
126    ) -> ColorSpec {
127        let mut color = ColorSpec::new();
128        color.set_fg(fg_color);
129        color.set_bg(bg_color);
130        color.set_bold(bold);
131        color
132    }
133
134    fn darken_color(color: &ColorSpec) -> ColorSpec {
135        let mut color = color.clone();
136        color.set_bold(false);
137        color
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use crate::util::colors::ColorMap;
144    use pretty_assertions::assert_eq;
145    use std::collections::HashMap;
146    use termcolor::Color;
147
148    #[test]
149    fn test_loads_colors_from_environment() {
150        let colors = String::from("DI=34:ex=32:ln=36:or=40;31:*.GZ=01;31:*.png=01;35");
151        let colors = ColorMap::new(Some(colors));
152        let expected = HashMap::from([
153            (String::from("gz"), ColorMap::create_color(Some(Color::Red), None, true)),
154            (String::from("png"), ColorMap::create_color(Some(Color::Magenta), None, true)),
155        ]);
156        assert_eq!(ColorMap::create_color(Some(Color::Blue), None, false), colors.dir_color);
157        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_color);
158        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_other);
159        assert_eq!(ColorMap::create_color(Some(Color::Cyan), None, false), colors.link_color);
160        assert_eq!(ColorMap::create_color(Some(Color::Red), Some(Color::Black), false), colors.bad_color);
161        assert_eq!(expected, colors.ext_colors);
162    }
163
164    #[test]
165    fn test_loads_colors_from_default() {
166        let colors = ColorMap::new(None);
167        let expected = HashMap::new();
168        assert_eq!(ColorMap::create_color(Some(Color::Blue), None, true), colors.dir_color);
169        assert_eq!(ColorMap::create_color(Some(Color::Green), None, true), colors.exec_color);
170        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_other);
171        assert_eq!(ColorMap::create_color(Some(Color::Cyan), None, true), colors.link_color);
172        assert_eq!(ColorMap::create_color(Some(Color::Red), Some(Color::Black), true), colors.bad_color);
173        assert_eq!(expected, colors.ext_colors);
174    }
175
176    #[test]
177    fn test_parses_color_from_invalid_string() {
178        assert_eq!(None, ColorMap::parse_color("key", ""));
179        assert_eq!(None, ColorMap::parse_color("key", "foo"));
180    }
181
182    #[test]
183    fn test_parses_color_from_unexpected_number() {
184        let expected = ColorMap::create_color(None, None, false);
185        assert_eq!(Some((String::from("key"), expected)), ColorMap::parse_color("key", "999"));
186    }
187
188    #[test]
189    fn test_parses_color_from_foreground_string() {
190        let expected1 = ColorMap::create_color(Some(Color::Black), None, false);
191        let expected2 = ColorMap::create_color(Some(Color::Red), None, false);
192        let expected3 = ColorMap::create_color(Some(Color::Green), None, false);
193        let expected4 = ColorMap::create_color(Some(Color::Yellow), None, false);
194        let expected5 = ColorMap::create_color(Some(Color::Blue), None, false);
195        let expected6 = ColorMap::create_color(Some(Color::Magenta), None, false);
196        let expected7 = ColorMap::create_color(Some(Color::Cyan), None, false);
197        let expected8 = ColorMap::create_color(Some(Color::White), None, false);
198        assert_eq!(Some((String::from("key"), expected1)), ColorMap::parse_color("key", "30"));
199        assert_eq!(Some((String::from("key"), expected2)), ColorMap::parse_color("key", "31"));
200        assert_eq!(Some((String::from("key"), expected3)), ColorMap::parse_color("key", "32"));
201        assert_eq!(Some((String::from("key"), expected4)), ColorMap::parse_color("key", "33"));
202        assert_eq!(Some((String::from("key"), expected5)), ColorMap::parse_color("key", "34"));
203        assert_eq!(Some((String::from("key"), expected6)), ColorMap::parse_color("key", "35"));
204        assert_eq!(Some((String::from("key"), expected7)), ColorMap::parse_color("key", "36"));
205        assert_eq!(Some((String::from("key"), expected8)), ColorMap::parse_color("key", "37"));
206    }
207
208    #[test]
209    fn test_parses_color_from_background_string() {
210        let expected1 = ColorMap::create_color(None, Some(Color::Black), false);
211        let expected2 = ColorMap::create_color(None, Some(Color::Red), false);
212        let expected3 = ColorMap::create_color(None, Some(Color::Green), false);
213        let expected4 = ColorMap::create_color(None, Some(Color::Yellow), false);
214        let expected5 = ColorMap::create_color(None, Some(Color::Blue), false);
215        let expected6 = ColorMap::create_color(None, Some(Color::Magenta), false);
216        let expected7 = ColorMap::create_color(None, Some(Color::Cyan), false);
217        let expected8 = ColorMap::create_color(None, Some(Color::White), false);
218        assert_eq!(Some((String::from("key"), expected1)), ColorMap::parse_color("key", "40"));
219        assert_eq!(Some((String::from("key"), expected2)), ColorMap::parse_color("key", "41"));
220        assert_eq!(Some((String::from("key"), expected3)), ColorMap::parse_color("key", "42"));
221        assert_eq!(Some((String::from("key"), expected4)), ColorMap::parse_color("key", "43"));
222        assert_eq!(Some((String::from("key"), expected5)), ColorMap::parse_color("key", "44"));
223        assert_eq!(Some((String::from("key"), expected6)), ColorMap::parse_color("key", "45"));
224        assert_eq!(Some((String::from("key"), expected7)), ColorMap::parse_color("key", "46"));
225        assert_eq!(Some((String::from("key"), expected8)), ColorMap::parse_color("key", "47"));
226    }
227
228    #[test]
229    fn test_parses_color_from_combined_string() {
230        let expected = ColorMap::create_color(Some(Color::Yellow), Some(Color::Blue), true);
231        assert_eq!(Some((String::from("key"), expected)), ColorMap::parse_color("key", "33;44;1"));
232    }
233
234    #[test]
235    fn test_creates_dark_color_from_light_color() {
236        let colors = vec![
237            Color::Black,
238            Color::Red,
239            Color::Green,
240            Color::Yellow,
241            Color::Blue,
242            Color::Magenta,
243            Color::Cyan,
244            Color::White,
245        ];
246        for color in colors {
247            let light = ColorMap::create_color(Some(color), Some(color), true);
248            let dark = ColorMap::create_color(Some(color), Some(color), false);
249            assert_eq!(ColorMap::darken_color(&light), dark);
250            assert_eq!(ColorMap::darken_color(&dark), dark);
251        }
252    }
253}