1use crate::config::TuiConfig;
4use crate::style::{Color, Theme};
5use crate::theme_loader::ThemeLoader;
6use crate::theme_registry::ThemeRegistry;
7use crate::theme_reset::ThemeResetManager;
8use anyhow::Result;
9use std::path::Path;
10use std::sync::{Arc, Mutex};
11
12type ThemeListeners = Arc<Mutex<Vec<Box<dyn Fn(&Theme) + Send>>>>;
14
15#[derive(Clone)]
17pub struct ThemeManager {
18 current_theme: Arc<Mutex<Theme>>,
20 listeners: ThemeListeners,
22 registry: ThemeRegistry,
24 reset_manager: Arc<ThemeResetManager>,
26}
27
28impl std::fmt::Debug for ThemeManager {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("ThemeManager")
31 .field("current_theme", &self.current_theme)
32 .finish()
33 }
34}
35
36impl ThemeManager {
37 pub fn new() -> Self {
39 Self {
40 current_theme: Arc::new(Mutex::new(Theme::default())),
41 listeners: Arc::new(Mutex::new(Vec::new())),
42 registry: ThemeRegistry::new(),
43 reset_manager: Arc::new(ThemeResetManager::new()),
44 }
45 }
46
47 pub fn with_theme(theme: Theme) -> Self {
49 Self {
50 current_theme: Arc::new(Mutex::new(theme)),
51 listeners: Arc::new(Mutex::new(Vec::new())),
52 registry: ThemeRegistry::new(),
53 reset_manager: Arc::new(ThemeResetManager::new()),
54 }
55 }
56
57 pub fn with_registry(registry: ThemeRegistry) -> Self {
59 Self {
60 current_theme: Arc::new(Mutex::new(Theme::default())),
61 listeners: Arc::new(Mutex::new(Vec::new())),
62 registry,
63 reset_manager: Arc::new(ThemeResetManager::new()),
64 }
65 }
66
67 pub fn current(&self) -> Result<Theme> {
69 let theme = self
70 .current_theme
71 .lock()
72 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?
73 .clone();
74 Ok(theme)
75 }
76
77 pub fn switch_by_name(&self, name: &str) -> Result<()> {
79 if let Some(theme) = Theme::by_name(name) {
80 self.switch_to(theme)
81 } else {
82 Err(anyhow::anyhow!("Unknown theme: {}", name))
83 }
84 }
85
86 pub fn switch_to(&self, theme: Theme) -> Result<()> {
88 let mut current = self
89 .current_theme
90 .lock()
91 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?;
92 *current = theme.clone();
93
94 let listeners = self
96 .listeners
97 .lock()
98 .map_err(|e| anyhow::anyhow!("Failed to lock listeners: {}", e))?;
99 for listener in listeners.iter() {
100 listener(&theme);
101 }
102
103 Ok(())
104 }
105
106 pub fn available_themes(&self) -> Vec<&'static str> {
108 Theme::available_themes()
109 }
110
111 pub fn current_name(&self) -> Result<String> {
113 Ok(self.current()?.name)
114 }
115
116 pub fn load_from_config(&self, config: &TuiConfig) -> Result<()> {
118 self.switch_by_name(&config.theme)
119 }
120
121 pub fn save_to_config(&self, config: &mut TuiConfig) -> Result<()> {
123 config.theme = self.current_name()?;
124 Ok(())
125 }
126
127 pub fn load_from_storage(&self) -> Result<()> {
129 use ricecoder_storage::ThemeStorage;
130 let preference = ThemeStorage::load_preference()?;
131 self.switch_by_name(&preference.current_theme)
132 }
133
134 pub fn save_to_storage(&self) -> Result<()> {
136 use ricecoder_storage::ThemeStorage;
137 let theme_name = self.current_name()?;
138 let preference = ricecoder_storage::ThemePreference {
139 current_theme: theme_name,
140 last_updated: Some(chrono::Local::now().to_rfc3339()),
141 };
142 ThemeStorage::save_preference(&preference)?;
143 Ok(())
144 }
145
146 pub fn load_custom_theme(&self, path: &Path) -> Result<()> {
148 let theme = ThemeLoader::load_from_file(path)?;
149 self.switch_to(theme)
150 }
151
152 pub fn load_custom_themes_from_directory(&self, dir: &Path) -> Result<Vec<Theme>> {
154 ThemeLoader::load_from_directory(dir)
155 }
156
157 pub fn load_and_register_custom_themes(&self, dir: &Path) -> Result<Vec<String>> {
159 let themes = self.load_custom_themes_from_directory(dir)?;
160 let mut names = Vec::new();
161 for theme in themes {
162 names.push(theme.name.clone());
163 self.register_theme(theme)?;
164 }
165 Ok(names)
166 }
167
168 pub fn load_custom_themes_from_storage(&self) -> Result<Vec<String>> {
170 use ricecoder_storage::ThemeStorage;
171 let theme_names = ThemeStorage::list_custom_themes()?;
172 let mut loaded_names = Vec::new();
173
174 for theme_name in theme_names {
175 match ThemeStorage::load_custom_theme(&theme_name) {
176 Ok(content) => {
177 match ThemeLoader::load_from_string(&content) {
178 Ok(theme) => {
179 self.register_theme(theme)?;
180 loaded_names.push(theme_name);
181 }
182 Err(e) => {
183 eprintln!("Failed to parse custom theme {}: {}", theme_name, e);
184 }
185 }
186 }
187 Err(e) => {
188 eprintln!("Failed to load custom theme {}: {}", theme_name, e);
189 }
190 }
191 }
192
193 Ok(loaded_names)
194 }
195
196 pub fn save_custom_theme(&self, path: &Path) -> Result<()> {
198 let theme = self.current()?;
199 ThemeLoader::save_to_file(&theme, path)?;
200 self.register_theme(theme)?;
202 Ok(())
203 }
204
205 pub fn save_custom_theme_to_storage(&self, theme_name: &str) -> Result<()> {
207 use ricecoder_storage::ThemeStorage;
208 let theme = self.current()?;
209 let content = serde_yaml::to_string(&theme)?;
210 ThemeStorage::save_custom_theme(theme_name, &content)?;
211 self.register_theme(theme)?;
213 Ok(())
214 }
215
216 pub fn save_theme_as_custom(&self, theme: &Theme, path: &Path) -> Result<()> {
218 ThemeLoader::save_to_file(theme, path)?;
219 self.register_theme(theme.clone())?;
221 Ok(())
222 }
223
224 pub fn save_theme_as_custom_to_storage(&self, theme: &Theme, theme_name: &str) -> Result<()> {
226 use ricecoder_storage::ThemeStorage;
227 let content = serde_yaml::to_string(theme)?;
228 ThemeStorage::save_custom_theme(theme_name, &content)?;
229 self.register_theme(theme.clone())?;
231 Ok(())
232 }
233
234 pub fn delete_custom_theme(&self, name: &str, path: &Path) -> Result<()> {
236 std::fs::remove_file(path)?;
238 self.unregister_theme(name)?;
240 Ok(())
241 }
242
243 pub fn delete_custom_theme_from_storage(&self, theme_name: &str) -> Result<()> {
245 use ricecoder_storage::ThemeStorage;
246 ThemeStorage::delete_custom_theme(theme_name)?;
247 self.unregister_theme(theme_name)?;
249 Ok(())
250 }
251
252 pub fn custom_themes_directory() -> Result<std::path::PathBuf> {
254 ThemeLoader::themes_directory()
255 }
256
257 pub fn registry(&self) -> &ThemeRegistry {
259 &self.registry
260 }
261
262 pub fn list_all_themes(&self) -> Result<Vec<String>> {
264 self.registry.list_all()
265 }
266
267 pub fn list_builtin_themes(&self) -> Vec<String> {
269 self.registry.list_builtin()
270 }
271
272 pub fn list_custom_themes(&self) -> Result<Vec<String>> {
274 self.registry.list_custom()
275 }
276
277 pub fn register_theme(&self, theme: Theme) -> Result<()> {
279 self.registry.register(theme)
280 }
281
282 pub fn unregister_theme(&self, name: &str) -> Result<()> {
284 self.registry.unregister(name)
285 }
286
287 pub fn theme_exists(&self, name: &str) -> bool {
289 self.registry.exists(name)
290 }
291
292 pub fn is_builtin_theme(&self, name: &str) -> bool {
294 self.registry.is_builtin(name)
295 }
296
297 pub fn is_custom_theme(&self, name: &str) -> Result<bool> {
299 self.registry.is_custom(name)
300 }
301
302 pub fn builtin_theme_count(&self) -> usize {
304 self.registry.builtin_count()
305 }
306
307 pub fn custom_theme_count(&self) -> Result<usize> {
309 self.registry.custom_count()
310 }
311
312 pub fn reset_colors(&self) -> Result<()> {
314 let mut current = self
315 .current_theme
316 .lock()
317 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?;
318
319 self.reset_manager.reset_colors(&mut current)?;
320
321 let listeners = self
323 .listeners
324 .lock()
325 .map_err(|e| anyhow::anyhow!("Failed to lock listeners: {}", e))?;
326 for listener in listeners.iter() {
327 listener(¤t);
328 }
329
330 Ok(())
331 }
332
333 pub fn reset_theme(&self) -> Result<()> {
335 let mut current = self
336 .current_theme
337 .lock()
338 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?;
339
340 self.reset_manager.reset_theme(&mut current)?;
341
342 let listeners = self
344 .listeners
345 .lock()
346 .map_err(|e| anyhow::anyhow!("Failed to lock listeners: {}", e))?;
347 for listener in listeners.iter() {
348 listener(¤t);
349 }
350
351 Ok(())
352 }
353
354 pub fn reset_color(&self, color_name: &str) -> Result<()> {
356 let mut current = self
357 .current_theme
358 .lock()
359 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?;
360
361 self.reset_manager.reset_color(&mut current, color_name)?;
362
363 let listeners = self
365 .listeners
366 .lock()
367 .map_err(|e| anyhow::anyhow!("Failed to lock listeners: {}", e))?;
368 for listener in listeners.iter() {
369 listener(¤t);
370 }
371
372 Ok(())
373 }
374
375 pub fn get_default_color(&self, color_name: &str) -> Result<Color> {
377 let current = self
378 .current_theme
379 .lock()
380 .map_err(|e| anyhow::anyhow!("Failed to lock theme: {}", e))?;
381
382 self.reset_manager
383 .get_default_color(¤t.name, color_name)
384 }
385
386 pub fn reset_manager(&self) -> &ThemeResetManager {
388 &self.reset_manager
389 }
390
391 pub fn on_theme_changed<F>(&self, listener: F) -> Result<()>
393 where
394 F: Fn(&Theme) + Send + 'static,
395 {
396 let mut listeners = self
397 .listeners
398 .lock()
399 .map_err(|e| anyhow::anyhow!("Failed to lock listeners: {}", e))?;
400 listeners.push(Box::new(listener));
401 Ok(())
402 }
403}
404
405impl Default for ThemeManager {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_theme_manager_creation() {
417 let manager = ThemeManager::new();
418 assert_eq!(manager.current().unwrap().name, "dark");
419 }
420
421 #[test]
422 fn test_theme_manager_with_theme() {
423 let theme = Theme::light();
424 let manager = ThemeManager::with_theme(theme);
425 assert_eq!(manager.current().unwrap().name, "light");
426 }
427
428 #[test]
429 fn test_switch_by_name() {
430 let manager = ThemeManager::new();
431 manager.switch_by_name("light").unwrap();
432 assert_eq!(manager.current().unwrap().name, "light");
433
434 manager.switch_by_name("monokai").unwrap();
435 assert_eq!(manager.current().unwrap().name, "monokai");
436 }
437
438 #[test]
439 fn test_switch_by_invalid_name() {
440 let manager = ThemeManager::new();
441 assert!(manager.switch_by_name("invalid").is_err());
442 }
443
444 #[test]
445 fn test_switch_to() {
446 let manager = ThemeManager::new();
447 let theme = Theme::dracula();
448 manager.switch_to(theme).unwrap();
449 assert_eq!(manager.current().unwrap().name, "dracula");
450 }
451
452 #[test]
453 fn test_available_themes() {
454 let manager = ThemeManager::new();
455 let themes = manager.available_themes();
456 assert_eq!(themes.len(), 6);
457 }
458
459 #[test]
460 fn test_current_name() {
461 let manager = ThemeManager::new();
462 assert_eq!(manager.current_name().unwrap(), "dark");
463
464 manager.switch_by_name("nord").unwrap();
465 assert_eq!(manager.current_name().unwrap(), "nord");
466 }
467
468 #[test]
469 fn test_load_from_config() {
470 let manager = ThemeManager::new();
471 let config = TuiConfig {
472 theme: "dracula".to_string(),
473 ..Default::default()
474 };
475 manager.load_from_config(&config).unwrap();
476 assert_eq!(manager.current().unwrap().name, "dracula");
477 }
478
479 #[test]
480 fn test_save_to_config() {
481 let manager = ThemeManager::new();
482 manager.switch_by_name("monokai").unwrap();
483
484 let mut config = TuiConfig::default();
485 manager.save_to_config(&mut config).unwrap();
486 assert_eq!(config.theme, "monokai");
487 }
488
489 #[test]
490 fn test_save_and_load_custom_theme() {
491 use tempfile::TempDir;
492
493 let temp_dir = TempDir::new().unwrap();
494 let theme_path = temp_dir.path().join("custom.yaml");
495
496 let manager = ThemeManager::new();
497 manager.switch_by_name("dracula").unwrap();
498 manager.save_custom_theme(&theme_path).unwrap();
499
500 let manager2 = ThemeManager::new();
501 manager2.load_custom_theme(&theme_path).unwrap();
502 assert_eq!(manager2.current().unwrap().name, "dracula");
503 }
504
505 #[test]
506 fn test_load_custom_themes_from_directory() {
507 use tempfile::TempDir;
508
509 let temp_dir = TempDir::new().unwrap();
510
511 let manager = ThemeManager::new();
512 manager.switch_by_name("dark").unwrap();
513 manager
514 .save_custom_theme(&temp_dir.path().join("dark.yaml"))
515 .unwrap();
516
517 manager.switch_by_name("light").unwrap();
518 manager
519 .save_custom_theme(&temp_dir.path().join("light.yaml"))
520 .unwrap();
521
522 let themes = manager
523 .load_custom_themes_from_directory(temp_dir.path())
524 .unwrap();
525 assert_eq!(themes.len(), 2);
526 }
527
528 #[test]
529 fn test_reset_colors() {
530 let manager = ThemeManager::new();
531 let original_primary = manager.current().unwrap().primary;
532
533 {
535 let mut current = manager.current_theme.lock().unwrap();
536 current.primary = crate::style::Color::new(255, 0, 0);
537 }
538
539 assert_ne!(manager.current().unwrap().primary, original_primary);
541
542 manager.reset_colors().unwrap();
544
545 assert_eq!(manager.current().unwrap().primary, original_primary);
547 }
548
549 #[test]
550 fn test_reset_theme() {
551 let manager = ThemeManager::new();
552 manager.switch_by_name("light").unwrap();
553
554 let original_theme = Theme::light();
555
556 {
558 let mut current = manager.current_theme.lock().unwrap();
559 current.primary = crate::style::Color::new(255, 0, 0);
560 current.background = crate::style::Color::new(100, 100, 100);
561 }
562
563 let modified = manager.current().unwrap();
565 assert_ne!(modified.primary, original_theme.primary);
566 assert_ne!(modified.background, original_theme.background);
567
568 manager.reset_theme().unwrap();
570
571 let reset = manager.current().unwrap();
573 assert_eq!(reset.primary, original_theme.primary);
574 assert_eq!(reset.background, original_theme.background);
575 }
576
577 #[test]
578 fn test_reset_color() {
579 let manager = ThemeManager::new();
580 let original_error = manager.current().unwrap().error;
581
582 {
584 let mut current = manager.current_theme.lock().unwrap();
585 current.error = crate::style::Color::new(255, 0, 0);
586 }
587
588 assert_ne!(manager.current().unwrap().error, original_error);
590
591 manager.reset_color("error").unwrap();
593
594 assert_eq!(manager.current().unwrap().error, original_error);
596 }
597
598 #[test]
599 fn test_get_default_color() {
600 let manager = ThemeManager::new();
601 let default_primary = manager.get_default_color("primary").unwrap();
602 let current_primary = manager.current().unwrap().primary;
603 assert_eq!(default_primary, current_primary);
604 }
605
606 #[test]
607 fn test_reset_notifies_listeners() {
608 let manager = ThemeManager::new();
609 let listener_called = std::sync::Arc::new(std::sync::Mutex::new(false));
610 let listener_called_clone = listener_called.clone();
611
612 manager
613 .on_theme_changed(move |_theme| {
614 *listener_called_clone.lock().unwrap() = true;
615 })
616 .unwrap();
617
618 manager.reset_colors().unwrap();
619
620 assert!(*listener_called.lock().unwrap());
621 }
622
623 #[test]
624 fn test_load_from_storage() {
625 use tempfile::TempDir;
626
627 let temp_dir = TempDir::new().unwrap();
628 std::env::set_var("RICECODER_HOME", temp_dir.path());
629
630 let pref = ricecoder_storage::ThemePreference {
632 current_theme: "light".to_string(),
633 last_updated: None,
634 };
635 ricecoder_storage::ThemeStorage::save_preference(&pref).unwrap();
636
637 let manager = ThemeManager::new();
639 manager.load_from_storage().unwrap();
640 assert_eq!(manager.current().unwrap().name, "light");
641
642 std::env::remove_var("RICECODER_HOME");
643 }
644
645 #[test]
646 fn test_save_to_storage() {
647 use tempfile::TempDir;
648
649 let temp_dir = TempDir::new().unwrap();
650 std::env::set_var("RICECODER_HOME", temp_dir.path());
651
652 let manager = ThemeManager::new();
653 manager.switch_by_name("dracula").unwrap();
654 manager.save_to_storage().unwrap();
655
656 let loaded_pref = ricecoder_storage::ThemeStorage::load_preference().unwrap();
658 assert_eq!(loaded_pref.current_theme, "dracula");
659
660 std::env::remove_var("RICECODER_HOME");
661 }
662
663 #[test]
664 fn test_save_custom_theme_to_storage() {
665 use tempfile::TempDir;
666
667 let temp_dir = TempDir::new().unwrap();
668 std::env::set_var("RICECODER_HOME", temp_dir.path());
669
670 let manager = ThemeManager::new();
671 manager.switch_by_name("monokai").unwrap();
672 manager.save_custom_theme_to_storage("my_custom").unwrap();
673
674 assert!(ricecoder_storage::ThemeStorage::custom_theme_exists("my_custom").unwrap());
676
677 std::env::remove_var("RICECODER_HOME");
678 }
679
680 #[test]
681 fn test_load_custom_themes_from_storage() {
682 use tempfile::TempDir;
683
684 let temp_dir = TempDir::new().unwrap();
685 std::env::set_var("RICECODER_HOME", temp_dir.path());
686
687 ricecoder_storage::ThemeStorage::save_custom_theme(
689 "custom1",
690 "name: custom1\nprimary: \"#0078ff\"\nsecondary: \"#5ac8fa\"\naccent: \"#ff2d55\"\nbackground: \"#111827\"\nforeground: \"#f3f4f6\"\nerror: \"#ef4444\"\nwarning: \"#f59e0b\"\nsuccess: \"#22c55e\""
691 ).unwrap();
692
693 let manager = ThemeManager::new();
694 let loaded = manager.load_custom_themes_from_storage().unwrap();
695 assert_eq!(loaded.len(), 1);
696 assert!(loaded.contains(&"custom1".to_string()));
697
698 std::env::remove_var("RICECODER_HOME");
699 }
700
701 #[test]
702 fn test_delete_custom_theme_from_storage() {
703 use tempfile::TempDir;
704
705 let temp_dir = TempDir::new().unwrap();
706 std::env::set_var("RICECODER_HOME", temp_dir.path());
707
708 ricecoder_storage::ThemeStorage::save_custom_theme(
710 "to_delete",
711 "name: to_delete\nprimary: \"#0078ff\"\nsecondary: \"#5ac8fa\"\naccent: \"#ff2d55\"\nbackground: \"#111827\"\nforeground: \"#f3f4f6\"\nerror: \"#ef4444\"\nwarning: \"#f59e0b\"\nsuccess: \"#22c55e\""
712 ).unwrap();
713
714 let manager = ThemeManager::new();
715 manager.delete_custom_theme_from_storage("to_delete").unwrap();
716
717 assert!(!ricecoder_storage::ThemeStorage::custom_theme_exists("to_delete").unwrap());
719
720 std::env::remove_var("RICECODER_HOME");
721 }
722}