clap_help/
printer.rs

1use {
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
11pub static TEMPLATE_TITLE: &str = "# **${name}** ${version}";
12
13/// Default template for the "author" section
14pub static TEMPLATE_AUTHOR: &str = "
15*by* ${author}
16";
17
18/// Default template for the "usage" section
19pub static TEMPLATE_USAGE: &str = "
20**Usage: ** `${name} [options]${positional-args}`
21";
22
23/// Default template for the "positionals" section
24pub static TEMPLATE_POSITIONALS: &str = "
25${positional-lines
26* `${key}` : ${help}
27}
28";
29
30/// Default template for the "options" section
31pub 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
43pub 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
55pub 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<String>,
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/// ```
100pub 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<usize>,
107}
108
109impl<'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<String> = 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<FmtText> = 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}