pacsea/theme/
paths.rs

1use std::env;
2use std::path::{Path, PathBuf};
3
4/// What: Locate the active theme configuration file, considering modern and legacy layouts.
5///
6/// Inputs:
7/// - None (reads environment variables to build candidate paths).
8///
9/// Output:
10/// - `Some(PathBuf)` pointing to the first readable theme file; `None` when nothing exists.
11///
12/// Details:
13/// - Prefers `$HOME/.config/pacsea/theme.conf`, then legacy `pacsea.conf`, and repeats for XDG paths.
14pub fn resolve_theme_config_path() -> Option<PathBuf> {
15    let home = env::var("HOME").ok();
16    let xdg_config = env::var("XDG_CONFIG_HOME").ok();
17    let mut candidates: Vec<PathBuf> = Vec::new();
18    if let Some(h) = home.as_deref() {
19        let base = Path::new(h).join(".config").join("pacsea");
20        candidates.push(base.join("theme.conf"));
21        candidates.push(base.join("pacsea.conf")); // legacy
22    }
23    if let Some(xdg) = xdg_config.as_deref() {
24        let x = Path::new(xdg).join("pacsea");
25        candidates.push(x.join("theme.conf"));
26        candidates.push(x.join("pacsea.conf")); // legacy
27    }
28    candidates.into_iter().find(|p| p.is_file())
29}
30
31/// What: Locate the active settings configuration file, prioritizing the split layout.
32///
33/// Inputs:
34/// - None.
35///
36/// Output:
37/// - `Some(PathBuf)` for the resolved settings file; `None` when no candidate exists.
38///
39/// Details:
40/// - Searches `$HOME` and `XDG_CONFIG_HOME` for `settings.conf`, then falls back to `pacsea.conf`.
41pub(super) fn resolve_settings_config_path() -> Option<PathBuf> {
42    let home = env::var("HOME").ok();
43    let xdg_config = env::var("XDG_CONFIG_HOME").ok();
44    let mut candidates: Vec<PathBuf> = Vec::new();
45    if let Some(h) = home.as_deref() {
46        let base = Path::new(h).join(".config").join("pacsea");
47        candidates.push(base.join("settings.conf"));
48        candidates.push(base.join("pacsea.conf")); // legacy
49    }
50    if let Some(xdg) = xdg_config.as_deref() {
51        let x = Path::new(xdg).join("pacsea");
52        candidates.push(x.join("settings.conf"));
53        candidates.push(x.join("pacsea.conf")); // legacy
54    }
55    candidates.into_iter().find(|p| p.is_file())
56}
57
58/// What: Locate the keybindings configuration file for Pacsea.
59///
60/// Inputs:
61/// - None.
62///
63/// Output:
64/// - `Some(PathBuf)` when a keybinds file is present; `None` otherwise.
65///
66/// Details:
67/// - Checks both `$HOME/.config/pacsea/keybinds.conf` and the legacy `pacsea.conf`, mirrored for XDG.
68pub(super) fn resolve_keybinds_config_path() -> Option<PathBuf> {
69    let home = env::var("HOME").ok();
70    let xdg_config = env::var("XDG_CONFIG_HOME").ok();
71    let mut candidates: Vec<PathBuf> = Vec::new();
72    if let Some(h) = home.as_deref() {
73        let base = Path::new(h).join(".config").join("pacsea");
74        candidates.push(base.join("keybinds.conf"));
75        candidates.push(base.join("pacsea.conf")); // legacy
76    }
77    if let Some(xdg) = xdg_config.as_deref() {
78        let x = Path::new(xdg).join("pacsea");
79        candidates.push(x.join("keybinds.conf"));
80        candidates.push(x.join("pacsea.conf")); // legacy
81    }
82    candidates.into_iter().find(|p| p.is_file())
83}
84
85/// What: Resolve an XDG base directory, falling back to `$HOME` with provided segments.
86///
87/// Inputs:
88/// - `var`: Environment variable name, e.g., `XDG_CONFIG_HOME`.
89/// - `home_default`: Path segments appended to `$HOME` when the variable is unset.
90///
91/// Output:
92/// - `PathBuf` pointing to the derived base directory.
93///
94/// Details:
95/// - Treats empty environment values as unset and gracefully handles missing `$HOME`.
96fn xdg_base_dir(var: &str, home_default: &[&str]) -> PathBuf {
97    if let Ok(p) = env::var(var)
98        && !p.trim().is_empty()
99    {
100        return PathBuf::from(p);
101    }
102    let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
103    let mut base = PathBuf::from(home);
104    for seg in home_default {
105        base = base.join(seg);
106    }
107    base
108}
109
110/// What: Build `$HOME/.config/pacsea`, ensuring the directory exists when `$HOME` is set.
111///
112/// Inputs:
113/// - None.
114///
115/// Output:
116/// - `Some(PathBuf)` when the directory is accessible; `None` if `$HOME` is missing or creation fails.
117///
118/// Details:
119/// - Serves as the preferred base for other configuration directories.
120/// - On Windows, also checks `APPDATA` and `USERPROFILE` if `HOME` is not set.
121fn home_config_dir() -> Option<PathBuf> {
122    // Try HOME first (works on Unix and Windows if set)
123    if let Ok(home) = env::var("HOME") {
124        let dir = Path::new(&home).join(".config").join("pacsea");
125        if std::fs::create_dir_all(&dir).is_ok() {
126            return Some(dir);
127        }
128    }
129    // Windows fallback: use APPDATA or USERPROFILE
130    #[cfg(windows)]
131    {
132        if let Ok(appdata) = env::var("APPDATA") {
133            let dir = Path::new(&appdata).join("pacsea");
134            if std::fs::create_dir_all(&dir).is_ok() {
135                return Some(dir);
136            }
137        }
138        if let Ok(userprofile) = env::var("USERPROFILE") {
139            let dir = Path::new(&userprofile).join(".config").join("pacsea");
140            if std::fs::create_dir_all(&dir).is_ok() {
141                return Some(dir);
142            }
143        }
144    }
145    None
146}
147
148/// What: Resolve the Pacsea configuration directory, ensuring it exists on disk.
149///
150/// Inputs:
151/// - None.
152///
153/// Output:
154/// - `PathBuf` pointing to the Pacsea config directory.
155///
156/// Details:
157/// - Prefers `$HOME/.config/pacsea`, falling back to `XDG_CONFIG_HOME/pacsea` when necessary.
158#[must_use]
159pub fn config_dir() -> PathBuf {
160    // Prefer HOME ~/.config/pacsea first
161    if let Some(dir) = home_config_dir() {
162        return dir;
163    }
164    // Fallback: use XDG_CONFIG_HOME (or default to ~/.config) and ensure
165    let base = xdg_base_dir("XDG_CONFIG_HOME", &[".config"]);
166    let dir = base.join("pacsea");
167    let _ = std::fs::create_dir_all(&dir);
168    dir
169}
170
171/// What: Obtain the logs subdirectory inside the Pacsea config folder.
172///
173/// Inputs:
174/// - None.
175///
176/// Output:
177/// - `PathBuf` leading to the `logs` directory (created if missing).
178///
179/// Details:
180/// - Builds upon `config_dir()` and ensures a stable location for log files.
181#[must_use]
182pub fn logs_dir() -> PathBuf {
183    let base = config_dir();
184    let dir = base.join("logs");
185    let _ = std::fs::create_dir_all(&dir);
186    dir
187}
188
189/// What: Obtain the lists subdirectory inside the Pacsea config folder.
190///
191/// Inputs:
192/// - None.
193///
194/// Output:
195/// - `PathBuf` leading to the `lists` directory (created if missing).
196///
197/// Details:
198/// - Builds upon `config_dir()` and ensures storage for exported package lists.
199#[must_use]
200pub fn lists_dir() -> PathBuf {
201    let base = config_dir();
202    let dir = base.join("lists");
203    let _ = std::fs::create_dir_all(&dir);
204    dir
205}
206
207#[cfg(test)]
208mod tests {
209    /// What: Manage temporary HOME override for path resolution tests.
210    ///
211    /// Inputs:
212    /// - `base`: Temporary HOME root directory.
213    ///
214    /// Output:
215    /// - Guard that restores `HOME` and removes temp directory on drop.
216    ///
217    /// Details:
218    /// - Provides panic-safe cleanup for tests mutating process-wide environment.
219    struct HomeTestGuard {
220        orig_home: Option<std::ffi::OsString>,
221        base: std::path::PathBuf,
222    }
223
224    impl HomeTestGuard {
225        /// What: Create a HOME override guard for test isolation.
226        ///
227        /// Inputs:
228        /// - `base`: Temporary path to use as `HOME`.
229        ///
230        /// Output:
231        /// - Initialized `HomeTestGuard`.
232        ///
233        /// Details:
234        /// - Captures original HOME and applies test HOME immediately.
235        fn new(base: std::path::PathBuf) -> Self {
236            let orig_home = std::env::var_os("HOME");
237            let _ = std::fs::create_dir_all(&base);
238            unsafe { std::env::set_var("HOME", base.display().to_string()) };
239            Self { orig_home, base }
240        }
241    }
242
243    impl Drop for HomeTestGuard {
244        fn drop(&mut self) {
245            unsafe {
246                if let Some(v) = self.orig_home.as_ref() {
247                    std::env::set_var("HOME", v);
248                } else {
249                    std::env::remove_var("HOME");
250                }
251            }
252            let _ = std::fs::remove_dir_all(&self.base);
253        }
254    }
255
256    #[test]
257    /// What: Verify path helpers resolve under the Pacsea config directory rooted at `HOME`.
258    ///
259    /// Inputs:
260    /// - Temporary `HOME` directory substituted to capture generated paths.
261    ///
262    /// Output:
263    /// - `config_dir`, `logs_dir`, and `lists_dir` end with `pacsea`, `logs`, and `lists` respectively.
264    ///
265    /// Details:
266    /// - Restores the original `HOME` afterwards to avoid polluting the real configuration tree.
267    fn paths_config_lists_logs_under_home() {
268        let _guard = crate::theme::test_mutex()
269            .lock()
270            .expect("Test mutex poisoned");
271        let base = std::env::temp_dir().join(format!(
272            "pacsea_test_paths_{}_{}",
273            std::process::id(),
274            std::time::SystemTime::now()
275                .duration_since(std::time::UNIX_EPOCH)
276                .expect("System time is before UNIX epoch")
277                .as_nanos()
278        ));
279        let _home_guard = HomeTestGuard::new(base);
280        let cfg = super::config_dir();
281        let logs = super::logs_dir();
282        let lists = super::lists_dir();
283        assert!(cfg.ends_with("pacsea"));
284        assert!(logs.ends_with("logs"));
285        assert!(lists.ends_with("lists"));
286    }
287}