Skip to main content

clap_help/
printer.rs

1use {
2    clap::{ArgAction, Command},
3    std::{
4        collections::HashMap,
5        io::{self, Write},
6    },
7    termimad::{
8        minimad::{OwningTemplateExpander, TextTemplate},
9        FmtText, MadSkin,
10    },
11};
12
13/// Default template for the "title" section
14pub static TEMPLATE_TITLE: &str = "# **${name}** ${version}";
15
16/// Default template for the "author" section
17pub static TEMPLATE_AUTHOR: &str = "
18*by* ${author}
19";
20
21/// Default template for the "usage" section
22pub static TEMPLATE_USAGE: &str = "
23**Usage: ** `${name} [options]${positional-args}`
24";
25
26/// Default template for the "positionals" section
27pub static TEMPLATE_POSITIONALS: &str = "
28${positional-lines
29* `${key}` : ${help}
30}
31";
32
33/// Default template for the "options" section
34pub static TEMPLATE_OPTIONS: &str = "
35**Options:**
36|:-:|:-:|:-:|:-|
37|short|long|value|description|
38|:-:|:-|:-:|:-|
39${option-lines
40|${short}|${long}|${value}|${help}${possible_values}${default}|
41}
42|-
43";
44
45/// a template for the "options" section with the value merged to short and long
46pub static TEMPLATE_OPTIONS_MERGED_VALUE: &str = "
47**Options:**
48|:-:|:-:|:-|
49|short|long|description|
50|:-:|:-|:-|
51${option-lines
52|${short} *${value-short-braced}*|${long} *${value-long-braced}*|${help}${possible_values}${default}|
53}
54|-
55";
56
57/// Keys used to enable/disable/change templates
58pub static TEMPLATES: &[&str] = &[
59    "title",
60    "author",
61    "introduction",
62    "usage",
63    "positionals",
64    "options",
65    "bugs",
66];
67
68/// An object which you can configure to print the help of a command
69///
70/// For example, changing the color of bold text and using an alternate
71///   template for the options section:
72///
73/// ```rust
74/// use clap::{CommandFactory, Parser, ValueEnum};
75/// use clap_help::Printer;
76///
77/// #[derive(Parser, Debug)]
78/// #[command(author, version, about, disable_help_flag = true)]
79/// struct Args {
80///
81///     /// Print help
82///     #[arg(long)]
83///     help: bool,
84///
85///     /// Comma separated list of features
86///     #[clap(long, value_name = "features")]
87///     pub features: Option<String>,
88/// }
89///
90/// fn main() {
91///     let args = Args::parse();
92///     if args.help {
93///         let mut printer = clap_help::Printer::new(Args::command())
94///             .with("options", clap_help::TEMPLATE_OPTIONS_MERGED_VALUE);
95///         printer.skin_mut().bold.set_fg(termimad::ansi(204));
96///         printer.print_help();
97///         return;
98///     }
99///     // rest of the program
100/// }
101///
102/// ```
103pub struct Printer<'t> {
104    skin: MadSkin,
105    expander: OwningTemplateExpander<'static>,
106    template_keys: Vec<&'static str>,
107    templates: HashMap<&'static str, &'t str>,
108    pub full_width: bool,
109    pub max_width: Option<usize>,
110}
111
112impl<'t> Printer<'t> {
113    pub fn new(mut cmd: Command) -> Self {
114        cmd.build();
115        let expander = Self::make_expander(&cmd);
116        let mut templates = HashMap::new();
117        templates.insert("title", TEMPLATE_TITLE);
118        templates.insert("author", TEMPLATE_AUTHOR);
119        templates.insert("usage", TEMPLATE_USAGE);
120        templates.insert("positionals", TEMPLATE_POSITIONALS);
121        templates.insert("options", TEMPLATE_OPTIONS);
122        Self {
123            skin: Self::make_skin(),
124            expander,
125            templates,
126            template_keys: TEMPLATES.to_vec(),
127            full_width: false,
128            max_width: None,
129        }
130    }
131    /// Build a skin for the detected theme of the terminal
132    /// (i.e. dark, light, or other)
133    pub fn make_skin() -> MadSkin {
134        match terminal_light::luma() {
135            Ok(luma) if luma > 0.85 => MadSkin::default_light(),
136            Ok(luma) if luma < 0.2 => MadSkin::default_dark(),
137            _ => MadSkin::default(),
138        }
139    }
140    /// Use the provided skin
141    pub fn with_skin(mut self, skin: MadSkin) -> Self {
142        self.skin = skin;
143        self
144    }
145    /// Set a maximal width, so that the whole terminal width isn't used.
146    ///
147    /// This may make some long sentences easier to read on super wide
148    /// terminals, especially when the whole text is short.
149    /// Depending on your texts and parameters, you may set up a width
150    /// of 100 or 150.
151    pub fn with_max_width(mut self, w: usize) -> Self {
152        self.max_width = Some(w);
153        self
154    }
155    /// Give a mutable reference to the current skin
156    /// (by default the automatically selected one)
157    /// so that it can be modified
158    pub fn skin_mut(&mut self) -> &mut MadSkin {
159        &mut self.skin
160    }
161    /// Change a template
162    pub fn set_template(&mut self, key: &'static str, template: &'t str) {
163        self.templates.insert(key, template);
164    }
165    /// Change or add a template
166    pub fn with(mut self, key: &'static str, template: &'t str) -> Self {
167        self.set_template(key, template);
168        self
169    }
170    /// Unset a template
171    pub fn without(mut self, key: &'static str) -> Self {
172        self.templates.remove(key);
173        self
174    }
175    /// A mutable reference to the list of template keys, so that you can
176    /// insert new keys, or change their order.
177    /// Any key without matching template will just be ignored
178    pub fn template_keys_mut(&mut self) -> &mut Vec<&'static str> {
179        &mut self.template_keys
180    }
181    /// A mutable reference to the list of template keys, so that you can
182    /// insert new keys, or change their order.
183    /// Any key without matching template will just be ignored
184    #[deprecated(since = "0.6.2", note = "use template_keys_mut instead")]
185    pub fn template_order_mut(&mut self) -> &mut Vec<&'static str> {
186        &mut self.template_keys
187    }
188    fn make_expander(cmd: &Command) -> OwningTemplateExpander<'static> {
189        let mut expander = OwningTemplateExpander::new();
190        expander.set_default("");
191        let name = cmd.get_bin_name().unwrap_or_else(|| cmd.get_name());
192        expander.set("name", name);
193        if let Some(author) = cmd.get_author() {
194            expander.set("author", author);
195        }
196        if let Some(version) = cmd.get_version() {
197            expander.set("version", version);
198        }
199        let options = cmd
200            .get_arguments()
201            .filter(|a| !a.is_hide_set())
202            .filter(|a| a.get_short().is_some() || a.get_long().is_some());
203        for arg in options {
204            let sub = expander.sub("option-lines");
205            if let Some(short) = arg.get_short() {
206                sub.set("short", format!("-{short}"));
207            }
208            if let Some(long) = arg.get_long() {
209                sub.set("long", format!("--{long}"));
210            }
211            if let Some(help) = arg.get_help() {
212                sub.set_md("help", help.to_string());
213            }
214            if arg.get_action().takes_values() {
215                if let Some(name) = arg.get_value_names().and_then(|arr| arr.first()) {
216                    sub.set("value", name);
217                    let braced = format!("<{name}>");
218                    sub.set("value-braced", &braced);
219                    if arg.get_short().is_some() {
220                        sub.set("value-short-braced", &braced);
221                        sub.set("value-short", name);
222                    }
223                    if arg.get_long().is_some() {
224                        sub.set("value-long-braced", &braced);
225                        sub.set("value-long", name);
226                    }
227                }
228            }
229            let mut possible_values = arg.get_possible_values();
230            if !possible_values.is_empty() {
231                let possible_values: Vec<String> = possible_values
232                    .drain(..)
233                    .map(|v| format!("`{}`", v.get_name()))
234                    .collect();
235                expander.sub("option-lines").set_md(
236                    "possible_values",
237                    format!(" Possible values: [{}]", possible_values.join(", ")),
238                );
239            }
240            if let Some(default) = arg.get_default_values().first() {
241                match arg.get_action() {
242                    ArgAction::Set | ArgAction::Append => {
243                        expander.sub("option-lines").set_md(
244                            "default",
245                            format!(" Default: `{}`", default.to_string_lossy()),
246                        );
247                    }
248                    _ => {}
249                }
250            }
251        }
252        let mut args = String::new();
253        for arg in cmd.get_positionals() {
254            let Some(key) = arg.get_value_names().and_then(|arr| arr.first()) else {
255                continue;
256            };
257            args.push(' ');
258            if !arg.is_required_set() {
259                args.push('[');
260            }
261            if arg.is_last_set() {
262                args.push_str("-- ");
263            }
264            args.push_str(key);
265            if !arg.is_required_set() {
266                args.push(']');
267            }
268            let sub = expander.sub("positional-lines");
269            sub.set("key", key);
270            if let Some(help) = arg.get_help() {
271                sub.set("help", help);
272            }
273        }
274        expander.set("positional-args", args);
275        expander
276    }
277    /// Give you a mut reference to the expander, so that you can overload
278    /// the variable of the expander used to fill the templates of the help,
279    /// or add new variables for your own templates
280    pub fn expander_mut(&mut self) -> &mut OwningTemplateExpander<'static> {
281        &mut self.expander
282    }
283    /// Print the provided template with the printer's expander
284    ///
285    /// It's normally more convenient to change `template_keys` or some
286    /// templates, unless you want none of the standard templates
287    pub fn print_template(&self, template: &str) {
288        self.skin.print_owning_expander_md(&self.expander, template);
289    }
290    /// Write the provided template with the printer's expander
291    ///
292    /// It's normally more convenient to change `template_keys` or some
293    /// templates, unless you want none of the standard templates
294    pub fn write_template<W: Write>(&self, w: &mut W, template: &str) -> io::Result<()> {
295        self.skin.write_owning_expander_md(w, &self.expander, template)
296    }
297    /// Print all the templates, in order
298    ///
299    /// # Panics
300    /// Panics if writing to stdout fails (use `write_help` to handle it)
301    pub fn print_help(&self) {
302        self.write_help(&mut io::stdout())
303            .expect("failed printing to stdout");
304    }
305    /// Write all the templates, in order
306    pub fn write_help<W: Write>(&self, w: &mut W) -> io::Result<()> {
307        if self.full_width {
308            self.write_help_full_width(w)
309        } else {
310            self.write_help_content_width(w)
311        }
312    }
313    fn write_help_full_width<W: Write>(&self, w: &mut W) -> io::Result<()> {
314        for key in &self.template_keys {
315            if let Some(template) = self.templates.get(key) {
316                self.write_template(w, template)?;
317            }
318        }
319        Ok(())
320    }
321    fn write_help_content_width<W: Write>(&self, w: &mut W) -> io::Result<()> {
322        let (width, _) = termimad::terminal_size();
323        let mut width = width as usize;
324        if let Some(max_width) = self.max_width {
325            width = width.min(max_width);
326        }
327        let mut texts: Vec<FmtText> = self
328            .template_keys
329            .iter()
330            .filter_map(|key| self.templates.get(key))
331            .map(|&template| {
332                let template = TextTemplate::from(template);
333                let text = self.expander.expand(&template);
334                FmtText::from_text(&self.skin, text, Some(width))
335            })
336            .collect();
337        let content_width = texts
338            .iter()
339            .fold(0, |cw, text| cw.max(text.content_width()));
340        for text in &mut texts {
341            text.set_rendering_width(content_width);
342            writeln!(w, "{text}")?;
343        }
344        Ok(())
345    }
346}