use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use directories::ProjectDirs;
use once_cell::sync::Lazy;
use vtcode_commons::paths::WorkspacePaths;
const DEFAULT_CONFIG_FILE_NAME: &str = "vtcode.toml";
const DEFAULT_CONFIG_DIR_NAME: &str = ".vtcode";
const DEFAULT_SYNTAX_THEME: &str = "base16-ocean.dark";
static DEFAULT_SYNTAX_LANGUAGES: Lazy<Vec<String>> = Lazy::new(Vec::new);
static CONFIG_DEFAULTS: Lazy<RwLock<Arc<dyn ConfigDefaultsProvider>>> =
Lazy::new(|| RwLock::new(Arc::new(DefaultConfigDefaults)));
#[cfg(test)]
mod test_env_overrides {
use hashbrown::HashMap;
use std::sync::{LazyLock, Mutex};
static OVERRIDES: LazyLock<Mutex<HashMap<String, Option<String>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub(super) fn get(key: &str) -> Option<Option<String>> {
OVERRIDES.lock().ok().and_then(|map| map.get(key).cloned())
}
pub(super) fn set(key: &str, value: Option<&str>) {
if let Ok(mut map) = OVERRIDES.lock() {
map.insert(key.to_string(), value.map(ToString::to_string));
}
}
pub(super) fn restore(key: &str, previous: Option<Option<String>>) {
if let Ok(mut map) = OVERRIDES.lock() {
match previous {
Some(value) => {
map.insert(key.to_string(), value);
}
None => {
map.remove(key);
}
}
}
}
}
fn read_env_var(key: &str) -> Option<String> {
#[cfg(test)]
if let Some(override_value) = test_env_overrides::get(key) {
return override_value;
}
std::env::var(key).ok()
}
pub trait ConfigDefaultsProvider: Send + Sync {
fn config_file_name(&self) -> &str {
DEFAULT_CONFIG_FILE_NAME
}
fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths>;
fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf>;
fn syntax_theme(&self) -> String;
fn syntax_languages(&self) -> Vec<String>;
}
#[derive(Debug, Default)]
struct DefaultConfigDefaults;
impl ConfigDefaultsProvider for DefaultConfigDefaults {
fn workspace_paths_for(&self, workspace_root: &Path) -> Box<dyn WorkspacePaths> {
Box::new(DefaultWorkspacePaths::new(workspace_root.to_path_buf()))
}
fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
default_home_paths(config_file_name)
}
fn syntax_theme(&self) -> String {
DEFAULT_SYNTAX_THEME.to_string()
}
fn syntax_languages(&self) -> Vec<String> {
default_syntax_languages()
}
}
pub fn install_config_defaults_provider(
provider: Arc<dyn ConfigDefaultsProvider>,
) -> Arc<dyn ConfigDefaultsProvider> {
let mut guard = CONFIG_DEFAULTS.write().unwrap_or_else(|poisoned| {
tracing::warn!(
"config defaults provider lock poisoned while installing provider; recovering"
);
poisoned.into_inner()
});
std::mem::replace(&mut *guard, provider)
}
pub fn reset_to_default_config_defaults() {
let _ = install_config_defaults_provider(Arc::new(DefaultConfigDefaults));
}
pub fn with_config_defaults<F, R>(operation: F) -> R
where
F: FnOnce(&dyn ConfigDefaultsProvider) -> R,
{
let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
tracing::warn!("config defaults provider lock poisoned while reading provider; recovering");
poisoned.into_inner()
});
operation(guard.as_ref())
}
pub fn current_config_defaults() -> Arc<dyn ConfigDefaultsProvider> {
let guard = CONFIG_DEFAULTS.read().unwrap_or_else(|poisoned| {
tracing::warn!("config defaults provider lock poisoned while cloning provider; recovering");
poisoned.into_inner()
});
Arc::clone(&*guard)
}
pub fn with_config_defaults_provider_for_test<F, R>(
provider: Arc<dyn ConfigDefaultsProvider>,
action: F,
) -> R
where
F: FnOnce() -> R,
{
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
let previous = install_config_defaults_provider(provider);
let result = catch_unwind(AssertUnwindSafe(action));
let _ = install_config_defaults_provider(previous);
match result {
Ok(value) => value,
Err(payload) => resume_unwind(payload),
}
}
pub fn get_config_dir() -> Option<PathBuf> {
if let Some(custom_dir) = read_env_var("VTCODE_CONFIG") {
let trimmed = custom_dir.trim();
if !trimmed.is_empty() {
return Some(PathBuf::from(trimmed));
}
}
if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
return Some(proj_dirs.config_local_dir().to_path_buf());
}
dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME))
}
pub fn get_data_dir() -> Option<PathBuf> {
if let Some(custom_dir) = read_env_var("VTCODE_DATA") {
let trimmed = custom_dir.trim();
if !trimmed.is_empty() {
return Some(PathBuf::from(trimmed));
}
}
if let Some(proj_dirs) = ProjectDirs::from("com", "vinhnx", "vtcode") {
return Some(proj_dirs.data_local_dir().to_path_buf());
}
dirs::home_dir().map(|home| home.join(DEFAULT_CONFIG_DIR_NAME).join("cache"))
}
fn default_home_paths(config_file_name: &str) -> Vec<PathBuf> {
get_config_dir()
.map(|config_dir| config_dir.join(config_file_name))
.into_iter()
.collect()
}
fn default_syntax_languages() -> Vec<String> {
DEFAULT_SYNTAX_LANGUAGES.clone()
}
#[derive(Debug, Clone)]
struct DefaultWorkspacePaths {
root: PathBuf,
}
impl DefaultWorkspacePaths {
fn new(root: PathBuf) -> Self {
Self { root }
}
fn config_dir_path(&self) -> PathBuf {
self.root.join(DEFAULT_CONFIG_DIR_NAME)
}
}
impl WorkspacePaths for DefaultWorkspacePaths {
fn workspace_root(&self) -> &Path {
&self.root
}
fn config_dir(&self) -> PathBuf {
self.config_dir_path()
}
fn cache_dir(&self) -> Option<PathBuf> {
Some(self.config_dir_path().join("cache"))
}
fn telemetry_dir(&self) -> Option<PathBuf> {
Some(self.config_dir_path().join("telemetry"))
}
}
#[derive(Debug, Clone)]
pub struct WorkspacePathsDefaults<P>
where
P: WorkspacePaths + ?Sized,
{
paths: Arc<P>,
config_file_name: String,
home_paths: Option<Vec<PathBuf>>,
syntax_theme: String,
syntax_languages: Vec<String>,
}
impl<P> WorkspacePathsDefaults<P>
where
P: WorkspacePaths + 'static,
{
pub fn new(paths: Arc<P>) -> Self {
Self {
paths,
config_file_name: DEFAULT_CONFIG_FILE_NAME.to_string(),
home_paths: None,
syntax_theme: DEFAULT_SYNTAX_THEME.to_string(),
syntax_languages: default_syntax_languages(),
}
}
pub fn with_config_file_name(mut self, file_name: impl Into<String>) -> Self {
self.config_file_name = file_name.into();
self
}
pub fn with_home_paths(mut self, home_paths: Vec<PathBuf>) -> Self {
self.home_paths = Some(home_paths);
self
}
pub fn with_syntax_theme(mut self, theme: impl Into<String>) -> Self {
self.syntax_theme = theme.into();
self
}
pub fn with_syntax_languages(mut self, languages: Vec<String>) -> Self {
self.syntax_languages = languages;
self
}
pub fn build(self) -> Box<dyn ConfigDefaultsProvider> {
Box::new(self)
}
}
impl<P> ConfigDefaultsProvider for WorkspacePathsDefaults<P>
where
P: WorkspacePaths + 'static,
{
fn config_file_name(&self) -> &str {
&self.config_file_name
}
fn workspace_paths_for(&self, _workspace_root: &Path) -> Box<dyn WorkspacePaths> {
Box::new(WorkspacePathsWrapper {
inner: Arc::clone(&self.paths),
})
}
fn home_config_paths(&self, config_file_name: &str) -> Vec<PathBuf> {
self.home_paths
.clone()
.unwrap_or_else(|| default_home_paths(config_file_name))
}
fn syntax_theme(&self) -> String {
self.syntax_theme.clone()
}
fn syntax_languages(&self) -> Vec<String> {
self.syntax_languages.clone()
}
}
#[derive(Debug, Clone)]
struct WorkspacePathsWrapper<P>
where
P: WorkspacePaths + ?Sized,
{
inner: Arc<P>,
}
impl<P> WorkspacePaths for WorkspacePathsWrapper<P>
where
P: WorkspacePaths + ?Sized,
{
fn workspace_root(&self) -> &Path {
self.inner.workspace_root()
}
fn config_dir(&self) -> PathBuf {
self.inner.config_dir()
}
fn cache_dir(&self) -> Option<PathBuf> {
self.inner.cache_dir()
}
fn telemetry_dir(&self) -> Option<PathBuf> {
self.inner.telemetry_dir()
}
}
#[cfg(test)]
mod tests {
use super::{get_config_dir, get_data_dir};
use serial_test::serial;
use std::path::PathBuf;
fn with_env_var<F>(key: &str, value: Option<&str>, f: F)
where
F: FnOnce(),
{
let previous = super::test_env_overrides::get(key);
super::test_env_overrides::set(key, value);
f();
super::test_env_overrides::restore(key, previous);
}
#[test]
#[serial]
fn get_config_dir_uses_env_override() {
with_env_var("VTCODE_CONFIG", Some("/tmp/vtcode-config-test"), || {
assert_eq!(
get_config_dir(),
Some(PathBuf::from("/tmp/vtcode-config-test"))
);
});
}
#[test]
#[serial]
fn get_data_dir_uses_env_override() {
with_env_var("VTCODE_DATA", Some("/tmp/vtcode-data-test"), || {
assert_eq!(get_data_dir(), Some(PathBuf::from("/tmp/vtcode-data-test")));
});
}
#[test]
#[serial]
fn get_config_dir_ignores_blank_env_override() {
with_env_var("VTCODE_CONFIG", Some(" "), || {
let resolved = get_config_dir();
assert!(resolved.is_some());
assert_ne!(resolved, Some(PathBuf::from(" ")));
assert_ne!(resolved, Some(PathBuf::new()));
});
}
#[test]
#[serial]
fn get_data_dir_ignores_blank_env_override() {
with_env_var("VTCODE_DATA", Some(" "), || {
let resolved = get_data_dir();
assert!(resolved.is_some());
assert_ne!(resolved, Some(PathBuf::from(" ")));
assert_ne!(resolved, Some(PathBuf::new()));
});
}
#[test]
#[serial]
fn env_guard_restores_original_value() {
let key = "VTCODE_CONFIG";
let initial = super::read_env_var(key);
with_env_var(key, Some("/tmp/vtcode-config-test"), || {
assert_eq!(
super::read_env_var(key),
Some("/tmp/vtcode-config-test".to_string())
);
});
assert_eq!(super::read_env_var(key), initial);
}
}