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