Skip to main content

codex_recall/
config.rs

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}