Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
231 changes: 166 additions & 65 deletions src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand All @@ -59,6 +71,7 @@ pub static TEMPLATES: &[&str] = &[
"usage",
"positionals",
"options",
"subcommands",
"bugs",
];

Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -149,141 +176,202 @@ 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
#[deprecated(since = "0.6.2", note = "use template_keys_mut instead")]
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<String> = 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::<Vec<_>>()
.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<String> = 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::<Vec<_>>().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::<Vec<_>>().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
/// templates, unless you want none of the standard templates
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 {
Expand All @@ -292,19 +380,23 @@ 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) {
self.print_template(template);
}
}
}

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<FmtText> = self
.template_keys
.iter()
Expand All @@ -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<Self> {
cmd.build();
cmd.find_subcommand(subcommand_name)
.map(|subcmd| Self::new(subcmd.clone()))
}
}