use etcetera::base_strategy::{BaseStrategy, choose_base_strategy};
use std::path::PathBuf;
use crate::path::home_dir;
pub fn home_dir_required() -> Result<PathBuf, std::io::Error> {
home_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot determine home directory. Set $HOME (Unix) or $USERPROFILE (Windows)",
)
})
}
fn parse_nu_config_output(stdout: &[u8]) -> Option<PathBuf> {
let path_str = std::str::from_utf8(stdout).ok()?;
let path = PathBuf::from(path_str.trim());
(!path.as_os_str().is_empty()).then_some(path)
}
fn query_nu_config_dir() -> Option<PathBuf> {
let output = crate::shell_exec::Cmd::new("nu")
.args(["-c", "echo $nu.default-config-dir"])
.run()
.ok()
.filter(|o| o.status.success())?;
parse_nu_config_output(&output.stdout)
}
fn resolve_nushell_config_dir(home: &std::path::Path, queried: Option<PathBuf>) -> PathBuf {
queried.unwrap_or_else(|| {
choose_base_strategy()
.map(|s| s.config_dir())
.unwrap_or_else(|_| home.join(".config"))
.join("nushell")
})
}
fn nushell_config_dir(home: &std::path::Path) -> PathBuf {
resolve_nushell_config_dir(home, query_nu_config_dir())
}
fn nushell_config_candidates(home: &std::path::Path) -> Vec<PathBuf> {
let mut candidates = vec![];
if let Some(queried) = query_nu_config_dir() {
candidates.push(queried);
}
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
candidates.push(PathBuf::from(xdg_config).join("nushell"));
}
candidates.push(home.join(".config").join("nushell"));
if let Ok(strategy) = choose_base_strategy() {
candidates.push(strategy.config_dir().join("nushell"));
}
let mut seen = std::collections::HashSet::new();
candidates.retain(|p| seen.insert(p.clone()));
candidates
}
pub fn powershell_profile_paths(home: &std::path::Path) -> Vec<PathBuf> {
#[cfg(windows)]
{
let docs = dirs::document_dir().unwrap_or_else(|| home.join("Documents"));
vec![
docs.join("PowerShell")
.join("Microsoft.PowerShell_profile.ps1"),
docs.join("WindowsPowerShell")
.join("Microsoft.PowerShell_profile.ps1"),
]
}
#[cfg(not(windows))]
{
vec![
home.join(".config")
.join("powershell")
.join("Microsoft.PowerShell_profile.ps1"),
]
}
}
pub fn config_paths(shell: super::Shell, cmd: &str) -> Result<Vec<PathBuf>, std::io::Error> {
let home = home_dir_required()?;
Ok(match shell {
super::Shell::Bash => {
vec![home.join(".bashrc")]
}
super::Shell::Zsh => {
let zdotdir = std::env::var("ZDOTDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| home.clone());
vec![zdotdir.join(".zshrc")]
}
super::Shell::Fish => {
vec![
home.join(".config")
.join("fish")
.join("functions")
.join(format!("{}.fish", cmd)),
]
}
super::Shell::Nushell => {
nushell_config_candidates(&home)
.into_iter()
.map(|config_dir| {
config_dir
.join("vendor")
.join("autoload")
.join(format!("{}.nu", cmd))
})
.collect()
}
super::Shell::PowerShell => powershell_profile_paths(&home),
})
}
pub fn legacy_fish_conf_d_path(cmd: &str) -> Result<PathBuf, std::io::Error> {
let home = home_dir_required()?;
Ok(home
.join(".config")
.join("fish")
.join("conf.d")
.join(format!("{}.fish", cmd)))
}
pub fn completion_path(shell: super::Shell, cmd: &str) -> Result<PathBuf, std::io::Error> {
let home = home_dir_required()?;
let strategy = choose_base_strategy().ok();
Ok(match shell {
super::Shell::Bash => {
let data_home = strategy
.as_ref()
.map(|s| s.data_dir())
.unwrap_or_else(|| home.join(".local").join("share"));
data_home
.join("bash-completion")
.join("completions")
.join(cmd)
}
super::Shell::Zsh => home.join(".zfunc").join(format!("_{}", cmd)),
super::Shell::Fish => {
let config_home = strategy
.as_ref()
.map(|s| s.config_dir())
.unwrap_or_else(|| home.join(".config"));
config_home
.join("fish")
.join("completions")
.join(format!("{}.fish", cmd))
}
super::Shell::Nushell => {
let config_dir = nushell_config_dir(&home);
config_dir
.join("vendor")
.join("autoload")
.join(format!("{}.nu", cmd))
}
super::Shell::PowerShell => {
home.join(format!(".{}-powershell-completions", cmd))
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_nu_config_output() {
assert_eq!(
parse_nu_config_output(b"/home/user/.config/nushell\n"),
Some(PathBuf::from("/home/user/.config/nushell"))
);
assert_eq!(
parse_nu_config_output(b" /home/user/.config/nushell \n"),
Some(PathBuf::from("/home/user/.config/nushell"))
);
assert_eq!(parse_nu_config_output(b""), None);
assert_eq!(parse_nu_config_output(b" \n"), None);
assert_eq!(parse_nu_config_output(&[0xFF, 0xFE]), None);
}
#[test]
fn test_nushell_config_candidates_includes_xdg_and_defaults() {
let home = PathBuf::from("/home/user");
let candidates = nushell_config_candidates(&home);
assert!(
candidates
.iter()
.any(|p| p == &home.join(".config").join("nushell")),
"Should include ~/.config/nushell in candidates"
);
if let Ok(strategy) = choose_base_strategy() {
let platform_dir = strategy.config_dir().join("nushell");
assert!(
candidates.iter().any(|p| p == &platform_dir),
"Should include platform config dir {platform_dir:?} in candidates"
);
}
assert!(
candidates.iter().all(|p| p.ends_with("nushell")),
"All candidates should end with 'nushell'"
);
}
#[test]
fn test_nushell_config_candidates_always_has_fallback() {
let home = PathBuf::from("/home/user");
let candidates = nushell_config_candidates(&home);
assert!(
!candidates.is_empty(),
"Should return at least 1 candidate path, got: {candidates:?}"
);
#[cfg(any(target_os = "macos", windows))]
assert!(
candidates.len() >= 2,
"Should have at least 2 candidates on this platform, got: {candidates:?}"
);
}
#[test]
fn test_nushell_config_candidates_no_duplicates() {
let home = PathBuf::from("/home/user");
let candidates = nushell_config_candidates(&home);
let unique: std::collections::HashSet<_> = candidates.iter().collect();
assert_eq!(
candidates.len(),
unique.len(),
"Candidates should not contain duplicates: {candidates:?}"
);
}
#[test]
fn test_resolve_nushell_config_dir_with_queried_path() {
let home = PathBuf::from("/home/user");
let queried = PathBuf::from("/custom/nushell");
assert_eq!(
resolve_nushell_config_dir(&home, Some(queried.clone())),
queried
);
}
#[test]
fn test_resolve_nushell_config_dir_without_queried_path() {
let home = PathBuf::from("/home/user");
let result = resolve_nushell_config_dir(&home, None);
assert!(
result.ends_with("nushell"),
"Fallback should end with 'nushell': {result:?}"
);
}
}