vtcode_core/config/loader/
mod.rs1use crate::config::context::ContextFeaturesConfig;
2use crate::config::core::{
3 AgentConfig, AutomationConfig, CommandsConfig, PromptCachingConfig, SecurityConfig, ToolsConfig,
4};
5use crate::config::router::RouterConfig;
6use crate::config::telemetry::TelemetryConfig;
7use crate::config::{PtyConfig, UiConfig};
8use crate::project::SimpleProjectManager;
9use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use std::fs;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct SyntaxHighlightingConfig {
17 #[serde(default = "default_true")]
19 pub enabled: bool,
20
21 #[serde(default = "default_theme")]
23 pub theme: String,
24
25 #[serde(default = "default_true")]
27 pub cache_themes: bool,
28
29 #[serde(default = "default_max_file_size")]
31 pub max_file_size_mb: usize,
32
33 #[serde(default = "default_enabled_languages")]
35 pub enabled_languages: Vec<String>,
36
37 #[serde(default = "default_highlight_timeout")]
39 pub highlight_timeout_ms: u64,
40}
41
42fn default_true() -> bool {
43 true
44}
45fn default_theme() -> String {
46 "base16-ocean.dark".to_string()
47}
48fn default_max_file_size() -> usize {
49 10
50}
51fn default_enabled_languages() -> Vec<String> {
52 vec![
53 "rust".to_string(),
54 "python".to_string(),
55 "javascript".to_string(),
56 "typescript".to_string(),
57 "go".to_string(),
58 "java".to_string(),
59 "cpp".to_string(),
60 "c".to_string(),
61 "php".to_string(),
62 "html".to_string(),
63 "css".to_string(),
64 "sql".to_string(),
65 "csharp".to_string(),
66 "bash".to_string(),
67 ]
68}
69fn default_highlight_timeout() -> u64 {
70 5000
71}
72
73impl Default for SyntaxHighlightingConfig {
74 fn default() -> Self {
75 Self {
76 enabled: default_true(),
77 theme: default_theme(),
78 cache_themes: default_true(),
79 max_file_size_mb: default_max_file_size(),
80 enabled_languages: default_enabled_languages(),
81 highlight_timeout_ms: default_highlight_timeout(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize)]
88pub struct VTCodeConfig {
89 #[serde(default)]
91 pub agent: AgentConfig,
92
93 #[serde(default)]
95 pub tools: ToolsConfig,
96
97 #[serde(default)]
99 pub commands: CommandsConfig,
100
101 #[serde(default)]
103 pub security: SecurityConfig,
104
105 #[serde(default)]
107 pub ui: UiConfig,
108
109 #[serde(default)]
111 pub pty: PtyConfig,
112
113 #[serde(default)]
115 pub context: ContextFeaturesConfig,
116
117 #[serde(default)]
119 pub router: RouterConfig,
120
121 #[serde(default)]
123 pub telemetry: TelemetryConfig,
124
125 #[serde(default)]
127 pub syntax_highlighting: SyntaxHighlightingConfig,
128
129 #[serde(default)]
131 pub automation: AutomationConfig,
132
133 #[serde(default)]
135 pub prompt_cache: PromptCachingConfig,
136}
137
138impl Default for VTCodeConfig {
139 fn default() -> Self {
140 Self {
141 agent: AgentConfig::default(),
142 tools: ToolsConfig::default(),
143 commands: CommandsConfig::default(),
144 security: SecurityConfig::default(),
145 ui: UiConfig::default(),
146 pty: PtyConfig::default(),
147 context: ContextFeaturesConfig::default(),
148 router: RouterConfig::default(),
149 telemetry: TelemetryConfig::default(),
150 syntax_highlighting: SyntaxHighlightingConfig::default(),
151 automation: AutomationConfig::default(),
152 prompt_cache: PromptCachingConfig::default(),
153 }
154 }
155}
156
157impl VTCodeConfig {
158 pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
160 Self::bootstrap_project_with_options(workspace, force, false)
161 }
162
163 pub fn bootstrap_project_with_options<P: AsRef<Path>>(
165 workspace: P,
166 force: bool,
167 use_home_dir: bool,
168 ) -> Result<Vec<String>> {
169 let workspace = workspace.as_ref();
170 let mut created_files = Vec::new();
171
172 let (config_path, gitignore_path) = if use_home_dir {
174 if let Some(home_dir) = ConfigManager::get_home_dir() {
176 let vtcode_dir = home_dir.join(".vtcode");
177 if !vtcode_dir.exists() {
179 fs::create_dir_all(&vtcode_dir).with_context(|| {
180 format!("Failed to create directory: {}", vtcode_dir.display())
181 })?;
182 }
183 (
184 vtcode_dir.join("vtcode.toml"),
185 vtcode_dir.join(".vtcodegitignore"),
186 )
187 } else {
188 let config_path = workspace.join("vtcode.toml");
190 let gitignore_path = workspace.join(".vtcodegitignore");
191 (config_path, gitignore_path)
192 }
193 } else {
194 let config_path = workspace.join("vtcode.toml");
196 let gitignore_path = workspace.join(".vtcodegitignore");
197 (config_path, gitignore_path)
198 };
199
200 if !config_path.exists() || force {
202 let default_config = VTCodeConfig::default();
203 let config_content = toml::to_string_pretty(&default_config)
204 .context("Failed to serialize default configuration")?;
205
206 fs::write(&config_path, config_content).with_context(|| {
207 format!("Failed to write config file: {}", config_path.display())
208 })?;
209
210 created_files.push("vtcode.toml".to_string());
211 }
212
213 if !gitignore_path.exists() || force {
215 let gitignore_content = Self::default_vtcode_gitignore();
216 fs::write(&gitignore_path, gitignore_content).with_context(|| {
217 format!(
218 "Failed to write gitignore file: {}",
219 gitignore_path.display()
220 )
221 })?;
222
223 created_files.push(".vtcodegitignore".to_string());
224 }
225
226 Ok(created_files)
227 }
228
229 fn default_vtcode_gitignore() -> String {
231 r#"# Security-focused exclusions
232.env, .env.local, secrets/, .aws/, .ssh/
233
234# Development artifacts
235target/, build/, dist/, node_modules/, vendor/
236
237# Database files
238*.db, *.sqlite, *.sqlite3
239
240# Binary files
241*.exe, *.dll, *.so, *.dylib, *.bin
242
243# IDE files (comprehensive)
244.vscode/, .idea/, *.swp, *.swo
245"#
246 .to_string()
247 }
248
249 pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
251 let output = output.as_ref();
252 let default_config = VTCodeConfig::default();
253 let config_content = toml::to_string_pretty(&default_config)
254 .context("Failed to serialize default configuration")?;
255
256 fs::write(output, config_content)
257 .with_context(|| format!("Failed to write config file: {}", output.display()))?;
258
259 Ok(())
260 }
261}
262
263#[derive(Clone)]
265pub struct ConfigManager {
266 config: VTCodeConfig,
267 config_path: Option<PathBuf>,
268 project_manager: Option<SimpleProjectManager>,
269 project_name: Option<String>,
270}
271
272impl ConfigManager {
273 pub fn load() -> Result<Self> {
275 Self::load_from_workspace(std::env::current_dir()?)
276 }
277
278 fn get_home_dir() -> Option<PathBuf> {
280 if let Ok(home) = std::env::var("HOME") {
282 return Some(PathBuf::from(home));
283 }
284
285 if let Ok(userprofile) = std::env::var("USERPROFILE") {
287 return Some(PathBuf::from(userprofile));
288 }
289
290 dirs::home_dir()
292 }
293
294 pub fn load_from_workspace(workspace: impl AsRef<Path>) -> Result<Self> {
296 let workspace = workspace.as_ref();
297
298 let project_manager = Some(SimpleProjectManager::new(workspace.to_path_buf()));
300 let project_name = project_manager
301 .as_ref()
302 .and_then(|pm| pm.identify_current_project().ok());
303
304 let config_path = workspace.join("vtcode.toml");
306 if config_path.exists() {
307 let config = Self::load_from_file(&config_path)?;
308 return Ok(Self {
309 config: config.config,
310 config_path: config.config_path,
311 project_manager,
312 project_name,
313 });
314 }
315
316 let fallback_path = workspace.join(".vtcode").join("vtcode.toml");
318 if fallback_path.exists() {
319 let config = Self::load_from_file(&fallback_path)?;
320 return Ok(Self {
321 config: config.config,
322 config_path: config.config_path,
323 project_manager,
324 project_name,
325 });
326 }
327
328 if let Some(home_dir) = Self::get_home_dir() {
330 let home_config_path = home_dir.join(".vtcode").join("vtcode.toml");
331 if home_config_path.exists() {
332 let config = Self::load_from_file(&home_config_path)?;
333 return Ok(Self {
334 config: config.config,
335 config_path: config.config_path,
336 project_manager,
337 project_name,
338 });
339 }
340 }
341
342 if let (Some(pm), Some(pname)) = (&project_manager, &project_name) {
344 let project_config_path = pm.config_dir(pname).join("vtcode.toml");
345 if project_config_path.exists() {
346 let config = Self::load_from_file(&project_config_path)?;
347 return Ok(Self {
348 config: config.config,
349 config_path: config.config_path,
350 project_manager: Some(pm.clone()),
351 project_name: Some(pname.clone()),
352 });
353 }
354 }
355
356 Ok(Self {
358 config: VTCodeConfig::default(),
359 config_path: None,
360 project_manager,
361 project_name,
362 })
363 }
364
365 pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
367 let path = path.as_ref();
368 let content = std::fs::read_to_string(path)
369 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
370
371 let config: VTCodeConfig = toml::from_str(&content)
372 .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
373
374 let project_manager = std::env::current_dir()
377 .ok()
378 .map(|cwd| SimpleProjectManager::new(cwd));
379
380 Ok(Self {
381 config,
382 config_path: Some(path.to_path_buf()),
383 project_manager,
384 project_name: None,
385 })
386 }
387
388 pub fn config(&self) -> &VTCodeConfig {
390 &self.config
391 }
392
393 pub fn config_path(&self) -> Option<&Path> {
395 self.config_path.as_deref()
396 }
397
398 pub fn session_duration(&self) -> std::time::Duration {
400 std::time::Duration::from_secs(60 * 60) }
402
403 pub fn project_manager(&self) -> Option<&SimpleProjectManager> {
405 self.project_manager.as_ref()
406 }
407
408 pub fn project_name(&self) -> Option<&str> {
410 self.project_name.as_deref()
411 }
412}