use crate::cli::output::Output;
use crate::cli::Cli;
use crate::errors::{CascadeError, Result};
use clap::CommandFactory;
use clap_complete::{generate, Shell};
use std::fs;
use std::io;
use std::path::PathBuf;
pub fn generate_completions(shell: Shell) -> Result<()> {
let mut cmd = Cli::command();
let bin_name = "ca";
generate(shell, &mut cmd, bin_name, &mut io::stdout());
Ok(())
}
pub fn install_completions(shell: Option<Shell>) -> Result<()> {
let shells_to_install = if let Some(shell) = shell {
vec![shell]
} else {
detect_current_and_available_shells()
};
let mut installed = Vec::new();
let mut errors = Vec::new();
for shell in shells_to_install {
match install_completion_for_shell(shell) {
Ok(path) => {
installed.push((shell, path));
}
Err(e) => {
errors.push((shell, e));
}
}
}
if !installed.is_empty() {
Output::success("Shell completions installed:");
for (shell, path) in &installed {
Output::sub_item(format!("{:?}: {}", shell, path.display()));
}
println!();
Output::tip("Next steps:");
for (shell, path) in &installed {
match shell {
Shell::Zsh => {
if path.to_string_lossy().contains(".zsh/completions") {
println!();
Output::warning("⚠️ Zsh requires additional setup:");
Output::bullet("Add this to your ~/.zshrc:");
println!(" fpath=(~/.zsh/completions $fpath)");
println!(" autoload -Uz compinit && compinit");
Output::bullet("Then reload: source ~/.zshrc");
}
}
Shell::Bash => {
if path.to_string_lossy().contains(".bash_completion.d") {
println!();
Output::info("For bash completions to work:");
Output::bullet("Ensure bash-completion is installed");
Output::bullet("Then reload: source ~/.bashrc");
}
}
_ => {}
}
}
println!();
Output::bullet("Try: ca <TAB><TAB>");
}
if !errors.is_empty() {
println!();
Output::warning("Some installations failed:");
for (shell, error) in errors {
Output::sub_item(format!("{shell:?}: {error}"));
}
}
Ok(())
}
fn detect_current_and_available_shells() -> Vec<Shell> {
let mut shells = Vec::new();
if let Some(current_shell) = detect_current_shell() {
shells.push(current_shell);
Output::info(format!("Detected current shell: {current_shell:?}"));
return shells; }
Output::info("Could not detect current shell, checking available shells...");
detect_available_shells()
}
fn detect_current_shell() -> Option<Shell> {
let shell_path = std::env::var("SHELL").ok()?;
let shell_name = std::path::Path::new(&shell_path).file_name()?.to_str()?;
match shell_name {
"bash" => Some(Shell::Bash),
"zsh" => Some(Shell::Zsh),
"fish" => Some(Shell::Fish),
_ => None,
}
}
fn detect_available_shells() -> Vec<Shell> {
let mut shells = Vec::new();
if which_shell("bash").is_some() {
shells.push(Shell::Bash);
}
if which_shell("zsh").is_some() {
shells.push(Shell::Zsh);
}
if which_shell("fish").is_some() {
shells.push(Shell::Fish);
}
if shells.is_empty() {
shells.push(Shell::Bash);
}
shells
}
fn which_shell(shell: &str) -> Option<PathBuf> {
std::env::var("PATH")
.ok()?
.split(crate::utils::platform::path_separator())
.map(PathBuf::from)
.find_map(|path| {
let shell_path = path.join(crate::utils::platform::executable_name(shell));
if crate::utils::platform::is_executable(&shell_path) {
Some(shell_path)
} else {
None
}
})
}
fn install_completion_for_shell(shell: Shell) -> Result<PathBuf> {
let completion_dirs = crate::utils::platform::shell_completion_dirs();
let (completion_dir, filename) = match shell {
Shell::Bash => {
let bash_dirs: Vec<_> = completion_dirs
.iter()
.filter(|(name, _)| name.contains("bash"))
.collect();
let user_dir = bash_dirs
.iter()
.find(|(name, _)| name.contains("user"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()));
let system_dir = if user_dir.is_none() {
bash_dirs
.iter()
.find(|(name, _)| name.contains("system"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
} else {
None
};
let dir = user_dir
.or(system_dir)
.or_else(|| {
dirs::home_dir().map(|h| h.join(".bash_completion.d"))
})
.ok_or_else(|| {
CascadeError::config("Could not find suitable bash completion directory")
})?;
(dir, "ca")
}
Shell::Zsh => {
let zsh_dirs: Vec<_> = completion_dirs
.iter()
.filter(|(name, _)| name.contains("zsh"))
.collect();
let user_dir = zsh_dirs
.iter()
.find(|(name, _)| name.contains("user"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()));
let system_dir = if user_dir.is_none() {
zsh_dirs
.iter()
.find(|(name, _)| name.contains("system"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
} else {
None
};
let dir = user_dir
.or(system_dir)
.or_else(|| {
dirs::home_dir().map(|h| h.join(".zsh/completions"))
})
.ok_or_else(|| {
CascadeError::config("Could not find suitable zsh completion directory")
})?;
(dir, "_ca")
}
Shell::Fish => {
let fish_dirs: Vec<_> = completion_dirs
.iter()
.filter(|(name, _)| name.contains("fish"))
.collect();
let user_dir = fish_dirs
.iter()
.find(|(name, _)| name.contains("user"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()));
let system_dir = if user_dir.is_none() {
fish_dirs
.iter()
.find(|(name, _)| name.contains("system"))
.map(|(_, path)| path.clone())
.filter(|d| d.exists() || d.parent().is_some_and(|p| p.exists()))
} else {
None
};
let dir = user_dir
.or(system_dir)
.or_else(|| {
dirs::home_dir().map(|h| h.join(".config/fish/completions"))
})
.ok_or_else(|| {
CascadeError::config("Could not find suitable fish completion directory")
})?;
(dir, "ca.fish")
}
_ => {
return Err(CascadeError::config(format!(
"Unsupported shell: {shell:?}"
)));
}
};
if !completion_dir.exists() {
fs::create_dir_all(&completion_dir)?;
}
let completion_file =
completion_dir.join(crate::utils::path_validation::sanitize_filename(filename));
crate::utils::path_validation::validate_config_path(&completion_file, &completion_dir)?;
let mut cmd = Cli::command();
let mut content = Vec::new();
generate(shell, &mut cmd, "ca", &mut content);
let custom_completion = generate_custom_completion(shell);
if !custom_completion.is_empty() {
content.extend_from_slice(custom_completion.as_bytes());
}
match crate::utils::atomic_file::write_bytes(&completion_file, &content) {
Ok(()) => {}
Err(e) if e.to_string().contains("Timeout waiting for lock") => {
if completion_dir.to_string_lossy().contains(
&dirs::home_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
) {
std::fs::write(&completion_file, &content)?;
} else {
return Err(e);
}
}
Err(e) => return Err(e),
}
Ok(completion_file)
}
pub fn show_completions_status() -> Result<()> {
Output::section("Shell Completions Status");
let available_shells = detect_available_shells();
Output::section("Available shells");
for shell in &available_shells {
let status = check_completion_installed(*shell);
if status {
Output::success(format!("{shell:?}"));
} else {
Output::error(format!("{shell:?}"));
}
}
let all_installed = available_shells
.iter()
.all(|s| check_completion_installed(*s));
if !all_installed {
println!();
Output::tip("To install completions:");
Output::command_example("ca completions install");
Output::command_example("ca completions install --shell bash # for specific shell");
} else {
println!();
Output::success("All available shells have completions installed!");
if available_shells.contains(&Shell::Zsh) {
println!();
let zshrc_path = dirs::home_dir()
.map(|h| h.join(".zshrc"))
.unwrap_or_else(|| PathBuf::from("~/.zshrc"));
let mut needs_fpath = true;
let mut needs_compinit = true;
let mut using_omz = false;
let mut omz_line = None;
if let Ok(zshrc_content) = std::fs::read_to_string(&zshrc_path) {
if zshrc_content.contains("oh-my-zsh.sh") {
using_omz = true;
for (i, line) in zshrc_content.lines().enumerate() {
if line.contains("source") && line.contains("oh-my-zsh.sh") {
omz_line = Some(i + 1);
break;
}
}
}
if zshrc_content.contains("fpath=(~/.zsh/completions")
|| zshrc_content.contains("fpath=(\"$HOME/.zsh/completions\"")
|| zshrc_content.contains("fpath=($HOME/.zsh/completions")
{
needs_fpath = false;
}
if zshrc_content.contains("compinit") {
needs_compinit = false;
}
}
if needs_fpath || needs_compinit {
Output::warning("Zsh requires additional setup for completions to work");
println!();
if using_omz {
Output::sub_item("Detected Oh-My-Zsh - special setup required:");
println!();
if let Some(line_num) = omz_line {
Output::info(format!("Oh-My-Zsh loads at line {} in ~/.zshrc", line_num));
Output::sub_item("The fpath MUST be set BEFORE Oh-My-Zsh loads");
Output::sub_item(
"Oh-My-Zsh calls compinit internally, so DON'T add compinit yourself",
);
println!();
}
Output::sub_item("Option 1: Manual edit (recommended)");
Output::bullet("Open ~/.zshrc in an editor");
Output::bullet("Find the line: source $ZSH/oh-my-zsh.sh");
Output::bullet("Add this line BEFORE it:");
println!(" fpath=(~/.zsh/completions $fpath)");
Output::bullet("Make sure there's NO 'compinit' line at the end of ~/.zshrc");
Output::bullet("Save, then clear Oh-My-Zsh cache and reload:");
println!(" rm -f ~/.zcompdump && exec zsh");
println!();
Output::sub_item("Option 2: Automatic (requires sed)");
if let Some(line_num) = omz_line {
let insert_line = line_num;
Output::command_example(format!(
"sed -i.bak '{}i\\fpath=(~/.zsh/completions $fpath)' ~/.zshrc",
insert_line
));
Output::command_example("rm -f ~/.zcompdump && exec zsh");
}
} else {
Output::sub_item("Run these commands to complete setup:");
println!();
if needs_fpath {
Output::command_example(
r#"echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc"#,
);
}
if needs_compinit {
Output::command_example(
r#"echo 'autoload -Uz compinit && compinit' >> ~/.zshrc"#,
);
}
Output::command_example("source ~/.zshrc");
}
} else {
Output::success("Zsh is properly configured for completions!");
if using_omz {
println!();
Output::tip("If completions aren't working, clear Oh-My-Zsh cache:");
Output::command_example("rm -f ~/.zcompdump && exec zsh");
}
}
}
}
println!();
Output::section("Manual installation");
Output::command_example("ca completions generate bash > ~/.bash_completion.d/ca");
Output::command_example("ca completions generate zsh > ~/.zsh/completions/_ca");
Output::command_example("ca completions generate fish > ~/.config/fish/completions/ca.fish");
Ok(())
}
fn check_completion_installed(shell: Shell) -> bool {
let home_dir = match dirs::home_dir() {
Some(dir) => dir,
None => return false,
};
let possible_paths = match shell {
Shell::Bash => vec![
home_dir.join(".bash_completion.d/ca"),
PathBuf::from("/usr/local/etc/bash_completion.d/ca"),
PathBuf::from("/etc/bash_completion.d/ca"),
],
Shell::Zsh => vec![
home_dir.join(".oh-my-zsh/completions/_ca"),
home_dir.join(".zsh/completions/_ca"),
PathBuf::from("/usr/local/share/zsh/site-functions/_ca"),
],
Shell::Fish => vec![home_dir.join(".config/fish/completions/ca.fish")],
_ => return false,
};
possible_paths.iter().any(|path| path.exists())
}
fn generate_custom_completion(shell: Shell) -> String {
match shell {
Shell::Bash => {
r#"
# Custom completion for ca switch command
_ca_switch_completion() {
local cur="${COMP_WORDS[COMP_CWORD]}"
local stacks=$(ca completion-helper stack-names 2>/dev/null)
COMPREPLY=($(compgen -W "$stacks" -- "$cur"))
}
# Replace the default completion for 'ca switch' with our custom function
complete -F _ca_switch_completion ca
"#.to_string()
}
Shell::Zsh => {
r#"
# Custom completion for ca switch command
_ca_switch_completion() {
local stacks=($(ca completion-helper stack-names 2>/dev/null))
_describe 'stacks' stacks
}
# Override the switch completion
compdef _ca_switch_completion ca switch
# Explicitly bind the main completion function to 'ca'
# This ensures the completion works even if Oh-My-Zsh or other plugins interfere
compdef _ca ca
"#.to_string()
}
Shell::Fish => {
r#"
# Custom completion for ca switch command
complete -c ca -f -n '__fish_seen_subcommand_from switch' -a '(ca completion-helper stack-names 2>/dev/null)'
"#.to_string()
}
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_shells() {
let shells = detect_available_shells();
assert!(!shells.is_empty());
}
#[test]
fn test_generate_bash_completion() {
let result = generate_completions(Shell::Bash);
assert!(result.is_ok());
}
#[test]
fn test_detect_current_shell() {
std::env::set_var("SHELL", "/bin/zsh");
let shell = detect_current_shell();
assert_eq!(shell, Some(Shell::Zsh));
std::env::set_var("SHELL", "/usr/bin/bash");
let shell = detect_current_shell();
assert_eq!(shell, Some(Shell::Bash));
std::env::set_var("SHELL", "/usr/local/bin/fish");
let shell = detect_current_shell();
assert_eq!(shell, Some(Shell::Fish));
std::env::set_var("SHELL", "/bin/unknown");
let shell = detect_current_shell();
assert_eq!(shell, None);
std::env::remove_var("SHELL");
}
}