1use std::path::PathBuf;
2
3pub fn resolve_home_dir() -> Option<PathBuf> {
7 if let Ok(home) = std::env::var("HOME") {
8 let trimmed = home.trim();
9 if !trimmed.is_empty() {
10 return Some(PathBuf::from(trimmed));
11 }
12 }
13
14 #[cfg(windows)]
15 {
16 if let Ok(profile) = std::env::var("USERPROFILE") {
17 let trimmed = profile.trim();
18 if !trimmed.is_empty() {
19 return Some(PathBuf::from(trimmed));
20 }
21 }
22
23 if let (Ok(drive), Ok(path)) = (std::env::var("HOMEDRIVE"), std::env::var("HOMEPATH")) {
24 if !drive.trim().is_empty() && !path.trim().is_empty() {
25 return Some(PathBuf::from(format!("{}{}", drive.trim(), path.trim())));
26 }
27 }
28 }
29
30 dirs::home_dir()
31}
32
33pub fn resolve_codex_dir() -> Option<PathBuf> {
37 if let Ok(val) = std::env::var("CODEX_HOME") {
38 let trimmed = val.trim();
39 if !trimmed.is_empty() {
40 return Some(PathBuf::from(trimmed));
41 }
42 }
43 resolve_home_dir().map(|h| h.join(".codex"))
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn resolve_codex_dir_respects_env_var() {
52 let _guard = env_lock();
53 std::env::set_var("CODEX_HOME", "/tmp/custom-codex");
54 let result = resolve_codex_dir();
55 assert_eq!(result, Some(PathBuf::from("/tmp/custom-codex")));
56 std::env::remove_var("CODEX_HOME");
57 }
58
59 #[test]
60 fn resolve_codex_dir_ignores_empty_env() {
61 let _guard = env_lock();
62 std::env::set_var("CODEX_HOME", " ");
63 let result = resolve_codex_dir();
64 assert!(result.is_some());
65 assert!(result.unwrap().ends_with(".codex"));
66 std::env::remove_var("CODEX_HOME");
67 }
68
69 #[test]
70 fn resolve_codex_dir_falls_back_to_home() {
71 let _guard = env_lock();
72 std::env::remove_var("CODEX_HOME");
73 let result = resolve_codex_dir();
74 assert!(result.is_some());
75 assert!(result.unwrap().ends_with(".codex"));
76 }
77
78 fn env_lock() -> std::sync::MutexGuard<'static, ()> {
79 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
80 LOCK.lock()
81 .unwrap_or_else(std::sync::PoisonError::into_inner)
82 }
83}