feat(zsh): add tv cli options/arguments/subcommands autocompletion (#665)

## 📺 PR Description

update `init` shell generation to include autocompleting options for
`tv` itself.

<img width="817" height="497" alt="image"
src="https://github.com/user-attachments/assets/e65bc0aa-410e-43ef-86a5-2d6e01c6c1df"
/>

<img width="837" height="186" alt="image"
src="https://github.com/user-attachments/assets/6d9665f6-0664-4e6e-bce3-e6a970c79524"
/>

I've only tested this on macOS (arm64) with `zsh` and `bash` so far:

<img width="893" height="349" alt="image"
src="https://github.com/user-attachments/assets/dd66ecec-fee3-4713-a68c-f69e961a4ba6"
/>



## Checklist

<!-- a quick pass through the following items to make sure you haven't
forgotten anything -->

- [x] my commits **and PR title** follow the [conventional
commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format
- [x] if this is a new feature, I have added tests to consolidate the
feature and prevent regressions
- [ ] if this is a bug fix, I have added a test that reproduces the bug
(if applicable)
- [ ] I have added a reasonable amount of documentation to the code
where appropriate

---------

Co-authored-by: Alex Pasmantier <47638216+alexpasmantier@users.noreply.github.com>
This commit is contained in:
Aiden Scandella 2025-07-25 16:34:06 -04:00 committed by GitHub
parent c93ddeeadb
commit 83f29f7418
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 3 deletions

10
Cargo.lock generated
View File

@ -305,6 +305,15 @@ dependencies = [
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.5.40"
@ -1989,6 +1998,7 @@ dependencies = [
"base64",
"better-panic",
"clap",
"clap_complete",
"clap_mangen",
"clipboard-win",
"colored",

View File

@ -51,6 +51,7 @@ serde_json = "1.0.140"
colored = "3.0.0"
serde_with = "3.13.0"
which = "8.0.0"
clap_complete = "4.5.55"
# target specific dependencies

View File

@ -1,10 +1,11 @@
use crate::{
cli::args::Shell as CliShell,
cli::args::{Cli, Shell as CliShell},
config::shell_integration::ShellIntegrationConfig,
};
use anyhow::Result;
use clap::CommandFactory;
use std::fmt::Display;
use tracing::debug;
use tracing::{debug, warn};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Shell {
@ -28,6 +29,31 @@ impl Default for Shell {
}
}
#[derive(Debug)]
pub enum ShellError {
UnsupportedShell(String),
}
impl TryFrom<Shell> for clap_complete::Shell {
type Error = ShellError;
fn try_from(value: Shell) -> std::result::Result<Self, Self::Error> {
match value {
Shell::Bash => Ok(clap_complete::Shell::Bash),
Shell::Zsh => Ok(clap_complete::Shell::Zsh),
Shell::Fish => Ok(clap_complete::Shell::Fish),
Shell::PowerShell => Ok(clap_complete::Shell::PowerShell),
Shell::Cmd => Err(ShellError::UnsupportedShell(
"Cmd shell is not supported for completion scripts"
.to_string(),
)),
Shell::Nu => Err(ShellError::UnsupportedShell(
"Nu shell is not supported for completion scripts".to_string(),
)),
}
}
}
impl Display for Shell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -143,6 +169,7 @@ pub fn render_autocomplete_script_template(
template: &str,
config: &ShellIntegrationConfig,
) -> Result<String> {
// Custom autocomplete
let script = template
.replace(
"{tv_smart_autocomplete_keybinding}",
@ -158,7 +185,33 @@ pub fn render_autocomplete_script_template(
config.get_command_history_keybinding_character(),
)?,
);
Ok(script)
let clap_autocomplete =
render_clap_autocomplete(shell).unwrap_or_default();
Ok(script + &clap_autocomplete)
}
fn render_clap_autocomplete(shell: Shell) -> Option<String> {
// Clap autocomplete
let mut clap_autocomplete = vec![];
let mut cmd = Cli::command();
let clap_shell: clap_complete::Shell = match shell.try_into() {
Ok(shell) => shell,
Err(err) => {
warn!("Failed to convert shell {:?}: {:?}", shell, err);
return None;
}
};
clap_complete::aot::generate(
clap_shell,
&mut cmd,
"tv", // the command defines the name as "television"
&mut clap_autocomplete,
);
String::from_utf8(clap_autocomplete).ok()
}
#[cfg(test)]
@ -208,4 +261,30 @@ mod tests {
assert!(result.is_ok());
assert_eq!(result.unwrap(), "Ctrl-s");
}
#[test]
fn test_zsh_clap_completion() {
let shell = Shell::Zsh;
let result = render_autocomplete_script_template(
shell,
"",
&ShellIntegrationConfig::default(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.contains("compdef _tv tv"));
}
#[test]
fn test_unsupported_clap_completion() {
let shell = Shell::Nu;
let result = render_autocomplete_script_template(
shell,
"",
&ShellIntegrationConfig::default(),
);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_empty());
}
}