From 83f29f7418a7adb6f5ca9ee5ac057ef38728fa28 Mon Sep 17 00:00:00 2001 From: Aiden Scandella Date: Fri, 25 Jul 2025 16:34:06 -0400 Subject: [PATCH] feat(zsh): add tv cli options/arguments/subcommands autocompletion (#665) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📺 PR Description update `init` shell generation to include autocompleting options for `tv` itself. image image I've only tested this on macOS (arm64) with `zsh` and `bash` so far: image ## Checklist - [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> --- Cargo.lock | 10 +++++ Cargo.toml | 1 + television/utils/shell.rs | 85 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccac82d..9d138e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index dede280..11ffb33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/television/utils/shell.rs b/television/utils/shell.rs index 090fa1c..9784964 100644 --- a/television/utils/shell.rs +++ b/television/utils/shell.rs @@ -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 for clap_complete::Shell { + type Error = ShellError; + + fn try_from(value: Shell) -> std::result::Result { + 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 { + // 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 { + // 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()); + } }