void-cli 0.0.2

CLI for void — anonymous encrypted source control
//! Generate shell completion scripts.
//!
//! This command outputs shell-specific completion scripts that can be sourced
//! to enable tab completion for void commands.
//!
//! # Usage
//!
//! ```bash
//! # Bash
//! eval "$(void completion bash)"
//!
//! # Zsh
//! eval "$(void completion zsh)"
//!
//! # Fish
//! void completion fish | source
//!
//! # PowerShell
//! void completion powershell | Out-String | Invoke-Expression
//! ```

use clap::CommandFactory;
use clap_complete::{generate, Shell};
use std::io;
use std::str::FromStr;

use crate::output::{CliError, CliOptions};
use crate::Cli;

/// Supported shell types for completion generation.
#[derive(Debug, Clone, Copy)]
pub enum ShellType {
    Bash,
    Zsh,
    Fish,
    PowerShell,
}

impl FromStr for ShellType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "bash" => Ok(ShellType::Bash),
            "zsh" => Ok(ShellType::Zsh),
            "fish" => Ok(ShellType::Fish),
            "powershell" | "pwsh" => Ok(ShellType::PowerShell),
            _ => Err(format!(
                "unknown shell '{}'. Supported: bash, zsh, fish, powershell",
                s
            )),
        }
    }
}

impl From<ShellType> for Shell {
    fn from(shell: ShellType) -> Self {
        match shell {
            ShellType::Bash => Shell::Bash,
            ShellType::Zsh => Shell::Zsh,
            ShellType::Fish => Shell::Fish,
            ShellType::PowerShell => Shell::PowerShell,
        }
    }
}

/// Generate shell completion script.
///
/// This command bypasses the normal JSON envelope and outputs raw text
/// directly to stdout, as the output is meant to be sourced by the shell.
///
/// # Arguments
///
/// * `shell` - The shell to generate completions for (bash, zsh, fish, powershell)
/// * `_opts` - CLI options (unused, but kept for consistency with other commands)
///
/// # Returns
///
/// Always returns Ok(()) on success, or a CliError if the shell is invalid.
pub fn run(shell: &str, _opts: &CliOptions) -> Result<(), CliError> {
    // Parse shell type
    let shell_type = shell
        .parse::<ShellType>()
        .map_err(|e| CliError::invalid_args(e))?;

    // Get the CLI command definition
    let mut cmd = Cli::command();

    // Convert to clap_complete Shell type and generate completions
    let clap_shell: Shell = shell_type.into();

    // Generate completions directly to stdout (no JSON envelope)
    generate::<Shell, _>(clap_shell, &mut cmd, "void", &mut io::stdout());

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_shell_type_parsing() {
        assert!(matches!("bash".parse::<ShellType>(), Ok(ShellType::Bash)));
        assert!(matches!("zsh".parse::<ShellType>(), Ok(ShellType::Zsh)));
        assert!(matches!("fish".parse::<ShellType>(), Ok(ShellType::Fish)));
        assert!(matches!(
            "powershell".parse::<ShellType>(),
            Ok(ShellType::PowerShell)
        ));
        assert!(matches!(
            "pwsh".parse::<ShellType>(),
            Ok(ShellType::PowerShell)
        ));

        // Case insensitive
        assert!(matches!("BASH".parse::<ShellType>(), Ok(ShellType::Bash)));
        assert!(matches!("Zsh".parse::<ShellType>(), Ok(ShellType::Zsh)));

        // Invalid
        assert!("unknown".parse::<ShellType>().is_err());
    }

    #[test]
    fn test_shell_type_to_clap_shell() {
        assert!(matches!(Shell::from(ShellType::Bash), Shell::Bash));
        assert!(matches!(Shell::from(ShellType::Zsh), Shell::Zsh));
        assert!(matches!(Shell::from(ShellType::Fish), Shell::Fish));
        assert!(matches!(
            Shell::from(ShellType::PowerShell),
            Shell::PowerShell
        ));
    }
}