bird_tool_utils_man/
man.rs

1use super::*;
2use roff::{bold, italic, list, Roff, Troffable};
3
4/// The main man page struct.
5#[derive(Debug)]
6pub struct Manual {
7  name: String,
8  about: Option<String>,
9  description: Option<String>,
10  authors: Vec<Author>,
11  flags: Vec<Flag>,
12  options: Vec<Opt>,
13  environment: Vec<Env>,
14  arguments: Vec<Arg>,
15  custom_sections: Vec<Section>,
16  examples: Vec<Example>,
17  custom_synopsis_expansion: Option<String>,
18}
19
20impl Manual {
21  /// Create a new instance.
22  pub fn new(name: &str) -> Self {
23    Self {
24      name: name.into(),
25      about: None,
26      description: None,
27      authors: vec![],
28      flags: vec![],
29      options: vec![],
30      arguments: vec![],
31      environment: vec![],
32      custom_sections: vec![],
33      examples: vec![],
34      custom_synopsis_expansion: None,
35    }
36  }
37
38  /// Add a short description.
39  pub fn about<S: Into<String>>(mut self, about: S) -> Self {
40    self.about = Some(about.into());
41    self
42  }
43
44  /// Add a long description.
45  pub fn description<S: Into<String>>(mut self, description: S) -> Self {
46    self.description = Some(description.into());
47    self
48  }
49
50  /// Add an author.
51  pub fn author(mut self, author: Author) -> Self {
52    self.authors.push(author);
53    self
54  }
55
56  /// Add an environment variable.
57  pub fn env(mut self, env: Env) -> Self {
58    self.environment.push(env);
59    self
60  }
61
62  /// Add an flag.
63  pub fn flag(mut self, flag: Flag) -> Self {
64    self.flags.push(flag);
65    self
66  }
67
68  /// Add an option.
69  pub fn option(mut self, opt: Opt) -> Self {
70    self.options.push(opt);
71    self
72  }
73
74  /// Add a custom section
75  pub fn custom(mut self, custom_section: Section) -> Self {
76    self.custom_sections.push(custom_section);
77    self
78  }
79
80  /// Add an examples section
81  pub fn example(mut self, example: Example) -> Self {
82    self.examples.push(example);
83    self
84  }
85
86  /// Add a positional argument. The items are displayed in the order they're
87  /// pushed.
88  // TODO: make this accept argument vecs / optional args too.  `arg...`, `arg?`
89  pub fn arg(mut self, arg: Arg) -> Self {
90    self.arguments.push(arg);
91    self
92  }
93
94  pub fn custom_synopsis_expansion<S: Into<String>>(mut self, expansion: S) -> Self {
95    self.custom_synopsis_expansion = Some(expansion.into());
96    self
97  }
98
99  /// Render to a string.
100  pub fn render(self) -> String {
101    let man_num = 1;
102    let mut page = Roff::new(&self.name, man_num);
103    page = about(page, &self.name, &self.about);
104    page = synopsis(
105      page,
106      &self.name,
107      &self.flags,
108      &self.options,
109      &self.arguments,
110      &self.custom_synopsis_expansion,
111    );
112    page = description(page, &self.description);
113    page = flags(page, &self.flags);
114    page = options(page, &self.options);
115    page = env(page, &self.environment);
116    for section in self.custom_sections.into_iter() {
117      page = custom(page, section);
118    }
119    page = exit_status(page);
120    page = examples(page, &self.examples);
121    page = authors(page, &self.authors);
122    page.render()
123  }
124}
125
126/// Create a `NAME` section.
127///
128/// ## Formatting
129/// ```txt
130/// NAME
131///         mycmd - brief help of the application
132/// ```
133fn about(page: Roff, name: &str, desc: &Option<String>) -> Roff {
134  let desc = match desc {
135    Some(ref desc) => format!("{} - {}", name, desc),
136    None => name.to_owned(),
137  };
138
139  page.section("NAME", &[desc])
140}
141
142/// Create a `DESCRIPTION` section.
143///
144/// ## Formatting
145/// ```txt
146/// DESCRIPTION
147///         Very long description of the application
148/// ```
149fn description(page: Roff, desc: &Option<String>) -> Roff {
150  if let Some(desc) = desc {
151    page.section("DESCRIPTION", &[desc.to_owned()])
152  } else {
153    page
154  }
155}
156
157/// Create a `SYNOPSIS` section.
158fn synopsis(
159  page: Roff,
160  name: &str,
161  flags: &[Flag],
162  options: &[Opt],
163  args: &[Arg],
164  custom_synopsis_expansion: &Option<String>,
165) -> Roff {
166  let flags = match flags.len() {
167    0 => "".into(),
168    _ => " [FLAGS]".into(),
169  };
170  let options = match options.len() {
171    0 => "".into(),
172    _ => " [OPTIONS]".into(),
173  };
174
175  let mut msg = vec![];
176  msg.push(bold(name));
177  match custom_synopsis_expansion {
178    Some(custom) => {
179      msg.push(format!(" {}",custom));
180    },
181    None => {
182      msg.push(flags);
183      msg.push(options);
184    }
185  };
186
187  for arg in args {
188    msg.push(format!(" {}", arg.name));
189  }
190
191  page.section("SYNOPSIS", &msg)
192}
193
194/// Create a `AUTHOR` or `AUTHORS` section.
195///
196/// ## Formatting
197/// ```txt
198/// AUTHORS
199///          Alice Person <alice@person.com>
200///          Bob Human <bob@human.com>
201/// ```
202fn authors(page: Roff, authors: &[Author]) -> Roff {
203  let title = match authors.len() {
204    0 => return page,
205    1 => "AUTHOR",
206    _ => "AUTHORS",
207  };
208
209  let last = authors.len() - 1;
210  let mut auth_values = vec![];
211  auth_values.push(init_list());
212  for (index, author) in authors.iter().enumerate() {
213    auth_values.push(author.name.to_owned());
214
215    if let Some(ref email) = author.email {
216      auth_values.push(format!(" <{}>", email))
217    };
218
219    if index != last {
220      auth_values.push(String::from("\n"));
221    }
222  }
223
224  page.section(title, &auth_values)
225}
226
227/// Create a `FLAGS` section.
228///
229/// ## Formatting
230/// ```txt
231/// FLAGS
232/// ```
233fn flags(page: Roff, flags: &[Flag]) -> Roff {
234  if flags.is_empty() {
235    return page;
236  }
237
238  let last = flags.len() - 1;
239  let mut arr: Vec<String> = vec![];
240  for (index, flag) in flags.iter().enumerate() {
241    let mut args: Vec<String> = vec![];
242    if let Some(ref short) = flag.short {
243      args.push(bold(&short));
244    }
245    if let Some(ref long) = flag.long {
246      if !args.is_empty() {
247        args.push(", ".to_string());
248      }
249      args.push(bold(&long));
250    }
251    let desc = match flag.help {
252      Some(ref desc) => desc.to_string(),
253      None => "".to_string(),
254    };
255    arr.push(list(&args, &[desc]));
256
257    if index != last {
258      arr.push(String::from("\n\n"));
259    }
260  }
261  page.section("FLAGS", &arr)
262}
263
264/// Create a `OPTIONS` section.
265///
266/// ## Formatting
267/// ```txt
268/// OPTIONS
269/// ```
270fn options(page: Roff, options: &[Opt]) -> Roff {
271  if options.is_empty() {
272    return page;
273  }
274
275  let last = options.len() - 1;
276  let mut arr: Vec<String> = vec![];
277  for (index, opt) in options.iter().enumerate() {
278    let mut args: Vec<String> = vec![];
279    if let Some(ref short) = opt.short {
280      args.push(bold(&short));
281    }
282    if let Some(ref long) = opt.long {
283      if !args.is_empty() {
284        args.push(", ".to_string());
285      }
286      args.push(bold(&long));
287    }
288    args.push(" ".into());
289    args.push(italic(&opt.name));
290    if let Some(ref default) = opt.default {
291      if !args.is_empty() {
292        args.push(" ".to_string());
293      }
294      args.push("[".into());
295      args.push("default:".into());
296      args.push(" ".into());
297      args.push(italic(&default));
298      args.push("]".into());
299    }
300    let desc = match opt.help {
301      Some(ref desc) => desc.to_string(),
302      None => "".to_string(),
303    };
304    arr.push(list(&args, &[desc]));
305
306    if index != last {
307      arr.push(String::from("\n\n"));
308    }
309  }
310  page.section("OPTIONS", &arr)
311}
312
313/// Create a `ENVIRONMENT` section.
314///
315/// ## Formatting
316///
317/// ```txt
318/// ENVIRONMENT
319/// ```
320fn env(page: Roff, environment: &[Env]) -> Roff {
321  if environment.is_empty() {
322    return page;
323  }
324
325  let last = environment.len() - 1;
326  let mut arr: Vec<String> = vec![];
327  for (index, env) in environment.iter().enumerate() {
328    let mut args: Vec<String> = vec![];
329    args.push(bold(&env.name));
330    if let Some(ref default) = env.default {
331      if !args.is_empty() {
332        args.push(" ".to_string());
333      }
334      args.push("[".into());
335      args.push("default:".into());
336      args.push(" ".into());
337      args.push(italic(&default));
338      args.push("]".into());
339    }
340    let desc = match env.help {
341      Some(ref desc) => desc.to_string(),
342      None => "".to_string(),
343    };
344    arr.push(list(&args, &[desc]));
345
346    if index != last {
347      arr.push(String::from("\n\n"));
348    }
349  }
350  page.section("ENVIRONMENT", &arr)
351}
352
353/// Create a `EXIT STATUS` section.
354///
355/// ## Implementation Note
356/// This currently only returns the status code `0`, and takes no arguments. We
357/// should let it take arguments.
358///
359/// ## Formatting
360/// ```txt
361/// EXIT STATUS
362///        0      Successful program execution
363///
364///        1      Usage, syntax or configuration file error
365///
366///        2      Optional error
367/// ```
368fn exit_status(page: Roff) -> Roff {
369  page.section(
370    "EXIT STATUS",
371    &[
372      list(&[bold("0")], &["Successful program execution.\n\n"]),
373      list(&[bold("1")], &["Unsuccessful program execution.\n\n"]),
374      list(&[bold("101")], &["The program panicked."]),
375    ],
376  )
377}
378
379/// Create a custom section.
380///
381/// The custom section will have the title you specify as the argument to the
382/// .new() method and may optionally be followed by one or more paragraphs
383/// using the .paragraph() method.
384///
385/// ## Formatting
386/// ```txt
387/// SECTION NAME
388///        Text of first paragraph
389///
390///        Text of second paragraph
391///
392/// ```
393fn custom(page: Roff, custom_section: Section) -> Roff {
394  let mut paragraphs: Vec<String> = vec![];
395  for paragraph in custom_section.paragraphs.into_iter() {
396    paragraphs.push(paragraph);
397    paragraphs.push("\n\n".into())
398  }
399  for flag_or_section in custom_section.flags_and_options {
400    paragraphs.push(flag_or_section.render());
401    paragraphs.push("\n\n".into())
402  }
403
404  page.section(&custom_section.name, &paragraphs)
405}
406
407/// Create an examples section
408///
409/// examples can have text (shown before the example command) and the command
410/// itself.  Optionally, you can also display the output of the command, but
411/// this is typically not necessary.  You may also change the prompt displayed
412/// before the command (the default is `$`).
413///
414/// The command is printed in bold.
415///
416/// ## Formatting
417/// ```txt
418/// EXAMPLES
419///        Explanatory text
420///        $ command
421///        output
422/// ```
423fn examples(page: Roff, examples: &[Example]) -> Roff {
424  if examples.is_empty() {
425    return page;
426  };
427  let mut arr = vec![];
428  for example in examples {
429    let text = example.text.unwrap_or("");
430    let mut full_command = String::from(example.prompt);
431    if let Some(command) = example.command {
432      full_command.push_str(" ");
433      full_command.push_str(command);
434    };
435    let output = match example.output {
436      Some(output) => {
437        // For now, we need to manually add the line break in the list
438        // see https://github.com/killercup/roff-rs/issues/5
439        let mut full_output = String::from("\n.br\n");
440        full_output.push_str(output);
441        full_output.push_str("\n");
442        full_output
443      }
444      None => String::from("\n"),
445    };
446    let example = list(&[text], &[bold(full_command.as_str()), output]);
447    arr.push(example);
448  }
449  page.section("examples", &arr)
450}
451
452// NOTE(yw): This code was taken from the npm-install(1) command. The location
453// on your system may vary. In all honesty I just copy-pasted this. We should
454// probably port this to troff-rs at some point.
455//
456// ```sh
457// $ less /usr/share/man/man1/npm-install.1
458// ```
459fn init_list() -> String {
460  String::from(".P\n.RS 2\n.nf\n")
461}