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