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}