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