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