ex-cli 1.21.0

Command line tool to find, filter, sort and list files.
Documentation
use crate::regex;
use std::collections::HashMap;
use termcolor::{Color, ColorSpec};

pub struct ColorMap {
    pub dir_color: ColorSpec,
    pub exec_color: ColorSpec,
    pub exec_other: ColorSpec,
    pub link_color: ColorSpec,
    pub bad_color: ColorSpec,
    #[cfg(debug_assertions)]
    pub debug_color: ColorSpec,
    ext_colors: HashMap<String, ColorSpec>,
}

impl ColorMap {
    pub fn new(colors: Option<String>) -> Self {
        let (std_colors, ext_colors) = Self::load_colors(colors);
        let dir_color = Self::default_color(&std_colors, "di", Color::Blue, None);
        let exec_color = Self::default_color(&std_colors, "ex", Color::Green, None);
        let exec_other = Self::darken_color(&exec_color);
        let link_color = Self::default_color(&std_colors, "ln", Color::Cyan, None);
        let bad_color = Self::default_color(&std_colors, "or", Color::Red, Some(Color::Black));
        #[cfg(debug_assertions)]
        let debug_color = Self::create_color(Some(Color::Yellow), None, true);
        Self {
            dir_color,
            exec_color,
            exec_other,
            link_color,
            bad_color,
            #[cfg(debug_assertions)]
            debug_color,
            ext_colors,
        }
    }

    pub fn find_color(&self, ext: &str) -> Option<&ColorSpec> {
        let ext = ext.to_lowercase();
        if ext.starts_with('.') {
            self.ext_colors.get(&ext[1..])
        } else {
            self.ext_colors.get(&ext)
        }
    }

    fn load_colors(colors: Option<String>) -> (HashMap<String, ColorSpec>, HashMap<String, ColorSpec>) {
        let mut std_colors = HashMap::new();
        let mut ext_colors = HashMap::new();
        colors
            .unwrap_or_default()
            .split(':')
            .map(Self::parse_item)
            .flat_map(std::convert::identity)
            .for_each(|(key, color)| Self::load_into(&mut std_colors, &mut ext_colors, key, color));
        (std_colors, ext_colors)
    }

    fn load_into(
        std_colors: &mut HashMap<String, ColorSpec>,
        ext_colors: &mut HashMap<String, ColorSpec>,
        key: String,
        color: ColorSpec,
    ) {
        let ext_regex = regex!(r"^\*\.(\w+)$");
        let ext = ext_regex
            .captures(&key)
            .and_then(|captures| captures.get(1))
            .map(|matched| matched.as_str());
        if let Some(ext) = ext {
            ext_colors.insert(ext.to_lowercase(), color);
        } else {
            std_colors.insert(key.to_lowercase(), color);
        }
    }

    fn parse_item(item: &str) -> Option<(String, ColorSpec)> {
        let pair_regex = regex!(r"^(.+)=(.+)$");
        pair_regex
            .captures(item)
            .map(|captures| captures.extract())
            .and_then(|(_, [key, value])| Self::parse_color(key, value))
    }

    fn parse_color(key: &str, value: &str) -> Option<(String, ColorSpec)> {
        let mut color = ColorSpec::new();
        for item in value.split(';') {
            let item = item.parse::<usize>().ok()?;
            match item {
                1 => { color.set_bold(true); }
                4 => { color.set_underline(true); }
                30 => { color.set_fg(Some(Color::Black)); }
                31 => { color.set_fg(Some(Color::Red)); }
                32 => { color.set_fg(Some(Color::Green)); }
                33 => { color.set_fg(Some(Color::Yellow)); }
                34 => { color.set_fg(Some(Color::Blue)); }
                35 => { color.set_fg(Some(Color::Magenta)); }
                36 => { color.set_fg(Some(Color::Cyan)); }
                37 => { color.set_fg(Some(Color::White)); }
                40 => { color.set_bg(Some(Color::Black)); }
                41 => { color.set_bg(Some(Color::Red)); }
                42 => { color.set_bg(Some(Color::Green)); }
                43 => { color.set_bg(Some(Color::Yellow)); }
                44 => { color.set_bg(Some(Color::Blue)); }
                45 => { color.set_bg(Some(Color::Magenta)); }
                46 => { color.set_bg(Some(Color::Cyan)); }
                47 => { color.set_bg(Some(Color::White)); }
                _ => (),
            }
        }
        Some((key.to_string(), color))
    }

    fn default_color(
        colors: &HashMap<String, ColorSpec>,
        key: &str,
        fg_color: Color,
        bg_color: Option<Color>,
    ) -> ColorSpec {
        colors
            .get(key)
            .map(ColorSpec::clone)
            .unwrap_or_else(|| Self::create_color(Some(fg_color), bg_color, true))
    }

    fn create_color(
        fg_color: Option<Color>,
        bg_color: Option<Color>,
        bold: bool,
    ) -> ColorSpec {
        let mut color = ColorSpec::new();
        color.set_fg(fg_color);
        color.set_bg(bg_color);
        color.set_bold(bold);
        color
    }

    fn darken_color(color: &ColorSpec) -> ColorSpec {
        let mut color = color.clone();
        color.set_bold(false);
        color
    }
}

#[cfg(test)]
mod tests {
    use crate::util::color::ColorMap;
    use pretty_assertions::assert_eq;
    use std::collections::HashMap;
    use termcolor::Color;

