vtcode_config/defaults/
provider.rs1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock};
3
4use directories::ProjectDirs;
5use once_cell::sync::Lazy;
6use vtcode_commons::paths::WorkspacePaths;
7
8const DEFAULT_CONFIG_FILE_NAME: &str = "vtcode.toml";
9const DEFAULT_CONFIG_DIR_NAME: &str = ".vtcode";
10const DEFAULT_SYNTAX_THEME: &str = "base16-ocean.dark";
11
12static DEFAULT_SYNTAX_LANGUAGES: Lazy<Vec<String>> = Lazy::new(Vec::new);
15
16static CONFIG_DEFAULTS: Lazy<RwLock<Arc<dyn ConfigDefaultsProvider>>> =
17 Lazy::new(|| RwLock::new(Arc::new(DefaultConfigDefaults)));
18
19fn read_env_var(key: &str) -> Option<String> {
20 crate::env_helpers::read_env_var(key)
21}
22
23pub trait ConfigDefaultsProvider: Send + Sync {
26 fn config_file_name(&self) -> &str {
28 DEFAULT_CONFIG_FILE_NAME
29 }
30
31 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
34
35 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
38
39 fn syntax_theme(&self) -> String;
41
42 fn syntax_languages(&self) -> Vec<String>;
44}
45
46#[derive(Debug, Default)]
47struct DefaultConfigDefaults;
48
49impl ConfigDefaultsProvider for DefaultConfigDefaults {
50 fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths> {
51 Box::new(DefaultWorkspacePaths::new(workspace_root.to_path_buf()))
52 }
53
54 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
55 default_home_paths(config_file_name)
56 }
57
58 fn syntax_theme(&self) -> String {
59 DEFAULT_SYNTAX_THEME.to_string()
60 }
61
62 fn syntax_languages(&self) -> Vec<String> {
63 default_syntax_languages()
64 }
65}
66
67pub fn install_config_defaults_provider(
69 provider: Arc<dyn ConfigDefaultsProvider>,
70) -> Arc<dyn ConfigDefaultsProvider> {
71 let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
72 tracing::warn!(
73 "config defaults provider lock poisoned while installing provider; recovering"
74 );
75 poisoned.into_inner()
76 });
77 std::mem::replace(&mut *guard, provider)
78}
79
80pub fn reset_to_default_config_defaults() {
82 let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
83}
84
85pub fn with_config_defaults<F, R>(operation: F) -> R
87where
88 F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
89{
90 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
91 tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
92 poisoned.into_inner()
93 });
94 operation(guard.as_ref())
95}
96
97pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
99 let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
100 tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
101 poisoned.into_inner()
102 });
103 Arc::clone(&*guard)
104}
105
106pub fn with_config_defaults_provider_for_test<F, R>(
107 provider: Arc<dyn ConfigDefaultsProvider>,
108 action: F,
109) -> R
110where
111 F: FnOnce() -> R,
112{
113 use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
114
115 let previous = install_config_defaults_provider(provider);
116 let result = catch_unwind(AssertUnwindSafe(action));
117 let _ = install_config_defaults_provider(previous);
118
119 match result {
120 Ok(value) => value,
121 Err(payload) => resume_unwind(payload),
122 }
123}
124
125pub fn get_config_dir() -> Option<PathBuf> {
134 if let Some(custom_dir) = read_env_var("VTCODE_CONFIG") {
136 let trimmed = custom_dir.trim();
137 if !trimmed.is_empty() {
138 return Some(PathBuf::from(trimmed));
139 }
140 }
141
142 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
144 return Some(proj_dirs.config_local_dir().to_path_buf());
145 }
146
147 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME))
149}
150
151pub fn get_data_dir() -> Option<PathBuf> {
160 if let Some(custom_dir) = read_env_var("VTCODE_DATA") {
162 let trimmed = custom_dir.trim();
163 if !trimmed.is_empty() {
164 return Some(PathBuf::from(trimmed));
165 }
166 }
167
168 if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
170 return Some(proj_dirs.data_local_dir().to_path_buf());
171 }
172
173 dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME).join("cache"))
175}
176
177fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
178 get_config_dir()
179 .map(|config_dir| config_dir.join(config_file_name))
180 .into_iter()
181 .collect()
182}
183
184fn default_syntax_languages() -> Vec<String> {
185 DEFAULT_SYNTAX_LANGUAGES.clone()
186}
187
188#[derive(Debug, Clone)]
189struct DefaultWorkspacePaths {
190 root: PathBuf,
191}
192
193impl DefaultWorkspacePaths {
194 fn new(root: PathBuf) -> Self {
195 Self { root }
196 }
197
198 fn config_dir_path(&self) -> PathBuf {
199 self.root.join(DEFAULT_CONFIG_DIR_NAME)
200 }
201}
202
203impl WorkspacePaths for DefaultWorkspacePaths {
204 fn workspace_root(&self) -> &Path {
205 &self.root
206 }
207
208 fn config_dir(&self) -> PathBuf {
209 self.config_dir_path()
210 }
211
212 fn cache_dir(&self) -> Option<PathBuf> {
213 Some(self.config_dir_path().join("cache"))
214 }
215
216 fn telemetry_dir(&self) -> Option<PathBuf> {
217 Some(self.config_dir_path().join("telemetry"))
218 }
219}
220
221#[derive(Debug, Clone)]
224pub struct WorkspacePathsDefaults<P>
225where
226 P: WorkspacePaths + ?Sized,
227{
228 paths: Arc<P>,
229 config_file_name: String,
230 home_paths: Option<Vec<PathBuf>>,
231 syntax_theme: String,
232 syntax_languages: Vec<String>,
233}
234
235impl<P> WorkspacePathsDefaults<P>
236where
237 P: WorkspacePaths + 'static,
238{
239 pub fn new(paths: Arc<P>) -> Self {
242 Self {
243 paths,
244 config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
245 home_paths: None,
246 syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
247 syntax_languages: default_syntax_languages(),
248 }
249 }
250
251 pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
253 self.config_file_name = file_name.into();
254 self
255 }
256
257 pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
259 self.home_paths = Some(home_paths);
260 self
261 }
262
263 pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
265 self.syntax_theme = theme.into();
266 self
267 }
268
269 pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
271 self.syntax_languages = languages;
272 self
273 }
274
275 pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
277 Box::new(self)
278 }
279}
280
281impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
282where
283 P: WorkspacePaths + 'static,
284{
285 fn config_file_name(&self) -> &str {
286 &self.config_file_name
287 }
288
289 fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
290 Box::new(WorkspacePathsWrapper {
291 inner: Arc::clone(&self.paths),
292 })
293 }
294
295 fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
296 self.home_paths
297 .clone()
298 .unwrap_or_else(|| default_home_paths(config_file_name))
299 }
300
301 fn syntax_theme(&self) -> String {
302 self.syntax_theme.clone()
303 }
304
305 fn syntax_languages(&self) -> Vec<String> {
306 self.syntax_languages.clone()
307 }
308}
309
310#[derive(Debug, Clone)]
311struct WorkspacePathsWrapper<P>
312where
313 P: WorkspacePaths + ?Sized,
314{
315 inner: Arc<P>,
316}
317
318impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
319where
320 P: WorkspacePaths + ?Sized,
321{
322 fn workspace_root(&self) -> &Path {
323 self.inner.workspace_root()
324 }
325
326 fn config_dir(&self) -> PathBuf {
327 self.inner.config_dir()
328 }
329
330 fn cache_dir(&self) -> Option<PathBuf> {
331 self.inner.cache_dir()
332 }
333
334 fn telemetry_dir(&self) -> Option<PathBuf> {
335 self.inner.telemetry_dir()
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use super::{get_config_dir, get_data_dir};
342 use serial_test::serial;
343 use std::path::PathBuf;
344
345 fn with_env_var<F>(key: &str, value: Option<&str>, f: F)
346 where
347 F: FnOnce(),
348 {
349 let previous = crate::env_helpers::test_env_overrides::get(key);
350 crate::env_helpers::test_env_overrides::set(key, value);
351
352 f();
353
354 crate::env_helpers::test_env_overrides::restore(key, previous);
355 }
356
357 #[test]
358 #[serial]
359 fn get_config_dir_uses_env_override() {
360 with_env_var("VTCODE_CONFIG", Some("/tmp/vtcode-config-test"), || {
361 assert_eq!(
362 get_config_dir(),
363 Some(PathBuf::from("/tmp/vtcode-config-test"))
364 );
365 });
366 }
367
368 #[test]
369 #[serial]
370 fn get_data_dir_uses_env_override() {
371 with_env_var("VTCODE_DATA", Some("/tmp/vtcode-data-test"), || {
372 assert_eq!(get_data_dir(), Some(PathBuf::from("/tmp/vtcode-data-test")));
373 });
374 }
375
376 #[test]
377 #[serial]
378 fn get_config_dir_ignores_blank_env_override() {
379 with_env_var("VTCODE_CONFIG", Some(" "), || {
380 let resolved = get_config_dir();
381 assert!(resolved.is_some());
382 assert_ne!(resolved, Some(PathBuf::from(" ")));
383 assert_ne!(resolved, Some(PathBuf::new()));
384 });
385 }
386
387 #[test]
388 #[serial]
389 fn get_data_dir_ignores_blank_env_override() {
390 with_env_var("VTCODE_DATA", Some(" "), || {
391 let resolved = get_data_dir();
392 assert!(resolved.is_some());
393 assert_ne!(resolved, Some(PathBuf::from(" ")));
394 assert_ne!(resolved, Some(PathBuf::new()));
395 });
396 }
397
398 #[test]
399 #[serial]
400 fn env_guard_restores_original_value() {
401 let key = "VTCODE_CONFIG";
402 let initial = super::read_env_var(key);
403 with_env_var(key, Some("/tmp/vtcode-config-test"), || {
404 assert_eq!(
405 super::read_env_var(key),
406 Some("/tmp/vtcode-config-test".to_string())
407 );
408 });
409 assert_eq!(super::read_env_var(key), initial);
410 }
411}