fresh/view/theme/
loader.rs1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use super::types::{Theme, ThemeFile, ThemeInfo, BUILTIN_THEMES};
11
12#[derive(Debug, Clone)]
17pub struct ThemeRegistry {
18 themes: HashMap<String, Theme>,
20 theme_list: Vec<ThemeInfo>,
22}
23
24impl ThemeRegistry {
25 pub fn get(&self, name: &str) -> Option<&Theme> {
27 let normalized = name.to_lowercase().replace('_', "-");
28 self.themes.get(&normalized)
29 }
30
31 pub fn get_cloned(&self, name: &str) -> Option<Theme> {
33 self.get(name).cloned()
34 }
35
36 pub fn list(&self) -> &[ThemeInfo] {
38 &self.theme_list
39 }
40
41 pub fn names(&self) -> Vec<String> {
43 self.theme_list.iter().map(|t| t.name.clone()).collect()
44 }
45
46 pub fn contains(&self, name: &str) -> bool {
48 let normalized = name.to_lowercase().replace('_', "-");
49 self.themes.contains_key(&normalized)
50 }
51
52 pub fn len(&self) -> usize {
54 self.themes.len()
55 }
56
57 pub fn is_empty(&self) -> bool {
59 self.themes.is_empty()
60 }
61}
62
63pub struct ThemeLoader {
65 user_themes_dir: Option<PathBuf>,
66}
67
68impl ThemeLoader {
69 pub fn new() -> Self {
71 Self {
72 user_themes_dir: dirs::config_dir().map(|p| p.join("fresh").join("themes")),
73 }
74 }
75
76 pub fn with_user_dir(user_themes_dir: Option<PathBuf>) -> Self {
78 Self { user_themes_dir }
79 }
80
81 pub fn user_themes_dir(&self) -> Option<&Path> {
83 self.user_themes_dir.as_deref()
84 }
85
86 pub fn load_all(&self) -> ThemeRegistry {
88 let mut themes = HashMap::new();
89 let mut theme_list = Vec::new();
90
91 for builtin in BUILTIN_THEMES {
93 if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(builtin.json) {
94 let theme: Theme = theme_file.into();
95 themes.insert(builtin.name.to_string(), theme);
96 theme_list.push(ThemeInfo::new(builtin.name, builtin.pack));
97 }
98 }
99
100 if let Some(ref user_dir) = self.user_themes_dir {
102 self.scan_directory(user_dir, "user", &mut themes, &mut theme_list);
103 }
104
105 if let Some(ref user_dir) = self.user_themes_dir {
108 let packages_dir = user_dir.join("packages");
109 if packages_dir.exists() {
110 if let Ok(entries) = std::fs::read_dir(&packages_dir) {
111 for entry in entries.flatten() {
112 let path = entry.path();
113 if path.is_dir() {
114 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
115 if !name.starts_with('.') {
117 let manifest_path = path.join("package.json");
119 if manifest_path.exists() {
120 self.load_package_themes(
121 &path,
122 name,
123 &mut themes,
124 &mut theme_list,
125 );
126 } else {
127 let pack_name = format!("pkg/{}", name);
129 self.scan_directory(
130 &path,
131 &pack_name,
132 &mut themes,
133 &mut theme_list,
134 );
135 }
136 }
137 }
138 }
139 }
140 }
141 }
142 }
143
144 ThemeRegistry { themes, theme_list }
145 }
146
147 fn load_package_themes(
149 &self,
150 pkg_dir: &Path,
151 pkg_name: &str,
152 themes: &mut HashMap<String, Theme>,
153 theme_list: &mut Vec<ThemeInfo>,
154 ) {
155 let manifest_path = pkg_dir.join("package.json");
156 let manifest_content = match std::fs::read_to_string(&manifest_path) {
157 Ok(c) => c,
158 Err(_) => return,
159 };
160
161 let manifest: serde_json::Value = match serde_json::from_str(&manifest_content) {
163 Ok(v) => v,
164 Err(_) => return,
165 };
166
167 if let Some(fresh) = manifest.get("fresh") {
169 if let Some(theme_entries) = fresh.get("themes").and_then(|t| t.as_array()) {
170 for entry in theme_entries {
171 if let (Some(file), Some(name)) = (
172 entry.get("file").and_then(|f| f.as_str()),
173 entry.get("name").and_then(|n| n.as_str()),
174 ) {
175 let theme_path = pkg_dir.join(file);
176 if theme_path.exists() {
177 if let Ok(content) = std::fs::read_to_string(&theme_path) {
178 if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(&content)
179 {
180 let theme: Theme = theme_file.into();
181 let normalized_name = name.to_lowercase().replace(' ', "-");
182 if !themes.contains_key(&normalized_name) {
184 themes.insert(normalized_name.clone(), theme);
185 let pack_name = format!("pkg/{}", pkg_name);
186 theme_list
187 .push(ThemeInfo::new(normalized_name, &pack_name));
188 }
189 }
190 }
191 }
192 }
193 }
194 return;
195 }
196 }
197
198 let pack_name = format!("pkg/{}", pkg_name);
200 self.scan_directory(pkg_dir, &pack_name, themes, theme_list);
201 }
202
203 fn scan_directory(
205 &self,
206 dir: &Path,
207 pack: &str,
208 themes: &mut HashMap<String, Theme>,
209 theme_list: &mut Vec<ThemeInfo>,
210 ) {
211 let entries = match std::fs::read_dir(dir) {
212 Ok(e) => e,
213 Err(_) => return,
214 };
215
216 for entry in entries.flatten() {
217 let path = entry.path();
218
219 if path.is_dir() {
220 let subdir_name = path.file_name().unwrap().to_string_lossy();
222 let new_pack = if pack == "user" {
223 format!("user/{}", subdir_name)
224 } else {
225 format!("{}/{}", pack, subdir_name)
226 };
227 self.scan_directory(&path, &new_pack, themes, theme_list);
228 } else if path.extension().is_some_and(|ext| ext == "json") {
229 let name = path.file_stem().unwrap().to_string_lossy().to_string();
231
232 if themes.contains_key(&name) {
234 continue;
235 }
236
237 if let Ok(content) = std::fs::read_to_string(&path) {
238 if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(&content) {
239 let theme: Theme = theme_file.into();
240 themes.insert(name.clone(), theme);
241 theme_list.push(ThemeInfo::new(name, pack));
242 }
243 }
244 }
245 }
246 }
247}
248
249impl Theme {
251 pub fn set_terminal_cursor_color(&self) {
254 use super::types::color_to_rgb;
255 use std::io::Write;
256 if let Some((r, g, b)) = color_to_rgb(self.cursor) {
257 let _ = write!(
259 std::io::stdout(),
260 "\x1b]12;#{:02x}{:02x}{:02x}\x07",
261 r,
262 g,
263 b
264 );
265 let _ = std::io::stdout().flush();
266 }
267 }
268
269 pub fn reset_terminal_cursor_color() {
271 use std::io::Write;
272 let _ = write!(std::io::stdout(), "\x1b]112\x07");
274 let _ = std::io::stdout().flush();
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn test_theme_registry_get() {
284 let loader = ThemeLoader::new();
285 let registry = loader.load_all();
286
287 assert!(registry.get("dark").is_some());
289 assert!(registry.get("light").is_some());
290 assert!(registry.get("high-contrast").is_some());
291
292 assert!(registry.get("Dark").is_some());
294 assert!(registry.get("DARK").is_some());
295 assert!(registry.get("high_contrast").is_some());
296
297 assert!(registry.get("nonexistent-theme").is_none());
299 }
300
301 #[test]
302 fn test_theme_registry_list() {
303 let loader = ThemeLoader::new();
304 let registry = loader.load_all();
305
306 let list = registry.list();
307 assert!(list.len() >= 7); assert!(list.iter().any(|t| t.name == "dark"));
311 assert!(list.iter().any(|t| t.name == "light"));
312 }
313
314 #[test]
315 fn test_theme_registry_contains() {
316 let loader = ThemeLoader::new();
317 let registry = loader.load_all();
318
319 assert!(registry.contains("dark"));
320 assert!(registry.contains("Dark")); assert!(!registry.contains("nonexistent"));
322 }
323
324 #[test]
325 fn test_theme_loader_load_all() {
326 let loader = ThemeLoader::new();
327 let registry = loader.load_all();
328
329 assert!(registry.len() >= 7); let dark = registry.get("dark").unwrap();
334 assert_eq!(dark.name, "dark");
335 }
336}