ricecoder_tui/
theme.rs

1//! Theme management for the TUI
2
3use 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
12/// Type alias for theme listeners
13type ThemeListeners = Arc<Mutex<Vec<Box<dyn Fn(&Theme) + Send>>>>;
14
15/// Theme manager for runtime theme management and switching
16#[derive(Clone)]
17pub struct ThemeManager {
18    /// Current active theme
19    current_theme: Arc<Mutex<Theme>>,
20    /// Theme change listeners
21    listeners: ThemeListeners,
22    /// Theme registry for managing all themes
23    registry: ThemeRegistry,
24    /// Theme reset manager for resetting themes to defaults
25    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    /// Create a new theme manager with default theme
38    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    /// Create a theme manager with a specific theme
48    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    /// Create a theme manager with a custom registry
58    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    /// Get the current theme
68    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    /// Switch to a theme by name
78    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    /// Switch to a specific theme
87    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        // Notify listeners
95        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    /// Get all available theme names
107    pub fn available_themes(&self) -> Vec<&'static str> {
108        Theme::available_themes()
109    }
110
111    /// Get the current theme name
112    pub fn current_name(&self) -> Result<String> {
113        Ok(self.current()?.name)
114    }
115
116    /// Load theme from config
117    pub fn load_from_config(&self, config: &TuiConfig) -> Result<()> {
118        self.switch_by_name(&config.theme)
119    }
120
121    /// Save current theme to config
122    pub fn save_to_config(&self, config: &mut TuiConfig) -> Result<()> {
123        config.theme = self.current_name()?;
124        Ok(())
125    }
126
127    /// Load theme preference from storage
128    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    /// Save current theme preference to storage
135    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    /// Load a custom theme from a file
147    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    /// Load all custom themes from a directory
153    pub fn load_custom_themes_from_directory(&self, dir: &Path) -> Result<Vec<Theme>> {
154        ThemeLoader::load_from_directory(dir)
155    }
156
157    /// Load all custom themes from a directory and register them
158    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    /// Load all custom themes from storage and register them
169    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    /// Save current theme as a custom theme to storage
197    pub fn save_custom_theme(&self, path: &Path) -> Result<()> {
198        let theme = self.current()?;
199        ThemeLoader::save_to_file(&theme, path)?;
200        // Also register it in the registry
201        self.register_theme(theme)?;
202        Ok(())
203    }
204
205    /// Save current theme as a custom theme to storage by name
206    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        // Also register it in the registry
212        self.register_theme(theme)?;
213        Ok(())
214    }
215
216    /// Save a specific theme as a custom theme
217    pub fn save_theme_as_custom(&self, theme: &Theme, path: &Path) -> Result<()> {
218        ThemeLoader::save_to_file(theme, path)?;
219        // Also register it in the registry
220        self.register_theme(theme.clone())?;
221        Ok(())
222    }
223
224    /// Save a specific theme as a custom theme to storage by name
225    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        // Also register it in the registry
230        self.register_theme(theme.clone())?;
231        Ok(())
232    }
233
234    /// Delete a custom theme file and unregister it
235    pub fn delete_custom_theme(&self, name: &str, path: &Path) -> Result<()> {
236        // Remove the file
237        std::fs::remove_file(path)?;
238        // Unregister from registry
239        self.unregister_theme(name)?;
240        Ok(())
241    }
242
243    /// Delete a custom theme from storage and unregister it
244    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        // Unregister from registry
248        self.unregister_theme(theme_name)?;
249        Ok(())
250    }
251
252    /// Get the default custom themes directory
253    pub fn custom_themes_directory() -> Result<std::path::PathBuf> {
254        ThemeLoader::themes_directory()
255    }
256
257    /// Get the theme registry
258    pub fn registry(&self) -> &ThemeRegistry {
259        &self.registry
260    }
261
262    /// List all available themes (built-in and custom)
263    pub fn list_all_themes(&self) -> Result<Vec<String>> {
264        self.registry.list_all()
265    }
266
267    /// List all built-in themes
268    pub fn list_builtin_themes(&self) -> Vec<String> {
269        self.registry.list_builtin()
270    }
271
272    /// List all custom themes
273    pub fn list_custom_themes(&self) -> Result<Vec<String>> {
274        self.registry.list_custom()
275    }
276
277    /// Register a custom theme in the registry
278    pub fn register_theme(&self, theme: Theme) -> Result<()> {
279        self.registry.register(theme)
280    }
281
282    /// Unregister a custom theme from the registry
283    pub fn unregister_theme(&self, name: &str) -> Result<()> {
284        self.registry.unregister(name)
285    }
286
287    /// Check if a theme exists
288    pub fn theme_exists(&self, name: &str) -> bool {
289        self.registry.exists(name)
290    }
291
292    /// Check if a theme is built-in
293    pub fn is_builtin_theme(&self, name: &str) -> bool {
294        self.registry.is_builtin(name)
295    }
296
297    /// Check if a theme is custom
298    pub fn is_custom_theme(&self, name: &str) -> Result<bool> {
299        self.registry.is_custom(name)
300    }
301
302    /// Get the number of built-in themes
303    pub fn builtin_theme_count(&self) -> usize {
304        self.registry.builtin_count()
305    }
306
307    /// Get the number of custom themes
308    pub fn custom_theme_count(&self) -> Result<usize> {
309        self.registry.custom_count()
310    }
311
312    /// Reset all colors in the current theme to their default values
313    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        // Notify listeners of the reset
322        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(&current);
328        }
329
330        Ok(())
331    }
332
333    /// Reset the current theme to its built-in default
334    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        // Notify listeners of the reset
343        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(&current);
349        }
350
351        Ok(())
352    }
353
354    /// Reset a specific color field in the current theme to its default value
355    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        // Notify listeners of the reset
364        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(&current);
370        }
371
372        Ok(())
373    }
374
375    /// Get the default color value for a specific color field in the current theme
376    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(&current.name, color_name)
384    }
385
386    /// Get the theme reset manager
387    pub fn reset_manager(&self) -> &ThemeResetManager {
388        &self.reset_manager
389    }
390
391    /// Register a listener for theme changes
392    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        // Modify current theme
534        {
535            let mut current = manager.current_theme.lock().unwrap();
536            current.primary = crate::style::Color::new(255, 0, 0);
537        }
538
539        // Verify modification
540        assert_ne!(manager.current().unwrap().primary, original_primary);
541
542        // Reset colors
543        manager.reset_colors().unwrap();
544
545        // Verify reset
546        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        // Modify current theme
557        {
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        // Verify modification
564        let modified = manager.current().unwrap();
565        assert_ne!(modified.primary, original_theme.primary);
566        assert_ne!(modified.background, original_theme.background);
567
568        // Reset theme
569        manager.reset_theme().unwrap();
570
571        // Verify reset
572        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        // Modify error color
583        {
584            let mut current = manager.current_theme.lock().unwrap();
585            current.error = crate::style::Color::new(255, 0, 0);
586        }
587
588        // Verify modification
589        assert_ne!(manager.current().unwrap().error, original_error);
590
591        // Reset error color
592        manager.reset_color("error").unwrap();
593
594        // Verify reset
595        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        // Save a preference
631        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        // Load it with theme manager
638        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        // Verify it was saved
657        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        // Verify it was saved
675        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        // Save some custom themes
688        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        // Save a custom theme
709        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        // Verify it was deleted
718        assert!(!ricecoder_storage::ThemeStorage::custom_theme_exists("to_delete").unwrap());
719
720        std::env::remove_var("RICECODER_HOME");
721    }
722}