1use std::path::{Path, PathBuf};
32
33use super::data_dir::{ensure_dir_permissions, has_data_files, lean_ctx_data_dir};
34
35fn env_path(name: &str) -> Option<PathBuf> {
37 std::env::var(name)
38 .ok()
39 .map(|v| v.trim().to_string())
40 .filter(|v| !v.is_empty())
41 .map(PathBuf::from)
42}
43
44fn xdg_base(env_name: &str, home_fallback: &str) -> Result<PathBuf, String> {
48 if let Some(p) = env_path(env_name) {
49 return Ok(p);
50 }
51 dirs::home_dir()
52 .map(|h| h.join(home_fallback))
53 .ok_or_else(|| "Cannot determine home directory".to_string())
54}
55
56fn resolve(
59 category_override: Option<PathBuf>,
60 single: Option<PathBuf>,
61 xdg_base_dir: &Path,
62) -> PathBuf {
63 category_override
64 .or(single)
65 .unwrap_or_else(|| xdg_base_dir.join("lean-ctx"))
66}
67
68pub(crate) fn single_dir_override() -> Option<PathBuf> {
75 if let Some(p) = env_path("LEAN_CTX_DATA_DIR") {
76 return Some(p);
77 }
78 let home = dirs::home_dir()?;
79 let xdg_config_base = xdg_base("XDG_CONFIG_HOME", ".config").ok()?;
80 single_dir_override_fs(&home, &xdg_config_base)
81}
82
83fn single_dir_override_fs(home: &Path, xdg_config_base: &Path) -> Option<PathBuf> {
85 let legacy = home.join(".lean-ctx");
86 if legacy.exists() && has_data_files(&legacy) {
87 return Some(legacy);
88 }
89 let mixed = xdg_config_base.join("lean-ctx");
90 if mixed.exists() && has_data_files(&mixed) {
91 return Some(mixed);
92 }
93 None
94}
95
96#[cfg_attr(test, allow(clippy::unnecessary_wraps))]
100fn category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
101 let category_override = env_path(cat_env);
102
103 #[cfg(test)]
106 {
107 if let Some(p) = category_override {
108 ensure_dir_permissions(&p);
109 return Ok(p);
110 }
111 let _ = (xdg_env, home_fallback);
115 Ok(super::data_dir::test_sandbox_dir())
116 }
117 #[cfg(not(test))]
118 {
119 let base = xdg_base(xdg_env, home_fallback)?;
120 let dir = resolve(category_override, single_dir_override(), &base);
121 ensure_dir_permissions(&dir);
122 Ok(dir)
123 }
124}
125
126pub fn config_dir() -> Result<PathBuf, String> {
129 category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
130}
131
132pub fn data_dir() -> Result<PathBuf, String> {
139 lean_ctx_data_dir()
140}
141
142pub fn state_dir() -> Result<PathBuf, String> {
145 category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
146}
147
148pub fn cache_dir() -> Result<PathBuf, String> {
151 category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
152}
153
154pub fn runtime_dir() -> Result<PathBuf, String> {
160 if let Some(base) = env_path("XDG_RUNTIME_DIR") {
161 return Ok(base.join("lean-ctx"));
162 }
163 state_dir()
164}
165
166fn raw_category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
174 if let Some(p) = env_path(cat_env) {
175 return Ok(p);
176 }
177 Ok(xdg_base(xdg_env, home_fallback)?.join("lean-ctx"))
178}
179
180pub(crate) fn config_split_target() -> Result<PathBuf, String> {
182 raw_category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
183}
184
185pub(crate) fn data_split_target() -> Result<PathBuf, String> {
187 raw_category_dir("LEAN_CTX_DATA_DIR", "XDG_DATA_HOME", ".local/share")
188}
189
190pub(crate) fn state_split_target() -> Result<PathBuf, String> {
192 raw_category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
193}
194
195pub(crate) fn cache_split_target() -> Result<PathBuf, String> {
197 raw_category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn resolve_prefers_override_then_single_then_xdg() {
206 let over = PathBuf::from("/over/ride");
207 let single = PathBuf::from("/single/dir");
208 let base = PathBuf::from("/xdg/base");
209
210 assert_eq!(
211 resolve(Some(over.clone()), Some(single.clone()), &base),
212 over
213 );
214 assert_eq!(resolve(None, Some(single.clone()), &base), single);
215 assert_eq!(
216 resolve(None, None, &base),
217 PathBuf::from("/xdg/base/lean-ctx")
218 );
219 }
220
221 #[test]
222 fn single_dir_fs_detects_legacy_with_data() {
223 let home = tempfile::tempdir().unwrap();
224 let xdg = tempfile::tempdir().unwrap();
225 let legacy = home.path().join(".lean-ctx");
226 std::fs::create_dir_all(&legacy).unwrap();
227 std::fs::write(legacy.join("stats.json"), "{}").unwrap();
228
229 assert_eq!(
230 single_dir_override_fs(home.path(), xdg.path()),
231 Some(legacy)
232 );
233 }
234
235 #[test]
236 fn single_dir_fs_detects_mixed_with_data() {
237 let home = tempfile::tempdir().unwrap();
238 let xdg = tempfile::tempdir().unwrap();
239 let mixed = xdg.path().join("lean-ctx");
240 std::fs::create_dir_all(&mixed).unwrap();
241 std::fs::write(mixed.join("stats.json"), "{}").unwrap();
244
245 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), Some(mixed));
246 }
247
248 #[test]
249 fn single_dir_fs_ignores_config_only_dir() {
250 let home = tempfile::tempdir().unwrap();
253 let xdg = tempfile::tempdir().unwrap();
254 let mixed = xdg.path().join("lean-ctx");
255 std::fs::create_dir_all(&mixed).unwrap();
256 std::fs::write(mixed.join("config.toml"), "").unwrap();
257 std::fs::write(mixed.join("shell-hook.zsh"), "").unwrap();
258
259 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
260 }
261
262 #[test]
263 fn single_dir_fs_prefers_legacy_over_mixed() {
264 let home = tempfile::tempdir().unwrap();
265 let xdg = tempfile::tempdir().unwrap();
266 let legacy = home.path().join(".lean-ctx");
267 std::fs::create_dir_all(&legacy).unwrap();
268 std::fs::write(legacy.join("sessions"), "x").unwrap();
269 let mixed = xdg.path().join("lean-ctx");
270 std::fs::create_dir_all(&mixed).unwrap();
271 std::fs::write(mixed.join("stats.json"), "{}").unwrap();
272
273 assert_eq!(
274 single_dir_override_fs(home.path(), xdg.path()),
275 Some(legacy)
276 );
277 }
278
279 #[test]
280 fn single_dir_fs_ignores_empty_dirs() {
281 let home = tempfile::tempdir().unwrap();
282 let xdg = tempfile::tempdir().unwrap();
283 std::fs::create_dir_all(home.path().join(".lean-ctx")).unwrap();
284 std::fs::create_dir_all(xdg.path().join("lean-ctx")).unwrap();
285
286 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
287 }
288
289 #[test]
290 fn single_dir_fs_ignores_non_marker_files() {
291 let home = tempfile::tempdir().unwrap();
292 let xdg = tempfile::tempdir().unwrap();
293 let mixed = xdg.path().join("lean-ctx");
294 std::fs::create_dir_all(&mixed).unwrap();
295 std::fs::write(mixed.join("random.txt"), "x").unwrap();
296
297 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
298 }
299
300 #[test]
301 fn xdg_base_honors_env_then_home_fallback() {
302 let _lock = crate::core::data_dir::test_env_lock();
303 let tmp = tempfile::tempdir().unwrap();
304 std::env::set_var("XDG_CONFIG_HOME", tmp.path());
305 let from_env = xdg_base("XDG_CONFIG_HOME", ".config").unwrap();
306 std::env::remove_var("XDG_CONFIG_HOME");
307 assert_eq!(from_env, tmp.path());
308
309 let fallback = xdg_base("LEAN_CTX_NONEXISTENT_XDG_VAR", ".cache").unwrap();
311 assert!(fallback.ends_with(".cache"), "got: {}", fallback.display());
312 }
313
314 #[test]
315 fn single_dir_override_honors_data_dir_env() {
316 let _lock = crate::core::data_dir::test_env_lock();
317 let tmp = tempfile::tempdir().unwrap();
318 std::env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
319 let got = single_dir_override();
320 std::env::remove_var("LEAN_CTX_DATA_DIR");
321 assert_eq!(got, Some(tmp.path().to_path_buf()));
322 }
323
324 #[test]
325 fn config_dir_honors_explicit_override() {
326 let _lock = crate::core::data_dir::test_env_lock();
327 let tmp = tempfile::tempdir().unwrap();
328 std::env::set_var("LEAN_CTX_CONFIG_DIR", tmp.path());
329 let got = config_dir().unwrap();
330 std::env::remove_var("LEAN_CTX_CONFIG_DIR");
331 assert_eq!(got, tmp.path());
332 }
333
334 #[test]
335 fn state_and_cache_dirs_honor_explicit_overrides() {
336 let _lock = crate::core::data_dir::test_env_lock();
337 let state = tempfile::tempdir().unwrap();
338 let cache = tempfile::tempdir().unwrap();
339 std::env::set_var("LEAN_CTX_STATE_DIR", state.path());
340 std::env::set_var("LEAN_CTX_CACHE_DIR", cache.path());
341 let got_state = state_dir().unwrap();
342 let got_cache = cache_dir().unwrap();
343 std::env::remove_var("LEAN_CTX_STATE_DIR");
344 std::env::remove_var("LEAN_CTX_CACHE_DIR");
345 assert_eq!(got_state, state.path());
346 assert_eq!(got_cache, cache.path());
347 }
348
349 #[test]
350 fn data_dir_matches_lean_ctx_data_dir() {
351 let _guard = crate::core::data_dir::isolated_data_dir();
352 assert_eq!(
353 data_dir().unwrap(),
354 crate::core::data_dir::lean_ctx_data_dir().unwrap()
355 );
356 }
357
358 #[test]
359 fn runtime_dir_honors_xdg_runtime_dir() {
360 let _lock = crate::core::data_dir::test_env_lock();
361 let tmp = tempfile::tempdir().unwrap();
362 std::env::set_var("XDG_RUNTIME_DIR", tmp.path());
363 let got = runtime_dir().unwrap();
364 std::env::remove_var("XDG_RUNTIME_DIR");
365 assert_eq!(got, tmp.path().join("lean-ctx"));
366 }
367}