use std::path::PathBuf;
pub(crate) trait HomeDirResolver: Send + Sync {
fn home_dir(&self) -> Option<PathBuf>;
fn env_var(&self, name: &str) -> Option<String>;
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct SystemHomeDir;
impl HomeDirResolver for SystemHomeDir {
fn home_dir(&self) -> Option<PathBuf> {
dirs::home_dir()
}
fn env_var(&self, name: &str) -> Option<String> {
match std::env::var(name) {
Ok(v) if !v.is_empty() => Some(v),
_ => None,
}
}
}
#[allow(dead_code)]
pub(crate) struct EnvHomeDir<F>
where
F: Fn(&str) -> Option<String> + Send + Sync,
{
lookup: F,
}
impl<F> EnvHomeDir<F>
where
F: Fn(&str) -> Option<String> + Send + Sync,
{
#[allow(dead_code)]
pub(crate) fn new(lookup: F) -> Self {
Self { lookup }
}
}
impl<F> HomeDirResolver for EnvHomeDir<F>
where
F: Fn(&str) -> Option<String> + Send + Sync,
{
fn home_dir(&self) -> Option<PathBuf> {
self.env_var("HOME")
.or_else(|| self.env_var("USERPROFILE"))
.map(PathBuf::from)
}
fn env_var(&self, name: &str) -> Option<String> {
match (self.lookup)(name) {
Some(v) if !v.is_empty() => Some(v),
_ => None,
}
}
}
use crate::domain::shell::Shell;
pub(crate) fn xdg_config_home_with(env: &dyn HomeDirResolver) -> Option<PathBuf> {
if let Some(p) = env.env_var("XDG_CONFIG_HOME") {
return Some(PathBuf::from(p));
}
env.home_dir().map(|h| h.join(".config"))
}
pub(crate) fn rc_file_for(shell: Shell, env: &dyn HomeDirResolver) -> Option<PathBuf> {
let home = env.home_dir()?;
match shell {
Shell::Bash => Some(home.join(".bashrc")),
Shell::Zsh => Some(home.join(".zshrc")),
Shell::Pwsh => {
let base = if cfg!(windows) {
home.join("Documents").join("PowerShell")
} else {
home.join(".config").join("powershell")
};
Some(base.join("Microsoft.PowerShell_profile.ps1"))
}
Shell::Nu => {
let cfg = xdg_config_home_with(env).unwrap_or_else(|| home.join(".config"));
Some(cfg.join("nushell").join("env.nu"))
}
Shell::Clink => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn map_resolver(map: HashMap<&'static str, &'static str>) -> EnvHomeDir<impl Fn(&str) -> Option<String> + Send + Sync> {
let owned: HashMap<String, String> = map
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
EnvHomeDir::new(move |name| owned.get(name).cloned())
}
#[test]
fn env_home_dir_prefers_home_over_userprofile() {
let r = map_resolver(HashMap::from([
("HOME", "/home/test"),
("USERPROFILE", r"C:\Users\test"),
]));
assert_eq!(r.home_dir(), Some(PathBuf::from("/home/test")));
}
#[test]
fn env_home_dir_falls_back_to_userprofile_when_home_unset() {
let r = map_resolver(HashMap::from([
("USERPROFILE", r"C:\Users\test"),
]));
assert_eq!(r.home_dir(), Some(PathBuf::from(r"C:\Users\test")));
}
#[test]
fn env_home_dir_returns_none_when_neither_set() {
let r = map_resolver(HashMap::new());
assert_eq!(r.home_dir(), None);
}
#[test]
fn env_home_dir_treats_empty_string_as_unset() {
let r = map_resolver(HashMap::from([("HOME", ""), ("USERPROFILE", "")]));
assert_eq!(r.home_dir(), None);
assert_eq!(r.env_var("HOME"), None);
}
#[test]
fn env_home_dir_env_var_returns_value_when_set() {
let r = map_resolver(HashMap::from([("XDG_CONFIG_HOME", "/etc/xdg")]));
assert_eq!(r.env_var("XDG_CONFIG_HOME"), Some("/etc/xdg".to_string()));
}
mod xdg_config_home_with_tests {
use super::*;
use std::collections::HashMap;
#[test]
fn honours_env_var() {
let owned: HashMap<String, String> = HashMap::from([
("XDG_CONFIG_HOME".to_string(), "/test/xdg".to_string()),
]);
let env = EnvHomeDir::new(move |n| owned.get(n).cloned());
assert_eq!(xdg_config_home_with(&env), Some(PathBuf::from("/test/xdg")));
}
#[test]
fn falls_back_to_home_when_xdg_unset() {
let owned: HashMap<String, String> = HashMap::from([
("HOME".to_string(), "/test/home".to_string()),
]);
let env = EnvHomeDir::new(move |n| owned.get(n).cloned());
assert_eq!(
xdg_config_home_with(&env),
Some(PathBuf::from("/test/home/.config"))
);
}
#[test]
fn returns_none_when_neither_set() {
let env = EnvHomeDir::new(|_| -> Option<String> { None });
assert_eq!(xdg_config_home_with(&env), None);
}
}
mod rc_file_for_tests {
use super::*;
use std::collections::HashMap;
fn map_env(map: HashMap<&'static str, &'static str>) -> EnvHomeDir<impl Fn(&str) -> Option<String> + Send + Sync> {
let owned: HashMap<String, String> = map
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
EnvHomeDir::new(move |name| owned.get(name).cloned())
}
#[test]
fn bash_uses_home_from_resolver() {
let env = map_env(HashMap::from([("HOME", "/test/home")]));
let p = rc_file_for(Shell::Bash, &env).expect("bash rcfile must resolve");
assert_eq!(p, PathBuf::from("/test/home/.bashrc"));
}
#[test]
fn zsh_uses_home_from_resolver() {
let env = map_env(HashMap::from([("HOME", "/test/home")]));
let p = rc_file_for(Shell::Zsh, &env).expect("zsh rcfile must resolve");
assert_eq!(p, PathBuf::from("/test/home/.zshrc"));
}
#[test]
fn nu_honours_xdg_config_home_from_resolver() {
let env = map_env(HashMap::from([
("HOME", "/test/home"),
("XDG_CONFIG_HOME", "/test/xdg"),
]));
let p = rc_file_for(Shell::Nu, &env).expect("nu rcfile must resolve");
assert_eq!(p, PathBuf::from("/test/xdg/nushell/env.nu"));
}
#[test]
fn nu_falls_back_to_home_config_when_xdg_unset() {
let env = map_env(HashMap::from([("HOME", "/test/home")]));
let p = rc_file_for(Shell::Nu, &env).expect("nu rcfile must resolve");
assert_eq!(p, PathBuf::from("/test/home/.config/nushell/env.nu"));
}
#[test]
fn clink_returns_none_regardless_of_resolver() {
let env = map_env(HashMap::from([("HOME", "/test/home")]));
assert_eq!(rc_file_for(Shell::Clink, &env), None);
}
#[test]
fn returns_none_when_resolver_has_no_home() {
let env = map_env(HashMap::new());
assert_eq!(rc_file_for(Shell::Bash, &env), None);
assert_eq!(rc_file_for(Shell::Zsh, &env), None);
}
#[test]
fn bash_rc_path_ends_with_bashrc_under_system_resolver() {
if let Some(path) = rc_file_for(Shell::Bash, &SystemHomeDir) {
assert!(path.to_str().unwrap().ends_with(".bashrc"));
}
}
}
}