diff --git a/Cargo.lock b/Cargo.lock index f5980f0..6d3b8b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f84a88507dbd05c695f2cb5e8558e747179134005e9893882dec964190ed89" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.6.4" @@ -1721,6 +1730,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "clap_complete", "crossterm", "flate2", "pulldown-cmark", diff --git a/Cargo.toml b/Cargo.toml index 65ec6a6..1b795de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ ureq = "3.3.0" tempfile = "3.27.0" unicode-segmentation = "1.13.3" unicode-width = "0.2.2" +clap_complete = "4.6" [dev-dependencies] assert_cmd = "2.2.2" diff --git a/src/cli.rs b/src/cli.rs index a6a2a4d..b1c474f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand, ValueHint}; +use clap_complete::Shell; use crate::dice; use crate::srd::fetch::{self, FetchOutcome, SrdSources}; @@ -66,7 +67,7 @@ enum Command { Sandbox { /// A file whose contents go to the DM as the player's first /// line, so a test scenario starts without retyping it. - #[arg(value_name = "SCENARIO")] + #[arg(value_name = "SCENARIO", value_hint = ValueHint::FilePath)] scenario: Option, /// The base URL of the OpenAI-compatible API to talk to. #[arg(long, value_name = "URL")] @@ -80,8 +81,20 @@ enum Command { #[arg(long, value_name = "LEVEL")] reasoning_effort: Option, }, + /// Print a completion script for a shell. + #[command(after_help = COMPLETIONS_AFTER_HELP)] + Completions { + /// The shell to print a script for. + #[arg(value_name = "SHELL")] + shell: Shell, + }, } +/// Where to put the script, printed after `storied completions --help`. +const COMPLETIONS_AFTER_HELP: &str = "\ +For bash, write the script where bash-completion loads it on demand: + storied completions bash > ~/.local/share/bash-completion/completions/storied"; + #[derive(Subcommand)] enum SrdCommand { /// Download and verify the SRD PDF. @@ -129,9 +142,21 @@ pub fn run( session_layers, scenario.as_deref(), ), + Some(Command::Completions { shell }) => run_completions(shell), } } +/// Prints the completion script for `shell` on stdout. +fn run_completions(shell: Shell) -> Result<(), String> { + clap_complete::generate( + shell, + &mut Cli::command(), + "storied", + &mut std::io::stdout(), + ); + Ok(()) +} + /// Boots a fresh, temporary world for a sandbox session, prints where it /// lives, and plays until the player quits. When `scenario` names a file, /// its contents open the session as the player's first line. @@ -266,6 +291,12 @@ mod tests { } } + fn completions_cli(shell: Shell) -> Cli { + Cli { + command: Some(Command::Completions { shell }), + } + } + fn unused_sources() -> SrdSources { SrdSources { pdf: FetchConfig { @@ -497,6 +528,20 @@ mod tests { assert!(result.is_err()); } + #[test] + fn run_completions_prints_a_script_for_bash() { + let layer_dir = TempDir::new().unwrap(); + + let result = run( + completions_cli(Shell::Bash), + &unused_sources(), + layer_dir.path(), + &[], + ); + + assert_eq!(result, Ok(())); + } + #[test] fn run_roll_prints_the_story_for_valid_notation() { let layer_dir = TempDir::new().unwrap();