├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── bacon.toml ├── doc ├── area-dark.png ├── area-light.png ├── bacon.png ├── broot-clap-help.png ├── broot-vanilla.png ├── custom.png └── with-examples.png ├── examples ├── area │ └── main.rs ├── custom │ └── main.rs ├── with-additional-option │ └── main.rs └── with-examples │ ├── examples.rs │ └── main.rs └── src ├── lib.rs └── printer.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .bacon-locations 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ### v1.4.0 - 2025-05-09 3 | - update termimad to 0.32, crossterm to 0.29 4 | 5 | 6 | ### v1.3.2 - 2025-02-24 7 | - don't list arguments flagged as hidden 8 | 9 | 10 | ### v1.3.1 - 2024-10-26 11 | - update Termimad to 0.31 12 | 13 | 14 | ### v1.3.0 - 2024-08-12 15 | - update Termimad to 0.30 16 | 17 | 18 | ### v1.2.0 - 2024-03-05 19 | - `TEMPLATE_OPTIONS_MERGED_VALUE` alterate options template, easier to read and more compact for most applications 20 | 21 | 22 | ### v1.1.2 - 2024-02-29 23 | - fix some unwanted possible values and default values with clap 4.4.14+ - Fix #2 24 | 25 | 26 | ### v1.1.1 - 2024-01-29 27 | - list items indented as blocks 28 | 29 | 30 | ### v1.1.0 - 2024-01-18 31 | - update termimad dependency to 0.28 32 | 33 | 34 | ### v1.0.0 - 2023-09-02 35 | - clap-help is ready for 1.0 - no change is planned 36 | 37 | 38 | ### v0.7.0 - 2023-08-20 39 | - upgrade termimad to 0.25 for `skin.limit_to_ascii` (see with-examples example) 40 | 41 | 42 | ### v0.6.2 - 2023-08-03 43 | - fix template_order_mut not returning a mut reference. Rename the method to template_keys_mut 44 | - demonstrate how to add Examples with a template after the Options table 45 | 46 | 47 | ### v0.6.1 - 2023-08-02 48 | - `set_max_width` forces wrapping for a width smaller than the terminal's width 49 | 50 | 51 | ### v0.6.0 - 2023-08-02 52 | - place centered titles based on the content's width, not the terminal's width 53 | 54 | 55 | ### v0.5.0 - 2023-07-18 56 | - add `--` before the last args 57 | 58 | 59 | ### v0.4.0 - 2023-07-16 60 | - templates and their order can be completely customized 61 | 62 | 63 | ### v0.3.0 - 2023-07-16 64 | - optional positional arguments displayed between square braces 65 | 66 | 67 | ### v0.2.0 - 2023-07-16 68 | - value column 69 | - option to show or not author 70 | 71 | 72 | ### v0.1.0 - 2023-07-15 73 | - first release 74 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "clap-help" 3 | version = "1.4.0" 4 | edition = "2021" 5 | authors = ["dystroy "] 6 | repository = "https://github.com/Canop/clap-help" 7 | description = "utility printing help of a clap application" 8 | keywords = ["terminal"] 9 | categories = ["command-line-interface"] 10 | license = "MIT" 11 | readme = "README.md" 12 | rust-version = "1.65" 13 | 14 | [features] 15 | default = [] 16 | 17 | [dependencies] 18 | clap = { version = "4.4", features = ["derive", "cargo"] } 19 | termimad = "0.32" 20 | terminal-light = "1.8" 21 | 22 | [patch.crates-io] 23 | # termimad = { path = "../termimad" } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clap-help 2 | 3 | [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] 4 | 5 | [s1]: https://img.shields.io/crates/v/clap-help.svg 6 | [l1]: https://crates.io/crates/clap-help 7 | 8 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 9 | [l2]: LICENSE 10 | 11 | [s3]: https://docs.rs/clap-help/badge.svg 12 | [l3]: https://docs.rs/clap-help/ 13 | 14 | [s4]: https://miaou.dystroy.org/static/shields/room.svg 15 | [l4]: https://miaou.dystroy.org/3768?rust 16 | 17 | ## Purpose and Features 18 | 19 | **clap-help** prints the --help message of [clap](https://docs.rs/clap/) based terminal applications. 20 | 21 | ### Differences with the vanilla help renderer of the clap crate: 22 | 23 | - more readable, thanks to a width aware layout 24 | - more compact: from 2 to 3 times less lines compared to vanilla 25 | - options rendered in a balanced table, optimized for the width of the terminal 26 | - introduction interpreted as Markdown, allowing lists, tables, code blocks, etc. 27 | - doc of options interpreted as Markdown 28 | - skin automatically selected for light or dark terminals 29 | - customizable [termimad](https://github.com/Canop/termimad/) skin 30 | - you can customize section templates, remove them, reorder them, add sections 31 | 32 | Note: there's no support for subcommands yet. 33 | 34 | ## Example 35 | 36 | The [bacon](https://dystroy.org/bacon) programs uses clap-help with an introduction text, a clearer options table, examples, and a skin consistent with the rest of the application: 37 | 38 | ![bacon](doc/bacon.png) 39 | 40 | How it's done: [https://github.com/Canop/bacon/blob/main/src/args.rs](https://github.com/Canop/bacon/blob/main/src/args.rs). 41 | 42 | ## Usage 43 | 44 | ### Basic usage 45 | 46 | Your program needs a clap `Command` defined. 47 | 48 | Here's for example with clap-derive: 49 | 50 | ```rust 51 | #[derive(Parser, Debug)] 52 | #[command(name="area", author, version, about, disable_help_flag = true)] 53 | struct Args { 54 | 55 | /// Print help 56 | #[arg(long)] 57 | help: bool, 58 | 59 | /// Height, that is the distance between bottom and top 60 | #[arg(short, long, default_value = "9")] 61 | height: u16, 62 | 63 | /// Width, from there, to there, eg `4` or `5` 64 | #[arg(short, long, default_value = "3")] 65 | width: u16, 66 | 67 | /// Kill all birds to improve computation 68 | #[arg(short, long)] 69 | kill_birds: bool, 70 | 71 | /// Computation strategy 72 | #[arg(long, default_value = "fast")] 73 | strategy: Strategy, 74 | 75 | /// Bird separator 76 | #[arg(short, long, value_name = "SEP")] 77 | separator: Option, 78 | 79 | /// Root Directory 80 | pub root: Option, 81 | } 82 | ``` 83 | 84 | Notice 85 | * the `disable_help_flag = true` disabling the standard behaviour of clap regarding help. 86 | * the explicit `help` argument. Here it's with only `#[arg(long)]` because `-h` is used for something more important but you would most often have `#[arg(short, long)]`. 87 | 88 | The help introduction (the part before usage) is defined as a string which will be interpreted as Markdown. It can contain tables, lists, bold, italic, inline code, code blocks, etc. 89 | 90 | ```rust 91 | static INTRO: &str = " 92 | 93 | Compute `height x width` 94 | *You can do it either precisely (enough) or fast (I mean not too slow)*. 95 | "; 96 | ``` 97 | 98 | On program launch, you should check the value of the `help` flag and, if necessary, print the help: 99 | 100 | ```rust 101 | let args = Args::parse(); 102 | if args.help { 103 | Printer::new(Args::command()) 104 | .with("introduction", INTRO) 105 | .without("author") 106 | .print_help(); 107 | return; 108 | } 109 | ``` 110 | 111 | Help rendered in a light terminal: 112 | 113 | ![area light](doc/area-light.png) 114 | 115 | Same help in a dark terminal: 116 | 117 | ![area dark](doc/area-dark.png) 118 | 119 | Complete example is in `/examples/area` and can be seen with `cargo run --example area -- --help` 120 | 121 | ### Adding custom sections 122 | 123 | Help is usually easier to grasp with a few examples. 124 | You can write a few ones in your intro, or you can add them in a later section, after the options. 125 | 126 | It's also possible to leverage the template system, which is what is done in the `with-examples` example, for this result: 127 | 128 | ![with-examples](doc/with-examples.png) 129 | 130 | Here's how it's done: 131 | 132 | ```rust 133 | static EXAMPLES_TEMPLATE: &str = " 134 | **Examples:** 135 | 136 | ${examples 137 | **${example-number})** ${example-title}: `${example-cmd}` 138 | ${example-comments} 139 | } 140 | "; 141 | ``` 142 | 143 | ```rust 144 | 145 | let mut printer = clap_help::Printer::new(Args::command()) 146 | .with("introduction", INTRO_TEMPLATE) 147 | .without("author"); 148 | printer.template_keys_mut().push("examples"); 149 | printer.set_template("examples", EXAMPLES_TEMPLATE); 150 | for (i, example) in EXAMPLES.iter().enumerate() { 151 | printer 152 | .expander_mut() 153 | .sub("examples") 154 | .set("example-number", i + 1) 155 | .set("example-title", example.title) 156 | .set("example-cmd", example.cmd) 157 | .set_md("example-comments", example.comments); 158 | } 159 | printer.print_help(); 160 | ``` 161 | 162 | [complete code of the example](examples/with-examples/main.rs) 163 | 164 | 165 | ### Changing the skin 166 | 167 | If your program has some kind of graphical identity, you may want to extend it to the help. 168 | 169 | You may change colors, preferably with more compatible [ansi color codes](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). 170 | 171 | See example in `examples/custom` mainly features: 172 | 173 | ![custom](doc/custom.png) 174 | 175 | The strategy for those changes is 176 | 177 | * to redefine the `bold`, `italic`, and `inline_code` styles to change their foreground color, to remove the background of the code, and to remove the Italic attribute of `italic` 178 | * to use the `TEMPLATE_OPTIONS_MERGED_VALUE` template for options 179 | 180 | Here are the relevant parts of the code: 181 | 182 | 183 | ```rust 184 | static INTRO: &str = " 185 | 186 | Compute `height x width` 187 | More info at *https://dystroy.org* 188 | "; 189 | 190 | let mut printer = Printer::new(Args::command()) 191 | .without("author") 192 | .with("introduction", INTRO) 193 | .with("options", clap_help::TEMPLATE_OPTIONS_MERGED_VALUE); 194 | let skin = printer.skin_mut(); 195 | skin.headers[0].compound_style.set_fg(ansi(202)); 196 | skin.bold.set_fg(ansi(202)); 197 | skin.italic = termimad::CompoundStyle::with_fg(ansi(45)); 198 | skin.inline_code = termimad::CompoundStyle::with_fg(ansi(223)); 199 | skin.table_border_chars = termimad::ROUNDED_TABLE_BORDER_CHARS; 200 | printer.print_help(); 201 | ``` 202 | 203 | Complete example is in `/examples/custom` and can be seen with `cargo run --example custom -- --help` 204 | 205 | Please note that not every customization is possible or easy. 206 | And some may be easy but not obvious. 207 | Come to [the chat](https://miaou.dystroy.org/3768?rust) and ask if needed. 208 | 209 | -------------------------------------------------------------------------------- /bacon.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the bacon tool 2 | # 3 | # Bacon repository: https://github.com/Canop/bacon 4 | # Complete help on configuration: https://dystroy.org/bacon/config/ 5 | # You can also check bacon's own bacon.toml file 6 | # as an example: https://github.com/Canop/bacon/blob/main/bacon.toml 7 | 8 | default_job = "check-all" 9 | 10 | [jobs.check] 11 | command = ["cargo", "check", "--color", "always"] 12 | need_stdout = false 13 | 14 | [jobs.check-all] 15 | command = ["cargo", "check", "--all-targets", "--color", "always"] 16 | need_stdout = false 17 | 18 | [jobs.clippy-all] 19 | command = [ 20 | "cargo", "clippy", 21 | "--all-targets", 22 | "--color", "always", 23 | "--", 24 | "-A", "clippy::needless_doctest_main", 25 | ] 26 | need_stdout = false 27 | 28 | [jobs.test] 29 | command = [ 30 | "cargo", "test", "--color", "always", 31 | "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 32 | ] 33 | need_stdout = true 34 | 35 | [jobs.doc] 36 | command = ["cargo", "doc", "--color", "always", "--no-deps"] 37 | need_stdout = false 38 | 39 | # If the doc compiles, then it opens in your browser and bacon switches 40 | # to the previous job 41 | [jobs.doc-open] 42 | command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] 43 | need_stdout = false 44 | on_success = "back" # so that we don't open the browser at each change 45 | 46 | # You can run your application and have the result displayed in bacon, 47 | # *if* it makes sense for this crate. You can run an example the same 48 | # way. Don't forget the `--color always` part or the errors won't be 49 | # properly parsed. 50 | [jobs.run] 51 | command = [ 52 | "cargo", "run", 53 | "--color", "always", 54 | # put launch parameters for your program behind a `--` separator 55 | ] 56 | need_stdout = true 57 | allow_warnings = true 58 | 59 | # You may define here keybindings that would be specific to 60 | # a project, for example a shortcut to launch a specific job. 61 | # Shortcuts to internal functions (scrolling, toggling, etc.) 62 | # should go in your personal global prefs.toml file instead. 63 | [keybindings] 64 | # alt-m = "job:my-job" 65 | -------------------------------------------------------------------------------- /doc/area-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/area-dark.png -------------------------------------------------------------------------------- /doc/area-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/area-light.png -------------------------------------------------------------------------------- /doc/bacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/bacon.png -------------------------------------------------------------------------------- /doc/broot-clap-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/broot-clap-help.png -------------------------------------------------------------------------------- /doc/broot-vanilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/broot-vanilla.png -------------------------------------------------------------------------------- /doc/custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/custom.png -------------------------------------------------------------------------------- /doc/with-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/clap-help/9df8c909395e0e4cd3d28ecba6ffdf90b4c8aee3/doc/with-examples.png -------------------------------------------------------------------------------- /examples/area/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::{CommandFactory, Parser, ValueEnum}, 3 | clap_help::Printer, 4 | }; 5 | 6 | static INTRO: &str = " 7 | Compute `height x width` 8 | *You can do it either precisely (enough) or fast (I mean not too slow)*. 9 | "; 10 | 11 | /// Application launch arguments 12 | #[derive(Parser, Debug)] 13 | #[command(name = "area", author, version, about, disable_help_flag = true)] 14 | struct Args { 15 | /// Print help 16 | #[arg(long)] 17 | help: bool, 18 | 19 | /// Height, that is the distance between bottom and top 20 | #[arg(short, long, default_value = "9")] 21 | height: u16, 22 | 23 | /// Width, from there, to there, eg `4` or `5` 24 | #[arg(short, long, default_value = "3")] 25 | width: u16, 26 | 27 | /// Kill all birds to improve computation 28 | #[arg(short, long)] 29 | kill_birds: bool, 30 | 31 | /// Computation strategy 32 | #[arg(long, default_value = "fast")] 33 | strategy: Strategy, 34 | 35 | /// Bird separator 36 | #[arg(short, long, value_name = "SEP")] 37 | separator: Option, 38 | 39 | /// Root Directory 40 | pub root: Option, 41 | } 42 | 43 | #[derive(ValueEnum, Clone, Copy, Debug)] 44 | enum Strategy { 45 | Fast, 46 | Precise, 47 | } 48 | 49 | fn main() { 50 | let args = Args::parse(); 51 | 52 | if args.help { 53 | Printer::new(Args::command()) 54 | .with("introduction", INTRO) 55 | .without("author") 56 | .print_help(); 57 | return; 58 | } 59 | 60 | let (w, h) = (args.width, args.height); 61 | println!("Computation strategy: {:?}", args.strategy); 62 | println!("{w} x {h} = {}", w * h); 63 | } 64 | -------------------------------------------------------------------------------- /examples/custom/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::{CommandFactory, Parser, ValueEnum}, 3 | clap_help::Printer, 4 | termimad::ansi, 5 | }; 6 | 7 | static INTRO: &str = " 8 | 9 | Compute `height x width` 10 | More info at *https://dystroy.org* 11 | "; 12 | 13 | /// Application launch arguments 14 | #[derive(Parser, Debug)] 15 | #[command(name = "custom", author, version, about, disable_help_flag = true)] 16 | struct Args { 17 | /// Print help 18 | #[arg(long)] 19 | help: bool, 20 | 21 | /// Height, that is the distance between bottom and top 22 | #[arg(short, long, default_value = "9")] 23 | height: u16, 24 | 25 | /// Width, from there, to there, eg `4` or `5` 26 | #[arg(short, long, default_value = "3")] 27 | width: u16, 28 | 29 | /// Kill all birds to improve computation 30 | #[arg(short, long)] 31 | kill_birds: bool, 32 | 33 | /// Computation strategy 34 | #[arg(long, default_value = "fast")] 35 | strategy: Strategy, 36 | 37 | /// Bird separator 38 | #[arg(short, long, value_name = "SEP")] 39 | separator: Option, 40 | 41 | /// Root Directory 42 | pub root: Option, 43 | } 44 | 45 | #[derive(ValueEnum, Clone, Copy, Debug)] 46 | enum Strategy { 47 | Fast, 48 | Precise, 49 | } 50 | 51 | fn main() { 52 | let args = Args::parse(); 53 | 54 | if args.help { 55 | let mut printer = Printer::new(Args::command()) 56 | .without("author") 57 | .with("introduction", INTRO) 58 | .with("options", clap_help::TEMPLATE_OPTIONS_MERGED_VALUE); 59 | let skin = printer.skin_mut(); 60 | skin.headers[0].compound_style.set_fg(ansi(202)); 61 | skin.bold.set_fg(ansi(202)); 62 | skin.italic = termimad::CompoundStyle::with_fg(ansi(45)); 63 | skin.inline_code = termimad::CompoundStyle::with_fg(ansi(223)); 64 | skin.table_border_chars = termimad::ROUNDED_TABLE_BORDER_CHARS; 65 | printer.print_help(); 66 | return; 67 | } 68 | 69 | let (w, h) = (args.width, args.height); 70 | println!("Computation strategy: {:?}", args.strategy); 71 | println!("{w} x {h} = {}", w * h); 72 | } 73 | -------------------------------------------------------------------------------- /examples/with-additional-option/main.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::{CommandFactory, Parser}, 3 | }; 4 | 5 | 6 | /// Application launch arguments 7 | #[derive(Parser, Debug)] 8 | #[command(name = "wao", author, version, about, disable_help_flag = true)] 9 | struct Args { 10 | /// Print help 11 | #[arg(long)] 12 | help: bool, 13 | 14 | /// Height, that is the distance between bottom and top 15 | #[arg(short, long, default_value = "9")] 16 | height: u16, 17 | 18 | /// Width, from there, to there, eg `4` or `5` 19 | #[arg(short, long, default_value = "3")] 20 | width: u16, 21 | 22 | /// Root Directory 23 | pub root: Option, 24 | } 25 | 26 | pub fn print_help() { 27 | let mut printer = clap_help::Printer::new(Args::command()) 28 | .without("author"); 29 | printer 30 | .expander_mut() 31 | .sub("option-lines") 32 | .set("short", "-z") 33 | .set("long", "--zeta") 34 | .set("value", "ZETA") 35 | .set("help", "Set the index of the last letter of the greek alphabet"); 36 | printer.print_help(); 37 | } 38 | 39 | fn main() { 40 | let args = Args::parse(); 41 | 42 | if args.help { 43 | print_help(); 44 | return; 45 | } 46 | 47 | let (w, h) = (args.width, args.height); 48 | println!("{w} x {h} = {}", w * h); 49 | } 50 | -------------------------------------------------------------------------------- /examples/with-examples/examples.rs: -------------------------------------------------------------------------------- 1 | pub struct Example { 2 | pub title: &'static str, 3 | pub cmd: &'static str, 4 | pub comments: &'static str, 5 | } 6 | 7 | impl Example { 8 | pub const fn new(title: &'static str, cmd: &'static str, comments: &'static str) -> Self { 9 | Self { 10 | title, 11 | cmd, 12 | comments, 13 | } 14 | } 15 | } 16 | 17 | pub static EXAMPLES: &[Example] = &[ 18 | Example::new("Default computation on your prefered path", "withex ~", ""), 19 | Example::new( 20 | "Compute for a height of 37", 21 | "withex -h 37", 22 | "This uses the default value (`3`) for the width 23 | ", 24 | ), 25 | Example::new( 26 | "Maximum precision", 27 | "withex -h 37 -w 28 --strategy precise", 28 | "This may take a while but it's *super* precise", 29 | ), 30 | ]; 31 | -------------------------------------------------------------------------------- /examples/with-examples/main.rs: -------------------------------------------------------------------------------- 1 | mod examples; 2 | 3 | use { 4 | clap::{CommandFactory, Parser, ValueEnum}, 5 | examples::*, 6 | }; 7 | 8 | static INTRO_TEMPLATE: &str = " 9 | Compute `height x width` 10 | "; 11 | 12 | static EXAMPLES_TEMPLATE: &str = " 13 | **Examples:** 14 | 15 | ${examples 16 | **${example-number})** ${example-title}: `${example-cmd}` 17 | ${example-comments} 18 | } 19 | "; 20 | 21 | /// Application launch arguments 22 | #[derive(Parser, Debug)] 23 | #[command(name = "withex", author, version, about, disable_help_flag = true)] 24 | struct Args { 25 | /// Print help 26 | #[arg(long)] 27 | help: bool, 28 | 29 | /// Only use ASCII characters 30 | #[arg(long)] 31 | ascii: bool, 32 | 33 | /// Height, that is the distance between bottom and top 34 | #[arg(short, long, default_value = "9")] 35 | height: u16, 36 | 37 | /// Width, from there, to there, eg `4` or `5` 38 | #[arg(short, long, default_value = "3")] 39 | width: u16, 40 | 41 | /// Computation strategy 42 | #[arg(long, default_value = "fast")] 43 | strategy: Strategy, 44 | 45 | /// Root Directory 46 | pub root: Option, 47 | } 48 | 49 | #[derive(ValueEnum, Clone, Copy, Debug)] 50 | enum Strategy { 51 | Fast, 52 | Precise, 53 | } 54 | 55 | pub fn print_help() { 56 | let args = Args::parse(); 57 | let mut printer = clap_help::Printer::new(Args::command()) 58 | .with("introduction", INTRO_TEMPLATE) 59 | .without("author"); 60 | if args.ascii { 61 | printer.skin_mut().limit_to_ascii(); 62 | } 63 | printer.template_keys_mut().push("examples"); 64 | printer.set_template("examples", EXAMPLES_TEMPLATE); 65 | for (i, example) in EXAMPLES.iter().enumerate() { 66 | printer 67 | .expander_mut() 68 | .sub("examples") 69 | .set("example-number", i + 1) 70 | .set("example-title", example.title) 71 | .set("example-cmd", example.cmd) 72 | .set_md("example-comments", example.comments); 73 | } 74 | printer.print_help(); 75 | } 76 | 77 | fn main() { 78 | let args = Args::parse(); 79 | 80 | if args.help { 81 | print_help(); 82 | return; 83 | } 84 | 85 | let (w, h) = (args.width, args.height); 86 | println!("Computation strategy: {:?}", args.strategy); 87 | println!("{w} x {h} = {}", w * h); 88 | } 89 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | clap-help is an alternate help printer for applications using clap. 4 | 5 | clap-help displays arguments in a more readable format, in a width-aware table and lets you customize the content and style. 6 | 7 | Minimal usage: 8 | 9 | 1. disable the standard clap help printer with `disable_help_flag = true` 10 | 2. add your own help flag 11 | 3. call `clap_help::print_help` in the handler for your help flag 12 | 13 | ```rust 14 | use clap::{CommandFactory, Parser, ValueEnum}; 15 | use clap_help::Printer; 16 | 17 | #[derive(Parser, Debug)] 18 | #[command(name="my_prog", author, version, about, disable_help_flag = true)] 19 | struct Args { 20 | 21 | /// Print help 22 | #[arg(long)] 23 | help: bool, 24 | 25 | // other arguments 26 | } 27 | 28 | fn main() { 29 | let args = Args::parse(); 30 | if args.help { 31 | Printer::new(Args::command()).print_help(); 32 | return; 33 | } 34 | 35 | // rest of the program 36 | } 37 | 38 | ``` 39 | 40 | The examples directory shows how to customize the help. 41 | 42 | */ 43 | 44 | mod printer; 45 | 46 | pub use printer::*; 47 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | clap::{ArgAction, Command}, 3 | std::collections::HashMap, 4 | termimad::{ 5 | minimad::{OwningTemplateExpander, TextTemplate}, 6 | FmtText, MadSkin, 7 | }, 8 | }; 9 | 10 | /// Default template for the "title" section 11 | pub static TEMPLATE_TITLE: &str = "# **${name}** ${version}"; 12 | 13 | /// Default template for the "author" section 14 | pub static TEMPLATE_AUTHOR: &str = " 15 | *by* ${author} 16 | "; 17 | 18 | /// Default template for the "usage" section 19 | pub static TEMPLATE_USAGE: &str = " 20 | **Usage: ** `${name} [options]${positional-args}` 21 | "; 22 | 23 | /// Default template for the "positionals" section 24 | pub static TEMPLATE_POSITIONALS: &str = " 25 | ${positional-lines 26 | * `${key}` : ${help} 27 | } 28 | "; 29 | 30 | /// Default template for the "options" section 31 | pub static TEMPLATE_OPTIONS: &str = " 32 | **Options:** 33 | |:-:|:-:|:-:|:-| 34 | |short|long|value|description| 35 | |:-:|:-|:-:|:-| 36 | ${option-lines 37 | |${short}|${long}|${value}|${help}${possible_values}${default}| 38 | } 39 | |- 40 | "; 41 | 42 | /// a template for the "options" section with the value merged to short and long 43 | pub static TEMPLATE_OPTIONS_MERGED_VALUE: &str = " 44 | **Options:** 45 | |:-:|:-:|:-| 46 | |short|long|description| 47 | |:-:|:-|:-| 48 | ${option-lines 49 | |${short} *${value-short-braced}*|${long} *${value-long-braced}*|${help}${possible_values}${default}| 50 | } 51 | |- 52 | "; 53 | 54 | /// Keys used to enable/disable/change templates 55 | pub static TEMPLATES: &[&str] = &[ 56 | "title", 57 | "author", 58 | "introduction", 59 | "usage", 60 | "positionals", 61 | "options", 62 | "bugs", 63 | ]; 64 | 65 | /// An object which you can configure to print the help of a command 66 | /// 67 | /// For example, changing the color of bold text and using an alternate 68 | /// template for the options section: 69 | /// 70 | /// ```rust 71 | /// use clap::{CommandFactory, Parser, ValueEnum}; 72 | /// use clap_help::Printer; 73 | /// 74 | /// #[derive(Parser, Debug)] 75 | /// #[command(author, version, about, disable_help_flag = true)] 76 | /// struct Args { 77 | /// 78 | /// /// Print help 79 | /// #[arg(long)] 80 | /// help: bool, 81 | /// 82 | /// /// Comma separated list of features 83 | /// #[clap(long, value_name = "features")] 84 | /// pub features: Option, 85 | /// } 86 | /// 87 | /// fn main() { 88 | /// let args = Args::parse(); 89 | /// if args.help { 90 | /// let mut printer = clap_help::Printer::new(Args::command()) 91 | /// .with("options", clap_help::TEMPLATE_OPTIONS_MERGED_VALUE); 92 | /// printer.skin_mut().bold.set_fg(termimad::ansi(204)); 93 | /// printer.print_help(); 94 | /// return; 95 | /// } 96 | /// // rest of the program 97 | /// } 98 | /// 99 | /// ``` 100 | pub struct Printer<'t> { 101 | skin: MadSkin, 102 | expander: OwningTemplateExpander<'static>, 103 | template_keys: Vec<&'static str>, 104 | templates: HashMap<&'static str, &'t str>, 105 | pub full_width: bool, 106 | pub max_width: Option, 107 | } 108 | 109 | impl<'t> Printer<'t> { 110 | pub fn new(mut cmd: Command) -> Self { 111 | cmd.build(); 112 | let expander = Self::make_expander(&cmd); 113 | let mut templates = HashMap::new(); 114 | templates.insert("title", TEMPLATE_TITLE); 115 | templates.insert("author", TEMPLATE_AUTHOR); 116 | templates.insert("usage", TEMPLATE_USAGE); 117 | templates.insert("positionals", TEMPLATE_POSITIONALS); 118 | templates.insert("options", TEMPLATE_OPTIONS); 119 | Self { 120 | skin: Self::make_skin(), 121 | expander, 122 | templates, 123 | template_keys: TEMPLATES.to_vec(), 124 | full_width: false, 125 | max_width: None, 126 | } 127 | } 128 | /// Build a skin for the detected theme of the terminal 129 | /// (i.e. dark, light, or other) 130 | pub fn make_skin() -> MadSkin { 131 | match terminal_light::luma() { 132 | Ok(luma) if luma > 0.85 => MadSkin::default_light(), 133 | Ok(luma) if luma < 0.2 => MadSkin::default_dark(), 134 | _ => MadSkin::default(), 135 | } 136 | } 137 | /// Use the provided skin 138 | pub fn with_skin(mut self, skin: MadSkin) -> Self { 139 | self.skin = skin; 140 | self 141 | } 142 | /// Set a maximal width, so that the whole terminal width isn't used. 143 | /// 144 | /// This may make some long sentences easier to read on super wide 145 | /// terminals, especially when the whole text is short. 146 | /// Depending on your texts and parameters, you may set up a width 147 | /// of 100 or 150. 148 | pub fn with_max_width(mut self, w: usize) -> Self { 149 | self.max_width = Some(w); 150 | self 151 | } 152 | /// Give a mutable reference to the current skin 153 | /// (by default the automatically selected one) 154 | /// so that it can be modified 155 | pub fn skin_mut(&mut self) -> &mut MadSkin { 156 | &mut self.skin 157 | } 158 | /// Change a template 159 | pub fn set_template(&mut self, key: &'static str, template: &'t str) { 160 | self.templates.insert(key, template); 161 | } 162 | /// Change or add a template 163 | pub fn with(mut self, key: &'static str, template: &'t str) -> Self { 164 | self.set_template(key, template); 165 | self 166 | } 167 | /// Unset a template 168 | pub fn without(mut self, key: &'static str) -> Self { 169 | self.templates.remove(key); 170 | self 171 | } 172 | /// A mutable reference to the list of template keys, so that you can 173 | /// insert new keys, or change their order. 174 | /// Any key without matching template will just be ignored 175 | pub fn template_keys_mut(&mut self) -> &mut Vec<&'static str> { 176 | &mut self.template_keys 177 | } 178 | /// A mutable reference to the list of template keys, so that you can 179 | /// insert new keys, or change their order. 180 | /// Any key without matching template will just be ignored 181 | #[deprecated(since = "0.6.2", note = "use template_keys_mut instead")] 182 | pub fn template_order_mut(&mut self) -> &mut Vec<&'static str> { 183 | &mut self.template_keys 184 | } 185 | fn make_expander(cmd: &Command) -> OwningTemplateExpander<'static> { 186 | let mut expander = OwningTemplateExpander::new(); 187 | expander.set_default(""); 188 | let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name()); 189 | expander.set("name", name); 190 | if let Some(author) = cmd.get_author() { 191 | expander.set("author", author); 192 | } 193 | if let Some(version) = cmd.get_version() { 194 | expander.set("version", version); 195 | } 196 | let options = cmd 197 | .get_arguments() 198 | .filter(|a| !a.is_hide_set()) 199 | .filter(|a| a.get_short().is_some() || a.get_long().is_some()); 200 | for arg in options { 201 | let sub = expander.sub("option-lines"); 202 | if let Some(short) = arg.get_short() { 203 | sub.set("short", format!("-{short}")); 204 | } 205 | if let Some(long) = arg.get_long() { 206 | sub.set("long", format!("--{long}")); 207 | } 208 | if let Some(help) = arg.get_help() { 209 | sub.set_md("help", help.to_string()); 210 | } 211 | if arg.get_action().takes_values() { 212 | if let Some(name) = arg.get_value_names().and_then(|arr| arr.first()) { 213 | sub.set("value", name); 214 | let braced = format!("<{}>", name); 215 | sub.set("value-braced", &braced); 216 | if arg.get_short().is_some() { 217 | sub.set("value-short-braced", &braced); 218 | sub.set("value-short", name); 219 | } 220 | if arg.get_long().is_some() { 221 | sub.set("value-long-braced", &braced); 222 | sub.set("value-long", name); 223 | } 224 | }; 225 | } 226 | let mut possible_values = arg.get_possible_values(); 227 | if !possible_values.is_empty() { 228 | let possible_values: Vec = possible_values 229 | .drain(..) 230 | .map(|v| format!("`{}`", v.get_name())) 231 | .collect(); 232 | expander.sub("option-lines").set_md( 233 | "possible_values", 234 | format!(" Possible values: [{}]", possible_values.join(", ")), 235 | ); 236 | } 237 | if let Some(default) = arg.get_default_values().first() { 238 | match arg.get_action() { 239 | ArgAction::Set | ArgAction::Append => { 240 | expander.sub("option-lines").set_md( 241 | "default", 242 | format!(" Default: `{}`", default.to_string_lossy()), 243 | ); 244 | } 245 | _ => {} 246 | } 247 | } 248 | } 249 | let mut args = String::new(); 250 | for arg in cmd.get_positionals() { 251 | let Some(key) = arg.get_value_names().and_then(|arr| arr.first()) else { 252 | continue; 253 | }; 254 | args.push(' '); 255 | if !arg.is_required_set() { 256 | args.push('['); 257 | } 258 | if arg.is_last_set() { 259 | args.push_str("-- "); 260 | } 261 | args.push_str(key); 262 | if !arg.is_required_set() { 263 | args.push(']'); 264 | } 265 | let sub = expander.sub("positional-lines"); 266 | sub.set("key", key); 267 | if let Some(help) = arg.get_help() { 268 | sub.set("help", help); 269 | } 270 | } 271 | expander.set("positional-args", args); 272 | expander 273 | } 274 | /// Give you a mut reference to the expander, so that you can overload 275 | /// the variable of the expander used to fill the templates of the help, 276 | /// or add new variables for your own templates 277 | pub fn expander_mut(&mut self) -> &mut OwningTemplateExpander<'static> { 278 | &mut self.expander 279 | } 280 | /// Print the provided template with the printer's expander 281 | /// 282 | /// It's normally more convenient to change template_keys or some 283 | /// templates, unless you want none of the standard templates 284 | pub fn print_template(&self, template: &str) { 285 | self.skin.print_owning_expander_md(&self.expander, template); 286 | } 287 | /// Print all the templates, in order 288 | pub fn print_help(&self) { 289 | if self.full_width { 290 | self.print_help_full_width() 291 | } else { 292 | self.print_help_content_width() 293 | } 294 | } 295 | fn print_help_full_width(&self) { 296 | for key in &self.template_keys { 297 | if let Some(template) = self.templates.get(key) { 298 | self.print_template(template); 299 | } 300 | } 301 | } 302 | fn print_help_content_width(&self) { 303 | let (width, _) = termimad::terminal_size(); 304 | let mut width = width as usize; 305 | if let Some(max_width) = self.max_width { 306 | width = width.min(max_width); 307 | } 308 | let mut texts: Vec = self 309 | .template_keys 310 | .iter() 311 | .filter_map(|key| self.templates.get(key)) 312 | .map(|&template| { 313 | let template = TextTemplate::from(template); 314 | let text = self.expander.expand(&template); 315 | FmtText::from_text(&self.skin, text, Some(width)) 316 | }) 317 | .collect(); 318 | let content_width = texts 319 | .iter() 320 | .fold(0, |cw, text| cw.max(text.content_width())); 321 | for text in &mut texts { 322 | text.set_rendering_width(content_width); 323 | println!("{}", text); 324 | } 325 | } 326 | } 327 | --------------------------------------------------------------------------------