use std::collections::HashMap;
use std::path::{Path, PathBuf};
use super::types::{Theme, ThemeFile, ThemeInfo, BUILTIN_THEMES};
#[derive(Debug, Clone)]
pub struct ThemeRegistry {
themes: HashMap<String, Theme>,
theme_list: Vec<ThemeInfo>,
}
impl ThemeRegistry {
pub fn get(&self, name: &str) -> Option<&Theme> {
let normalized = name.to_lowercase().replace('_', "-");
self.themes.get(&normalized)
}
pub fn get_cloned(&self, name: &str) -> Option<Theme> {
self.get(name).cloned()
}
pub fn list(&self) -> &[ThemeInfo] {
&self.theme_list
}
pub fn names(&self) -> Vec<String> {
self.theme_list.iter().map(|t| t.name.clone()).collect()
}
pub fn contains(&self, name: &str) -> bool {
let normalized = name.to_lowercase().replace('_', "-");
self.themes.contains_key(&normalized)
}
pub fn len(&self) -> usize {
self.themes.len()
}
pub fn is_empty(&self) -> bool {
self.themes.is_empty()
}
}
pub struct ThemeLoader {
user_themes_dir: Option<PathBuf>,
}
impl ThemeLoader {
pub fn new() -> Self {
Self {
user_themes_dir: dirs::config_dir().map(|p| p.join("fresh").join("themes")),
}
}
pub fn with_user_dir(user_themes_dir: Option<PathBuf>) -> Self {
Self { user_themes_dir }
}
pub fn user_themes_dir(&self) -> Option<&Path> {
self.user_themes_dir.as_deref()
}
pub fn load_all(&self) -> ThemeRegistry {
let mut themes = HashMap::new();
let mut theme_list = Vec::new();
for builtin in BUILTIN_THEMES {
if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(builtin.json) {
let theme: Theme = theme_file.into();
themes.insert(builtin.name.to_string(), theme);
theme_list.push(ThemeInfo::new(builtin.name, builtin.pack));
}
}
if let Some(ref user_dir) = self.user_themes_dir {
self.scan_directory(user_dir, "user", &mut themes, &mut theme_list);
}
if let Some(ref user_dir) = self.user_themes_dir {
let packages_dir = user_dir.join("packages");
if packages_dir.exists() {
if let Ok(entries) = std::fs::read_dir(&packages_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if !name.starts_with('.') {
let manifest_path = path.join("package.json");
if manifest_path.exists() {
self.load_package_themes(
&path,
name,
&mut themes,
&mut theme_list,
);
} else {
let pack_name = format!("pkg/{}", name);
self.scan_directory(
&path,
&pack_name,
&mut themes,
&mut theme_list,
);
}
}
}
}
}
}
}
}
ThemeRegistry { themes, theme_list }
}
fn load_package_themes(
&self,
pkg_dir: &Path,
pkg_name: &str,
themes: &mut HashMap<String, Theme>,
theme_list: &mut Vec<ThemeInfo>,
) {
let manifest_path = pkg_dir.join("package.json");
let manifest_content = match std::fs::read_to_string(&manifest_path) {
Ok(c) => c,
Err(_) => return,
};
let manifest: serde_json::Value = match serde_json::from_str(&manifest_content) {
Ok(v) => v,
Err(_) => return,
};
if let Some(fresh) = manifest.get("fresh") {
if let Some(theme_entries) = fresh.get("themes").and_then(|t| t.as_array()) {
for entry in theme_entries {
if let (Some(file), Some(name)) = (
entry.get("file").and_then(|f| f.as_str()),
entry.get("name").and_then(|n| n.as_str()),
) {
let theme_path = pkg_dir.join(file);
if theme_path.exists() {
if let Ok(content) = std::fs::read_to_string(&theme_path) {
if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(&content)
{
let theme: Theme = theme_file.into();
let normalized_name = name.to_lowercase().replace(' ', "-");
if !themes.contains_key(&normalized_name) {
themes.insert(normalized_name.clone(), theme);
let pack_name = format!("pkg/{}", pkg_name);
theme_list
.push(ThemeInfo::new(normalized_name, &pack_name));
}
}
}
}
}
}
return;
}
}
let pack_name = format!("pkg/{}", pkg_name);
self.scan_directory(pkg_dir, &pack_name, themes, theme_list);
}
fn scan_directory(
&self,
dir: &Path,
pack: &str,
themes: &mut HashMap<String, Theme>,
theme_list: &mut Vec<ThemeInfo>,
) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let subdir_name = path.file_name().unwrap().to_string_lossy();
let new_pack = if pack == "user" {
format!("user/{}", subdir_name)
} else {
format!("{}/{}", pack, subdir_name)
};
self.scan_directory(&path, &new_pack, themes, theme_list);
} else if path.extension().is_some_and(|ext| ext == "json") {
let name = path.file_stem().unwrap().to_string_lossy().to_string();
if themes.contains_key(&name) {
continue;
}
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(&content) {
let theme: Theme = theme_file.into();
themes.insert(name.clone(), theme);
theme_list.push(ThemeInfo::new(name, pack));
}
}
}
}
}
}
impl Theme {
pub fn set_terminal_cursor_color(&self) {
use super::types::color_to_rgb;
use std::io::Write;
if let Some((r, g, b)) = color_to_rgb(self.cursor) {
let _ = write!(
std::io::stdout(),
"\x1b]12;#{:02x}{:02x}{:02x}\x07",
r,
g,
b
);
let _ = std::io::stdout().flush();
}
}
pub fn reset_terminal_cursor_color() {
use std::io::Write;
let _ = write!(std::io::stdout(), "\x1b]112\x07");
let _ = std::io::stdout().flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_registry_get() {
let loader = ThemeLoader::new();
let registry = loader.load_all();
assert!(registry.get("dark").is_some());
assert!(registry.get("light").is_some());
assert!(registry.get("high-contrast").is_some());
assert!(registry.get("Dark").is_some());
assert!(registry.get("DARK").is_some());
assert!(registry.get("high_contrast").is_some());
assert!(registry.get("nonexistent-theme").is_none());
}
#[test]
fn test_theme_registry_list() {
let loader = ThemeLoader::new();
let registry = loader.load_all();
let list = registry.list();
assert!(list.len() >= 7);
assert!(list.iter().any(|t| t.name == "dark"));
assert!(list.iter().any(|t| t.name == "light"));
}
#[test]
fn test_theme_registry_contains() {
let loader = ThemeLoader::new();
let registry = loader.load_all();
assert!(registry.contains("dark"));
assert!(registry.contains("Dark")); assert!(!registry.contains("nonexistent"));
}
#[test]
fn test_theme_loader_load_all() {
let loader = ThemeLoader::new();
let registry = loader.load_all();
assert!(registry.len() >= 7);
let dark = registry.get("dark").unwrap();
assert_eq!(dark.name, "dark");
}
}