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