use std::path::{Path, PathBuf};
use super::data_dir::{ensure_dir_permissions, has_data_files, lean_ctx_data_dir};
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var(name)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
.map(PathBuf::from)
}
fn xdg_base(env_name: &str, home_fallback: &str) -> Result<PathBuf, String> {
if let Some(p) = env_path(env_name) {
return Ok(p);
}
dirs::home_dir()
.map(|h| h.join(home_fallback))
.ok_or_else(|| "Cannot determine home directory".to_string())
}
fn resolve(
category_override: Option<PathBuf>,
single: Option<PathBuf>,
xdg_base_dir: &Path,
) -> PathBuf {
category_override
.or(single)
.unwrap_or_else(|| xdg_base_dir.join("lean-ctx"))
}
pub(crate) fn single_dir_override() -> Option<PathBuf> {
if let Some(p) = env_path("LEAN_CTX_DATA_DIR") {
return Some(p);
}
let home = dirs::home_dir()?;
let xdg_config_base = xdg_base("XDG_CONFIG_HOME", ".config").ok()?;
single_dir_override_fs(&home, &xdg_config_base)
}
fn single_dir_override_fs(home: &Path, xdg_config_base: &Path) -> Option<PathBuf> {
let legacy = home.join(".lean-ctx");
if legacy.exists() && has_data_files(&legacy) {
return Some(legacy);
}
let mixed = xdg_config_base.join("lean-ctx");
if mixed.exists() && has_data_files(&mixed) {
return Some(mixed);
}
None
}
#[cfg_attr(test, allow(clippy::unnecessary_wraps))]
fn category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
let category_override = env_path(cat_env);
#[cfg(test)]
{
if let Some(p) = category_override {
ensure_dir_permissions(&p);
return Ok(p);
}
let _ = (xdg_env, home_fallback);
Ok(super::data_dir::test_sandbox_dir())
}
#[cfg(not(test))]
{
let base = xdg_base(xdg_env, home_fallback)?;
let dir = resolve(category_override, single_dir_override(), &base);
ensure_dir_permissions(&dir);
Ok(dir)
}
}
pub fn config_dir() -> Result<PathBuf, String> {
category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
}
pub fn data_dir() -> Result<PathBuf, String> {
lean_ctx_data_dir()
}
pub fn state_dir() -> Result<PathBuf, String> {
category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
}
pub fn cache_dir() -> Result<PathBuf, String> {
category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
}
pub fn runtime_dir() -> Result<PathBuf, String> {
if let Some(base) = env_path("XDG_RUNTIME_DIR") {
return Ok(base.join("lean-ctx"));
}
state_dir()
}
fn raw_category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
if let Some(p) = env_path(cat_env) {
return Ok(p);
}
Ok(xdg_base(xdg_env, home_fallback)?.join("lean-ctx"))
}
pub(crate) fn config_split_target() -> Result<PathBuf, String> {
raw_category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
}
pub(crate) fn data_split_target() -> Result<PathBuf, String> {
raw_category_dir("LEAN_CTX_DATA_DIR", "XDG_DATA_HOME", ".local/share")
}
pub(crate) fn state_split_target() -> Result<PathBuf, String> {
raw_category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
}
pub(crate) fn cache_split_target() -> Result<PathBuf, String> {
raw_category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_prefers_override_then_single_then_xdg() {
let over = PathBuf::from("/over/ride");
let single = PathBuf::from("/single/dir");
let base = PathBuf::from("/xdg/base");
assert_eq!(
resolve(Some(over.clone()), Some(single.clone()), &base),
over
);
assert_eq!(resolve(None, Some(single.clone()), &base), single);
assert_eq!(
resolve(None, None, &base),
PathBuf::from("/xdg/base/lean-ctx")
);
}
#[test]
fn single_dir_fs_detects_legacy_with_data() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let legacy = home.path().join(".lean-ctx");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(legacy.join("stats.json"), "{}").unwrap();
assert_eq!(
single_dir_override_fs(home.path(), xdg.path()),
Some(legacy)
);
}
#[test]
fn single_dir_fs_detects_mixed_with_data() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let mixed = xdg.path().join("lean-ctx");
std::fs::create_dir_all(&mixed).unwrap();
std::fs::write(mixed.join("stats.json"), "{}").unwrap();
assert_eq!(single_dir_override_fs(home.path(), xdg.path()), Some(mixed));
}
#[test]
fn single_dir_fs_ignores_config_only_dir() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let mixed = xdg.path().join("lean-ctx");
std::fs::create_dir_all(&mixed).unwrap();
std::fs::write(mixed.join("config.toml"), "").unwrap();
std::fs::write(mixed.join("shell-hook.zsh"), "").unwrap();
assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
}
#[test]
fn single_dir_fs_prefers_legacy_over_mixed() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let legacy = home.path().join(".lean-ctx");
std::fs::create_dir_all(&legacy).unwrap();
std::fs::write(legacy.join("sessions"), "x").unwrap();
let mixed = xdg.path().join("lean-ctx");
std::fs::create_dir_all(&mixed).unwrap();
std::fs::write(mixed.join("stats.json"), "{}").unwrap();
assert_eq!(
single_dir_override_fs(home.path(), xdg.path()),
Some(legacy)
);
}
#[test]
fn single_dir_fs_ignores_empty_dirs() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".lean-ctx")).unwrap();
std::fs::create_dir_all(xdg.path().join("lean-ctx")).unwrap();
assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
}
#[test]
fn single_dir_fs_ignores_non_marker_files() {
let home = tempfile::tempdir().unwrap();
let xdg = tempfile::tempdir().unwrap();
let mixed = xdg.path().join("lean-ctx");
std::fs::create_dir_all(&mixed).unwrap();
std::fs::write(mixed.join("random.txt"), "x").unwrap();
assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
}
#[test]
fn xdg_base_honors_env_then_home_fallback() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
crate::test_env::set_var("XDG_CONFIG_HOME", tmp.path());
let from_env = xdg_base("XDG_CONFIG_HOME", ".config").unwrap();
crate::test_env::remove_var("XDG_CONFIG_HOME");
assert_eq!(from_env, tmp.path());
let fallback = xdg_base("LEAN_CTX_NONEXISTENT_XDG_VAR", ".cache").unwrap();
assert!(fallback.ends_with(".cache"), "got: {}", fallback.display());
}
#[test]
fn single_dir_override_honors_data_dir_env() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
let got = single_dir_override();
crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
assert_eq!(got, Some(tmp.path().to_path_buf()));
}
#[test]
fn config_dir_honors_explicit_override() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_CONFIG_DIR", tmp.path());
let got = config_dir().unwrap();
crate::test_env::remove_var("LEAN_CTX_CONFIG_DIR");
assert_eq!(got, tmp.path());
}
#[test]
fn state_and_cache_dirs_honor_explicit_overrides() {
let _lock = crate::core::data_dir::test_env_lock();
let state = tempfile::tempdir().unwrap();
let cache = tempfile::tempdir().unwrap();
crate::test_env::set_var("LEAN_CTX_STATE_DIR", state.path());
crate::test_env::set_var("LEAN_CTX_CACHE_DIR", cache.path());
let got_state = state_dir().unwrap();
let got_cache = cache_dir().unwrap();
crate::test_env::remove_var("LEAN_CTX_STATE_DIR");
crate::test_env::remove_var("LEAN_CTX_CACHE_DIR");
assert_eq!(got_state, state.path());
assert_eq!(got_cache, cache.path());
}
#[test]
fn data_dir_matches_lean_ctx_data_dir() {
let _guard = crate::core::data_dir::isolated_data_dir();
assert_eq!(
data_dir().unwrap(),
crate::core::data_dir::lean_ctx_data_dir().unwrap()
);
}
#[test]
fn runtime_dir_honors_xdg_runtime_dir() {
let _lock = crate::core::data_dir::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
crate::test_env::set_var("XDG_RUNTIME_DIR", tmp.path());
let got = runtime_dir().unwrap();
crate::test_env::remove_var("XDG_RUNTIME_DIR");
assert_eq!(got, tmp.path().join("lean-ctx"));
}
}