Skip to main content

ex_cli/util/
color.rs

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