Skip to main content

vtcode_config/defaults/
provider.rs

1use 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
12/// Empty by default — all ~250 syntect grammars are enabled.
13/// Users can set `enabled_languages` in vtcode.toml to restrict to a subset.
14static 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
19/// Provides access to filesystem and syntax defaults used by the configuration
20/// loader.
21pub trait ConfigDefaultsProvider: Send + Sync {
22    /// Returns the primary configuration file name expected in a workspace.
23    fn config_file_name(&self) -> &str {
24        DEFAULT_CONFIG_FILE_NAME
25    }
26
27    /// Creates a [`WorkspacePaths`] implementation for the provided workspace
28    /// root.
29    fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
30
31    /// Returns the fallback configuration locations searched outside the
32    /// workspace.
33    fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
34
35    /// Returns the default syntax highlighting theme identifier.
36    fn syntax_theme(&self) -> String;
37
38    /// Returns the default list of syntax highlighting languages.
39    fn syntax_languages(&self) -> Vec<String>;
40}
41
42#[derive(Debug, Default)]
43struct DefaultConfigDefaults;
44
45impl ConfigDefaultsProvider for DefaultConfigDefaults {
46    fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths> {
47        Box::new(DefaultWorkspacePaths::new(workspace_root.to_path_buf()))
48    }
49
50    fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
51        default_home_paths(config_file_name)
52    }
53
54    fn syntax_theme(&self) -> String {
55        DEFAULT_SYNTAX_THEME.to_string()
56    }
57
58    fn syntax_languages(&self) -> Vec<String> {
59        default_syntax_languages()
60    }
61}
62
63/// Installs a new [`ConfigDefaultsProvider`], returning the previous provider.
64pub fn install_config_defaults_provider(
65    provider: Arc<dyn ConfigDefaultsProvider>,
66) -> Arc<dyn ConfigDefaultsProvider> {
67    let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
68        tracing::warn!(
69            "config defaults provider lock poisoned while installing provider; recovering"
70        );
71        poisoned.into_inner()
72    });
73    std::mem::replace(&mut *guard, provider)
74}
75
76/// Restores the built-in defaults provider.
77pub fn reset_to_default_config_defaults() {
78    let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
79}
80
81/// Executes the provided function with the currently installed provider.
82pub fn with_config_defaults<F, R>(operation: F) -> R
83where
84    F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
85{
86    let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
87        tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
88        poisoned.into_inner()
89    });
90    operation(guard.as_ref())
91}
92
93/// Returns the currently installed provider as an [`Arc`].
94pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
95    let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
96        tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
97        poisoned.into_inner()
98    });
99    Arc::clone(&*guard)
100}
101
102pub fn with_config_defaults_provider_for_test<F, R>(
103    provider: Arc<dyn ConfigDefaultsProvider>,
104    action: F,
105) -> R
106where
107    F: FnOnce() -> R,
108{
109    use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
110
111    let previous = install_config_defaults_provider(provider);
112    let result = catch_unwind(AssertUnwindSafe(action));
113    let _ = install_config_defaults_provider(previous);
114
115    match result {
116        Ok(value) => value,
117        Err(payload) => resume_unwind(payload),
118    }
119}
120
121/// Get the XDG-compliant configuration directory for vtcode.
122///
123/// Follows the Ratatui recipe pattern for config directories:
124/// 1. Check environment variable VTCODE_CONFIG for custom location
125/// 2. Use XDG Base Directory Specification via ProjectDirs
126/// 3. Fallback to legacy ~/.vtcode/ for backwards compatibility
127///
128/// Returns `None` if no suitable directory can be determined.
129pub fn get_config_dir() -> Option<PathBuf> {
130    // Allow custom config directory via environment variable
131    if let Ok(custom_dir) = std::env::var("VTCODE_CONFIG") {
132        return Some(PathBuf::from(custom_dir));
133    }
134
135    // Use XDG-compliant directories (e.g., ~/.config/vtcode on Linux)
136    if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
137        return Some(proj_dirs.config_local_dir().to_path_buf());
138    }
139
140    // Fallback to legacy ~/.vtcode/ for backwards compatibility
141    dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME))
142}
143
144/// Get the XDG-compliant data directory for vtcode.
145///
146/// Follows the Ratatui recipe pattern for data directories:
147/// 1. Check environment variable VTCODE_DATA for custom location
148/// 2. Use XDG Base Directory Specification via ProjectDirs
149/// 3. Fallback to legacy ~/.vtcode/cache for backwards compatibility
150///
151/// Returns `None` if no suitable directory can be determined.
152pub fn get_data_dir() -> Option<PathBuf> {
153    // Allow custom data directory via environment variable
154    if let Ok(custom_dir) = std::env::var("VTCODE_DATA") {
155        return Some(PathBuf::from(custom_dir));
156    }
157
158    // Use XDG-compliant directories (e.g., ~/.local/share/vtcode on Linux)
159    if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
160        return Some(proj_dirs.data_local_dir().to_path_buf());
161    }
162
163    // Fallback to legacy ~/.vtcode/cache for backwards compatibility
164    dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME).join("cache"))
165}
166
167fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
168    get_config_dir()
169        .map(|config_dir| config_dir.join(config_file_name))
170        .into_iter()
171        .collect()
172}
173
174fn default_syntax_languages() -> Vec<String> {
175    DEFAULT_SYNTAX_LANGUAGES.clone()
176}
177
178#[derive(Debug, Clone)]
179struct DefaultWorkspacePaths {
180    root: PathBuf,
181}
182
183impl DefaultWorkspacePaths {
184    fn new(root: PathBuf) -> Self {
185        Self { root }
186    }
187
188    fn config_dir_path(&self) -> PathBuf {
189        self.root.join(DEFAULT_CONFIG_DIR_NAME)
190    }
191}
192
193impl WorkspacePaths for DefaultWorkspacePaths {
194    fn workspace_root(&self) -> &Path {
195        &self.root
196    }
197
198    fn config_dir(&self) -> PathBuf {
199        self.config_dir_path()
200    }
201
202    fn cache_dir(&self) -> Option<PathBuf> {
203        Some(self.config_dir_path().join("cache"))
204    }
205
206    fn telemetry_dir(&self) -> Option<PathBuf> {
207        Some(self.config_dir_path().join("telemetry"))
208    }
209}
210
211/// Adapter that maps an existing [`WorkspacePaths`] implementation into a
212/// [`ConfigDefaultsProvider`].
213#[derive(Debug, Clone)]
214pub struct WorkspacePathsDefaults<P>
215where
216    P: WorkspacePaths + ?Sized,
217{
218    paths: Arc<P>,
219    config_file_name: String,
220    home_paths: Option<Vec<PathBuf>>,
221    syntax_theme: String,
222    syntax_languages: Vec<String>,
223}
224
225impl<P> WorkspacePathsDefaults<P>
226where
227    P: WorkspacePaths + 'static,
228{
229    /// Creates a defaults provider that delegates to the supplied
230    /// [`WorkspacePaths`] implementation.
231    pub fn new(paths: Arc<P>) -> Self {
232        Self {
233            paths,
234            config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
235            home_paths: None,
236            syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
237            syntax_languages: default_syntax_languages(),
238        }
239    }
240
241    /// Overrides the configuration file name returned by the provider.
242    pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
243        self.config_file_name = file_name.into();
244        self
245    }
246
247    /// Overrides the fallback configuration search paths returned by the provider.
248    pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
249        self.home_paths = Some(home_paths);
250        self
251    }
252
253    /// Overrides the default syntax theme returned by the provider.
254    pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
255        self.syntax_theme = theme.into();
256        self
257    }
258
259    /// Overrides the default syntax languages returned by the provider.
260    pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
261        self.syntax_languages = languages;
262        self
263    }
264
265    /// Consumes the builder, returning a boxed provider implementation.
266    pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
267        Box::new(self)
268    }
269}
270
271impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
272where
273    P: WorkspacePaths + 'static,
274{
275    fn config_file_name(&self) -> &str {
276        &self.config_file_name
277    }
278
279    fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
280        Box::new(WorkspacePathsWrapper {
281            inner: Arc::clone(&self.paths),
282        })
283    }
284
285    fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
286        self.home_paths
287            .clone()
288            .unwrap_or_else(|| default_home_paths(config_file_name))
289    }
290
291    fn syntax_theme(&self) -> String {
292        self.syntax_theme.clone()
293    }
294
295    fn syntax_languages(&self) -> Vec<String> {
296        self.syntax_languages.clone()
297    }
298}
299
300#[derive(Debug, Clone)]
301struct WorkspacePathsWrapper<P>
302where
303    P: WorkspacePaths + ?Sized,
304{
305    inner: Arc<P>,
306}
307
308impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
309where
310    P: WorkspacePaths + ?Sized,
311{
312    fn workspace_root(&self) -> &Path {
313        self.inner.workspace_root()
314    }
315
316    fn config_dir(&self) -> PathBuf {
317        self.inner.config_dir()
318    }
319
320    fn cache_dir(&self) -> Option<PathBuf> {
321        self.inner.cache_dir()
322    }
323
324    fn telemetry_dir(&self) -> Option<PathBuf> {
325        self.inner.telemetry_dir()
326    }
327}