use crate::regex;
use std::collections::HashMap;
use termcolor::{Color, ColorSpec};
#[derive(Debug)]
pub struct ColorMap {
pub dir_color: ColorSpec,
pub exec_color: ColorSpec,
pub exec_other: ColorSpec,
pub link_color: ColorSpec,
pub bad_color: ColorSpec,
pub block_color: ColorSpec,
pub char_color: ColorSpec,
pub pipe_color: ColorSpec,
pub socket_color: ColorSpec,
#[cfg(debug_assertions)]
pub debug_color: ColorSpec,
pub 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, true);
let exec_color = Self::default_color(&std_colors, "ex", Color::Green, None, true);
let exec_other = Self::darken_color(&exec_color);
let link_color = Self::default_color(&std_colors, "ln", Color::Cyan, None, true);
let bad_color = Self::default_color(&std_colors, "or", Color::Red, Some(Color::Black), true);
let block_color = Self::default_color(&std_colors, "bd", Color::White, None, false);
let char_color = Self::default_color(&std_colors, "cd", Color::White, None, false);
let pipe_color = Self::default_color(&std_colors, "pi", Color::White, None, false);
let socket_color = Self::default_color(&std_colors, "so", Color::White, None, false);
#[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,
block_color,
char_color,
pipe_color,
socket_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>,
bold: bool,
) -> ColorSpec {
colors
.get(key)
.map(ColorSpec::clone)
.unwrap_or_else(|| Self::create_color(Some(fg_color), bg_color, bold))
}
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 crate::util::matcher::tests::*;
use googletest::prelude::*;
use termcolor::Color;
const LS_COLORS: &'static str = "\
di=34:\
ex=32:\
ln=36:\
or=31;40:\
bd=33;40:\
cd=33;40:\
pi=33;40:\
so=35:\
*.GZ=31;01:\
*.png=35;01:\
";
#[gtest]
fn test_loads_colors_from_environment() {
let colors = ColorMap::new(Some(String::from(LS_COLORS)));
expect_that!(colors, pat!(ColorMap {
dir_color: color_eq(Some(Color::Blue), None, false, false),
exec_color: color_eq(Some(Color::Green), None, false, false),
exec_other: color_eq(Some(Color::Green), None, false, false),
link_color: color_eq(Some(Color::Cyan), None, false, false),
bad_color: color_eq(Some(Color::Red), Some(Color::Black), false, false),
block_color: color_eq(Some(Color::Yellow), Some(Color::Black), false, false),
char_color: color_eq(Some(Color::Yellow), Some(Color::Black), false, false),
pipe_color: color_eq(Some(Color::Yellow), Some(Color::Black), false, false),
socket_color: color_eq(Some(Color::Magenta), None, false, false),
ext_colors: unordered_elements_are![
(eq("gz"), color_eq(Some(Color::Red), None, true, false)),
(eq("png"), color_eq(Some(Color::Magenta), None, true, false)),
],
..
}));
}
#[gtest]
fn test_loads_colors_from_default() {
let colors = ColorMap::new(None);
expect_that!(colors, pat!(ColorMap {
dir_color: color_eq(Some(Color::Blue), None, true, false),
exec_color: color_eq(Some(Color::Green), None, true, false),
exec_other: color_eq(Some(Color::Green), None, false, false),
link_color: color_eq(Some(Color::Cyan), None, true, false),
bad_color: color_eq(Some(Color::Red), Some(Color::Black), true, false),
block_color: color_eq(Some(Color::White), None, false, false),
char_color: color_eq(Some(Color::White), None, false, false),
pipe_color: color_eq(Some(Color::White), None, false, false),
socket_color: color_eq(Some(Color::White), None, false, false),
ext_colors: is_empty(),
..
}));
}
#[gtest]
fn test_parses_color_from_invalid_string() {
expect_that!(ColorMap::parse_color("key", ""), none());
expect_that!(ColorMap::parse_color("key", "foo"), none());
}
#[gtest]
fn test_parses_color_from_unexpected_number() {
expect_that!(ColorMap::parse_color("key", "999"), some((eq("key"), color_eq(None, None, false, false))));
}
#[gtest]
fn test_parses_color_from_foreground_string() {
expect_that!(ColorMap::parse_color("key", "30"), some((eq("key"), color_eq(Some(Color::Black), None, false, false))));
expect_that!(ColorMap::parse_color("key", "31"), some((eq("key"), color_eq(Some(Color::Red), None, false, false))));
expect_that!(ColorMap::parse_color("key", "32"), some((eq("key"), color_eq(Some(Color::Green), None, false, false))));
expect_that!(ColorMap::parse_color("key", "33"), some((eq("key"), color_eq(Some(Color::Yellow), None, false, false))));
expect_that!(ColorMap::parse_color("key", "34"), some((eq("key"), color_eq(Some(Color::Blue), None, false, false))));
expect_that!(ColorMap::parse_color("key", "35"), some((eq("key"), color_eq(Some(Color::Magenta), None, false, false))));
expect_that!(ColorMap::parse_color("key", "36"), some((eq("key"), color_eq(Some(Color::Cyan), None, false, false))));
expect_that!(ColorMap::parse_color("key", "37"), some((eq("key"), color_eq(Some(Color::White), None, false, false))));
}
#[gtest]
fn test_parses_color_from_background_string() {
expect_that!(ColorMap::parse_color("key", "40"), some((eq("key"), color_eq(None, Some(Color::Black), false, false))));
expect_that!(ColorMap::parse_color("key", "41"), some((eq("key"), color_eq(None, Some(Color::Red), false, false))));
expect_that!(ColorMap::parse_color("key", "42"), some((eq("key"), color_eq(None, Some(Color::Green), false, false))));
expect_that!(ColorMap::parse_color("key", "43"), some((eq("key"), color_eq(None, Some(Color::Yellow), false, false))));
expect_that!(ColorMap::parse_color("key", "44"), some((eq("key"), color_eq(None, Some(Color::Blue), false, false))));
expect_that!(ColorMap::parse_color("key", "45"), some((eq("key"), color_eq(None, Some(Color::Magenta), false, false))));
expect_that!(ColorMap::parse_color("key", "46"), some((eq("key"), color_eq(None, Some(Color::Cyan), false, false))));
expect_that!(ColorMap::parse_color("key", "47"), some((eq("key"), color_eq(None, Some(Color::White), false, false))));
}
#[gtest]
fn test_parses_color_from_bold_underline_flags() {
expect_that!(ColorMap::parse_color("key", "0"), some((eq("key"), color_eq(None, None, false, false))));
expect_that!(ColorMap::parse_color("key", "1"), some((eq("key"), color_eq(None, None, true, false))));
expect_that!(ColorMap::parse_color("key", "4"), some((eq("key"), color_eq(None, None, false, true))));
}
#[gtest]
fn test_parses_color_from_combined_string() {
expect_that!(ColorMap::parse_color("key", "33;44;1;4"), some((eq("key"), color_eq(Some(Color::Yellow), Some(Color::Blue), true, true))));
}
#[gtest]
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);
expect_eq!(ColorMap::darken_color(&light), dark);
expect_eq!(ColorMap::darken_color(&dark), dark);
}
}
}