    #[test]
    fn test_loads_colors_from_environment() {
        let colors = String::from("DI=34:ex=32:ln=36:or=40;31:*.GZ=01;31:*.png=01;35");
        let colors = ColorMap::new(Some(colors));
        let expected = HashMap::from([
            (String::from("gz"), ColorMap::create_color(Some(Color::Red), None, true)),
            (String::from("png"), ColorMap::create_color(Some(Color::Magenta), None, true)),
        ]);
        assert_eq!(ColorMap::create_color(Some(Color::Blue), None, false), colors.dir_color);
        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_color);
        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_other);
        assert_eq!(ColorMap::create_color(Some(Color::Cyan), None, false), colors.link_color);
        assert_eq!(ColorMap::create_color(Some(Color::Red), Some(Color::Black), false), colors.bad_color);
        assert_eq!(expected, colors.ext_colors);
    }

    #[test]
    fn test_loads_colors_from_default() {
        let colors = ColorMap::new(None);
        let expected = HashMap::new();
        assert_eq!(ColorMap::create_color(Some(Color::Blue), None, true), colors.dir_color);
        assert_eq!(ColorMap::create_color(Some(Color::Green), None, true), colors.exec_color);
        assert_eq!(ColorMap::create_color(Some(Color::Green), None, false), colors.exec_other);
        assert_eq!(ColorMap::create_color(Some(Color::Cyan), None, true), colors.link_color);
        assert_eq!(ColorMap::create_color(Some(Color::Red), Some(Color::Black), true), colors.bad_color);
        assert_eq!(expected, colors.ext_colors);
    }

    #[test]
    fn test_parses_color_from_invalid_string() {
        assert_eq!(None, ColorMap::parse_color("key", ""));
        assert_eq!(None, ColorMap::parse_color("key", "foo"));
    }

    #[test]
    fn test_parses_color_from_unexpected_number() {
        let expected = ColorMap::create_color(None, None, false);
        assert_eq!(Some((String::from("key"), expected)), ColorMap::parse_color("key", "999"));
    }

    #[test]
    fn test_parses_color_from_foreground_string() {
        let expected1 = ColorMap::create_color(Some(Color::Black), None, false);
        let expected2 = ColorMap::create_color(Some(Color::Red), None, false);
        let expected3 = ColorMap::create_color(Some(Color::Green), None, false);
        let expected4 = ColorMap::create_color(Some(Color::Yellow), None, false);
        let expected5 = ColorMap::create_color(Some(Color::Blue), None, false);
        let expected6 = ColorMap::create_color(Some(Color::Magenta), None, false);
        let expected7 = ColorMap::create_color(Some(Color::Cyan), None, false);
        let expected8 = ColorMap::create_color(Some(Color::White), None, false);
        assert_eq!(Some((String::from("key"), expected1)), ColorMap::parse_color("key", "30"));
        assert_eq!(Some((String::from("key"), expected2)), ColorMap::parse_color("key", "31"));
        assert_eq!(Some((String::from("key"), expected3)), ColorMap::parse_color("key", "32"));
        assert_eq!(Some((String::from("key"), expected4)), ColorMap::parse_color("key", "33"));
        assert_eq!(Some((String::from("key"), expected5)), ColorMap::parse_color("key", "34"));
        assert_eq!(Some((String::from("key"), expected6)), ColorMap::parse_color("key", "35"));
        assert_eq!(Some((String::from("key"), expected7)), ColorMap::parse_color("key", "36"));
        assert_eq!(Some((String::from("key"), expected8)), ColorMap::parse_color("key", "37"));
    }

    #[test]
    fn test_parses_color_from_background_string() {
        let expected1 = ColorMap::create_color(None, Some(Color::Black), false);
        let expected2 = ColorMap::create_color(None, Some(Color::Red), false);
        let expected3 = ColorMap::create_color(None, Some(Color::Green), false);
        let expected4 = ColorMap::create_color(None, Some(Color::Yellow), false);
        let expected5 = ColorMap::create_color(None, Some(Color::Blue), false);
        let expected6 = ColorMap::create_color(None, Some(Color::Magenta), false);
        let expected7 = ColorMap::create_color(None, Some(Color::Cyan), false);
        let expected8 = ColorMap::create_color(None, Some(Color::White), false);
        assert_eq!(Some((String::from("key"), expected1)), ColorMap::parse_color("key", "40"));
        assert_eq!(Some((String::from("key"), expected2)), ColorMap::parse_color("key", "41"));
        assert_eq!(Some((String::from("key"), expected3)), ColorMap::parse_color("key", "42"));
        assert_eq!(Some((String::from("key"), expected4)), ColorMap::parse_color("key", "43"));
        assert_eq!(Some((String::from("key"), expected5)), ColorMap::parse_color("key", "44"));
        assert_eq!(Some((String::from("key"), expected6)), ColorMap::parse_color("key", "45"));
        assert_eq!(Some((String::from("key"), expected7)), ColorMap::parse_color("key", "46"));
        assert_eq!(Some((String::from("key"), expected8)), ColorMap::parse_color("key", "47"));
    }

    #[test]
    fn test_parses_color_from_combined_string() {
        let expected = ColorMap::create_color(Some(Color::Yellow), Some(Color::Blue), true);
        assert_eq!(Some((String::from("key"), expected)), ColorMap::parse_color("key", "33;44;1"));
    }

    #[test]
    fn test_creates_dark_color_from_light_color() {
        let colors = vec![
            Color::Black,
            Color::Red,
            Color::Green,
            Color::Yellow,
            Color::Blue,
            Color::Magenta,
            Color::Cyan,
            Color::White,
        ];
        for color in colors {
            let light = ColorMap::create_color(Some(color), Some(color), true);
            let dark = ColorMap::create_color(Some(color), Some(color), false);
            assert_eq!(ColorMap::darken_color(&light), dark);
            assert_eq!(ColorMap::darken_color(&dark), dark);
        }
    }
}