From e8adff444c8afd0f5ac3d30f75780c78ce792504 Mon Sep 17 00:00:00 2001 From: Thomas Frans <48214567+ThomasFrans@users.noreply.github.com> Date: Thu, 9 Mar 2023 19:02:11 +0100 Subject: [PATCH] Automatic shell completion generation * Add automatic shell completion generation. Add automatic generation of shell completion scripts for various shells (the ones supported by `clap_complete`). The scripts can be generated using the `generate-shell-completion` xtask, which outputs the shell script to stdout. * Improve shell completion generation xtask. General improvements to both the shell completion generation as well as the xtask package itself. Update the README to match the new additions. --- .gitignore | 7 +++- Cargo.lock | 10 +++++ README.md | 7 ++++ src/lib.rs | 1 + xtask/Cargo.toml | 1 + xtask/src/main.rs | 104 +++++++++++++++++++++++++++++++++++++++------- 6 files changed, 113 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index b6762ae..7f14922 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,10 @@ tags /.vscode/ -# Ignore generated manpages +# Ignore generated resources *.1 +misc/_ncspot +misc/ncspot.bash +misc/ncspot.fish +misc/ncspot.elv +misc/_ncspot.ps1 diff --git a/Cargo.lock b/Cargo.lock index cdd7402..6b43344 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,6 +310,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "clap_complete" +version = "4.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501ff0a401473ea1d4c3b125ff95506b62c5bc5768d818634195fbb7c4ad5ff4" +dependencies = [ + "clap", +] + [[package]] name = "clap_lex" version = "0.3.2" @@ -3851,6 +3860,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "clap", + "clap_complete", "clap_mangen", "ncspot", ] diff --git a/README.md b/README.md index 72ce0fb..43fd2cb 100644 --- a/README.md +++ b/README.md @@ -172,13 +172,20 @@ cargo deb You can find the package under `target/debian`. #### Packaging Information + The following files are provided and should be bundled together with ncspot: - LICENSE - images/logo.svg (optional) - misc/ncspot.desktop (for Linux systems) - misc/ncspot.1 (for Linux systems) +- misc/ncspot.bash (bash completions) +- misc/\_ncspot (zsh completions) +- misc/ncspot.fish (fish completions) +- misc/ncspot.elv (elvish completions) +- misc/\_ncspot.ps1 (powershell completions) Some of these files have to be generated. Execute `cargo xtask --help` for more information. + ### Audio Backends By default `ncspot` is built using the PulseAudio backend. To make it use the diff --git a/src/lib.rs b/src/lib.rs index 41a32cc..3db26d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use librespot_playback::audio_backend; pub const AUTHOR: &str = "Henrik Friedrichsen and contributors"; +pub const BIN_NAME: &str = "ncspot"; /// Return the [Command](clap::Command) that models the program's command line arguments. The /// command can be used to parse the actual arguments passed to the program, or to automatically diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index ac19eb5..9142a83 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] clap_mangen = "0.2.8" +clap_complete = "4.1.4" clap = "4.1.6" [dependencies.ncspot] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 70a710d..4a5f541 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -4,10 +4,14 @@ use std::{env, fs}; use clap::builder::PathBufValueParser; use clap::error::{Error, ErrorKind}; use clap::ArgMatches; -use ncspot::AUTHOR; +use clap_complete::Shell; +use ncspot::{AUTHOR, BIN_NAME}; + +static DEFAULT_OUTPUT_DIRECTORY: &str = "misc"; enum XTaskSubcommand { GenerateManpage, + GenerateShellCompletion, } impl TryFrom<&ArgMatches> for XTaskSubcommand { @@ -17,6 +21,7 @@ impl TryFrom<&ArgMatches> for XTaskSubcommand { if let Some(subcommand) = value.subcommand() { match subcommand.0 { "generate-manpage" => Ok(XTaskSubcommand::GenerateManpage), + "generate-shell-completion" => Ok(XTaskSubcommand::GenerateShellCompletion), _ => Err(Error::new(clap::error::ErrorKind::InvalidSubcommand)), } } else { @@ -45,48 +50,115 @@ fn try_main() -> Result<(), DynError> { .long_about( " Cargo xtask is a convention that allows easy integration of third party commands into the regular -cargo workflox. Xtask's are defined as a separate package and can be used for all kinds of -automation. - ", +cargo workflow. Xtask's are defined as a separate package and can be used for all kinds of +automation.", ) - .subcommand( + .subcommands([ clap::Command::new("generate-manpage") .visible_alias("gm") .args([clap::Arg::new("output") .short('o') .long("output") .value_name("PATH") + .default_value("misc") .help("Output directory for the generated man page.") .value_parser(PathBufValueParser::new())]) .about("Automatic man page generation."), - ); + clap::Command::new("generate-shell-completion") + .visible_alias("gsc") + .args([ + clap::Arg::new("shells") + .short('s') + .long("shells") + .value_name("SHELLS") + .default_values(["bash", "zsh", "fish"]) + .value_delimiter(',') + .help("The shells for which completion should be generated."), + clap::Arg::new("output") + .short('o') + .long("output") + .value_name("PATH") + .default_value("misc") + .help("Output directory for the generated completion script.") + .value_parser(PathBufValueParser::new()), + ]) + .about("Automatic shell completion generation.") + .long_about( + " +Automatic shell completion generation. +Supported shells: bash,zsh,fish,elvish,powershell", + ), + ]); let program_parsed_arguments = arguments_model.get_matches(); let parsed_subcommand = XTaskSubcommand::try_from(&program_parsed_arguments)?; + let subcommand_parsed_arguments = program_parsed_arguments.subcommand().unwrap().1; + match parsed_subcommand { - XTaskSubcommand::GenerateManpage => { - generate_manpage(program_parsed_arguments.subcommand().unwrap().1) + XTaskSubcommand::GenerateManpage => generate_manpage(subcommand_parsed_arguments), + XTaskSubcommand::GenerateShellCompletion => { + generate_shell_completion(subcommand_parsed_arguments) } } } fn generate_manpage(subcommand_arguments: &ArgMatches) -> Result<(), DynError> { - let output_directory = - if let Some(output_argument) = subcommand_arguments.get_one::("output") { - output_argument.clone() - } else { - fs::create_dir_all("misc")?; - PathBuf::from("misc") - }; + let default_output_directory = PathBuf::from(DEFAULT_OUTPUT_DIRECTORY); + let output_directory = subcommand_arguments + .get_one::("output") + .unwrap_or(&default_output_directory); let cmd = ncspot::program_arguments(); let man = clap_mangen::Man::new(cmd); let mut buffer: Vec = Default::default(); - man.render(&mut buffer)?; + if *output_directory == default_output_directory { + fs::create_dir_all(DEFAULT_OUTPUT_DIRECTORY)?; + } + man.render(&mut buffer)?; std::fs::write(output_directory.join("ncspot.1"), buffer)?; Ok(()) } + +fn generate_shell_completion(subcommand_arguments: &ArgMatches) -> Result<(), DynError> { + let default_output_directory = PathBuf::from(DEFAULT_OUTPUT_DIRECTORY); + let output_directory = subcommand_arguments + .get_one::("output") + .unwrap_or(&default_output_directory); + let shells = subcommand_arguments + .get_many::("shells") + .map(|shells| { + shells + .map(|shell| match shell.as_str() { + "bash" => Shell::Bash, + "zsh" => Shell::Zsh, + "fish" => Shell::Fish, + "elvish" => Shell::Elvish, + "powershell" => Shell::PowerShell, + _ => { + eprintln!("Unrecognized shell: {}", shell); + std::process::exit(-1); + } + }) + .collect() + }) + .unwrap_or(vec![Shell::Bash, Shell::Zsh, Shell::Fish]); + + if *output_directory == default_output_directory { + fs::create_dir_all(DEFAULT_OUTPUT_DIRECTORY)?; + } + + for shell in shells { + clap_complete::generate_to( + shell, + &mut ncspot::program_arguments(), + BIN_NAME, + output_directory, + )?; + } + + Ok(()) +}