use super::structs::{ThemeConfig, PartialThemeConfig, ROLES};
use super::colors::is_valid_color_str;
use super::builtins::load_named_theme;
use std::fs;
use std::collections::HashMap;
use std::path::Path;
use serde::Deserialize;
use tracing::warn;
use crate::config::settings::Settings;
use crate::config::structs::PartialConfig;
pub fn config_dir() -> Option<std::path::PathBuf> {
dirs::config_dir().map(|p| p.join("eazygit"))
}
#[derive(Debug, Clone, Deserialize, Default)]
struct ExternalTheme {
#[serde(default)]
extend: Option<ExtendBlock>,
#[serde(default)]
palette: HashMap<String, String>,
#[serde(default)]
ui: HashMap<String, String>,
}
#[derive(Debug, Clone, Deserialize)]
struct ExtendBlock {
base: String,
}
pub fn load_external_theme(path: &Path, name: &str) -> Option<ThemeConfig> {
let raw = fs::read_to_string(path).ok()?;
let parsed: ExternalTheme = match toml::from_str(&raw) {
Ok(v) => v,
Err(e) => {
warn!("theme {} parse error: {}", name, e);
return None;
}
};
let base = if let Some(extend) = parsed.extend {
load_named_theme(&extend.base)
} else {
ThemeConfig::default()
};
let palette = parsed.palette;
if palette.is_empty() {
warn!("theme {} has empty palette", name);
}
let ui_map = parsed.ui;
let mut cfg = base.clone();
for role in ROLES {
let palette_key = ui_map.get(*role).cloned().unwrap_or_else(|| role.to_string());
if let Some(val) = palette.get(&palette_key) {
if is_valid_color_str(val) {
cfg.set_role(role, val.clone());
} else {
warn!("theme {} invalid color '{}' for {}", name, val, role);
}
}
}
Some(cfg)
}
pub fn merge_partial(cfg: &mut ThemeConfig, other: PartialThemeConfig) {
if let Some(v) = other.fg { cfg.fg = v; }
if let Some(v) = other.bg { cfg.bg = v; }
if let Some(v) = other.muted { cfg.muted = v; }
if let Some(v) = other.accent { cfg.accent = v; }
if let Some(v) = other.border { cfg.border = v; }
if let Some(v) = other.border_focused { cfg.border_focused = v; }
if let Some(v) = other.selection_bg { cfg.selection_bg = v; }
if let Some(v) = other.selection_fg { cfg.selection_fg = v; }
if let Some(v) = other.diff_add { cfg.diff_add = v; }
if let Some(v) = other.diff_remove { cfg.diff_remove = v; }
if let Some(v) = other.diff_context { cfg.diff_context = v; }
if let Some(v) = other.diff_hunk { cfg.diff_hunk = v; }
if let Some(v) = other.staged { cfg.staged = v; }
if let Some(v) = other.unstaged { cfg.unstaged = v; }
if let Some(v) = other.untracked { cfg.untracked = v; }
if let Some(v) = other.header { cfg.header = v; }
if let Some(v) = other.footer { cfg.footer = v; }
if let Some(v) = other.error { cfg.error = v; }
if let Some(v) = other.warning { cfg.warning = v; }
if let Some(v) = other.success { cfg.success = v; }
}
impl ThemeConfig {
pub fn set_role(&mut self, role: &str, value: String) {
match role {
"fg" => self.fg = value,
"bg" => self.bg = value,
"muted" => self.muted = value,
"accent" => self.accent = value,
"border" => self.border = value,
"border_focused" => self.border_focused = value,
"selection_bg" => self.selection_bg = value,
"selection_fg" => self.selection_fg = value,
"diff_add" => self.diff_add = value,
"diff_remove" => self.diff_remove = value,
"diff_context" => self.diff_context = value,
"diff_hunk" => self.diff_hunk = value,
"staged" => self.staged = value,
"unstaged" => self.unstaged = value,
"untracked" => self.untracked = value,
"header" => self.header = value,
"footer" => self.footer = value,
"error" => self.error = value,
"warning" => self.warning = value,
"success" => self.success = value,
_ => {}
}
}
}
pub fn load_theme_by_name(name: &str) -> ThemeConfig {
let mut theme = load_named_theme(name);
if let Some(config_dir) = config_dir() {
let theme_path = config_dir.join("themes").join(format!("{}.toml", name));
if theme_path.exists() {
if let Some(external) = load_external_theme(&theme_path, name) {
theme = external;
}
}
}
theme
}
pub fn list_themes() -> Vec<String> {
let mut themes = vec![
"default_dark".to_string(),
"default_light".to_string(),
"dracula".to_string(),
"gruvbox".to_string(),
"monokai".to_string(),
"nord".to_string(),
"one_dark".to_string(),
"solarized_dark".to_string(),
"solarized_light".to_string(),
"tokyo_night".to_string(),
];
if let Some(config_dir) = config_dir() {
let themes_dir = config_dir.join("themes");
if themes_dir.exists() {
if let Ok(entries) = fs::read_dir(themes_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |e| e == "toml") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if !themes.contains(&stem.to_string()) {
themes.push(stem.to_string());
}
}
}
}
}
}
}
themes.sort();
themes
}