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