use std::collections::HashMap;
use termcolor::{Color, ColorSpec};
use crate::regex;
pub struct ColorMap {
pub dir_color: ColorSpec,
pub exec_color: ColorSpec,
pub link_color: ColorSpec,
pub bad_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 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));
return Self { dir_color, exec_color, link_color, bad_color, ext_colors };
}
pub fn find_color(&self, ext: &str) -> Option<&ColorSpec> {
let ext = ext.to_lowercase();
return 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));
return (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"^(.+)=(.+)$");
return 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 spec = ColorSpec::new();
for item in value.split(';') {
let item = item.parse::<usize>().ok()?;
match item {
1 => { spec.set_bold(true); }
4 => { spec.set_underline(true); }
30 => { spec.set_fg(Some(Color::Black)); }
31 => { spec.set_fg(Some(Color::Red)); }
32 => { spec.set_fg(Some(Color::Green)); }
33 => { spec.set_fg(Some(Color::Yellow)); }
34 => { spec.set_fg(Some(Color::Blue)); }
35 => { spec.set_fg(Some(Color::Magenta)); }
36 => { spec.set_fg(Some(Color::Cyan)); }
37 => { spec.set_fg(Some(Color::White)); }
40 => { spec.set_bg(Some(Color::Black)); }
41 => { spec.set_bg(Some(Color::Red)); }
42 => { spec.set_bg(Some(Color::Green)); }
43 => { spec.set_bg(Some(Color::Yellow)); }
44 => { spec.set_bg(Some(Color::Blue)); }
45 => { spec.set_bg(Some(Color::Magenta)); }
46 => { spec.set_bg(Some(Color::Cyan)); }
47 => { spec.set_bg(Some(Color::White)); }
_ => (),
}
}
return Some((key.to_string(), spec));
}
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 spec = ColorSpec::new();
spec.set_fg(fg_color);
spec.set_bg(bg_color);
spec.set_bold(bold);
return spec;
}
}
#[cfg(test)]
mod tests {
use termcolor::Color;
use crate::colors::ColorMap;
#[test]
fn test_loads_colors_from_environment_var() {
let variable = Some(String::from("di=01;34:EX=01;32:fi=missing:*.GZ=01;31:*.png=01;35:*.foo=bar"));
let (std_colors, ext_colors) = ColorMap::load_colors(variable);
assert_eq!(2, std_colors.len());
assert_eq!(2, ext_colors.len());
assert_eq!(true, std_colors.contains_key("di"));
assert_eq!(true, std_colors.contains_key("ex"));
assert_eq!(true, ext_colors.contains_key("gz"));
assert_eq!(true, ext_colors.contains_key("png"));
}
#[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"));
}
}