From 3f9fa0d04bd0cdd7f8c8a268e80982faa18bf1b9 Mon Sep 17 00:00:00 2001 From: The Bearodactyl Date: Tue, 3 Feb 2026 12:12:14 -0600 Subject: [PATCH 1/2] subcommand support --- src/printer.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/printer.rs b/src/printer.rs index 49fb424..c548396 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", ]; @@ -116,6 +129,7 @@ impl<'t> Printer<'t> { templates.insert("usage", TEMPLATE_USAGE); templates.insert("positionals", TEMPLATE_POSITIONALS); templates.insert("options", TEMPLATE_OPTIONS); + templates.insert("subcommands", TEMPLATE_SUBCOMMANDS); Self { skin: Self::make_skin(), expander, @@ -125,6 +139,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 +149,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 +166,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,58 +205,74 @@ 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 => { @@ -246,37 +285,65 @@ impl<'t> Printer<'t> { } } } + 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(']'); } + let sub = expander.sub("positional-lines"); sub.set("key", key); + if let Some(help) = arg.get_help() { sub.set("help", help); } } + + if cmd.has_subcommands() { + args.push_str(" [COMMAND]"); + } + expander.set("positional-args", args); + + 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 } + /// 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 +351,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 +360,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 +368,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 +387,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())) + } } From babbe97dcd081d6500ef04f7177748fa4f94a3f8 Mon Sep 17 00:00:00 2001 From: The Bearodactyl Date: Tue, 3 Feb 2026 14:28:49 -0600 Subject: [PATCH 2/2] only show sections that actually have entries makes it so that sections with variable rows are only displayed if more than one row is needed. e.g., if no subcommands are defined on the command being processed, there will be no subcommands section as it'd be empty anyways --- Cargo.toml | 6 +- src/printer.rs | 186 +++++++++++++++++++++++++++---------------------- 2 files changed, 106 insertions(+), 86 deletions(-) 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 c548396..e2deb53 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -127,9 +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); - templates.insert("subcommands", TEMPLATE_SUBCOMMANDS); + + 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, @@ -226,114 +236,124 @@ impl<'t> Printer<'t> { .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()); - } + // 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 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 let Some(long) = arg.get_long() { + sub.set("long", format!("--{long}")); + } - if arg.get_short().is_some() { - sub.set("value-short-braced", &braced); - sub.set("value-short", name); - } + if let Some(help) = arg.get_help() { + sub.set_md("help", help.to_string()); + } - if arg.get_long().is_some() { - sub.set("value-long-braced", &braced); - sub.set("value-long", name); - } - }; - } + 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(); + 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(); + 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(", ")), - ); - } + 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()), - ); + 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; - }; + 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(' '); + args.push(' '); - if !arg.is_required_set() { - args.push('['); - } + if !arg.is_required_set() { + args.push('['); + } - if arg.is_last_set() { - args.push_str("-- "); - } + if arg.is_last_set() { + args.push_str("-- "); + } - args.push_str(key); + args.push_str(key); - if !arg.is_required_set() { - args.push(']'); - } + if !arg.is_required_set() { + args.push(']'); + } - let sub = expander.sub("positional-lines"); - sub.set("key", key); + let sub = expander.sub("positional-lines"); + sub.set("key", key); - if let Some(help) = arg.get_help() { - sub.set("help", help); + if let Some(help) = arg.get_help() { + sub.set("help", help); + } } } - if cmd.has_subcommands() { + if !cmd.get_subcommands().collect::>().is_empty() { args.push_str(" [COMMAND]"); - } - - expander.set("positional-args", args); - - 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", ""); + 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 }