Skip to main content

par_term/agent_usage/
mod.rs

1//! Agent usage panel: a display surface over a directory of JSON usage
2//! records written by external collectors.
3//!
4//! The file contract is omarchy's (ported verbatim — see
5//! `docs/features/AGENT_USAGE.md`): one `<agent-id>.json`
6//! per agent under the records directory, parsed by [`records`], watched and
7//! snapshotted by [`store`], and rendered by the status-bar widget and the
8//! [`panel`] popup. par-term itself ships no collectors.
9
10pub(crate) mod panel;
11pub(crate) mod records;
12pub(crate) mod store;
13pub(crate) mod update;
14
15use std::path::PathBuf;
16
17/// Resolve the records directory.
18///
19/// `PAR_TERM_AGENT_USAGE_RECORDS_DIR` overrides everything (hermetic ui-test
20/// runs and debugging — the same isolation pattern as `XDG_CONFIG_HOME` for
21/// the config). Otherwise the platform default, mirroring
22/// `Config::state_file_path`'s cfg split: `~/.local/state/par-term/agents/usage`
23/// on Linux/macOS (the design's XDG-state contract) and
24/// `%LOCALAPPDATA%\par-term\agents\usage` on Windows.
25pub(crate) fn default_records_dir() -> PathBuf {
26    if let Some(dir) = std::env::var_os("PAR_TERM_AGENT_USAGE_RECORDS_DIR") {
27        return PathBuf::from(dir);
28    }
29    #[cfg(target_os = "windows")]
30    {
31        dirs::data_local_dir()
32            .map(|d| d.join("par-term").join("agents").join("usage"))
33            .unwrap_or_else(|| PathBuf::from("agents").join("usage"))
34    }
35    #[cfg(not(target_os = "windows"))]
36    {
37        dirs::home_dir()
38            .map(|h| {
39                h.join(".local")
40                    .join("state")
41                    .join("par-term")
42                    .join("agents")
43                    .join("usage")
44            })
45            .unwrap_or_else(|| {
46                PathBuf::from(".local")
47                    .join("state")
48                    .join("par-term")
49                    .join("agents")
50                    .join("usage")
51            })
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::ffi::OsString;
59
60    /// SAFETY: sole writer of this key is the caller, momentarily.
61    unsafe fn restore(key: &str, saved: Option<&OsString>) {
62        match saved {
63            Some(v) => unsafe { std::env::set_var(key, v) },
64            None => unsafe { std::env::remove_var(key) },
65        }
66    }
67
68    /// One test for both scenarios because env mutation is process-global:
69    /// two tests racing on the same variable would flake. Edition 2024 makes
70    /// set_var/remove_var unsafe (they are not thread-safe); the unsafety is
71    /// contained by this being the only writer of the variable, and lib tests
72    /// touching nothing else that reads it.
73    #[test]
74    #[cfg(not(target_os = "windows"))]
75    fn env_override_wins_and_unix_default_follows() {
76        let key = "PAR_TERM_AGENT_USAGE_RECORDS_DIR";
77        let saved = std::env::var_os(key);
78
79        let overridden = {
80            // SAFETY: sole writer of this key for the duration of the test.
81            unsafe { std::env::set_var(key, "/tmp/agent-usage-override-test") };
82            let resolved = default_records_dir();
83            unsafe { restore(key, saved.as_ref()) };
84            resolved
85        };
86        assert_eq!(overridden, PathBuf::from("/tmp/agent-usage-override-test"));
87
88        let defaulted = {
89            // SAFETY: as above.
90            unsafe { std::env::remove_var(key) };
91            let resolved = default_records_dir();
92            unsafe { restore(key, saved.as_ref()) };
93            resolved
94        };
95        let expected = dirs::home_dir()
96            .expect("home resolves in tests")
97            .join(".local")
98            .join("state")
99            .join("par-term")
100            .join("agents")
101            .join("usage");
102        assert_eq!(defaulted, expected);
103    }
104}