use std::{
fmt, fs,
io::{self, Write},
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shell {
Bash,
Zsh,
Fish,
Pwsh,
Nu,
}
impl Shell {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"bash" => Some(Self::Bash),
"zsh" => Some(Self::Zsh),
"fish" => Some(Self::Fish),
"powershell" | "pwsh" => Some(Self::Pwsh),
"nu" | "nushell" => Some(Self::Nu),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Bash => "bash",
Self::Zsh => "zsh",
Self::Fish => "fish",
Self::Pwsh => "powershell",
Self::Nu => "nushell",
}
}
}
impl fmt::Display for Shell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
pub fn detect_shell() -> Option<Shell> {
if std::env::var_os("NU_VERSION").is_some() {
return Some(Shell::Nu);
}
if cfg!(windows) {
if std::env::var_os("PSModulePath").is_some() {
return Some(Shell::Pwsh);
}
return None;
}
let shell_var = std::env::var("SHELL").ok()?;
let name = Path::new(&shell_var).file_name()?.to_str()?;
Shell::from_str(name)
}
pub(crate) fn nu_config_dir_default() -> Option<PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") {
return Some(PathBuf::from(xdg).join("nushell"));
}
#[cfg(target_os = "macos")]
{
if let Some(home) = std::env::var_os("HOME") {
return Some(
PathBuf::from(home)
.join("Library")
.join("Application Support")
.join("nushell"),
);
}
None
}
#[cfg(windows)]
{
if let Some(appdata) = std::env::var_os("APPDATA") {
return Some(PathBuf::from(appdata).join("nushell"));
}
return None;
}
#[cfg(not(any(target_os = "macos", windows)))]
{
if let Some(home) = std::env::var_os("HOME") {
return Some(PathBuf::from(home).join(".config").join("nushell"));
}
None
}
}
const SENTINEL: &str = "# tfe-shell-init";
pub const SOURCE_DIRECTIVE_PREFIX: &str = "source:";
pub fn snippet(shell: Shell) -> String {
match shell {
Shell::Bash | Shell::Zsh => format!(
"\n{SENTINEL}\n\
tfe() {{\n\
\x20 local line\n\
\x20 while IFS= read -r line; do\n\
\x20 case \"$line\" in\n\
\x20 source:*) source \"${{line#source:}}\" ;;\n\
\x20 ?*) cd \"$line\" ;;\n\
\x20 esac\n\
\x20 done < <(command tfe \"$@\")\n\
}}\n"
),
Shell::Fish => format!(
"\n{SENTINEL}\n\
function tfe\n\
\x20 for line in (command tfe $argv | string split0 | string split \"\\n\")\n\
\x20 if string match -q 'source:*' -- $line\n\
\x20 source (string replace 'source:' '' -- $line)\n\
\x20 else if test -n \"$line\"\n\
\x20 cd $line\n\
\x20 end\n\
\x20 end\n\
end\n"
),
Shell::Pwsh => format!(
"\n{SENTINEL}\n\
function tfe {{\n\
\x20 & (Get-Command tfe -CommandType Application).Source @args | ForEach-Object {{\n\
\x20 if ($_ -like 'source:*') {{ . ($_ -replace '^source:', '') }}\n\
\x20 elseif ($_) {{ Set-Location $_ }}\n\
\x20 }}\n\
}}\n"
),
Shell::Nu => format!(
"\n{SENTINEL}\n\
# tfe wrapper: cd to the directory printed by tfe on exit.\n\
# def --env is required so the cd takes effect in the caller's shell.\n\
def --env --wrapped tfe [...rest] {{\n\
\x20 let dir = (^tfe ...$rest | str trim)\n\
\x20 if ($dir | is-not-empty) {{ cd $dir }}\n\
}}\n"
),
}
}
pub fn rc_path_with(
shell: Shell,
home: Option<&Path>,
xdg_config_home: Option<&Path>,
zdotdir: Option<&Path>,
bash_profile: Option<&Path>,
zshenv: Option<&Path>,
nu_config_dir: Option<&Path>,
) -> Option<PathBuf> {
match shell {
Shell::Bash => {
if let Some(bp) = bash_profile {
if bp.exists() {
return Some(bp.to_path_buf());
}
}
home.map(|h| h.join(".bashrc"))
}
Shell::Zsh => {
if let Some(z) = zdotdir {
return Some(z.join(".zshrc"));
}
if let Some(h) = home {
let zshrc = h.join(".zshrc");
if zshrc.exists() {
return Some(zshrc);
}
if let Some(env) = zshenv {
if env.exists() {
return Some(env.to_path_buf());
}
}
return Some(zshrc);
}
None
}
Shell::Fish => {
let config = xdg_config_home
.map(|p| p.to_path_buf())
.or_else(|| home.map(|h| h.join(".config")))?;
Some(config.join("fish/functions/tfe.fish"))
}
Shell::Pwsh => {
if let Some(profile) = std::env::var_os("PROFILE") {
return Some(PathBuf::from(profile));
}
home.map(|h| {
if cfg!(windows) {
h.join("Documents")
.join("PowerShell")
.join("Microsoft.PowerShell_profile.ps1")
} else {
h.join(".config")
.join("powershell")
.join("Microsoft.PowerShell_profile.ps1")
}
})
}
Shell::Nu => nu_config_dir.map(|d| d.join("config.nu")),
}
}
fn home() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from)
}
fn xdg_config_home() -> Option<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME").map(PathBuf::from)
}
fn zdotdir() -> Option<PathBuf> {
std::env::var_os("ZDOTDIR").map(PathBuf::from)
}
fn bash_profile(home: Option<&Path>) -> Option<PathBuf> {
home.map(|h| h.join(".bash_profile"))
}
fn zshenv(home: Option<&Path>) -> Option<PathBuf> {
home.map(|h| h.join(".zshenv"))
}
fn nu_config_dir() -> Option<PathBuf> {
nu_config_dir_default()
}
pub fn is_installed(path: &Path) -> bool {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
if content.contains(SENTINEL) {
return true;
}
let lines: Vec<&str> = content.lines().collect();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
let window_end = (i + 7).min(lines.len());
if (trimmed == "tfe() {" || trimmed == "function tfe")
&& lines[i..window_end]
.iter()
.any(|l| l.contains("command tfe"))
{
return true;
}
if trimmed.starts_with("def --wrapped tfe")
&& lines[i..window_end].iter().any(|l| l.contains("^tfe"))
{
return true;
}
}
false
}
pub fn install(shell: Shell, rc: &Path) -> io::Result<()> {
if let Some(parent) = rc.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
let mut file = fs::OpenOptions::new().create(true).append(true).open(rc)?;
file.write_all(snippet(shell).as_bytes())?;
file.flush()
}
pub fn emit_source_directive(rc_path: &Path) {
let _ = std::io::stdout()
.write_all(format!("{}{}\n", SOURCE_DIRECTIVE_PREFIX, rc_path.display()).as_bytes());
let _ = std::io::stdout().flush();
}
pub fn auto_install() -> InitOutcome {
let h = home();
auto_install_with(
None,
h.as_deref(),
xdg_config_home().as_deref(),
zdotdir().as_deref(),
bash_profile(h.as_deref()).as_deref(),
zshenv(h.as_deref()).as_deref(),
nu_config_dir().as_deref(),
)
}
pub(crate) fn auto_install_with(
shell: Option<Shell>,
home: Option<&Path>,
xdg_config_home: Option<&Path>,
zdotdir: Option<&Path>,
bash_profile: Option<&Path>,
zshenv: Option<&Path>,
nu_config_dir: Option<&Path>,
) -> InitOutcome {
let shell = match shell.or_else(detect_shell) {
Some(s) => s,
None => return InitOutcome::UnknownShell, };
let candidates: Vec<PathBuf> = match shell {
Shell::Zsh => {
let mut v = Vec::new();
if let Some(z) = zdotdir {
v.push(z.join(".zshrc"));
}
if let Some(h) = home {
v.push(h.join(".zshrc"));
v.push(h.join(".zshenv"));
v.push(h.join(".zprofile"));
}
v
}
Shell::Bash => {
let mut v = Vec::new();
if let Some(h) = home {
v.push(h.join(".bashrc"));
v.push(h.join(".bash_profile"));
v.push(h.join(".profile"));
}
v
}
Shell::Fish => {
let config = xdg_config_home
.map(|p| p.to_path_buf())
.or_else(|| home.map(|h| h.join(".config")));
if let Some(c) = config {
vec![c.join("fish/functions/tfe.fish")]
} else {
vec![]
}
}
Shell::Pwsh => {
if let Some(profile) = std::env::var_os("PROFILE") {
vec![PathBuf::from(profile)]
} else if let Some(h) = home {
vec![h
.join(".config")
.join("powershell")
.join("Microsoft.PowerShell_profile.ps1")]
} else {
vec![]
}
}
Shell::Nu => {
if let Some(d) = nu_config_dir {
vec![d.join("config.nu")]
} else {
vec![]
}
}
};
if candidates.iter().any(|p| is_installed(p)) {
return InitOutcome::AlreadyInstalled(
candidates
.into_iter()
.find(|p| is_installed(p))
.unwrap_or_default(),
);
}
install_or_print_to(
Some(shell),
home,
xdg_config_home,
zdotdir,
bash_profile,
zshenv,
nu_config_dir,
)
}
#[derive(Debug, PartialEq, Eq)]
pub enum InitOutcome {
Installed(PathBuf),
AlreadyInstalled(PathBuf),
PrintedToStdout,
UnknownShell,
}
pub fn install_or_print(shell: Option<Shell>) -> InitOutcome {
let h = home();
install_or_print_to(
shell,
h.as_deref(),
xdg_config_home().as_deref(),
zdotdir().as_deref(),
bash_profile(h.as_deref()).as_deref(),
zshenv(h.as_deref()).as_deref(),
nu_config_dir().as_deref(),
)
}
pub(crate) fn install_or_print_to(
shell: Option<Shell>,
home: Option<&Path>,
xdg_config_home: Option<&Path>,
zdotdir: Option<&Path>,
bash_profile: Option<&Path>,
zshenv: Option<&Path>,
nu_config_dir: Option<&Path>,
) -> InitOutcome {
if cfg!(windows) {
if let Some(s) = shell {
if s != Shell::Pwsh && s != Shell::Nu {
eprintln!(
"tfe: on Windows only PowerShell and Nushell are supported.\n\
Use: tfe --init powershell or tfe --init nushell\n\
For WSL (bash/zsh/fish) run tfe --init <shell> inside WSL."
);
return InitOutcome::UnknownShell;
}
}
}
let resolved = match shell.or_else(detect_shell) {
Some(s) => s,
None => {
eprintln!(
"tfe: could not detect shell from $SHELL. \
Re-run with an explicit shell: tfe --init zsh"
);
print!("{}", snippet(Shell::Bash));
return InitOutcome::UnknownShell;
}
};
let rc = match rc_path_with(
resolved,
home,
xdg_config_home,
zdotdir,
bash_profile,
zshenv,
nu_config_dir,
) {
Some(p) => p,
None => {
eprintln!(
"tfe: could not determine rc file path ($HOME is not set). \
Add the following to your shell config manually:"
);
print!("{}", snippet(resolved));
return InitOutcome::PrintedToStdout;
}
};
if is_installed(&rc) {
return InitOutcome::AlreadyInstalled(rc);
}
match install(resolved, &rc) {
Ok(()) => InitOutcome::Installed(rc),
Err(e) => {
eprintln!(
"tfe: could not write to {}: {e}\n\
Add the following to your shell config manually:",
rc.display()
);
print!("{}", snippet(resolved));
InitOutcome::PrintedToStdout
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
fn auto_install_nu(nu_config_dir: &Path) -> InitOutcome {
auto_install_with(
Some(Shell::Nu),
None,
None,
None,
None,
None,
Some(nu_config_dir),
)
}
#[test]
fn from_str_recognises_bash() {
assert_eq!(Shell::from_str("bash"), Some(Shell::Bash));
}
#[test]
fn from_str_recognises_zsh() {
assert_eq!(Shell::from_str("zsh"), Some(Shell::Zsh));
}
#[test]
fn from_str_recognises_fish() {
assert_eq!(Shell::from_str("fish"), Some(Shell::Fish));
}
#[test]
fn from_str_recognises_powershell() {
assert_eq!(Shell::from_str("powershell"), Some(Shell::Pwsh));
assert_eq!(Shell::from_str("pwsh"), Some(Shell::Pwsh));
}
#[test]
fn from_str_recognises_nushell() {
assert_eq!(Shell::from_str("nu"), Some(Shell::Nu));
assert_eq!(Shell::from_str("nushell"), Some(Shell::Nu));
}
#[test]
fn from_str_is_case_insensitive() {
assert_eq!(Shell::from_str("BASH"), Some(Shell::Bash));
assert_eq!(Shell::from_str("ZSH"), Some(Shell::Zsh));
assert_eq!(Shell::from_str("FISH"), Some(Shell::Fish));
assert_eq!(Shell::from_str("NU"), Some(Shell::Nu));
assert_eq!(Shell::from_str("Bash"), Some(Shell::Bash));
assert_eq!(Shell::from_str("FISH"), Some(Shell::Fish));
assert_eq!(Shell::from_str("PowerShell"), Some(Shell::Pwsh));
assert_eq!(Shell::from_str("PWSH"), Some(Shell::Pwsh));
}
#[test]
fn from_str_returns_none_for_unknown() {
assert_eq!(Shell::from_str("cmd"), None);
assert_eq!(Shell::from_str(""), None);
assert_eq!(Shell::from_str("sh"), None);
}
#[test]
fn display_returns_lowercase_name() {
assert_eq!(Shell::Bash.to_string(), "bash");
assert_eq!(Shell::Zsh.to_string(), "zsh");
assert_eq!(Shell::Fish.to_string(), "fish");
assert_eq!(Shell::Pwsh.to_string(), "powershell");
assert_eq!(Shell::Nu.to_string(), "nushell");
}
#[test]
fn snippet_contains_sentinel() {
for shell in [Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Pwsh] {
assert!(
snippet(shell).contains(SENTINEL),
"{shell} snippet missing sentinel"
);
}
}
#[test]
fn snippet_bash_contains_function_body() {
let s = snippet(Shell::Bash);
assert!(
s.contains("command tfe"),
"bash snippet missing command tfe"
);
assert!(s.contains("cd \"$line\""), "bash snippet missing cd");
assert!(
s.contains("source:"),
"bash snippet missing source: handler"
);
}
#[test]
fn snippet_zsh_identical_to_bash() {
assert_eq!(snippet(Shell::Zsh), snippet(Shell::Bash));
}
#[test]
fn snippet_nushell_contains_def_wrapped() {
let s = snippet(Shell::Nu);
assert!(
s.contains("def --env --wrapped tfe"),
"nushell snippet must use 'def --env --wrapped tfe', got:\n{s}"
);
}
#[test]
fn snippet_nushell_contains_def_env() {
let s = snippet(Shell::Nu);
assert!(
s.contains("--env"),
"nushell snippet must use --env so cd propagates to caller, got:\n{s}"
);
}
#[test]
fn snippet_nushell_uses_caret_tfe() {
let s = snippet(Shell::Nu);
assert!(
s.contains("^tfe"),
"nushell snippet must call the external binary with ^tfe, got:\n{s}"
);
}
#[test]
fn snippet_nushell_uses_cd() {
let s = snippet(Shell::Nu);
assert!(
s.contains("cd"),
"nushell snippet must cd to the dir, got:\n{s}"
);
}
#[test]
fn snippet_nushell_does_not_use_each_closure() {
let s = snippet(Shell::Nu);
assert!(
!s.contains("each {"),
"nushell snippet must not use an each closure for cd, got:\n{s}"
);
}
#[test]
fn snippet_nushell_uses_str_trim() {
let s = snippet(Shell::Nu);
assert!(
s.contains("str trim"),
"nushell snippet must use str trim to strip trailing newline, got:\n{s}"
);
}
#[test]
fn snippet_nushell_differs_from_bash() {
assert_ne!(snippet(Shell::Nu), snippet(Shell::Bash));
}
#[test]
fn snippet_bash_handles_source_directive() {
let s = snippet(Shell::Bash);
assert!(
s.contains("source:"),
"bash snippet must handle source: directives, got:\n{s}"
);
}
#[test]
fn snippet_fish_handles_source_directive() {
let s = snippet(Shell::Fish);
assert!(
s.contains("source:"),
"fish snippet must handle source: directives, got:\n{s}"
);
}
#[test]
fn snippet_pwsh_handles_source_directive() {
let s = snippet(Shell::Pwsh);
assert!(
s.contains("source:"),
"powershell snippet must handle source: directives, got:\n{s}"
);
}
#[test]
fn source_directive_prefix_constant_value() {
assert_eq!(SOURCE_DIRECTIVE_PREFIX, "source:");
}
#[test]
fn emit_source_directive_does_not_panic() {
emit_source_directive(std::path::Path::new("/tmp/test_rc"));
}
#[test]
fn snippet_fish_contains_function_body() {
let s = snippet(Shell::Fish);
assert!(
s.contains("command tfe"),
"fish snippet missing command tfe"
);
assert!(s.contains("cd $line"), "fish snippet missing cd");
assert!(
s.contains("function tfe"),
"fish snippet missing function keyword"
);
assert!(
s.contains("source:"),
"fish snippet missing source: handler"
);
}
#[test]
fn snippet_fish_differs_from_bash() {
assert_ne!(snippet(Shell::Fish), snippet(Shell::Bash));
}
#[test]
fn snippet_powershell_contains_function_body() {
let s = snippet(Shell::Pwsh);
assert!(s.contains("function tfe"), "missing function tfe");
assert!(s.contains("Set-Location"), "missing Set-Location");
assert!(s.contains("Get-Command tfe"), "missing Get-Command tfe");
}
#[test]
fn snippet_powershell_differs_from_bash() {
assert_ne!(snippet(Shell::Pwsh), snippet(Shell::Bash));
}
#[test]
fn rc_path_bash_ends_with_bashrc() {
let p = rc_path_with(
Shell::Bash,
Some(Path::new("/test/home")),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(p, PathBuf::from("/test/home/.bashrc"));
}
#[test]
fn rc_path_zsh_defaults_to_zshrc() {
let p = rc_path_with(
Shell::Zsh,
Some(Path::new("/test/home")),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(p, PathBuf::from("/test/home/.zshrc"));
}
#[test]
fn rc_path_zsh_prefers_zdotdir() {
let p = rc_path_with(
Shell::Zsh,
Some(Path::new("/home/user")),
None,
Some(Path::new("/custom/zdotdir")),
None,
None,
None,
)
.unwrap();
assert_eq!(p, PathBuf::from("/custom/zdotdir/.zshrc"));
}
#[test]
fn rc_path_zsh_falls_back_to_zshenv_when_it_exists() {
let dir = tempdir().unwrap();
let home = dir.path();
let zshenv = home.join(".zshenv");
fs::write(&zshenv, b"# existing zshenv\n").unwrap();
let p = rc_path_with(
Shell::Zsh,
Some(home),
None,
None,
None,
Some(&zshenv),
None,
)
.unwrap();
assert_eq!(p, zshenv, "must write to .zshenv when .zshrc absent");
}
#[test]
fn rc_path_zsh_prefers_zshrc_over_zshenv_when_both_exist() {
let dir = tempdir().unwrap();
let home = dir.path();
let zshrc = home.join(".zshrc");
let zshenv = home.join(".zshenv");
fs::write(&zshrc, b"# existing zshrc\n").unwrap();
fs::write(&zshenv, b"# existing zshenv\n").unwrap();
let p = rc_path_with(
Shell::Zsh,
Some(home),
None,
None,
None,
Some(&zshenv),
None,
)
.unwrap();
assert_eq!(p, zshrc, "must prefer .zshrc over .zshenv when both exist");
}
#[test]
#[cfg(not(windows))]
fn rc_path_powershell_falls_back_to_home_config_on_unix() {
std::env::remove_var("PROFILE");
let p = rc_path_with(
Shell::Pwsh,
Some(Path::new("/test/home")),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(
p,
PathBuf::from("/test/home/.config/powershell/Microsoft.PowerShell_profile.ps1")
);
}
#[test]
fn rc_path_fish_uses_xdg_config_home_when_set() {
let p = rc_path_with(
Shell::Fish,
Some(Path::new("/test/home")),
Some(Path::new("/custom/config")),
None,
None,
None,
None,
)
.unwrap();
assert_eq!(p, PathBuf::from("/custom/config/fish/functions/tfe.fish"));
}
#[test]
fn rc_path_fish_falls_back_to_home_config() {
let p = rc_path_with(
Shell::Fish,
Some(Path::new("/test/home")),
None,
None,
None,
None,
None,
)
.unwrap();
assert_eq!(
p,
PathBuf::from("/test/home/.config/fish/functions/tfe.fish")
);
}
#[test]
fn rc_path_returns_none_when_home_unset() {
std::env::remove_var("PROFILE");
assert!(rc_path_with(Shell::Bash, None, None, None, None, None, None).is_none());
assert!(rc_path_with(Shell::Zsh, None, None, None, None, None, None).is_none());
assert!(rc_path_with(Shell::Fish, None, None, None, None, None, None).is_none());
assert!(rc_path_with(Shell::Pwsh, None, None, None, None, None, None).is_none());
assert!(rc_path_with(Shell::Nu, None, None, None, None, None, None).is_none());
}
#[test]
fn rc_path_nu_uses_nu_config_dir() {
let p = rc_path_with(
Shell::Nu,
None,
None,
None,
None,
None,
Some(Path::new("/test/nushell")),
)
.unwrap();
assert_eq!(p, PathBuf::from("/test/nushell/config.nu"));
}
#[test]
fn rc_path_nu_returns_none_when_nu_config_dir_unset() {
assert!(
rc_path_with(Shell::Nu, None, None, None, None, None, None).is_none(),
"Nu rc_path must be None when nu_config_dir is not provided"
);
}
#[test]
fn is_installed_detects_sentinel_based_wrapper() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
install(Shell::Zsh, &rc).unwrap();
assert!(is_installed(&rc));
}
#[test]
fn is_installed_detects_sentinel_in_nushell_config() {
let dir = tempdir().unwrap();
let rc = dir.path().join("config.nu");
install(Shell::Nu, &rc).unwrap();
assert!(is_installed(&rc));
}
#[test]
fn is_installed_detects_hand_written_bash_wrapper() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
fs::write(
&rc,
"# tfe — cd to the directory browsed when dismissing the file explorer\n\
tfe() {\n\
\x20 local dir\n\
\x20 dir=$(command tfe \"$@\")\n\
\x20 [ -n \"$dir\" ] && cd \"$dir\"\n\
}\n",
)
.unwrap();
assert!(
is_installed(&rc),
"hand-written bash wrapper must be detected"
);
}
#[test]
fn is_installed_detects_hand_written_fish_wrapper() {
let dir = tempdir().unwrap();
let rc = dir.path().join("tfe.fish");
fs::write(
&rc,
"function tfe\n\
\x20 set dir (command tfe $argv)\n\
\x20 if test -n \"$dir\"\n\
\x20 cd $dir\n\
\x20 end\n\
end\n",
)
.unwrap();
assert!(
is_installed(&rc),
"hand-written fish wrapper must be detected"
);
}
#[test]
fn is_installed_returns_false_for_missing_file() {
let dir = tempdir().unwrap();
assert!(!is_installed(&dir.path().join("nonexistent")));
}
#[test]
fn is_installed_returns_false_for_empty_file() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
fs::write(&rc, b"").unwrap();
assert!(!is_installed(&rc));
}
#[test]
fn is_installed_returns_false_when_sentinel_absent() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
fs::write(&rc, b"export PATH=$PATH:/usr/local/bin\n").unwrap();
assert!(!is_installed(&rc));
}
#[test]
fn is_installed_nushell_returns_false_when_sentinel_absent() {
let dir = tempdir().unwrap();
let rc = dir.path().join("config.nu");
fs::write(&rc, b"$env.config.show_banner = false\n").unwrap();
assert!(!is_installed(&rc));
}
#[test]
fn is_installed_returns_true_when_sentinel_present() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
fs::write(&rc, format!("some content\n{SENTINEL}\nmore\n").as_bytes()).unwrap();
assert!(is_installed(&rc));
}
#[test]
fn is_installed_nushell_returns_true_when_sentinel_present() {
let dir = tempdir().unwrap();
let rc = dir.path().join("config.nu");
fs::write(
&rc,
format!("$env.config.show_banner = false\n{SENTINEL}\n").as_bytes(),
)
.unwrap();
assert!(is_installed(&rc));
}
#[test]
fn install_creates_rc_file_when_missing() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
assert!(!rc.exists());
install(Shell::Zsh, &rc).unwrap();
assert!(rc.exists());
}
#[test]
fn install_nushell_creates_config_nu_when_missing() {
let dir = tempdir().unwrap();
let nu_dir = dir.path().join("nushell");
let rc = nu_dir.join("config.nu");
assert!(!rc.exists());
install(Shell::Nu, &rc).unwrap();
assert!(rc.exists());
assert!(is_installed(&rc));
}
#[test]
fn install_nushell_creates_parent_directories() {
let dir = tempdir().unwrap();
let rc = dir
.path()
.join("Library")
.join("Application Support")
.join("nushell")
.join("config.nu");
install(Shell::Nu, &rc).unwrap();
assert!(rc.exists());
assert!(is_installed(&rc));
}
#[test]
fn install_nushell_snippet_passes_is_installed() {
let dir = tempdir().unwrap();
let rc = dir.path().join("config.nu");
install(Shell::Nu, &rc).unwrap();
assert!(
is_installed(&rc),
"installed nushell snippet must pass is_installed"
);
}
#[test]
fn install_nushell_does_not_duplicate_when_called_twice() {
let dir = tempdir().unwrap();
let rc = dir.path().join("config.nu");
install(Shell::Nu, &rc).unwrap();
let before = fs::read_to_string(&rc).unwrap();
assert!(is_installed(&rc), "must be detected after first install");
let count = before.matches(SENTINEL).count();
assert_eq!(
count, 1,
"sentinel should appear exactly once after one install"
);
}
#[test]
fn install_creates_parent_directories() {
let dir = tempdir().unwrap();
let rc = dir.path().join("fish/functions/tfe.fish");
install(Shell::Fish, &rc).unwrap();
assert!(rc.exists());
}
#[test]
fn install_appends_snippet_to_existing_file() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
fs::write(&rc, b"export FOO=bar\n").unwrap();
install(Shell::Zsh, &rc).unwrap();
let content = fs::read_to_string(&rc).unwrap();
assert!(
content.starts_with("export FOO=bar\n"),
"existing content must be preserved"
);
assert!(content.contains(SENTINEL), "snippet must be appended");
}
#[test]
fn install_written_snippet_passes_is_installed() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".bashrc");
install(Shell::Bash, &rc).unwrap();
assert!(is_installed(&rc));
}
#[test]
fn install_does_not_duplicate_when_called_twice() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
install(Shell::Zsh, &rc).unwrap();
install(Shell::Zsh, &rc).unwrap();
let content = fs::read_to_string(&rc).unwrap();
let count = content.matches(SENTINEL).count();
assert_eq!(
count, 2,
"raw install appends each time — guard is is_installed"
);
}
#[test]
fn install_or_print_installs_when_rc_writable() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
let outcome = install_or_print_to(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert_eq!(outcome, InitOutcome::Installed(rc.clone()));
assert!(is_installed(&rc));
}
#[test]
fn install_or_print_returns_already_installed_when_sentinel_present() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
install(Shell::Zsh, &rc).unwrap();
let outcome = install_or_print_to(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert_eq!(outcome, InitOutcome::AlreadyInstalled(rc));
}
#[test]
fn install_or_print_zsh_uses_zdotdir() {
let dir = tempdir().unwrap();
let zdotdir = dir.path().join("zdotdir");
fs::create_dir(&zdotdir).unwrap();
let rc = zdotdir.join(".zshrc");
let outcome = install_or_print_to(
Some(Shell::Zsh),
Some(dir.path()),
None,
Some(&zdotdir),
None,
None,
None,
);
assert_eq!(
outcome,
InitOutcome::Installed(rc.clone()),
"must install into $ZDOTDIR/.zshrc not $HOME/.zshrc"
);
assert!(is_installed(&rc));
}
#[test]
fn install_or_print_zsh_falls_back_to_zshenv_when_zshrc_absent() {
let dir = tempdir().unwrap();
let home = dir.path();
let zshenv = home.join(".zshenv");
fs::write(&zshenv, b"# existing zshenv\n").unwrap();
let outcome = install_or_print_to(
Some(Shell::Zsh),
Some(home),
None,
None,
None,
Some(&zshenv),
None,
);
assert_eq!(
outcome,
InitOutcome::Installed(zshenv.clone()),
"must install into .zshenv when .zshrc is absent"
);
assert!(is_installed(&zshenv));
}
#[test]
fn install_or_print_bash_uses_bash_profile_when_present() {
let dir = tempdir().unwrap();
let bp = dir.path().join(".bash_profile");
fs::write(&bp, b"# existing profile\n").unwrap();
let outcome = install_or_print_to(
Some(Shell::Bash),
Some(dir.path()),
None,
None,
Some(&bp),
None,
None,
);
assert_eq!(
outcome,
InitOutcome::Installed(bp.clone()),
"must install into .bash_profile when it exists"
);
assert!(is_installed(&bp));
}
#[test]
fn install_or_print_returns_printed_when_rc_not_writable() {
let dir = tempdir().unwrap();
let ro_dir = dir.path().join("readonly");
fs::create_dir(&ro_dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&ro_dir).unwrap().permissions();
perms.set_mode(0o444);
fs::set_permissions(&ro_dir, perms).unwrap();
let outcome = install_or_print_to(
Some(Shell::Zsh),
Some(&ro_dir),
None,
None,
None,
None,
None,
);
assert_eq!(
outcome,
InitOutcome::PrintedToStdout,
"read-only dir must fall back to stdout"
);
}
#[cfg(not(unix))]
{
let _ = ro_dir;
}
}
#[test]
fn auto_install_writes_wrapper_on_first_run() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
assert!(!is_installed(&rc));
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert!(
is_installed(&rc),
"auto_install must write wrapper on first run"
);
}
#[test]
fn auto_install_does_not_duplicate_when_already_installed() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
install(Shell::Zsh, &rc).unwrap();
let before = fs::read_to_string(&rc).unwrap();
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
let after = fs::read_to_string(&rc).unwrap();
assert_eq!(
before, after,
"auto_install must not append a duplicate when wrapper already present"
);
}
#[test]
fn auto_install_detects_wrapper_in_zshenv_and_skips_install() {
let dir = tempdir().unwrap();
let zshenv = dir.path().join(".zshenv");
install(Shell::Zsh, &zshenv).unwrap();
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
Some(&zshenv),
None,
);
let zshrc = dir.path().join(".zshrc");
assert!(
!zshrc.exists(),
"auto_install must not create .zshrc when wrapper already in .zshenv"
);
}
#[test]
fn auto_install_detects_wrapper_in_zprofile_and_skips_install() {
let dir = tempdir().unwrap();
let zprofile = dir.path().join(".zprofile");
fs::write(&zprofile, format!("{SENTINEL}\ntfe() {{}}\n")).unwrap();
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
let zshrc = dir.path().join(".zshrc");
if zshrc.exists() {
let content = fs::read_to_string(&zshrc).unwrap();
assert!(
!content.contains(SENTINEL),
"auto_install must not write to .zshrc when sentinel already in .zprofile"
);
}
}
#[test]
fn auto_install_uses_zdotdir_when_set() {
let dir = tempdir().unwrap();
let zdotdir = dir.path().join("zdotdir");
fs::create_dir(&zdotdir).unwrap();
let rc = zdotdir.join(".zshrc");
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
Some(&zdotdir),
None,
None,
None,
);
assert!(
is_installed(&rc),
"auto_install must write to $ZDOTDIR/.zshrc when ZDOTDIR is set"
);
let home_rc = dir.path().join(".zshrc");
assert!(
!home_rc.exists(),
"auto_install must not write to $HOME/.zshrc when ZDOTDIR is set"
);
}
#[test]
fn auto_install_falls_back_to_zshenv_when_zshrc_absent() {
let dir = tempdir().unwrap();
let zshenv = dir.path().join(".zshenv");
fs::write(&zshenv, b"# existing zshenv\n").unwrap();
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
Some(&zshenv),
None,
);
assert!(
is_installed(&zshenv),
"auto_install must install into .zshenv when .zshrc is absent"
);
}
#[test]
fn auto_install_nushell_writes_config_nu_on_first_run() {
let dir = tempdir().unwrap();
let nu_dir = dir.path().join("nushell");
let config_nu = nu_dir.join("config.nu");
assert!(!config_nu.exists());
let outcome = auto_install_nu(&nu_dir);
assert!(
matches!(outcome, InitOutcome::Installed(_)),
"expected Installed, got {outcome:?}"
);
assert!(
is_installed(&config_nu),
"config.nu must contain the wrapper"
);
}
#[test]
fn auto_install_nushell_does_not_duplicate_when_already_installed() {
let dir = tempdir().unwrap();
let nu_dir = dir.path().join("nushell");
let config_nu = nu_dir.join("config.nu");
install(Shell::Nu, &config_nu).unwrap();
let before = fs::read_to_string(&config_nu).unwrap();
auto_install_nu(&nu_dir);
let after = fs::read_to_string(&config_nu).unwrap();
assert_eq!(
before, after,
"auto_install must not duplicate the nushell wrapper"
);
}
#[test]
fn auto_install_nushell_returns_already_installed_when_config_nu_has_sentinel() {
let dir = tempdir().unwrap();
let nu_dir = dir.path().join("nushell");
let config_nu = nu_dir.join("config.nu");
install(Shell::Nu, &config_nu).unwrap();
let outcome = auto_install_nu(&nu_dir);
assert!(
matches!(outcome, InitOutcome::AlreadyInstalled(_)),
"expected AlreadyInstalled, got {outcome:?}"
);
}
#[test]
fn auto_install_nushell_creates_parent_directories_for_config_nu() {
let dir = tempdir().unwrap();
let nu_dir = dir.path().join("deep").join("nested").join("nushell");
let config_nu = nu_dir.join("config.nu");
assert!(!nu_dir.exists());
let outcome = auto_install_nu(&nu_dir);
assert!(
matches!(outcome, InitOutcome::Installed(_)),
"expected Installed, got {outcome:?}"
);
assert!(config_nu.exists(), "config.nu must have been created");
}
#[test]
fn auto_install_does_nothing_when_shell_unrecognised() {
let outcome = auto_install_with(None, None, None, None, None, None, None);
assert!(
matches!(
outcome,
InitOutcome::UnknownShell
| InitOutcome::AlreadyInstalled(_)
| InitOutcome::PrintedToStdout
| InitOutcome::Installed(_)
),
"unexpected outcome: {outcome:?}"
);
}
#[test]
fn auto_install_returns_installed_on_first_run() {
let dir = tempdir().unwrap();
let outcome = auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert!(
matches!(outcome, InitOutcome::Installed(_)),
"expected Installed on first run, got {outcome:?}"
);
}
#[test]
fn auto_install_installed_outcome_carries_rc_path() {
let dir = tempdir().unwrap();
let outcome = auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
if let InitOutcome::Installed(path) = outcome {
assert!(
path.ends_with(".zshrc"),
"installed path should be .zshrc, got {path:?}"
);
} else {
panic!("expected Installed, got {outcome:?}");
}
}
#[test]
fn auto_install_returns_already_installed_on_second_run() {
let dir = tempdir().unwrap();
auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
let outcome = auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert!(
matches!(outcome, InitOutcome::AlreadyInstalled(_)),
"expected AlreadyInstalled on second run, got {outcome:?}"
);
}
#[test]
fn auto_install_already_installed_outcome_carries_rc_path() {
let dir = tempdir().unwrap();
let rc = dir.path().join(".zshrc");
install(Shell::Zsh, &rc).unwrap();
let outcome = auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
None,
None,
);
if let InitOutcome::AlreadyInstalled(path) = outcome {
assert!(
path.ends_with(".zshrc") || is_installed(&path),
"AlreadyInstalled path should point to a file containing the wrapper, got {path:?}"
);
} else {
panic!("expected AlreadyInstalled, got {outcome:?}");
}
}
#[test]
fn auto_install_returns_unknown_shell_when_shell_is_none_and_home_is_none() {
let outcome = auto_install_with(None, None, None, None, None, None, None);
assert!(
matches!(
outcome,
InitOutcome::UnknownShell
| InitOutcome::AlreadyInstalled(_)
| InitOutcome::Installed(_)
| InitOutcome::PrintedToStdout
),
"unexpected outcome variant: {outcome:?}"
);
}
#[test]
fn auto_install_bash_returns_installed_on_first_run() {
let dir = tempdir().unwrap();
let outcome = auto_install_with(
Some(Shell::Bash),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert!(
matches!(outcome, InitOutcome::Installed(_)),
"expected Installed for bash on first run, got {outcome:?}"
);
let rc = dir.path().join(".bashrc");
assert!(is_installed(&rc), ".bashrc should contain the wrapper");
}
#[test]
fn auto_install_fish_returns_installed_on_first_run() {
let dir = tempdir().unwrap();
let outcome = auto_install_with(
Some(Shell::Fish),
Some(dir.path()),
None,
None,
None,
None,
None,
);
assert!(
matches!(outcome, InitOutcome::Installed(_)),
"expected Installed for fish on first run, got {outcome:?}"
);
let rc = dir
.path()
.join(".config")
.join("fish")
.join("functions")
.join("tfe.fish");
assert!(is_installed(&rc), "tfe.fish should contain the wrapper");
}
#[test]
fn auto_install_zsh_already_installed_in_zshenv_returns_already_installed() {
let dir = tempdir().unwrap();
let zshenv = dir.path().join(".zshenv");
install(Shell::Zsh, &zshenv).unwrap();
let outcome = auto_install_with(
Some(Shell::Zsh),
Some(dir.path()),
None,
None,
None,
Some(&zshenv),
None,
);
assert!(
matches!(outcome, InitOutcome::AlreadyInstalled(_)),
"expected AlreadyInstalled when wrapper is in .zshenv, got {outcome:?}"
);
}
}