use std::collections::HashMap;
use termcolor::{Color, ColorSpec};
use crate::regex;
pub struct ColorMap {
pub dir_color: ColorSpec,
pub exec_color: ColorSpec,
pub exec_other: ColorSpec,
pub link_color: ColorSpec,
pub bad_color: ColorSpec,
ext_colors: HashMap<String, ColorSpec>,
}
impl ColorMap {
pub fn new(colors: Option<String>) -> ColorMap {
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));
return Self { dir_color, exec_color, exec_other, 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 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)); }
_ => (),
}
}
return 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);
return color;
}
fn darken_color(color: &ColorSpec) -> ColorSpec {
let mut color = color.clone();
color.set_bold(false);
return color;
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use pretty_assertions::assert_eq;
use termcolor::Color;
use crate::colors::ColorMap;
#[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);
}
}
}