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
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn layout_under_state_dir() {
69        let paths = Paths::new("/tmp/stackless-test");
70        assert_eq!(paths.state_dir(), Path::new("/tmp/stackless-test"));
71        assert_eq!(
72            paths.db_path(),
73            PathBuf::from("/tmp/stackless-test/state.db")
74        );
75        assert_eq!(
76            paths.socket_path(),
77            PathBuf::from("/tmp/stackless-test/daemon.sock")
78        );
79        assert_eq!(
80            paths.daemon_log(),
81            PathBuf::from("/tmp/stackless-test/daemon.log")
82        );
83        assert_eq!(
84            paths.spawn_lock(),
85            PathBuf::from("/tmp/stackless-test/daemon.spawn.lock")
86        );
87        assert_eq!(
88            paths.logs_dir("demo"),
89            PathBuf::from("/tmp/stackless-test/logs/demo")
90        );
91        assert_eq!(
92            paths.persistence_marker(),
93            PathBuf::from("/tmp/stackless-test/daemon.persistence")
94        );
95    }
96
97    #[test]
98    fn from_env_matches_store_wrappers() {
99        let paths = Paths::from_env();
100        assert_eq!(paths.state_dir(), crate::state::Store::state_dir());
101        assert_eq!(paths.db_path(), crate::state::Store::default_path());
102    }
103}