use std::path::PathBuf;
#[cfg(unix)]
fn xdg_bin_home() -> Option<PathBuf> {
std::env::var_os("XDG_BIN_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
}
#[cfg_attr(test, mockall::automock)]
pub trait BaseDirs {
fn home_dir(&self) -> Option<PathBuf>;
fn bin_dir(&self) -> Option<PathBuf>;
fn whitaker_data_dir(&self) -> Option<PathBuf>;
}
#[derive(Debug, Clone)]
pub struct SystemBaseDirs {
user_dirs: directories_next::UserDirs,
project_dirs: directories_next::ProjectDirs,
}
impl SystemBaseDirs {
#[must_use]
pub fn new() -> Option<Self> {
let user_dirs = directories_next::UserDirs::new()?;
let project_dirs = directories_next::ProjectDirs::from("io", "github", "whitaker")?;
Some(Self {
user_dirs,
project_dirs,
})
}
}
impl BaseDirs for SystemBaseDirs {
fn home_dir(&self) -> Option<PathBuf> {
Some(self.user_dirs.home_dir().to_owned())
}
fn bin_dir(&self) -> Option<PathBuf> {
#[cfg(unix)]
if let Some(path) = xdg_bin_home() {
return Some(path);
}
self.home_dir().map(|h| h.join(".local").join("bin"))
}
fn whitaker_data_dir(&self) -> Option<PathBuf> {
Some(self.project_dirs.data_dir().to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::env_test_guard;
#[test]
fn system_base_dirs_returns_some_on_supported_platforms() {
let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs");
assert!(
dirs.home_dir().is_some(),
"expected home_dir to return Some"
);
}
#[test]
fn whitaker_data_dir_contains_whitaker() {
let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs");
let data_dir = dirs.whitaker_data_dir();
assert!(
data_dir.is_some(),
"expected whitaker_data_dir to return Some"
);
assert!(
data_dir
.as_ref()
.is_some_and(|p| p.to_string_lossy().contains("whitaker")),
"expected path to contain 'whitaker'"
);
}
#[test]
fn bin_dir_defaults_to_local_bin() {
let _guard = env_test_guard();
temp_env::with_var_unset("XDG_BIN_HOME", || {
let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs");
let bin_dir = dirs.bin_dir();
assert!(bin_dir.is_some(), "expected bin_dir to return Some");
assert!(
bin_dir
.as_ref()
.is_some_and(|p| p.to_string_lossy().contains(".local")),
"expected path to contain '.local'"
);
});
}
#[cfg(unix)]
#[test]
fn bin_dir_respects_xdg_bin_home() {
let _guard = env_test_guard();
temp_env::with_var("XDG_BIN_HOME", Some("/custom/bin"), || {
let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs");
let bin_dir = dirs.bin_dir().expect("expected bin_dir to return Some");
assert_eq!(bin_dir, PathBuf::from("/custom/bin"));
});
}
#[cfg(unix)]
#[test]
fn bin_dir_ignores_relative_xdg_bin_home() {
let _guard = env_test_guard();
temp_env::with_var("XDG_BIN_HOME", Some("relative/path"), || {
let dirs = SystemBaseDirs::new().expect("failed to create SystemBaseDirs");
let bin_dir = dirs.bin_dir().expect("expected bin_dir to return Some");
assert!(
bin_dir.to_string_lossy().contains(".local"),
"expected path to contain '.local' when XDG_BIN_HOME is relative"
);
});
}
}