1use anyhow::{anyhow, Result};
2use std::path::PathBuf;
3
4pub const DEFAULT_LAUNCH_AGENT_LABEL: &str = "dev.codex-recall.watch";
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Config {
8 pub index_path: PathBuf,
9 pub source_roots: Vec<PathBuf>,
10}
11
12pub fn default_db_path() -> Result<PathBuf> {
13 if let Ok(path) = std::env::var("CODEX_RECALL_DB") {
14 return Ok(PathBuf::from(path));
15 }
16
17 Ok(data_home()?.join("codex-recall").join("index.sqlite"))
18}
19
20pub fn default_state_path() -> Result<PathBuf> {
21 if let Ok(path) = std::env::var("CODEX_RECALL_STATE") {
22 return Ok(PathBuf::from(path));
23 }
24
25 Ok(state_home()?.join("codex-recall").join("watch.json"))
26}
27
28pub fn default_pins_path() -> Result<PathBuf> {
29 if let Ok(path) = std::env::var("CODEX_RECALL_PINS") {
30 return Ok(PathBuf::from(path));
31 }
32
33 Ok(data_home()?.join("codex-recall").join("pins.json"))
34}
35
36pub fn default_source_roots() -> Result<Vec<PathBuf>> {
37 let codex_home = codex_home()?;
38 Ok(vec![
39 codex_home.join("sessions"),
40 codex_home.join("archived_sessions"),
41 ])
42}
43
44fn home_dir() -> Result<PathBuf> {
45 std::env::var_os("HOME")
46 .map(PathBuf::from)
47 .ok_or_else(|| anyhow!("HOME is not set"))
48}
49
50fn data_home() -> Result<PathBuf> {
51 Ok(env_path("XDG_DATA_HOME").unwrap_or(home_dir()?.join(".local").join("share")))
52}
53
54fn state_home() -> Result<PathBuf> {
55 Ok(env_path("XDG_STATE_HOME").unwrap_or(home_dir()?.join(".local").join("state")))
56}
57
58fn codex_home() -> Result<PathBuf> {
59 Ok(env_path("CODEX_HOME").unwrap_or(home_dir()?.join(".codex")))
60}
61
62fn env_path(name: &str) -> Option<PathBuf> {
63 let value = std::env::var_os(name)?;
64 if value.is_empty() {
65 None
66 } else {
67 Some(PathBuf::from(value))
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::ffi::OsString;
75 use std::sync::{LazyLock, Mutex};
76
77 static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
78
79 struct EnvGuard {
80 values: Vec<(&'static str, Option<OsString>)>,
81 }
82
83 impl EnvGuard {
84 fn set(pairs: &[(&'static str, Option<&str>)]) -> Self {
85 let values = pairs
86 .iter()
87 .map(|(key, _)| (*key, std::env::var_os(key)))
88 .collect::<Vec<_>>();
89 for (key, value) in pairs {
90 match value {
91 Some(value) => std::env::set_var(key, value),
92 None => std::env::remove_var(key),
93 }
94 }
95 Self { values }
96 }
97 }
98
99 impl Drop for EnvGuard {
100 fn drop(&mut self) {
101 for (key, value) in &self.values {
102 match value {
103 Some(value) => std::env::set_var(key, value),
104 None => std::env::remove_var(key),
105 }
106 }
107 }
108 }
109
110 #[test]
111 fn data_and_state_paths_honor_xdg_locations() {
112 let _lock = ENV_LOCK.lock().unwrap();
113 let _guard = EnvGuard::set(&[
114 ("CODEX_RECALL_DB", None),
115 ("CODEX_RECALL_STATE", None),
116 ("CODEX_RECALL_PINS", None),
117 ("XDG_DATA_HOME", Some("/tmp/xdg-data")),
118 ("XDG_STATE_HOME", Some("/tmp/xdg-state")),
119 ("HOME", Some("/tmp/home")),
120 ]);
121
122 assert_eq!(
123 default_db_path().unwrap(),
124 PathBuf::from("/tmp/xdg-data/codex-recall/index.sqlite")
125 );
126 assert_eq!(
127 default_state_path().unwrap(),
128 PathBuf::from("/tmp/xdg-state/codex-recall/watch.json")
129 );
130 assert_eq!(
131 default_pins_path().unwrap(),
132 PathBuf::from("/tmp/xdg-data/codex-recall/pins.json")
133 );
134 }
135
136 #[test]
137 fn source_roots_honor_codex_home_when_set() {
138 let _lock = ENV_LOCK.lock().unwrap();
139 let _guard = EnvGuard::set(&[
140 ("CODEX_HOME", Some("/tmp/codex-home")),
141 ("HOME", Some("/tmp/home")),
142 ]);
143
144 assert_eq!(
145 default_source_roots().unwrap(),
146 vec![
147 PathBuf::from("/tmp/codex-home/sessions"),
148 PathBuf::from("/tmp/codex-home/archived_sessions"),
149 ]
150 );
151 }
152}