diff --git a/docker/fish/foo.tabry b/docker/fish/foo.tabry index 3f31416..48f6c82 100644 --- a/docker/fish/foo.tabry +++ b/docker/fish/foo.tabry @@ -1,21 +1,23 @@ cmd foo -sub bar { +sub bar "The bar command" { arg file { opts const (car motorcycle) opts file } + flag dry-run,d "Don't act, only show what would be done" + flag something-else,s "This is another flag" } -sub baz { - arg directory { +sub baz "The baz command" { + arg directory "a directory, yo" { opts const (car motorcycle) opts dir opts file } } -sub qux { +sub qux "The qux command" { arg directory { opts const (car motorcycle) opts dir diff --git a/shell/tabry_fish.fish b/shell/tabry_fish.fish index 052e0da..d77d32c 100644 --- a/shell/tabry_fish.fish +++ b/shell/tabry_fish.fish @@ -98,7 +98,7 @@ function __tabry_offer_completions set cursor_position (commandline -C) set cmd (commandline) - set -l result ($_tabry_executable complete "$cmd" "$cursor_position") + set -l result ($_tabry_executable complete --include-descriptions "$cmd" "$cursor_position") # get the last item diff --git a/src/app/mod.rs b/src/app/mod.rs index d76c8be..458bbed 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -13,7 +13,7 @@ use crate::{ lang, }; -fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> Result<()> { +fn print_options(config_filename: &str, tokens: &[String], last_token: &str, include_descriptions: bool) -> Result<()> { let config = config::TabryConf::from_file(config_filename).with_context(|| "invalid config file")?; let result = @@ -23,11 +23,14 @@ fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> println!("{}", serde_json::to_string_pretty(&result.state)?); } - let options_finder = options_finder::OptionsFinder::new(result); + let options_finder = options_finder::OptionsFinder::new(result, include_descriptions); let opts = options_finder.options(last_token)?; for opt in &opts.options { - println!("{}", opt); + match opt.desc.as_ref() { + Some(desc) => println!("{} {}", opt.value, desc), + None => println!("{}", opt.value), + } } if !opts.special_options.is_empty() { @@ -44,7 +47,7 @@ fn print_options(config_filename: &str, tokens: &[String], last_token: &str) -> } // This runs using the filename plus 2nd arg as compline (shellsplits ARGV[2]) -pub fn run_as_compline(compline: &str, comppoint: &str) -> Result<()> { +pub fn run_as_compline(compline: &str, comppoint: &str, include_descriptions: bool) -> Result<()> { let comppoint = comppoint.parse::().wrap_err_with(|| eyre!("Invalid compoint: {}", comppoint))?; let tokenized_result = shell_tokenizer::split_with_comppoint(compline, comppoint).wrap_err_with(|| eyre!("Failed to split compline {} on comppoint {}", compline, comppoint))?; @@ -55,7 +58,7 @@ pub fn run_as_compline(compline: &str, comppoint: &str) -> Result<()> { let config_file = config_finder::find_tabry_config(&tokenized_result.command_basename)?; let compiled_config_file = cached_jsons::resolve_and_compile_cache_file(&config_file)?; - print_options(&compiled_config_file, &args[..], &last_arg)?; + print_options(&compiled_config_file, &args[..], &last_arg, include_descriptions)?; Ok(()) } diff --git a/src/engine/options_finder.rs b/src/engine/options_finder.rs index 9839cd8..a77ab5d 100644 --- a/src/engine/options_finder.rs +++ b/src/engine/options_finder.rs @@ -8,19 +8,26 @@ use serde_json::json; pub struct OptionsFinder { result: TabryResult, + include_descriptions: bool, +} + +#[derive(PartialEq, Eq, Hash)] +pub struct OptionResult { + pub value: String, + pub desc: Option, } pub struct OptionsResults { prefix: String, - pub options: HashSet, + pub options: HashSet, pub special_options: HashSet, } impl OptionsResults { - fn insert(&mut self, value: &str) { + fn insert(&mut self, value: &str, desc: Option<&str>) { if value.starts_with(&self.prefix) { // TODO get_or_insert_owned() in nightly would be ideal - self.options.insert(value.to_owned()); + self.options.insert(OptionResult { value: value.to_owned(), desc: desc.map(str::to_owned) }); } } @@ -30,8 +37,8 @@ impl OptionsResults { } impl OptionsFinder { - pub fn new(result: TabryResult) -> Self { - Self { result } + pub fn new(result: TabryResult, include_descriptions: bool) -> Self { + Self { result, include_descriptions } } pub fn options(&self, token: &str) -> Result { @@ -68,7 +75,7 @@ impl OptionsFinder { let concrete_subs = self.result.config.flatten_subs(opaque_subs).unwrap(); for s in concrete_subs { // TODO: error here if no name -- only allowable for top level - res.insert(s.name.as_ref().unwrap()); + res.insert(s.name.as_ref().unwrap(), if self.include_descriptions { s.description.as_deref() } else { None }); } } @@ -77,13 +84,13 @@ impl OptionsFinder { || self.result.state.flag_args.contains_key(&flag.name) } - fn add_option_for_flag(res: &mut OptionsResults, flag: &TabryConcreteFlag) { + fn add_option_for_flag(res: &mut OptionsResults, flag: &TabryConcreteFlag, include_descriptions: bool) { let flag_str = if flag.name.len() == 1 { format!("-{}", flag.name) } else { format!("--{}", flag.name) }; - res.insert(&flag_str); + res.insert(&flag_str, if include_descriptions { flag.description.as_deref() } else { None }); } fn add_options_subcommand_flags(&self, res: &mut OptionsResults) -> Result<(), TabryConfError> { @@ -97,7 +104,7 @@ impl OptionsFinder { .expand_flags(&self.result.current_sub().flags); let first_reqd_flag = current_sub_flags.find(|f| f.required && !self.flag_is_used(f)); if let Some(first_reqd_flag) = first_reqd_flag { - Self::add_option_for_flag(res, first_reqd_flag); + Self::add_option_for_flag(res, first_reqd_flag, self.include_descriptions); return Ok(()); } @@ -109,7 +116,7 @@ impl OptionsFinder { for sub in self.result.sub_stack.iter() { for flag in self.result.config.expand_flags(&sub.flags) { if !self.flag_is_used(flag) { - Self::add_option_for_flag(res, flag); + Self::add_option_for_flag(res, flag, self.include_descriptions); } } } @@ -126,7 +133,7 @@ impl OptionsFinder { match &opt { TabryOpt::File => res.insert_special("file"), TabryOpt::Dir => res.insert_special("dir"), - TabryOpt::Const { value } => res.insert(value), + TabryOpt::Const { value } => res.insert(value, None), TabryOpt::Delegate { value } => { res.insert_special(format!("delegate {}", value).as_str()) } @@ -151,7 +158,7 @@ impl OptionsFinder { let output_str = std::str::from_utf8(&output_bytes.stdout[..]).unwrap(); for line in output_str.split('\n') { if !line.is_empty() { - res.insert(line); + res.insert(line, None); } } } @@ -213,7 +220,7 @@ mod tests { fn options_with_machine_state(machine_state: MachineState, token: &str) -> OptionsResults { let tabry_conf: TabryConf = load_fixture_file("vehicles.json"); let tabry_result = TabryResult::new(tabry_conf, machine_state); - let options_finder = OptionsFinder::new(tabry_result); + let options_finder = OptionsFinder::new(tabry_result, false); options_finder.options(token).unwrap() } @@ -241,7 +248,7 @@ mod tests { }; let options_results = options_with_machine_state(machine_state, token); let actual_strs : HashSet<&str> = - options_results.options.iter().map(|s| s.as_str()).collect(); + options_results.options.iter().map(|s| s.value.as_str()).collect(); let actual_specials_strs : HashSet<&str> = options_results.special_options.iter().map(|s| s.as_str()).collect(); diff --git a/src/main.rs b/src/main.rs index 86b4244..88a4094 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,8 +40,8 @@ enum Subcommands { import_path: Option, }, - /// Output completion script for bash - /// Usage in ~/.bash_profile: `tabry fish | source` or + /// Output completion script for fish + /// Usage in ~/.config/fish/config.fish: `tabry fish | source` or /// `tabry fish | source; tabry_completion_init mycmd` Fish { #[arg(long)] @@ -67,6 +67,10 @@ enum Subcommands { compline: String, /// TODO desc comppoint: String, + + /// Include descriptions in completions (for fish shell only) + #[clap(long, short, action)] + include_descriptions: bool, }, } @@ -77,13 +81,12 @@ fn main() -> Result<()> { use tabry::app::*; let cli = Cli::parse(); match cli.command { - Complete { compline, comppoint } => run_as_compline(&compline, &comppoint)?, + Complete { compline, comppoint, include_descriptions } => run_as_compline(&compline, &comppoint, include_descriptions)?, Compile => compile()?, Commands => commands(), Bash { import_path, no_auto } => bash(import_path.as_deref(), no_auto), Zsh { import_path, no_auto } => zsh(import_path.as_deref(), no_auto), Fish { import_path, no_auto } => fish(import_path.as_deref(), no_auto), } - Ok(()) }