diff --git a/Cargo.toml b/Cargo.toml index cc855f5..ad7fd97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,9 +15,9 @@ rust-version = "1.65" default = [] [dependencies] -clap = { version = "4.4", features = ["derive", "cargo"] } -termimad = "0.34" -terminal-light = "1.8" +clap = { version = "4.5.57", features = ["derive", "cargo"] } +termimad = "0.34.1" +terminal-light = "1.8.0" [patch.crates-io] # termimad = { path = "../termimad" } diff --git a/src/printer.rs b/src/printer.rs index 49fb424..e2deb53 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -39,6 +39,18 @@ ${option-lines |- "; +/// Default template for the "subcommands" section +pub static TEMPLATE_SUBCOMMANDS: &str = " +**Subcommands:** +|:-|:-| +|name|description| +|:-|:-| +${subcommand-lines +|**${name}**|${help}| +} +|- +"; + /// a template for the "options" section with the value merged to short and long pub static TEMPLATE_OPTIONS_MERGED_VALUE: &str = " **Options:** @@ -59,6 +71,7 @@ pub static TEMPLATES: &[&str] = &[ "usage", "positionals", "options", + "subcommands", "bugs", ]; @@ -114,8 +127,19 @@ impl<'t> Printer<'t> { templates.insert("title", TEMPLATE_TITLE); templates.insert("author", TEMPLATE_AUTHOR); templates.insert("usage", TEMPLATE_USAGE); - templates.insert("positionals", TEMPLATE_POSITIONALS); - templates.insert("options", TEMPLATE_OPTIONS); + + if cmd.get_positionals().count() != 0 { + templates.insert("positionals", TEMPLATE_POSITIONALS); + } + + if cmd.get_opts().count() != 0 { + templates.insert("options", TEMPLATE_OPTIONS); + } + + if cmd.has_subcommands() { + templates.insert("subcommands", TEMPLATE_SUBCOMMANDS); + } + Self { skin: Self::make_skin(), expander, @@ -125,6 +149,7 @@ impl<'t> Printer<'t> { max_width: None, } } + /// Build a skin for the detected theme of the terminal /// (i.e. dark, light, or other) pub fn make_skin() -> MadSkin { @@ -134,11 +159,13 @@ impl<'t> Printer<'t> { _ => MadSkin::default(), } } + /// Use the provided skin pub fn with_skin(mut self, skin: MadSkin) -> Self { self.skin = skin; self } + /// Set a maximal width, so that the whole terminal width isn't used. /// /// This may make some long sentences easier to read on super wide @@ -149,32 +176,38 @@ impl<'t> Printer<'t> { self.max_width = Some(w); self } + /// Give a mutable reference to the current skin /// (by default the automatically selected one) /// so that it can be modified pub fn skin_mut(&mut self) -> &mut MadSkin { &mut self.skin } + /// Change a template pub fn set_template(&mut self, key: &'static str, template: &'t str) { self.templates.insert(key, template); } + /// Change or add a template pub fn with(mut self, key: &'static str, template: &'t str) -> Self { self.set_template(key, template); self } + /// Unset a template pub fn without(mut self, key: &'static str) -> Self { self.templates.remove(key); self } + /// A mutable reference to the list of template keys, so that you can /// insert new keys, or change their order. /// Any key without matching template will just be ignored pub fn template_keys_mut(&mut self) -> &mut Vec<&'static str> { &mut self.template_keys } + /// A mutable reference to the list of template keys, so that you can /// insert new keys, or change their order. /// Any key without matching template will just be ignored @@ -182,101 +215,155 @@ impl<'t> Printer<'t> { pub fn template_order_mut(&mut self) -> &mut Vec<&'static str> { &mut self.template_keys } + fn make_expander(cmd: &Command) -> OwningTemplateExpander<'static> { let mut expander = OwningTemplateExpander::new(); expander.set_default(""); + let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); expander.set("name", name); + if let Some(author) = cmd.get_author() { expander.set("author", author); } + if let Some(version) = cmd.get_version() { expander.set("version", version); } + let options = cmd .get_arguments() .filter(|a| !a.is_hide_set()) .filter(|a| a.get_short().is_some() || a.get_long().is_some()); - for arg in options { - let sub = expander.sub("option-lines"); - if let Some(short) = arg.get_short() { - sub.set("short", format!("-{short}")); - } - if let Some(long) = arg.get_long() { - sub.set("long", format!("--{long}")); - } - if let Some(help) = arg.get_help() { - sub.set_md("help", help.to_string()); - } - if arg.get_action().takes_values() { - if let Some(name) = arg.get_value_names().and_then(|arr| arr.first()) { - sub.set("value", name); - let braced = format!("<{}>", name); - sub.set("value-braced", &braced); - if arg.get_short().is_some() { - sub.set("value-short-braced", &braced); - sub.set("value-short", name); - } - if arg.get_long().is_some() { - sub.set("value-long-braced", &braced); - sub.set("value-long", name); - } - }; - } - let mut possible_values = arg.get_possible_values(); - if !possible_values.is_empty() { - let possible_values: Vec = possible_values - .drain(..) - .map(|v| format!("`{}`", v.get_name())) - .collect(); - expander.sub("option-lines").set_md( - "possible_values", - format!(" Possible values: [{}]", possible_values.join(", ")), - ); - } - if let Some(default) = arg.get_default_values().first() { - match arg.get_action() { - ArgAction::Set | ArgAction::Append => { - expander.sub("option-lines").set_md( - "default", - format!(" Default: `{}`", default.to_string_lossy()), - ); + + // they say it's the hackiest solution of all time + if !cmd + .clone() + .get_arguments() + .filter(|a| !a.is_hide_set()) + .filter(|a| a.get_short().is_some() || a.get_long().is_some()) + .collect::>() + .is_empty() + { + for arg in options { + let sub = expander.sub("option-lines"); + + if let Some(short) = arg.get_short() { + sub.set("short", format!("-{short}")); + } + + if let Some(long) = arg.get_long() { + sub.set("long", format!("--{long}")); + } + + if let Some(help) = arg.get_help() { + sub.set_md("help", help.to_string()); + } + + if arg.get_action().takes_values() { + if let Some(name) = arg.get_value_names().and_then(|arr| arr.first()) { + sub.set("value", name); + let braced = format!("<{}>", name); + sub.set("value-braced", &braced); + + if arg.get_short().is_some() { + sub.set("value-short-braced", &braced); + sub.set("value-short", name); + } + + if arg.get_long().is_some() { + sub.set("value-long-braced", &braced); + sub.set("value-long", name); + } + }; + } + + let mut possible_values = arg.get_possible_values(); + + if !possible_values.is_empty() { + let possible_values: Vec = possible_values + .drain(..) + .map(|v| format!("`{}`", v.get_name())) + .collect(); + + expander.sub("option-lines").set_md( + "possible_values", + format!(" Possible values: [{}]", possible_values.join(", ")), + ); + } + + if let Some(default) = arg.get_default_values().first() { + match arg.get_action() { + ArgAction::Set | ArgAction::Append => { + expander.sub("option-lines").set_md( + "default", + format!(" Default: `{}`", default.to_string_lossy()), + ); + } + _ => {} } - _ => {} } } } + let mut args = String::new(); - for arg in cmd.get_positionals() { - let Some(key) = arg.get_value_names().and_then(|arr| arr.first()) else { - continue; - }; - args.push(' '); - if !arg.is_required_set() { - args.push('['); - } - if arg.is_last_set() { - args.push_str("-- "); - } - args.push_str(key); - if !arg.is_required_set() { - args.push(']'); + if !cmd.get_positionals().collect::>().is_empty() { + for arg in cmd.get_positionals() { + let Some(key) = arg.get_value_names().and_then(|arr| arr.first()) else { + continue; + }; + + args.push(' '); + + if !arg.is_required_set() { + args.push('['); + } + + if arg.is_last_set() { + args.push_str("-- "); + } + + args.push_str(key); + + if !arg.is_required_set() { + args.push(']'); + } + + let sub = expander.sub("positional-lines"); + sub.set("key", key); + + if let Some(help) = arg.get_help() { + sub.set("help", help); + } } - let sub = expander.sub("positional-lines"); - sub.set("key", key); - if let Some(help) = arg.get_help() { - sub.set("help", help); + } + + if !cmd.get_subcommands().collect::>().is_empty() { + args.push_str(" [COMMAND]"); + for subcommand in cmd.get_subcommands() { + if !subcommand.is_hide_set() { + let sub = expander.sub("subcommand-lines"); + sub.set("name", subcommand.get_name()); + if let Some(about) = subcommand.get_about() { + sub.set_md("help", about.to_string()); + } else { + sub.set("help", ""); + } + } } } + expander.set("positional-args", args); expander } + /// Give you a mut reference to the expander, so that you can overload /// the variable of the expander used to fill the templates of the help, /// or add new variables for your own templates pub fn expander_mut(&mut self) -> &mut OwningTemplateExpander<'static> { &mut self.expander } + /// Print the provided template with the printer's expander /// /// It's normally more convenient to change template_keys or some @@ -284,6 +371,7 @@ impl<'t> Printer<'t> { pub fn print_template(&self, template: &str) { self.skin.print_owning_expander_md(&self.expander, template); } + /// Print all the templates, in order pub fn print_help(&self) { if self.full_width { @@ -292,6 +380,7 @@ impl<'t> Printer<'t> { self.print_help_content_width() } } + fn print_help_full_width(&self) { for key in &self.template_keys { if let Some(template) = self.templates.get(key) { @@ -299,12 +388,15 @@ impl<'t> Printer<'t> { } } } + fn print_help_content_width(&self) { let (width, _) = termimad::terminal_size(); let mut width = width as usize; + if let Some(max_width) = self.max_width { width = width.min(max_width); } + let mut texts: Vec = self .template_keys .iter() @@ -315,12 +407,21 @@ impl<'t> Printer<'t> { FmtText::from_text(&self.skin, text, Some(width)) }) .collect(); + let content_width = texts .iter() .fold(0, |cw, text| cw.max(text.content_width())); + for text in &mut texts { text.set_rendering_width(content_width); println!("{}", text); } } + + /// Create a printer for a specific subcommand by name + pub fn for_subcommand(mut cmd: Command, subcommand_name: &str) -> Option { + cmd.build(); + cmd.find_subcommand(subcommand_name) + .map(|subcmd| Self::new(subcmd.clone())) + } }