use clap::{Command, CommandFactory};
use clap_complete::{Generator, generate};
use std::path::PathBuf;
pub fn print_completions<G: Generator>(generator: G, app: &mut Command) {
generate(
generator,
app,
app.get_name().to_string(),
&mut std::io::stdout(),
);
}
pub fn install_completion(shell: clap_complete::Shell) -> Result<String, String> {
use std::fs;
let completion_dir = get_completion_directory(shell)?;
let filename = get_completion_filename(shell);
let completion_path = completion_dir.join(filename);
let completion_script = generate_completion_script(shell)?;
fs::write(&completion_path, completion_script).map_err(|e| {
format!(
"Failed to write completion file to {}: {}",
completion_path.display(),
e
)
})?;
let instructions = get_shell_setup_instructions(shell, &completion_path);
Ok(format!(
"✅ Shell completion installed successfully!\n\n{instructions}"
))
}
fn get_completion_directory(shell: clap_complete::Shell) -> Result<PathBuf, String> {
use std::fs;
let home =
std::env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?;
match shell {
clap_complete::Shell::Bash => {
let dirs = vec![
format!("{}/.local/share/bash-completion/completions", home),
format!("{}/.bash_completion.d", home),
];
for dir in dirs {
let path = PathBuf::from(&dir);
if path.parent().is_some_and(|p| p.exists()) {
if !path.exists() {
fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory {dir}: {e}"))?;
}
return Ok(path);
}
}
let fallback = PathBuf::from(format!("{home}/.local/share/bash-completion/completions"));
fs::create_dir_all(&fallback)
.map_err(|e| format!("Failed to create completion directory: {e}"))?;
Ok(fallback)
}
clap_complete::Shell::Zsh => {
let dirs = vec![
format!("{}/.local/share/zsh/site-functions", home),
format!("{}/.zsh/completions", home),
];
for dir in dirs {
let path = PathBuf::from(&dir);
if path.parent().is_some_and(|p| p.exists()) {
if !path.exists() {
fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory {dir}: {e}"))?;
}
return Ok(path);
}
}
let fallback = PathBuf::from(format!("{home}/.local/share/zsh/site-functions"));
fs::create_dir_all(&fallback)
.map_err(|e| format!("Failed to create completion directory: {e}"))?;
Ok(fallback)
}
clap_complete::Shell::Fish => {
let dir = format!("{home}/.config/fish/completions");
let path = PathBuf::from(&dir);
fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create fish completions directory: {e}"))?;
Ok(path)
}
clap_complete::Shell::PowerShell => Err(
"PowerShell completion installation not supported. Use 'urlsup completion-generate powershell' and add to your profile manually.".to_string(),
),
clap_complete::Shell::Elvish => Err(
"Elvish completion installation not supported. Use 'urlsup completion-generate elvish' and add to rc.elv manually.".to_string(),
),
_ => Err(format!("Unsupported shell: {shell:?}")),
}
}
fn get_completion_filename(shell: clap_complete::Shell) -> &'static str {
match shell {
clap_complete::Shell::Bash => "urlsup",
clap_complete::Shell::Zsh => "_urlsup",
clap_complete::Shell::Fish => "urlsup.fish",
_ => "urlsup",
}
}
fn generate_completion_script(shell: clap_complete::Shell) -> Result<String, String> {
use std::io::Cursor;
let mut cmd = crate::ui::cli::Cli::command();
let mut buf = Cursor::new(Vec::new());
match shell {
clap_complete::Shell::Bash => {
generate(clap_complete::shells::Bash, &mut cmd, "urlsup", &mut buf);
}
clap_complete::Shell::Zsh => {
generate(clap_complete::shells::Zsh, &mut cmd, "urlsup", &mut buf);
}
clap_complete::Shell::Fish => {
generate(clap_complete::shells::Fish, &mut cmd, "urlsup", &mut buf);
}
clap_complete::Shell::PowerShell => {
generate(
clap_complete::shells::PowerShell,
&mut cmd,
"urlsup",
&mut buf,
);
}
clap_complete::Shell::Elvish => {
generate(clap_complete::shells::Elvish, &mut cmd, "urlsup", &mut buf);
}
_ => {
return Err(format!("Completion generation not supported for {shell:?}"));
}
}
String::from_utf8(buf.into_inner())
.map_err(|e| format!("Failed to generate completion script: {e}"))
}
fn get_shell_setup_instructions(
shell: clap_complete::Shell,
completion_path: &std::path::Path,
) -> String {
match shell {
clap_complete::Shell::Bash => {
format!(
"Completion installed to: {}\n\n\
To enable bash completions, add this to your ~/.bashrc or ~/.bash_profile:\n\
if [[ -d ~/.local/share/bash-completion/completions ]]; then\n\
for completion in ~/.local/share/bash-completion/completions/*; do\n\
[[ -r \"$completion\" ]] && source \"$completion\"\n\
done\n\
fi\n\n\
Then restart your shell or run: source ~/.bashrc",
completion_path.display()
)
}
clap_complete::Shell::Zsh => {
format!(
"Completion installed to: {}\n\n\
To enable zsh completions, add this to your ~/.zshrc:\n\
if [[ -d ~/.local/share/zsh/site-functions ]]; then\n\
fpath=(~/.local/share/zsh/site-functions $fpath)\n\
autoload -U compinit && compinit\n\
fi\n\n\
Then restart your shell or run: source ~/.zshrc\n\
You may also need to clear the completion cache: rm -f ~/.zcompdump*",
completion_path.display()
)
}
clap_complete::Shell::Fish => {
format!(
"Completion installed to: {}\n\n\
Fish completions are automatically loaded from ~/.config/fish/completions/\n\
Restart your shell or run: fish -c 'complete --erase; source ~/.config/fish/config.fish'",
completion_path.display()
)
}
clap_complete::Shell::PowerShell => {
"Completion generated. To install PowerShell completions:\n\
1. Run: urlsup completion-generate powershell >> $PROFILE\n\
2. Restart PowerShell or run: . $PROFILE"
.to_string()
}
clap_complete::Shell::Elvish => "Completion generated. To install Elvish completions:\n\
1. Run: urlsup completion-generate elvish >> ~/.elvish/rc.elv\n\
2. Restart Elvish or run: source ~/.elvish/rc.elv"
.to_string(),
_ => format!("Completion installed to: {}", completion_path.display()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use tempfile::TempDir;
#[allow(dead_code)] struct TestWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl TestWriter {
#[allow(dead_code)] fn new() -> Self {
Self {
buffer: Arc::new(Mutex::new(Vec::new())),
}
}
#[allow(dead_code)] fn get_content(&self) -> String {
let buffer = self.buffer.lock().unwrap();
String::from_utf8(buffer.clone()).unwrap_or_default()
}
}
impl Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn test_print_completions_bash() {
let mut cmd = crate::ui::cli::Cli::command();
let mut buf = Vec::new();
clap_complete::generate(clap_complete::shells::Bash, &mut cmd, "urlsup", &mut buf);
assert!(!buf.is_empty(), "Bash completion should generate output");
}
#[test]
fn test_print_completions_zsh() {
let mut cmd = crate::ui::cli::Cli::command();
let mut buf = Vec::new();
clap_complete::generate(clap_complete::shells::Zsh, &mut cmd, "urlsup", &mut buf);
assert!(!buf.is_empty(), "Zsh completion should generate output");
}
#[test]
fn test_print_completions_fish() {
let mut cmd = crate::ui::cli::Cli::command();
let mut buf = Vec::new();
clap_complete::generate(clap_complete::shells::Fish, &mut cmd, "urlsup", &mut buf);
assert!(!buf.is_empty(), "Fish completion should generate output");
}
#[test]
fn test_get_completion_filename() {
assert_eq!(
get_completion_filename(clap_complete::Shell::Bash),
"urlsup"
);
assert_eq!(
get_completion_filename(clap_complete::Shell::Zsh),
"_urlsup"
);
assert_eq!(
get_completion_filename(clap_complete::Shell::Fish),
"urlsup.fish"
);
assert_eq!(
get_completion_filename(clap_complete::Shell::PowerShell),
"urlsup"
);
assert_eq!(
get_completion_filename(clap_complete::Shell::Elvish),
"urlsup"
);
}
#[test]
fn test_generate_completion_script_bash() {
let result = generate_completion_script(clap_complete::Shell::Bash);
assert!(result.is_ok());
let script = result.unwrap();
assert!(!script.is_empty());
assert!(script.contains("urlsup"));
}
#[test]
fn test_generate_completion_script_zsh() {
let result = generate_completion_script(clap_complete::Shell::Zsh);
assert!(result.is_ok());
let script = result.unwrap();
assert!(!script.is_empty());
assert!(script.contains("urlsup"));
}
#[test]
fn test_generate_completion_script_fish() {
let result = generate_completion_script(clap_complete::Shell::Fish);
assert!(result.is_ok());
let script = result.unwrap();
assert!(!script.is_empty());
assert!(script.contains("urlsup"));
}
#[test]
fn test_generate_completion_script_powershell() {
let result = generate_completion_script(clap_complete::Shell::PowerShell);
assert!(result.is_ok());
let script = result.unwrap();
assert!(!script.is_empty());
assert!(script.contains("urlsup"));
}
#[test]
fn test_generate_completion_script_elvish() {
let result = generate_completion_script(clap_complete::Shell::Elvish);
assert!(result.is_ok());
let script = result.unwrap();
assert!(!script.is_empty());
assert!(script.contains("urlsup"));
}
#[test]
fn test_get_shell_setup_instructions_bash() {
let path = std::path::Path::new("/tmp/urlsup");
let instructions = get_shell_setup_instructions(clap_complete::Shell::Bash, path);
assert!(instructions.contains("bash"));
assert!(instructions.contains("~/.bashrc"));
assert!(instructions.contains("/tmp/urlsup"));
}
#[test]
fn test_get_shell_setup_instructions_zsh() {
let path = std::path::Path::new("/tmp/_urlsup");
let instructions = get_shell_setup_instructions(clap_complete::Shell::Zsh, path);
assert!(instructions.contains("zsh"));
assert!(instructions.contains("~/.zshrc"));
assert!(instructions.contains("/tmp/_urlsup"));
assert!(instructions.contains("fpath"));
}
#[test]
fn test_get_shell_setup_instructions_fish() {
let path = std::path::Path::new("/tmp/urlsup.fish");
let instructions = get_shell_setup_instructions(clap_complete::Shell::Fish, path);
assert!(instructions.contains("Fish"));
assert!(instructions.contains("~/.config/fish"));
assert!(instructions.contains("/tmp/urlsup.fish"));
}
#[test]
fn test_get_shell_setup_instructions_powershell() {
let path = std::path::Path::new("/tmp/urlsup.ps1");
let instructions = get_shell_setup_instructions(clap_complete::Shell::PowerShell, path);
assert!(instructions.contains("PowerShell"));
assert!(instructions.contains("$PROFILE"));
}
#[test]
fn test_get_shell_setup_instructions_elvish() {
let path = std::path::Path::new("/tmp/urlsup.elv");
let instructions = get_shell_setup_instructions(clap_complete::Shell::Elvish, path);
assert!(instructions.contains("Elvish"));
assert!(instructions.contains("rc.elv"));
}
#[test]
#[serial]
fn test_get_completion_directory_no_home() {
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", "/nonexistent/invalid/path/that/should/not/exist");
}
let result = get_completion_directory(clap_complete::Shell::Bash);
match result {
Err(error) => {
assert!(
error.contains("HOME") || error.contains("Failed to create"),
"Error should mention HOME or directory creation failure: {error}"
);
}
Ok(completion_dir) => {
assert!(completion_dir.exists(), "Created directory should exist");
}
}
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
fn test_get_completion_directory_powershell() {
let result = get_completion_directory(clap_complete::Shell::PowerShell);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("PowerShell completion installation not supported")
);
}
#[test]
fn test_get_completion_directory_elvish() {
let result = get_completion_directory(clap_complete::Shell::Elvish);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("Elvish completion installation not supported")
);
}
#[test]
#[serial]
fn test_get_completion_directory_bash_with_temp_home() {
let temp_dir = TempDir::new().unwrap();
let temp_home = temp_dir.path().to_str().unwrap();
let original_home = std::env::var("HOME").ok();
assert!(temp_dir.path().exists(), "Temp directory should exist");
assert!(temp_dir.path().is_dir(), "Temp path should be a directory");
unsafe {
std::env::set_var("HOME", temp_home);
}
assert_eq!(
std::env::var("HOME").unwrap(),
temp_home,
"HOME should be set to temp directory"
);
let result = get_completion_directory(clap_complete::Shell::Bash);
let completion_dir = match result {
Ok(dir) => dir,
Err(e) => {
if e.contains("Permission denied") || e.contains("Access is denied") {
eprintln!(
"Skipping test due to permission restrictions in CI environment: {e}"
);
return;
}
panic!("get_completion_directory failed with temp home {temp_home}: {e}");
}
};
let temp_home_path = std::path::Path::new(temp_home);
assert!(
completion_dir.starts_with(temp_home_path),
"Completion dir {completion_dir:?} should start with temp home {temp_home_path:?}"
);
assert!(completion_dir.exists());
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
#[serial]
fn test_get_completion_directory_zsh_with_temp_home() {
let temp_dir = TempDir::new().unwrap();
let temp_home = temp_dir.path().to_str().unwrap();
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", temp_home);
}
let result = get_completion_directory(clap_complete::Shell::Zsh);
let completion_dir = match result {
Ok(dir) => dir,
Err(e) => {
if e.contains("Permission denied") || e.contains("Access is denied") {
eprintln!(
"Skipping test due to permission restrictions in CI environment: {e}"
);
return;
}
panic!("get_completion_directory failed: {e}");
}
};
let temp_home_path = std::path::Path::new(temp_home);
assert!(
completion_dir.starts_with(temp_home_path),
"Completion dir {completion_dir:?} should start with temp home {temp_home_path:?}"
);
assert!(completion_dir.exists());
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
#[serial]
fn test_get_completion_directory_fish_with_temp_home() {
let temp_dir = TempDir::new().unwrap();
let temp_home = temp_dir.path().to_path_buf(); let temp_home_str = temp_home.to_str().unwrap();
let original_home = std::env::var("HOME").ok();
assert!(temp_home.exists(), "Temp directory should exist");
assert!(temp_home.is_dir(), "Temp path should be a directory");
unsafe {
std::env::set_var("HOME", temp_home_str);
}
assert_eq!(
std::env::var("HOME").unwrap(),
temp_home_str,
"HOME should be set to temp directory"
);
let result = get_completion_directory(clap_complete::Shell::Fish);
let completion_dir = match result {
Ok(dir) => dir,
Err(e) => {
if e.contains("Permission denied") || e.contains("Access is denied") {
eprintln!(
"Skipping test due to permission restrictions in CI environment: {e}"
);
return;
}
panic!("get_completion_directory failed with temp home {temp_home_str}: {e}");
}
};
let canon_completion_dir = completion_dir
.canonicalize()
.unwrap_or(completion_dir.clone());
let canon_temp_home = temp_home.canonicalize().unwrap_or(temp_home.clone());
assert!(
canon_completion_dir.starts_with(&canon_temp_home),
"Completion dir {canon_completion_dir:?} should start with temp home {canon_temp_home:?}"
);
assert!(
completion_dir
.to_string_lossy()
.contains(".config/fish/completions"),
"Completion dir should contain fish completions path"
);
assert!(completion_dir.exists(), "Completion directory should exist");
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
#[serial]
fn test_install_completion_bash() {
let temp_dir = TempDir::new().unwrap();
let temp_home = temp_dir.path().to_str().unwrap();
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", temp_home);
}
let result = install_completion(clap_complete::Shell::Bash);
assert!(result.is_ok());
let message = result.unwrap();
assert!(message.contains("Shell completion installed successfully"));
assert!(message.contains("bash"));
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
#[serial]
fn test_install_completion_fish() {
let temp_dir = TempDir::new().unwrap();
let temp_home = temp_dir.path().to_str().unwrap();
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::set_var("HOME", temp_home);
}
let result = install_completion(clap_complete::Shell::Fish);
assert!(result.is_ok());
let message = result.unwrap();
assert!(message.contains("Shell completion installed successfully"));
assert!(message.contains("Fish"));
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
} else {
unsafe {
std::env::remove_var("HOME");
}
}
}
#[test]
#[serial]
fn test_install_completion_powershell() {
let result = install_completion(clap_complete::Shell::PowerShell);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("PowerShell completion installation not supported")
);
}
#[test]
#[serial]
fn test_install_completion_elvish() {
let result = install_completion(clap_complete::Shell::Elvish);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("Elvish completion installation not supported")
);
}
#[test]
#[serial]
fn test_install_completion_no_home() {
let original_home = std::env::var("HOME").ok();
unsafe {
std::env::remove_var("HOME");
}
let result = install_completion(clap_complete::Shell::Bash);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("HOME environment variable not set")
);
if let Some(home) = original_home {
unsafe {
std::env::set_var("HOME", home);
}
}
}
}