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