1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//! 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
));
}
}