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