use anstyle::Style as AnsiStyle;
use hashbrown::HashMap;
use std::env;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct FileColorizer {
ls_colors_map: HashMap<String, AnsiStyle>,
has_ls_colors: bool,
}
impl FileColorizer {
pub fn new() -> Self {
let ls_colors = env::var("LS_COLORS").unwrap_or_default();
let ls_colors_map = if ls_colors.is_empty() {
HashMap::new()
} else {
Self::parse_ls_colors(&ls_colors)
};
Self {
has_ls_colors: !ls_colors_map.is_empty(),
ls_colors_map,
}
}
fn parse_ls_colors(ls_colors: &str) -> HashMap<String, AnsiStyle> {
let mut map = HashMap::new();
for pair in ls_colors.split(':') {
if pair.is_empty() {
continue;
}
if let Some((key, value)) = pair.split_once('=') {
let parser = crate::utils::CachedStyleParser::default();
if let Ok(style) = parser.parse_ls_colors(value) {
map.insert(key.to_owned(), style);
}
}
}
map
}
pub fn style_for_path(&self, path: &Path) -> Option<AnsiStyle> {
if !self.has_ls_colors {
return None;
}
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let ext_key = format!("*.{}", ext);
if let Some(style) = self.get_style(&ext_key) {
return Some(style);
}
}
let file_type_key = self.determine_file_type_key(path);
if let Some(style) = self.get_style(&file_type_key) {
return Some(style);
}
self.get_style("fi") }
fn get_style(&self, key: &str) -> Option<AnsiStyle> {
if let Some(style) = self.ls_colors_map.get(key) {
return Some(*style);
}
if key.starts_with("*.")
&& let Some(style) = self.ls_colors_map.get("fi")
{
return Some(*style);
}
None
}
pub fn determine_file_type_key(&self, path: &Path) -> String {
let path_str = path.to_string_lossy();
if path_str.ends_with('/') || path_str.ends_with('\\') {
return "di".to_string(); }
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& (name.ends_with(".sh")
|| name.ends_with(".py")
|| name.ends_with(".rb")
|| name.ends_with(".pl")
|| name.ends_with(".php"))
{
return "ex".to_string(); }
match path.file_name().and_then(|n| n.to_str()) {
Some(name) if name.starts_with('.') => "so".to_string(), _ => "fi".to_string(), }
}
}
impl Default for FileColorizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_without_ls_colors() {
let ls_colors_val = env::var("LS_COLORS").unwrap_or_default();
let colorizer = FileColorizer::new();
if ls_colors_val.is_empty() {
assert!(!colorizer.has_ls_colors);
assert!(colorizer.ls_colors_map.is_empty());
}
}
#[test]
fn test_parse_ls_colors() {
let ls_colors = "di=01;34:ln=01;36:ex=01;32:*rs=00;35";
let map = FileColorizer::parse_ls_colors(ls_colors);
assert_eq!(map.len(), 4);
assert!(map.contains_key("di"));
assert!(map.contains_key("ln"));
assert!(map.contains_key("ex"));
assert!(map.contains_key("*rs"));
}
#[test]
fn test_style_for_path_no_ls_colors() {
let colorizer = FileColorizer {
ls_colors_map: HashMap::new(),
has_ls_colors: false,
};
let path = Path::new("/tmp/test.rs");
let style = colorizer.style_for_path(path);
assert!(style.is_none());
}
#[test]
fn test_determine_file_type_key_directory() {
let colorizer = FileColorizer {
ls_colors_map: {
let mut map = HashMap::new();
map.insert("di".to_string(), anstyle::Style::new().bold());
map
},
has_ls_colors: true,
};
assert_eq!(
colorizer.determine_file_type_key(Path::new("/tmp/dir/")),
"di"
);
}
#[test]
fn test_determine_file_type_key_extension() {
let colorizer = FileColorizer {
ls_colors_map: {
let mut map = HashMap::new();
map.insert("*.rs".to_string(), anstyle::Style::new().bold());
map.insert("fi".to_string(), anstyle::Style::new().underline());
map
},
has_ls_colors: true,
};
let rs_path = Path::new("/tmp/test.rs");
let rs_style = colorizer.style_for_path(rs_path);
assert!(rs_style.is_some());
let txt_path = Path::new("/tmp/test.txt");
let txt_style = colorizer.style_for_path(txt_path);
assert!(txt_style.is_some()); }
#[test]
fn test_determine_file_type_key_executables() {
let colorizer = FileColorizer {
ls_colors_map: {
let mut map = HashMap::new();
map.insert("ex".to_string(), anstyle::Style::new().bold());
map
},
has_ls_colors: true,
};
assert_eq!(
colorizer.determine_file_type_key(Path::new("/tmp/script.sh")),
"ex"
);
assert_eq!(
colorizer.determine_file_type_key(Path::new("/tmp/main.py")),
"ex"
);
assert_eq!(
colorizer.determine_file_type_key(Path::new("/tmp/main.rb")),
"ex"
);
}
}