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}