1use crate::config::constants::defaults;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct DotConfig {
13 pub version: String,
14 pub last_updated: u64,
15 pub preferences: UserPreferences,
16 pub providers: ProviderConfigs,
17 pub cache: CacheConfig,
18 pub ui: UiConfig,
19 #[serde(default)]
20 pub workspace_trust: WorkspaceTrustStore,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct UserPreferences {
25 pub default_model: String,
26 pub default_provider: String,
27 pub max_tokens: Option<u32>,
28 pub temperature: Option<f32>,
29 pub auto_save: bool,
30 pub theme: String,
31 pub keybindings: HashMap<String, String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ProviderConfigs {
36 pub openai: Option<ProviderConfig>,
37 pub anthropic: Option<ProviderConfig>,
38 pub gemini: Option<ProviderConfig>,
39 pub openrouter: Option<ProviderConfig>,
40 pub xai: Option<ProviderConfig>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44pub struct WorkspaceTrustStore {
45 #[serde(default)]
46 pub entries: HashMap<String, WorkspaceTrustRecord>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct WorkspaceTrustRecord {
51 pub level: WorkspaceTrustLevel,
52 pub trusted_at: u64,
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "snake_case")]
57pub enum WorkspaceTrustLevel {
58 ToolsPolicy,
59 FullAuto,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default)]
63pub struct ProviderConfig {
64 pub api_key: Option<String>,
65 pub base_url: Option<String>,
66 pub model: Option<String>,
67 pub enabled: bool,
68 pub priority: i32, }
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CacheConfig {
73 pub enabled: bool,
74 pub max_size_mb: u64,
75 pub ttl_days: u64,
76 pub prompt_cache_enabled: bool,
77 pub context_cache_enabled: bool,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct UiConfig {
82 pub show_timestamps: bool,
83 pub max_output_lines: usize,
84 pub syntax_highlighting: bool,
85 pub auto_complete: bool,
86 pub history_size: usize,
87}
88
89impl Default for DotConfig {
90 fn default() -> Self {
91 Self {
92 version: env!("CARGO_PKG_VERSION").to_string(),
93 last_updated: std::time::SystemTime::now()
94 .duration_since(std::time::UNIX_EPOCH)
95 .unwrap()
96 .as_secs(),
97 preferences: UserPreferences::default(),
98 providers: ProviderConfigs::default(),
99 cache: CacheConfig::default(),
100 ui: UiConfig::default(),
101 workspace_trust: WorkspaceTrustStore::default(),
102 }
103 }
104}
105
106impl Default for UserPreferences {
107 fn default() -> Self {
108 Self {
109 default_model: defaults::DEFAULT_MODEL.to_string(),
110 default_provider: defaults::DEFAULT_PROVIDER.to_string(),
111 max_tokens: Some(4096),
112 temperature: Some(0.7),
113 auto_save: true,
114 theme: defaults::DEFAULT_THEME.to_string(),
115 keybindings: HashMap::new(),
116 }
117 }
118}
119
120impl Default for ProviderConfigs {
121 fn default() -> Self {
122 Self {
123 openai: None,
124 anthropic: None,
125 gemini: None,
126 openrouter: None,
127 xai: None,
128 }
129 }
130}
131
132impl Default for WorkspaceTrustLevel {
133 fn default() -> Self {
134 Self::ToolsPolicy
135 }
136}
137
138impl fmt::Display for WorkspaceTrustLevel {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 WorkspaceTrustLevel::ToolsPolicy => write!(f, "tools policy"),
142 WorkspaceTrustLevel::FullAuto => write!(f, "full auto"),
143 }
144 }
145}
146
147impl Default for CacheConfig {
148 fn default() -> Self {
149 Self {
150 enabled: true,
151 max_size_mb: 100,
152 ttl_days: 30,
153 prompt_cache_enabled: true,
154 context_cache_enabled: true,
155 }
156 }
157}
158
159impl Default for UiConfig {
160 fn default() -> Self {
161 Self {
162 show_timestamps: true,
163 max_output_lines: 1000,
164 syntax_highlighting: true,
165 auto_complete: true,
166 history_size: 1000,
167 }
168 }
169}
170
171pub struct DotManager {
173 config_dir: PathBuf,
174 cache_dir: PathBuf,
175 config_file: PathBuf,
176}
177
178impl DotManager {
179 pub fn new() -> Result<Self, DotError> {
180 let home_dir = dirs::home_dir().ok_or_else(|| DotError::HomeDirNotFound)?;
181
182 let config_dir = home_dir.join(".vtcode");
183 let cache_dir = config_dir.join("cache");
184 let config_file = config_dir.join("config.toml");
185
186 Ok(Self {
187 config_dir,
188 cache_dir,
189 config_file,
190 })
191 }
192
193 pub fn initialize(&self) -> Result<(), DotError> {
195 fs::create_dir_all(&self.config_dir).map_err(|e| DotError::Io(e))?;
197 fs::create_dir_all(&self.cache_dir).map_err(|e| DotError::Io(e))?;
198
199 let subdirs = [
201 "cache/prompts",
202 "cache/context",
203 "cache/models",
204 "logs",
205 "sessions",
206 "backups",
207 ];
208
209 for subdir in &subdirs {
210 fs::create_dir_all(self.config_dir.join(subdir)).map_err(|e| DotError::Io(e))?;
211 }
212
213 if !self.config_file.exists() {
215 let default_config = DotConfig::default();
216 self.save_config(&default_config)?;
217 }
218
219 Ok(())
220 }
221
222 pub fn load_config(&self) -> Result<DotConfig, DotError> {
224 if !self.config_file.exists() {
225 return Ok(DotConfig::default());
226 }
227
228 let content = fs::read_to_string(&self.config_file).map_err(|e| DotError::Io(e))?;
229
230 toml::from_str(&content).map_err(|e| DotError::TomlDe(e))
231 }
232
233 pub fn save_config(&self, config: &DotConfig) -> Result<(), DotError> {
235 let content = toml::to_string_pretty(config).map_err(|e| DotError::Toml(e))?;
236
237 fs::write(&self.config_file, content).map_err(|e| DotError::Io(e))?;
238
239 Ok(())
240 }
241
242 pub fn update_config<F>(&self, updater: F) -> Result<(), DotError>
244 where
245 F: FnOnce(&mut DotConfig),
246 {
247 let mut config = self.load_config()?;
248 updater(&mut config);
249 config.last_updated = std::time::SystemTime::now()
250 .duration_since(std::time::UNIX_EPOCH)
251 .unwrap()
252 .as_secs();
253 self.save_config(&config)
254 }
255
256 pub fn cache_dir(&self, cache_type: &str) -> PathBuf {
258 self.cache_dir.join(cache_type)
259 }
260
261 pub fn logs_dir(&self) -> PathBuf {
263 self.config_dir.join("logs")
264 }
265
266 pub fn sessions_dir(&self) -> PathBuf {
268 self.config_dir.join("sessions")
269 }
270
271 pub fn backups_dir(&self) -> PathBuf {
273 self.config_dir.join("backups")
274 }
275
276 pub fn cleanup_cache(&self) -> Result<CacheCleanupStats, DotError> {
278 let config = self.load_config()?;
279 let max_age = std::time::Duration::from_secs(config.cache.ttl_days * 24 * 60 * 60);
280 let now = std::time::SystemTime::now();
281
282 let mut stats = CacheCleanupStats::default();
283
284 if config.cache.prompt_cache_enabled {
286 stats.prompts_cleaned =
287 self.cleanup_directory(&self.cache_dir("prompts"), max_age, now)?;
288 }
289
290 if config.cache.context_cache_enabled {
292 stats.context_cleaned =
293 self.cleanup_directory(&self.cache_dir("context"), max_age, now)?;
294 }
295
296 stats.models_cleaned = self.cleanup_directory(&self.cache_dir("models"), max_age, now)?;
298
299 Ok(stats)
300 }
301
302 fn cleanup_directory(
304 &self,
305 dir: &Path,
306 max_age: std::time::Duration,
307 now: std::time::SystemTime,
308 ) -> Result<u64, DotError> {
309 if !dir.exists() {
310 return Ok(0);
311 }
312
313 let mut cleaned = 0u64;
314
315 for entry in fs::read_dir(dir).map_err(|e| DotError::Io(e))? {
316 let entry = entry.map_err(|e| DotError::Io(e))?;
317 let path = entry.path();
318
319 if let Ok(metadata) = entry.metadata() {
320 if let Ok(modified) = metadata.modified() {
321 if let Ok(age) = now.duration_since(modified) {
322 if age > max_age {
323 if path.is_file() {
324 fs::remove_file(&path).map_err(|e| DotError::Io(e))?;
325 cleaned += 1;
326 } else if path.is_dir() {
327 fs::remove_dir_all(&path).map_err(|e| DotError::Io(e))?;
328 cleaned += 1;
329 }
330 }
331 }
332 }
333 }
334 }
335
336 Ok(cleaned)
337 }
338
339 pub fn disk_usage(&self) -> Result<DiskUsageStats, DotError> {
341 let mut stats = DiskUsageStats::default();
342
343 stats.config_size = self.calculate_dir_size(&self.config_dir)?;
344 stats.cache_size = self.calculate_dir_size(&self.cache_dir)?;
345 stats.logs_size = self.calculate_dir_size(&self.logs_dir())?;
346 stats.sessions_size = self.calculate_dir_size(&self.sessions_dir())?;
347 stats.backups_size = self.calculate_dir_size(&self.backups_dir())?;
348
349 stats.total_size = stats.config_size
350 + stats.cache_size
351 + stats.logs_size
352 + stats.sessions_size
353 + stats.backups_size;
354
355 Ok(stats)
356 }
357
358 fn calculate_dir_size(&self, dir: &Path) -> Result<u64, DotError> {
360 if !dir.exists() {
361 return Ok(0);
362 }
363
364 let mut size = 0u64;
365
366 fn calculate_recursive(path: &Path, current_size: &mut u64) -> Result<(), DotError> {
367 if path.is_file() {
368 if let Ok(metadata) = path.metadata() {
369 *current_size += metadata.len();
370 }
371 } else if path.is_dir() {
372 for entry in fs::read_dir(path).map_err(|e| DotError::Io(e))? {
373 let entry = entry.map_err(|e| DotError::Io(e))?;
374 calculate_recursive(&entry.path(), current_size)?;
375 }
376 }
377 Ok(())
378 }
379
380 calculate_recursive(dir, &mut size)?;
381 Ok(size)
382 }
383
384 pub fn backup_config(&self) -> Result<PathBuf, DotError> {
386 let timestamp = std::time::SystemTime::now()
387 .duration_since(std::time::UNIX_EPOCH)
388 .unwrap()
389 .as_secs();
390
391 let backup_name = format!("config_backup_{}.toml", timestamp);
392 let backup_path = self.backups_dir().join(backup_name);
393
394 if self.config_file.exists() {
395 fs::copy(&self.config_file, &backup_path).map_err(|e| DotError::Io(e))?;
396 }
397
398 Ok(backup_path)
399 }
400
401 pub fn list_backups(&self) -> Result<Vec<PathBuf>, DotError> {
403 let backups_dir = self.backups_dir();
404 if !backups_dir.exists() {
405 return Ok(vec![]);
406 }
407
408 let mut backups = vec![];
409
410 for entry in fs::read_dir(backups_dir).map_err(|e| DotError::Io(e))? {
411 let entry = entry.map_err(|e| DotError::Io(e))?;
412 if entry.path().extension().and_then(|e| e.to_str()) == Some("toml") {
413 backups.push(entry.path());
414 }
415 }
416
417 backups.sort_by(|a, b| {
419 let a_time = a.metadata().and_then(|m| m.modified()).ok();
420 let b_time = b.metadata().and_then(|m| m.modified()).ok();
421 b_time.cmp(&a_time)
422 });
423
424 Ok(backups)
425 }
426
427 pub fn restore_backup(&self, backup_path: &Path) -> Result<(), DotError> {
429 if !backup_path.exists() {
430 return Err(DotError::BackupNotFound(backup_path.to_path_buf()));
431 }
432
433 fs::copy(backup_path, &self.config_file).map_err(|e| DotError::Io(e))?;
434
435 Ok(())
436 }
437}
438
439#[derive(Debug, Default)]
440pub struct CacheCleanupStats {
441 pub prompts_cleaned: u64,
442 pub context_cleaned: u64,
443 pub models_cleaned: u64,
444}
445
446#[derive(Debug, Default)]
447pub struct DiskUsageStats {
448 pub config_size: u64,
449 pub cache_size: u64,
450 pub logs_size: u64,
451 pub sessions_size: u64,
452 pub backups_size: u64,
453 pub total_size: u64,
454}
455
456#[derive(Debug, thiserror::Error)]
458pub enum DotError {
459 #[error("Home directory not found")]
460 HomeDirNotFound,
461
462 #[error("IO error: {0}")]
463 Io(#[from] std::io::Error),
464
465 #[error("TOML serialization error: {0}")]
466 Toml(#[from] toml::ser::Error),
467
468 #[error("TOML deserialization error: {0}")]
469 TomlDe(#[from] toml::de::Error),
470
471 #[error("Backup not found: {0}")]
472 BackupNotFound(PathBuf),
473}
474
475use std::sync::{LazyLock, Mutex};
476
477static DOT_MANAGER: LazyLock<Mutex<DotManager>> =
479 LazyLock::new(|| Mutex::new(DotManager::new().unwrap()));
480
481pub fn get_dot_manager() -> &'static Mutex<DotManager> {
483 &DOT_MANAGER
484}
485
486pub fn initialize_dot_folder() -> Result<(), DotError> {
488 let manager = get_dot_manager().lock().unwrap();
489 manager.initialize()
490}
491
492pub fn load_user_config() -> Result<DotConfig, DotError> {
494 let manager = get_dot_manager().lock().unwrap();
495 manager.load_config()
496}
497
498pub fn save_user_config(config: &DotConfig) -> Result<(), DotError> {
500 let manager = get_dot_manager().lock().unwrap();
501 manager.save_config(config)
502}
503
504pub fn update_theme_preference(theme: &str) -> Result<(), DotError> {
506 let manager = get_dot_manager().lock().unwrap();
507 manager.update_config(|cfg| cfg.preferences.theme = theme.to_string())
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use tempfile::TempDir;
514
515 #[test]
516 fn test_dot_manager_initialization() {
517 let temp_dir = TempDir::new().unwrap();
518 let config_dir = temp_dir.path().join(".vtcode");
519
520 assert!(!config_dir.exists());
522
523 let manager = DotManager {
524 config_dir: config_dir.clone(),
525 cache_dir: config_dir.join("cache"),
526 config_file: config_dir.join("config.toml"),
527 };
528
529 manager.initialize().unwrap();
530 assert!(config_dir.exists());
531 assert!(config_dir.join("cache").exists());
532 assert!(config_dir.join("logs").exists());
533 }
534
535 #[test]
536 fn test_config_save_load() {
537 let temp_dir = TempDir::new().unwrap();
538 let config_dir = temp_dir.path().join(".vtcode");
539
540 let manager = DotManager {
541 config_dir: config_dir.clone(),
542 cache_dir: config_dir.join("cache"),
543 config_file: config_dir.join("config.toml"),
544 };
545
546 manager.initialize().unwrap();
547
548 let mut config = DotConfig::default();
549 config.preferences.default_model = "test-model".to_string();
550
551 manager.save_config(&config).unwrap();
552 let loaded_config = manager.load_config().unwrap();
553
554 assert_eq!(loaded_config.preferences.default_model, "test-model");
555 }
556}