vtcode_core/config/loader/
mod.rs1use crate::config::PtyConfig;
2use crate::config::context::ContextFeaturesConfig;
3use crate::config::core::{
4 AgentConfig, AutomationConfig, CommandsConfig, SecurityConfig, ToolsConfig,
5};
6use crate::config::router::RouterConfig;
7use crate::config::telemetry::TelemetryConfig;
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 pty: PtyConfig,
108
109 #[serde(default)]
111 pub context: ContextFeaturesConfig,
112
113 #[serde(default)]
115 pub router: RouterConfig,
116
117 #[serde(default)]
119 pub telemetry: TelemetryConfig,
120
121 #[serde(default)]
123 pub syntax_highlighting: SyntaxHighlightingConfig,
124
125 #[serde(default)]
127 pub automation: AutomationConfig,
128}
129
130impl Default for VTCodeConfig {
131 fn default() -> Self {
132 Self {
133 agent: AgentConfig::default(),
134 tools: ToolsConfig::default(),
135 commands: CommandsConfig::default(),
136 security: SecurityConfig::default(),
137 pty: PtyConfig::default(),
138 context: ContextFeaturesConfig::default(),
139 router: RouterConfig::default(),
140 telemetry: TelemetryConfig::default(),
141 syntax_highlighting: SyntaxHighlightingConfig::default(),
142 automation: AutomationConfig::default(),
143 }
144 }
145}
146
147impl VTCodeConfig {
148 pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
150 Self::bootstrap_project_with_options(workspace, force, false)
151 }
152
153 pub fn bootstrap_project_with_options<P: AsRef<Path>>(
155 workspace: P,
156 force: bool,
157 use_home_dir: bool,
158 ) -> Result<Vec<String>> {
159 let workspace = workspace.as_ref();
160 let mut created_files = Vec::new();
161
162 let (config_path, gitignore_path) = if use_home_dir {
164 if let Some(home_dir) = ConfigManager::get_home_dir() {
166 let vtcode_dir = home_dir.join(".vtcode");
167 if !vtcode_dir.exists() {
169 fs::create_dir_all(&vtcode_dir).with_context(|| {
170 format!("Failed to create directory: {}", vtcode_dir.display())
171 })?;
172 }
173 (
174 vtcode_dir.join("vtcode.toml"),
175 vtcode_dir.join(".vtcodegitignore"),
176 )
177 } else {
178 let config_path = workspace.join("vtcode.toml");
180 let gitignore_path = workspace.join(".vtcodegitignore");
181 (config_path, gitignore_path)
182 }
183 } else {
184 let config_path = workspace.join("vtcode.toml");
186 let gitignore_path = workspace.join(".vtcodegitignore");
187 (config_path, gitignore_path)
188 };
189
190 if !config_path.exists() || force {
192 let default_config = VTCodeConfig::default();
193 let config_content = toml::to_string_pretty(&default_config)
194 .context("Failed to serialize default configuration")?;
195
196 fs::write(&config_path, config_content).with_context(|| {
197 format!("Failed to write config file: {}", config_path.display())
198 })?;
199
200 created_files.push("vtcode.toml".to_string());
201 }
202
203 if !gitignore_path.exists() || force {
205 let gitignore_content = Self::default_vtcode_gitignore();
206 fs::write(&gitignore_path, gitignore_content).with_context(|| {
207 format!(
208 "Failed to write gitignore file: {}",
209 gitignore_path.display()
210 )
211 })?;
212
213 created_files.push(".vtcodegitignore".to_string());
214 }
215
216 Ok(created_files)
217 }
218
219 fn default_vtcode_gitignore() -> String {
221 r#"# Security-focused exclusions
222.env, .env.local, secrets/, .aws/, .ssh/
223
224# Development artifacts
225target/, build/, dist/, node_modules/, vendor/
226
227# Database files
228*.db, *.sqlite, *.sqlite3
229
230# Binary files
231*.exe, *.dll, *.so, *.dylib, *.bin
232
233# IDE files (comprehensive)
234.vscode/, .idea/, *.swp, *.swo
235"#
236 .to_string()
237 }
238
239 pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
241 let output = output.as_ref();
242 let default_config = VTCodeConfig::default();
243 let config_content = toml::to_string_pretty(&default_config)
244 .context("Failed to serialize default configuration")?;
245
246 fs::write(output, config_content)
247 .with_context(|| format!("Failed to write config file: {}", output.display()))?;
248
249 Ok(())
250 }
251}
252
253#[derive(Clone)]
255pub struct ConfigManager {
256 config: VTCodeConfig,
257 config_path: Option<PathBuf>,
258 project_manager: Option<SimpleProjectManager>,
259 project_name: Option<String>,
260}
261
262impl ConfigManager {
263 pub fn load() -> Result<Self> {
265 Self::load_from_workspace(std::env::current_dir()?)
266 }
267
268 fn get_home_dir() -> Option<PathBuf> {
270 if let Ok(home) = std::env::var("HOME") {
272 return Some(PathBuf::from(home));
273 }
274
275 if let Ok(userprofile) = std::env::var("USERPROFILE") {
277 return Some(PathBuf::from(userprofile));
278 }
279
280 dirs::home_dir()
282 }
283
284 pub fn load_from_workspace(workspace: impl AsRef<Path>) -> Result<Self> {
286 let workspace = workspace.as_ref();
287
288 let project_manager = Some(SimpleProjectManager::new(workspace.to_path_buf()));
290 let project_name = project_manager
291 .as_ref()
292 .and_then(|pm| pm.identify_current_project().ok());
293
294 let config_path = workspace.join("vtcode.toml");
296 if config_path.exists() {
297 let config = Self::load_from_file(&config_path)?;
298 return Ok(Self {
299 config: config.config,
300 config_path: config.config_path,
301 project_manager,
302 project_name,
303 });
304 }
305
306 let fallback_path = workspace.join(".vtcode").join("vtcode.toml");
308 if fallback_path.exists() {
309 let config = Self::load_from_file(&fallback_path)?;
310 return Ok(Self {
311 config: config.config,
312 config_path: config.config_path,
313 project_manager,
314 project_name,
315 });
316 }
317
318 if let Some(home_dir) = Self::get_home_dir() {
320 let home_config_path = home_dir.join(".vtcode").join("vtcode.toml");
321 if home_config_path.exists() {
322 let config = Self::load_from_file(&home_config_path)?;
323 return Ok(Self {
324 config: config.config,
325 config_path: config.config_path,
326 project_manager,
327 project_name,
328 });
329 }
330 }
331
332 if let (Some(pm), Some(pname)) = (&project_manager, &project_name) {
334 let project_config_path = pm.config_dir(pname).join("vtcode.toml");
335 if project_config_path.exists() {
336 let config = Self::load_from_file(&project_config_path)?;
337 return Ok(Self {
338 config: config.config,
339 config_path: config.config_path,
340 project_manager: Some(pm.clone()),
341 project_name: Some(pname.clone()),
342 });
343 }
344 }
345
346 Ok(Self {
348 config: VTCodeConfig::default(),
349 config_path: None,
350 project_manager,
351 project_name,
352 })
353 }
354
355 pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
357 let path = path.as_ref();
358 let content = std::fs::read_to_string(path)
359 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
360
361 let config: VTCodeConfig = toml::from_str(&content)
362 .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
363
364 let project_manager = std::env::current_dir()
367 .ok()
368 .map(|cwd| SimpleProjectManager::new(cwd));
369
370 Ok(Self {
371 config,
372 config_path: Some(path.to_path_buf()),
373 project_manager,
374 project_name: None,
375 })
376 }
377
378 pub fn config(&self) -> &VTCodeConfig {
380 &self.config
381 }
382
383 pub fn config_path(&self) -> Option<&Path> {
385 self.config_path.as_deref()
386 }
387
388 pub fn session_duration(&self) -> std::time::Duration {
390 std::time::Duration::from_secs(60 * 60) }
392
393 pub fn project_manager(&self) -> Option<&SimpleProjectManager> {
395 self.project_manager.as_ref()
396 }
397
398 pub fn project_name(&self) -> Option<&str> {
400 self.project_name.as_deref()
401 }
402}