Skip to main content

stackless_core/
paths.rs

1//! Per-user state layout under `$XDG_STATE_HOME/stackless` (or the
2//! `~/.local/state/stackless` fallback). Callers that need an injectable
3//! root construct [`Paths`] explicitly; the CLI default is [`Paths::from_env`].
4
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Paths {
9    state_dir: PathBuf,
10}
11
12impl Paths {
13    pub fn new(state_dir: impl Into<PathBuf>) -> Self {
14        Self {
15            state_dir: state_dir.into(),
16        }
17    }
18
19    /// `$XDG_STATE_HOME/stackless`, falling back to `~/.local/state/stackless`.
20    pub fn from_env() -> Self {
21        let base = std::env::var_os("XDG_STATE_HOME")
22            .map(PathBuf::from)
23            .filter(|p| p.is_absolute())
24            .unwrap_or_else(|| {
25                std::env::var_os("HOME")
26                    .map(PathBuf::from)
27                    .unwrap_or_else(|| PathBuf::from("."))
28                    .join(".local/state")
29            });
30        Self::new(base.join("stackless"))
31    }
32
33    pub fn state_dir(&self) -> &Path {
34        &self.state_dir
35    }
36
37    pub fn db_path(&self) -> PathBuf {
38        self.state_dir.join("state.db")
39    }
40
41    pub fn socket_path(&self) -> PathBuf {
42        self.state_dir.join("daemon.sock")
43    }
44
45    pub fn daemon_log(&self) -> PathBuf {
46        self.state_dir.join("daemon.log")
47    }
48
49    pub fn spawn_lock(&self) -> PathBuf {
50        self.state_dir.join("daemon.spawn.lock")
51    }
52
53    pub fn logs_dir(&self, instance: &str) -> PathBuf {
54        self.state_dir.join("logs").join(instance)
55    }
56
57    /// Launchd registration outcome file under the state dir.
58    pub fn persistence_marker(&self) -> PathBuf {
59        self.state_dir.join("daemon.persistence")
60    }
61
62    /// Throttle / outcome ledger for CLI self-update checks.
63    pub fn self_update_ledger(&self) -> PathBuf {
64        self.state_dir.join("self_update.ledger.json")
65    }
66
67    /// Cross-process lock for a single in-flight self-update attempt.
68    pub fn self_update_lock(&self) -> PathBuf {
69        self.state_dir.join("self_update.lock")
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn layout_under_state_dir() {
79        let paths = Paths::new("/tmp/stackless-test");
80        assert_eq!(paths.state_dir(), Path::new("/tmp/stackless-test"));
81        assert_eq!(
82            paths.db_path(),
83            PathBuf::from("/tmp/stackless-test/state.db")
84        );
85        assert_eq!(
86            paths.socket_path(),
87            PathBuf::from("/tmp/stackless-test/daemon.sock")
88        );
89        assert_eq!(
90            paths.daemon_log(),
91            PathBuf::from("/tmp/stackless-test/daemon.log")
92        );
93        assert_eq!(
94            paths.spawn_lock(),
95            PathBuf::from("/tmp/stackless-test/daemon.spawn.lock")
96        );
97        assert_eq!(
98            paths.logs_dir("demo"),
99            PathBuf::from("/tmp/stackless-test/logs/demo")
100        );
101        assert_eq!(
102            paths.persistence_marker(),
103            PathBuf::from("/tmp/stackless-test/daemon.persistence")
104        );
105        assert_eq!(
106            paths.self_update_ledger(),
107            PathBuf::from("/tmp/stackless-test/self_update.ledger.json")
108        );
109        assert_eq!(
110            paths.self_update_lock(),
111            PathBuf::from("/tmp/stackless-test/self_update.lock")
112        );
113    }
114
115    #[test]
116    fn from_env_matches_store_wrappers() {
117        let paths = Paths::from_env();
118        assert_eq!(paths.state_dir(), crate::state::Store::state_dir());
119        assert_eq!(paths.db_path(), crate::state::Store::default_path());
120    }
121}