Skip to main content

openlogi_core/
paths.rs

1//! Per-OS application directories, following the XDG Base Directory spec on
2//! **every** platform — including macOS, so configuration lives at the
3//! familiar `~/.config/openlogi/` rather than macOS's
4//! `~/Library/Application Support/`.
5//!
6//! | kind   | env override        | default                       |
7//! |--------|---------------------|-------------------------------|
8//! | config | `$XDG_CONFIG_HOME`  | `~/.config/openlogi`          |
9//! | data   | `$XDG_DATA_HOME`    | `~/.local/share/openlogi`     |
10//!
11//! On Windows `$HOME` falls back to `%USERPROFILE%`, so paths resolve to
12//! `%USERPROFILE%\.config\openlogi` etc.
13//!
14//! **Decision (#347):** the Windows location is final, not best-effort.
15//! XDG-on-every-platform is this module's deliberate design — macOS also
16//! skips its native `~/Library/Application Support` — and Windows follows
17//! the same rule rather than `%APPDATA%`. Recorded before the agent first
18//! shipped in Windows artifacts, because moving it afterwards would strand
19//! every existing user's `config.toml` and the agent's first-run state.
20
21use std::path::PathBuf;
22
23use etcetera::{BaseStrategy, base_strategy::Xdg};
24use thiserror::Error;
25
26/// Subdirectory created under each XDG base directory.
27const APP_DIR: &str = "openlogi";
28
29/// Failure resolving the per-user base directories.
30#[derive(Debug, Error)]
31pub enum PathsError {
32    /// No home directory could be determined for the current user, so none
33    /// of the XDG bases resolve.
34    #[error("could not resolve a home directory for the current user")]
35    HomeNotFound,
36}
37
38fn xdg() -> Result<Xdg, PathsError> {
39    Xdg::new().map_err(|_| PathsError::HomeNotFound)
40}
41
42/// The current user's home directory.
43///
44/// The plain home, not an XDG base — for callers placing files under
45/// OS-native locations (e.g. macOS `~/Library/LaunchAgents`).
46pub fn home_dir() -> Result<PathBuf, PathsError> {
47    Ok(xdg()?.home_dir().to_path_buf())
48}
49
50/// The raw XDG config home directory (without the `openlogi` subdirectory).
51///
52/// Honours an absolute `$XDG_CONFIG_HOME`; falls back to `~/.config`.
53/// Useful when placing files that belong to other apps under the same base
54/// (e.g. systemd user units at `$XDG_CONFIG_HOME/systemd/user/`).
55pub fn xdg_config_home() -> Result<PathBuf, PathsError> {
56    Ok(xdg()?.config_dir())
57}
58
59/// Directory holding the user's `config.toml`.
60///
61/// `$XDG_CONFIG_HOME/openlogi`, default `~/.config/openlogi`.
62pub fn config_dir() -> Result<PathBuf, PathsError> {
63    Ok(xdg_config_home()?.join(APP_DIR))
64}
65
66/// Full path to the user config file.
67pub fn config_path() -> Result<PathBuf, PathsError> {
68    Ok(config_dir()?.join("config.toml"))
69}
70
71/// Directory for downloaded application data; the device-render asset cache
72/// lives under `data_dir()/assets`.
73///
74/// `$XDG_DATA_HOME/openlogi`, default `~/.local/share/openlogi`.
75pub fn data_dir() -> Result<PathBuf, PathsError> {
76    Ok(xdg()?.data_dir().join(APP_DIR))
77}
78
79/// Directory for runtime sockets — the background agent's IPC endpoint.
80pub fn runtime_dir() -> Result<PathBuf, PathsError> {
81    let xdg = xdg()?;
82    Ok(xdg
83        .runtime_dir()
84        .map_or_else(|| xdg.config_dir().join(APP_DIR), |dir| dir.join(APP_DIR)))
85}
86
87/// Path to the background agent's Unix-domain IPC socket: the GUI connects here
88/// to reach the agent that owns device I/O.
89pub fn agent_socket_path() -> Result<PathBuf, PathsError> {
90    Ok(runtime_dir()?.join("agent.sock"))
91}
92
93#[cfg(all(test, unix))]
94#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn config_dir_keeps_openlogi_under_xdg_config_home() {
100        assert!(config_dir().expect("config dir").ends_with("openlogi"));
101    }
102
103    #[test]
104    fn data_dir_keeps_openlogi_under_xdg_data_home() {
105        assert!(data_dir().expect("data dir").ends_with("openlogi"));
106    }
107
108    #[test]
109    fn runtime_dir_keeps_openlogi_suffix() {
110        assert!(runtime_dir().expect("runtime dir").ends_with("openlogi"));
111    }
112}