Skip to main content

vtcode_core/ui/
theme_manager.rs

1//! Theme Manager for loading and applying custom theme configurations
2//!
3//! This module provides functionality to load custom theme configurations
4//! from .vtcode/theme.toml files and apply them to the application.
5
6use crate::ui::{FileColorizer, GitColorConfig, ThemeConfig};
7use anyhow::Result;
8use std::path::Path;
9
10/// Loads and manages theme configuration from `.vtcode/theme.toml` and Git config.
11#[derive(Debug, Clone)]
12pub struct ThemeManager {
13    /// Custom theme configuration loaded from theme.toml
14    pub custom_config: Option<ThemeConfig>,
15
16    /// System Git configuration
17    pub git_config: Option<GitColorConfig>,
18
19    /// System file colorizer
20    pub file_colorizer: FileColorizer,
21}
22
23impl ThemeManager {
24    /// Create a new ThemeManager with loaded configurations
25    pub fn new(workspace_root: Option<&Path>) -> Self {
26        let custom_config = Self::load_custom_config(workspace_root);
27        let git_config = Self::load_git_config(workspace_root);
28        let file_colorizer = FileColorizer::new();
29
30        Self { custom_config, git_config, file_colorizer }
31    }
32
33    /// Load custom theme configuration from .vtcode/theme.toml
34    fn load_custom_config(workspace_root: Option<&Path>) -> Option<ThemeConfig> {
35        if let Some(workspace) = workspace_root {
36            let theme_path = workspace.join(".vtcode").join("theme.toml");
37            match ThemeConfig::load_from_file(&theme_path) {
38                Ok(config) => {
39                    tracing::info!("Loaded custom theme configuration from: {}", theme_path.display());
40                    Some(config)
41                }
42                Err(e) => {
43                    if theme_path.exists() {
44                        tracing::warn!("Failed to load theme config from {}: {}", theme_path.display(), e);
45                    }
46                    None
47                }
48            }
49        } else {
50            None
51        }
52    }
53
54    /// Load Git configuration for diff/status colors
55    fn load_git_config(workspace_root: Option<&Path>) -> Option<GitColorConfig> {
56        if let Some(workspace) = workspace_root {
57            let git_config_path = workspace.join(".git").join("config");
58            if git_config_path.exists() {
59                match GitColorConfig::from_git_config(&git_config_path) {
60                    Ok(config) => {
61                        tracing::info!("Loaded Git color configuration from: {}", git_config_path.display());
62                        Some(config)
63                    }
64                    Err(e) => {
65                        tracing::warn!("Failed to load Git config from {}: {}", git_config_path.display(), e);
66                        GitColorConfig::default().into()
67                    }
68                }
69            } else {
70                None
71            }
72        } else {
73            None
74        }
75    }
76
77    /// Get the active theme configuration, falling back to defaults
78    pub fn active_theme_config(&self) -> ThemeConfig {
79        self.custom_config.clone().unwrap_or_default()
80    }
81
82    /// Load theme from a specific file path
83    pub fn load_theme_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
84        let config = ThemeConfig::load_from_file(path)?;
85        self.custom_config = Some(config);
86        Ok(())
87    }
88
89    /// Reset to default theme configuration
90    pub fn reset_to_default(&mut self) {
91        self.custom_config = None;
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use std::fs;
99    use tempfile::tempdir;
100
101    #[test]
102    fn test_theme_manager_default() {
103        let manager = ThemeManager::new(None);
104        assert!(manager.custom_config.is_none());
105        assert!(manager.git_config.is_none());
106        // FileColorizer should always be initialized
107        assert_eq!(manager.file_colorizer.style_for_path(Path::new("/tmp/test.rs")), None);
108    }
109
110    #[test]
111    fn test_load_custom_config() {
112        let temp_dir = tempdir().unwrap();
113        let vtcode_dir = temp_dir.path().join(".vtcode");
114        fs::create_dir_all(&vtcode_dir).unwrap();
115
116        let theme_content = r#"
117[cli]
118success = "bold green"
119error = "bold red"
120
121[diff]
122new = "green"
123old = "red"
124
125[status]
126added = "green"
127
128[files]
129directory = "bold cyan"
130"#;
131
132        let theme_path = vtcode_dir.join("theme.toml");
133        fs::write(&theme_path, theme_content).unwrap();
134
135        let manager = ThemeManager::new(Some(temp_dir.path()));
136        assert!(manager.custom_config.is_some());
137
138        let config = manager.active_theme_config();
139        assert_eq!(config.cli.success, "bold green");
140        assert_eq!(config.diff.new, "green");
141        assert_eq!(config.status.added, "green");
142    }
143
144    #[test]
145    fn test_load_theme_from_file() {
146        let temp_dir = tempdir().unwrap();
147        let theme_content = r#"
148[cli]
149success = "cyan"
150"#;
151
152        let theme_path = temp_dir.path().join("custom_theme.toml");
153        fs::write(&theme_path, theme_content).unwrap();
154
155        let mut manager = ThemeManager::new(None);
156        assert!(manager.custom_config.is_none());
157
158        manager.load_theme_from_file(&theme_path).unwrap();
159        assert!(manager.custom_config.is_some());
160
161        let config = manager.active_theme_config();
162        assert_eq!(config.cli.success, "cyan");
163    }
164
165    #[test]
166    fn test_reset_to_default() {
167        let temp_dir = tempdir().unwrap();
168        let theme_content = r#"
169[cli]
170success = "red"
171"#;
172
173        let theme_path = temp_dir.path().join("custom_theme.toml");
174        fs::write(&theme_path, theme_content).unwrap();
175
176        let mut manager = ThemeManager::new(None);
177        manager.load_theme_from_file(&theme_path).unwrap();
178        assert!(manager.custom_config.is_some());
179
180        manager.reset_to_default();
181        assert!(manager.custom_config.is_none());
182    }
183}