use std::env;
use std::path::{Path, PathBuf};
pub fn resolve_theme_config_path() -> Option<PathBuf> {
let home = env::var("HOME").ok();
let xdg_config = env::var("XDG_CONFIG_HOME").ok();
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(h) = home.as_deref() {
let base = Path::new(h).join(".config").join("pacsea");
candidates.push(base.join("theme.conf"));
candidates.push(base.join("pacsea.conf")); }
if let Some(xdg) = xdg_config.as_deref() {
let x = Path::new(xdg).join("pacsea");
candidates.push(x.join("theme.conf"));
candidates.push(x.join("pacsea.conf")); }
candidates.into_iter().find(|p| p.is_file())
}
pub(super) fn resolve_settings_config_path() -> Option<PathBuf> {
let home = env::var("HOME").ok();
let xdg_config = env::var("XDG_CONFIG_HOME").ok();
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(h) = home.as_deref() {
let base = Path::new(h).join(".config").join("pacsea");
candidates.push(base.join("settings.conf"));
candidates.push(base.join("pacsea.conf")); }
if let Some(xdg) = xdg_config.as_deref() {
let x = Path::new(xdg).join("pacsea");
candidates.push(x.join("settings.conf"));
candidates.push(x.join("pacsea.conf")); }
candidates.into_iter().find(|p| p.is_file())
}
pub(super) fn resolve_keybinds_config_path() -> Option<PathBuf> {
let home = env::var("HOME").ok();
let xdg_config = env::var("XDG_CONFIG_HOME").ok();
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(h) = home.as_deref() {
let base = Path::new(h).join(".config").join("pacsea");
candidates.push(base.join("keybinds.conf"));
candidates.push(base.join("pacsea.conf")); }
if let Some(xdg) = xdg_config.as_deref() {
let x = Path::new(xdg).join("pacsea");
candidates.push(x.join("keybinds.conf"));
candidates.push(x.join("pacsea.conf")); }
candidates.into_iter().find(|p| p.is_file())
}
#[must_use]
pub fn resolve_repos_config_path() -> Option<PathBuf> {
let home = env::var("HOME").ok();
let xdg_config = env::var("XDG_CONFIG_HOME").ok();
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(h) = home.as_deref() {
let base = Path::new(h).join(".config").join("pacsea");
candidates.push(base.join("repos.conf"));
}
if let Some(xdg) = xdg_config.as_deref() {
let x = Path::new(xdg).join("pacsea");
candidates.push(x.join("repos.conf"));
}
candidates.into_iter().find(|p| p.is_file())
}
fn xdg_base_dir(var: &str, home_default: &[&str]) -> PathBuf {
if let Ok(p) = env::var(var)
&& !p.trim().is_empty()
{
return PathBuf::from(p);
}
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
let mut base = PathBuf::from(home);
for seg in home_default {
base = base.join(seg);
}
base
}
fn home_config_dir() -> Option<PathBuf> {
if let Ok(home) = env::var("HOME") {
let dir = Path::new(&home).join(".config").join("pacsea");
if std::fs::create_dir_all(&dir).is_ok() {
return Some(dir);
}
}
#[cfg(windows)]
{
if let Ok(appdata) = env::var("APPDATA") {
let dir = Path::new(&appdata).join("pacsea");
if std::fs::create_dir_all(&dir).is_ok() {
return Some(dir);
}
}
if let Ok(userprofile) = env::var("USERPROFILE") {
let dir = Path::new(&userprofile).join(".config").join("pacsea");
if std::fs::create_dir_all(&dir).is_ok() {
return Some(dir);
}
}
}
None
}
#[must_use]
pub fn config_dir() -> PathBuf {
if let Some(dir) = home_config_dir() {
return dir;
}
let base = xdg_base_dir("XDG_CONFIG_HOME", &[".config"]);
let dir = base.join("pacsea");
let _ = std::fs::create_dir_all(&dir);
dir
}
#[must_use]
pub fn logs_dir() -> PathBuf {
let base = config_dir();
let dir = base.join("logs");
let _ = std::fs::create_dir_all(&dir);
dir
}
#[must_use]
pub fn lists_dir() -> PathBuf {
let base = config_dir();
let dir = base.join("lists");
let _ = std::fs::create_dir_all(&dir);
dir
}
#[cfg(test)]
mod tests {
struct HomeTestGuard {
orig_home: Option<std::ffi::OsString>,
orig_xdg: Option<std::ffi::OsString>,
base: std::path::PathBuf,
}
impl HomeTestGuard {
fn new(base: std::path::PathBuf) -> Self {
let orig_home = std::env::var_os("HOME");
let orig_xdg = std::env::var_os("XDG_CONFIG_HOME");
let _ = std::fs::create_dir_all(&base);
unsafe {
std::env::set_var("HOME", base.display().to_string());
std::env::remove_var("XDG_CONFIG_HOME");
}
Self {
orig_home,
orig_xdg,
base,
}
}
}
impl Drop for HomeTestGuard {
fn drop(&mut self) {
unsafe {
if let Some(v) = self.orig_home.as_ref() {
std::env::set_var("HOME", v);
} else {
std::env::remove_var("HOME");
}
if let Some(v) = self.orig_xdg.as_ref() {
std::env::set_var("XDG_CONFIG_HOME", v);
} else {
std::env::remove_var("XDG_CONFIG_HOME");
}
}
let _ = std::fs::remove_dir_all(&self.base);
}
}
#[test]
fn paths_config_lists_logs_under_home() {
let _guard = crate::theme::test_mutex()
.lock()
.expect("Test mutex poisoned");
let base = std::env::temp_dir().join(format!(
"pacsea_test_paths_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let _home_guard = HomeTestGuard::new(base);
let cfg = super::config_dir();
let logs = super::logs_dir();
let lists = super::lists_dir();
assert!(cfg.ends_with("pacsea"));
assert!(logs.ends_with("logs"));
assert!(lists.ends_with("lists"));
}
#[test]
fn paths_config_stays_under_temp_home_when_xdg_config_home_was_set() {
let _guard = crate::theme::test_mutex()
.lock()
.expect("Test mutex poisoned");
let base = std::env::temp_dir().join(format!(
"pacsea_test_paths_xdg_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
let home_root = base.clone();
unsafe {
std::env::set_var(
"XDG_CONFIG_HOME",
"/nonexistent/pacsea_test_xdg_decoy_must_not_be_used",
);
}
let _home_guard = HomeTestGuard::new(base);
let cfg = super::config_dir();
assert!(
cfg.starts_with(&home_root),
"config_dir should resolve under test HOME, not decoy XDG_CONFIG_HOME; got {cfg:?}"
);
}
}