use crate::fsops::FileType;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tabled::settings::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ColorValue {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
impl ColorValue {
pub fn to_tabled_color(self) -> Color {
match self {
ColorValue::Black => Color::FG_BLACK,
ColorValue::Red => Color::FG_RED,
ColorValue::Green => Color::FG_GREEN,
ColorValue::Yellow => Color::FG_YELLOW,
ColorValue::Blue => Color::FG_BLUE,
ColorValue::Magenta => Color::FG_MAGENTA,
ColorValue::Cyan => Color::FG_CYAN,
ColorValue::White => Color::FG_WHITE,
ColorValue::BrightBlack => Color::FG_BRIGHT_BLACK,
ColorValue::BrightRed => Color::FG_BRIGHT_RED,
ColorValue::BrightGreen => Color::FG_BRIGHT_GREEN,
ColorValue::BrightYellow => Color::FG_BRIGHT_YELLOW,
ColorValue::BrightBlue => Color::FG_BRIGHT_BLUE,
ColorValue::BrightMagenta => Color::FG_BRIGHT_MAGENTA,
ColorValue::BrightCyan => Color::FG_BRIGHT_CYAN,
ColorValue::BrightWhite => Color::FG_BRIGHT_WHITE,
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"black" => Some(ColorValue::Black),
"red" => Some(ColorValue::Red),
"green" => Some(ColorValue::Green),
"yellow" => Some(ColorValue::Yellow),
"blue" => Some(ColorValue::Blue),
"magenta" => Some(ColorValue::Magenta),
"cyan" => Some(ColorValue::Cyan),
"white" => Some(ColorValue::White),
"bright_black" => Some(ColorValue::BrightBlack),
"bright_red" => Some(ColorValue::BrightRed),
"bright_green" => Some(ColorValue::BrightGreen),
"bright_yellow" => Some(ColorValue::BrightYellow),
"bright_blue" => Some(ColorValue::BrightBlue),
"bright_magenta" => Some(ColorValue::BrightMagenta),
"bright_cyan" => Some(ColorValue::BrightCyan),
"bright_white" => Some(ColorValue::BrightWhite),
_ => None,
}
}
}
impl std::fmt::Display for ColorValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
ColorValue::Black => "black",
ColorValue::Red => "red",
ColorValue::Green => "green",
ColorValue::Yellow => "yellow",
ColorValue::Blue => "blue",
ColorValue::Magenta => "magenta",
ColorValue::Cyan => "cyan",
ColorValue::White => "white",
ColorValue::BrightBlack => "bright_black",
ColorValue::BrightRed => "bright_red",
ColorValue::BrightGreen => "bright_green",
ColorValue::BrightYellow => "bright_yellow",
ColorValue::BrightBlue => "bright_blue",
ColorValue::BrightMagenta => "bright_magenta",
ColorValue::BrightCyan => "bright_cyan",
ColorValue::BrightWhite => "bright_white",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FileTypeColors {
pub file: ColorValue,
pub directory: ColorValue,
pub symlink: ColorValue,
}
impl Default for FileTypeColors {
fn default() -> Self {
Self {
file: ColorValue::BrightCyan,
directory: ColorValue::BrightBlue,
symlink: ColorValue::BrightMagenta,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Theme {
pub file_types: FileTypeColors,
pub extensions: HashMap<String, ColorValue>,
pub table: TableColors,
}
impl Default for Theme {
fn default() -> Self {
Self {
file_types: FileTypeColors::default(),
extensions: default_extension_colors(),
table: TableColors::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TableColors {
pub name: ColorValue,
pub size: ColorValue,
pub date: ColorValue,
pub header: ColorValue,
}
impl Default for TableColors {
fn default() -> Self {
Self {
name: ColorValue::BrightCyan,
size: ColorValue::BrightMagenta,
date: ColorValue::BrightYellow,
header: ColorValue::BrightGreen,
}
}
}
fn default_extension_colors() -> HashMap<String, ColorValue> {
[
("rs", ColorValue::Yellow), ("py", ColorValue::Blue), ("js", ColorValue::Yellow), ("ts", ColorValue::Blue), ("go", ColorValue::BrightCyan), ("c", ColorValue::BrightBlue), ("cpp", ColorValue::BrightBlue), ("java", ColorValue::Red), ("md", ColorValue::Cyan), ("txt", ColorValue::White), ("pdf", ColorValue::Red), ("toml", ColorValue::Red), ("json", ColorValue::Green), ("yaml", ColorValue::Magenta), ("yml", ColorValue::Magenta), ("xml", ColorValue::Yellow), ("zip", ColorValue::Red), ("tar", ColorValue::Red), ("gz", ColorValue::Red), ("png", ColorValue::Magenta), ("jpg", ColorValue::Magenta), ("jpeg", ColorValue::Magenta), ("gif", ColorValue::Magenta), ("svg", ColorValue::Yellow), ]
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect()
}
pub fn load_theme() -> Theme {
if let Ok(theme) = load_theme_from_config() {
return theme;
}
Theme::default()
}
fn load_theme_from_config() -> Result<Theme, Box<dyn std::error::Error>> {
let config_dir = dirs::config_dir()
.ok_or("Could not determine config directory")?
.join("bestls");
let config_path = config_dir.join("config.toml");
if !config_path.exists() {
return Err("Config file not found".into());
}
let content = std::fs::read_to_string(&config_path)?;
let config: ThemeConfig = toml::from_str(&content)?;
Ok(config.into_theme())
}
#[derive(Debug, Serialize, Deserialize)]
struct ThemeConfig {
#[serde(default)]
colors: ColorConfig,
}
#[derive(Debug, Serialize, Deserialize, Default)]
struct ColorConfig {
#[serde(default)]
file_types: Option<FileTypeColors>,
#[serde(default)]
extensions: Option<HashMap<String, String>>,
#[serde(default)]
table: Option<TableColors>,
}
impl ThemeConfig {
fn into_theme(self) -> Theme {
let mut theme = Theme::default();
if let Some(ft) = self.colors.file_types {
theme.file_types = ft;
}
if let Some(exts) = self.colors.extensions {
for (ext, color_str) in exts {
if let Some(color) = ColorValue::from_str(&color_str) {
theme.extensions.insert(ext, color);
}
}
}
if let Some(tc) = self.colors.table {
theme.table = tc;
}
theme
}
}
#[allow(dead_code)]
pub fn get_file_color(file_type: &FileType, filename: &str, theme: &Theme) -> ColorValue {
match file_type {
FileType::File => {
if let Some(pos) = filename.rfind('.') {
let ext = &filename[pos + 1..].to_lowercase();
if let Some(color) = theme.extensions.get(ext) {
return *color;
}
}
theme.file_types.file
}
FileType::Directory => theme.file_types.directory,
FileType::Symlink => theme.file_types.symlink,
}
}
pub fn create_sample_config() -> Result<PathBuf, Box<dyn std::error::Error>> {
let config_dir = dirs::config_dir()
.ok_or("Could not determine config directory")?
.join("bestls");
std::fs::create_dir_all(&config_dir)?;
let config_path = config_dir.join("config.toml");
if !config_path.exists() {
let sample_config = r#"# bestls Configuration File
# Location: ~/.config/bestls/config.toml
[colors]
# File type colors
file = "bright_cyan"
directory = "bright_blue"
symlink = "bright_magenta"
[colors.table]
# Table column colors
name = "bright_cyan"
size = "bright_magenta"
date = "bright_yellow"
header = "bright_green"
[colors.extensions]
# Extension-based file colors (case-insensitive)
rs = "yellow"
py = "blue"
js = "yellow"
ts = "blue"
go = "bright_cyan"
md = "cyan"
json = "green"
toml = "red"
yaml = "magenta"
yml = "magenta"
"#;
std::fs::write(&config_path, sample_config)?;
}
Ok(config_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_value_from_str() {
assert_eq!(
ColorValue::from_str("bright_cyan"),
Some(ColorValue::BrightCyan)
);
assert_eq!(ColorValue::from_str("red"), Some(ColorValue::Red));
assert_eq!(ColorValue::from_str("invalid"), None);
}
#[test]
fn test_get_file_color() {
let theme = Theme::default();
let color = get_file_color(&FileType::File, "test.rs", &theme);
assert_eq!(color, ColorValue::Yellow);
let color = get_file_color(&FileType::File, "test.unknown", &theme);
assert_eq!(color, theme.file_types.file);
let color = get_file_color(&FileType::Directory, "src", &theme);
assert_eq!(color, theme.file_types.directory);
}
#[test]
fn test_default_extension_colors() {
let colors = default_extension_colors();
assert!(colors.contains_key("rs"));
assert_eq!(colors.get("rs"), Some(&ColorValue::Yellow));
assert!(colors.contains_key("py"));
}
}