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