use anyhow::{anyhow, Result};
use clap::CommandFactory;
use clap_complete::{generate, generate_to, shells::Shell};
use std::{env, fs, io, path::PathBuf};
use crate::Cli;
pub fn handle_completions(install: bool, shell: Option<Shell>) -> Result<()> {
let mut cmd = Cli::command();
let bin_name = cmd.get_name().to_string();
let shell = shell.or_else(detect_shell).unwrap_or(Shell::Bash);
if install {
let path = get_completion_install_path(shell)?;
fs::create_dir_all(path.parent().unwrap())?;
generate_to(shell, &mut cmd, bin_name.clone(), path.parent().unwrap())?;
println!(
"✅ Installed completions for {shell:?} at {}",
path.display()
);
print_post_install_message(shell);
} else {
generate(shell, &mut cmd, bin_name, &mut io::stdout());
}
Ok(())
}
fn detect_shell() -> Option<Shell> {
env::var("SHELL").ok().and_then(|path| {
if path.contains("bash") {
Some(Shell::Bash)
} else if path.contains("zsh") {
Some(Shell::Zsh)
} else if path.contains("fish") {
Some(Shell::Fish)
} else if path.contains("elvish") {
Some(Shell::Elvish)
} else if path.contains("powershell") {
Some(Shell::PowerShell)
} else {
None
}
})
}
fn get_completion_install_path(shell: Shell) -> Result<PathBuf> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not detect home directory"))?;
let path = match shell {
Shell::Bash => home.join(".local/share/bash-completion/completions/flk"),
Shell::Zsh => home.join(".zsh/completions/_flk"),
Shell::Fish => home.join(".config/fish/completions/flk.fish"),
Shell::PowerShell => home.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"),
Shell::Elvish => home.join(".config/elvish/lib/completions/flk.elv"),
_ => return Err(anyhow!("Unsupported shell for auto-install")),
};
Ok(path)
}
fn print_post_install_message(shell: Shell) {
match shell {
Shell::Zsh => println!(
"\nℹ️ Make sure your ~/.zshrc contains:\n fpath+=~/.zsh/completions\n autoload -Uz compinit && compinit"
),
Shell::Bash => println!(
"\nℹ️ You may need to reload your shell or run:\n source ~/.bashrc"
),
Shell::Fish => println!(
"\nℹ️ Restart your terminal or run:\n exec fish"
),
_ => (),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn with_shell_env<F: FnOnce()>(value: Option<&str>, f: F) {
let prev = env::var("SHELL").ok();
match value {
Some(v) => unsafe { env::set_var("SHELL", v) },
None => unsafe { env::remove_var("SHELL") },
}
f();
match prev {
Some(v) => unsafe { env::set_var("SHELL", v) },
None => unsafe { env::remove_var("SHELL") },
}
}
#[test]
fn detect_shell_recognises_known_shells() {
with_shell_env(Some("/bin/bash"), || {
assert_eq!(detect_shell(), Some(Shell::Bash));
});
with_shell_env(Some("/usr/bin/zsh"), || {
assert_eq!(detect_shell(), Some(Shell::Zsh));
});
with_shell_env(Some("/opt/homebrew/bin/fish"), || {
assert_eq!(detect_shell(), Some(Shell::Fish));
});
with_shell_env(Some("/usr/local/bin/elvish"), || {
assert_eq!(detect_shell(), Some(Shell::Elvish));
});
with_shell_env(Some("pwsh-or-powershell"), || {
assert_eq!(detect_shell(), Some(Shell::PowerShell));
});
}
#[test]
fn detect_shell_returns_none_for_unknown_or_missing() {
with_shell_env(Some("/bin/tcsh"), || {
assert_eq!(detect_shell(), None);
});
with_shell_env(None, || {
assert_eq!(detect_shell(), None);
});
}
#[test]
fn install_paths_are_shell_specific_and_under_home() {
let home = dirs::home_dir().expect("test environment must have a home dir");
let bash = get_completion_install_path(Shell::Bash).unwrap();
assert!(bash.starts_with(&home));
assert!(bash.ends_with("flk"));
assert!(bash.to_string_lossy().contains("bash-completion"));
let zsh = get_completion_install_path(Shell::Zsh).unwrap();
assert!(
zsh.ends_with("_flk"),
"zsh completions need leading underscore"
);
let fish = get_completion_install_path(Shell::Fish).unwrap();
assert!(fish.extension().is_some_and(|e| e == "fish"));
let elvish = get_completion_install_path(Shell::Elvish).unwrap();
assert!(elvish.extension().is_some_and(|e| e == "elv"));
let pwsh = get_completion_install_path(Shell::PowerShell).unwrap();
assert!(pwsh.to_string_lossy().contains("PowerShell"));
}
#[test]
fn all_currently_known_shells_have_install_paths() {
for shell in [
Shell::Bash,
Shell::Zsh,
Shell::Fish,
Shell::Elvish,
Shell::PowerShell,
] {
assert!(
get_completion_install_path(shell).is_ok(),
"{shell:?} should resolve to an install path"
);
}
}
}