├── .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 | 
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 | 
114 |
115 | Same help in a dark terminal:
116 |
117 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------