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.
This commit is contained in:
Thomas Frans
2023-03-09 19:02:11 +01:00
committed by GitHub
parent c457efc6fc
commit e8adff444c
6 changed files with 113 additions and 17 deletions

7
.gitignore vendored
View File

@@ -12,5 +12,10 @@ tags
/.vscode/ /.vscode/
# Ignore generated manpages # Ignore generated resources
*.1 *.1
misc/_ncspot
misc/ncspot.bash
misc/ncspot.fish
misc/ncspot.elv
misc/_ncspot.ps1

10
Cargo.lock generated
View File

@@ -310,6 +310,15 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "clap_complete"
version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501ff0a401473ea1d4c3b125ff95506b62c5bc5768d818634195fbb7c4ad5ff4"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.3.2" version = "0.3.2"
@@ -3851,6 +3860,7 @@ name = "xtask"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"clap_complete",
"clap_mangen", "clap_mangen",
"ncspot", "ncspot",
] ]

View File

@@ -172,13 +172,20 @@ cargo deb
You can find the package under `target/debian`. You can find the package under `target/debian`.
#### Packaging Information #### Packaging Information
The following files are provided and should be bundled together with ncspot: The following files are provided and should be bundled together with ncspot:
- LICENSE - LICENSE
- images/logo.svg (optional) - images/logo.svg (optional)
- misc/ncspot.desktop (for Linux systems) - misc/ncspot.desktop (for Linux systems)
- misc/ncspot.1 (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. Some of these files have to be generated. Execute `cargo xtask --help` for more information.
### Audio Backends ### Audio Backends
By default `ncspot` is built using the PulseAudio backend. To make it use the By default `ncspot` is built using the PulseAudio backend. To make it use the

View File

@@ -1,6 +1,7 @@
use librespot_playback::audio_backend; use librespot_playback::audio_backend;
pub const AUTHOR: &str = "Henrik Friedrichsen <henrik@affekt.org> and contributors"; pub const AUTHOR: &str = "Henrik Friedrichsen <henrik@affekt.org> and contributors";
pub const BIN_NAME: &str = "ncspot";
/// Return the [Command](clap::Command) that models the program's command line arguments. The /// 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 /// command can be used to parse the actual arguments passed to the program, or to automatically

View File

@@ -7,6 +7,7 @@ edition = "2021"
[dependencies] [dependencies]
clap_mangen = "0.2.8" clap_mangen = "0.2.8"
clap_complete = "4.1.4"
clap = "4.1.6" clap = "4.1.6"
[dependencies.ncspot] [dependencies.ncspot]

View File

@@ -4,10 +4,14 @@ use std::{env, fs};
use clap::builder::PathBufValueParser; use clap::builder::PathBufValueParser;
use clap::error::{Error, ErrorKind}; use clap::error::{Error, ErrorKind};
use clap::ArgMatches; use clap::ArgMatches;
use ncspot::AUTHOR; use clap_complete::Shell;
use ncspot::{AUTHOR, BIN_NAME};
static DEFAULT_OUTPUT_DIRECTORY: &str = "misc";
enum XTaskSubcommand { enum XTaskSubcommand {
GenerateManpage, GenerateManpage,
GenerateShellCompletion,
} }
impl TryFrom<&ArgMatches> for XTaskSubcommand { impl TryFrom<&ArgMatches> for XTaskSubcommand {
@@ -17,6 +21,7 @@ impl TryFrom<&ArgMatches> for XTaskSubcommand {
if let Some(subcommand) = value.subcommand() { if let Some(subcommand) = value.subcommand() {
match subcommand.0 { match subcommand.0 {
"generate-manpage" => Ok(XTaskSubcommand::GenerateManpage), "generate-manpage" => Ok(XTaskSubcommand::GenerateManpage),
"generate-shell-completion" => Ok(XTaskSubcommand::GenerateShellCompletion),
_ => Err(Error::new(clap::error::ErrorKind::InvalidSubcommand)), _ => Err(Error::new(clap::error::ErrorKind::InvalidSubcommand)),
} }
} else { } else {
@@ -45,48 +50,115 @@ fn try_main() -> Result<(), DynError> {
.long_about( .long_about(
" "
Cargo xtask is a convention that allows easy integration of third party commands into the regular 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 cargo workflow. Xtask's are defined as a separate package and can be used for all kinds of
automation. automation.",
",
) )
.subcommand( .subcommands([
clap::Command::new("generate-manpage") clap::Command::new("generate-manpage")
.visible_alias("gm") .visible_alias("gm")
.args([clap::Arg::new("output") .args([clap::Arg::new("output")
.short('o') .short('o')
.long("output") .long("output")
.value_name("PATH") .value_name("PATH")
.default_value("misc")
.help("Output directory for the generated man page.") .help("Output directory for the generated man page.")
.value_parser(PathBufValueParser::new())]) .value_parser(PathBufValueParser::new())])
.about("Automatic man page generation."), .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 program_parsed_arguments = arguments_model.get_matches();
let parsed_subcommand = XTaskSubcommand::try_from(&program_parsed_arguments)?; let parsed_subcommand = XTaskSubcommand::try_from(&program_parsed_arguments)?;
let subcommand_parsed_arguments = program_parsed_arguments.subcommand().unwrap().1;
match parsed_subcommand { match parsed_subcommand {
XTaskSubcommand::GenerateManpage => { XTaskSubcommand::GenerateManpage => generate_manpage(subcommand_parsed_arguments),
generate_manpage(program_parsed_arguments.subcommand().unwrap().1) XTaskSubcommand::GenerateShellCompletion => {
generate_shell_completion(subcommand_parsed_arguments)
} }
} }
} }
fn generate_manpage(subcommand_arguments: &ArgMatches) -> Result<(), DynError> { fn generate_manpage(subcommand_arguments: &ArgMatches) -> Result<(), DynError> {
let output_directory = let default_output_directory = PathBuf::from(DEFAULT_OUTPUT_DIRECTORY);
if let Some(output_argument) = subcommand_arguments.get_one::<PathBuf>("output") { let output_directory = subcommand_arguments
output_argument.clone() .get_one::<PathBuf>("output")
} else { .unwrap_or(&default_output_directory);
fs::create_dir_all("misc")?;
PathBuf::from("misc")
};
let cmd = ncspot::program_arguments(); let cmd = ncspot::program_arguments();
let man = clap_mangen::Man::new(cmd); let man = clap_mangen::Man::new(cmd);
let mut buffer: Vec<u8> = Default::default(); let mut buffer: Vec<u8> = 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)?; std::fs::write(output_directory.join("ncspot.1"), buffer)?;
Ok(()) 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::<PathBuf>("output")
.unwrap_or(&default_output_directory);
let shells = subcommand_arguments
.get_many::<String>("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(())
}