argwerk_no_std/
helpers.rs

1//! Helper module for the macros.
2//!
3//! Unless something is specifically re-exported, all implementation details are
4//! expected to be private and might change between minor releases.
5
6use alloc::boxed::Box;
7use alloc::string::String;
8use alloc::vec::Vec;
9use core::fmt;
10
11/// Default width to use when wrapping lines.
12///
13/// See [HelpFormat::width].
14const WIDTH: usize = 80;
15
16/// Default padding to use between switch summary and help text.
17///
18/// See [HelpFormat::padding].
19const PADDING: usize = 2;
20
21/// Default max usage width to use for switches and arguments.
22///
23/// See [HelpFormat::max_usage].
24const MAX_USAGE: usize = 24;
25
26/// A boxed error type.
27type BoxError = Box<dyn core_error::Error + Send + Sync + 'static>;
28
29/// Helper for converting a value into a result.
30///
31/// This is used to convert the value of a branch into a result.
32#[doc(hidden)]
33#[inline]
34pub fn into_result<T>(value: T) -> Result<(), BoxError>
35where
36  T: IntoResult,
37{
38  value.into_result()
39}
40
41#[doc(hidden)]
42pub trait IntoResult {
43  fn into_result(self) -> Result<(), BoxError>;
44}
45
46impl IntoResult for () {
47  #[inline]
48  fn into_result(self) -> Result<(), BoxError> {
49    Ok(())
50  }
51}
52
53impl<E> IntoResult for Result<(), E>
54where
55  BoxError: From<E>,
56{
57  #[inline]
58  fn into_result(self) -> Result<(), BoxError> {
59    Ok(self?)
60  }
61}
62
63/// Documentation over a single switch.
64pub struct Switch {
65  /// The usage summary of the switch.
66  ///
67  /// Like `--file, -f <path>`.
68  pub usage: &'static str,
69  /// Documentation comments associated with the switch.
70  pub docs: &'static [&'static str],
71}
72
73/// Helper that can be formatted into documentation text.
74pub struct Help {
75  /// The verbatim usage summary specified when invoking the macro.
76  pub usage: &'static str,
77  /// Documentation comments associated with the command.
78  pub docs: &'static [&'static str],
79  /// Switches associated with the command.
80  pub switches: &'static [Switch],
81}
82
83impl Help {
84  /// Format the help with the given config.
85  ///
86  /// # Examples
87  ///
88  /// ```rust
89  /// argwerk_no_std::define! {
90  ///     /// A simple test command.
91  ///     #[usage = "command [-h]"]
92  ///     struct Args {
93  ///         help: bool,
94  ///     }
95  ///     /// Prints the help.
96  ///     ///
97  ///     /// This includes:
98  ///     ///    * All the available switches.
99  ///     ///    * All the available positional arguments.
100  ///     ///    * Whatever else the developer decided to put in here! We even support wrapping comments which are overly long.
101  ///     ["-h" | "--help"] => {
102  ///         help = true;
103  ///     }
104  /// }
105  ///
106  /// # fn main() -> Result<(), argwerk_no_std::Error> {
107  /// let formatted = format!("{}", Args::help().format().width(120));
108  ///
109  /// assert!(formatted.split('\n').any(|line| line.len() > 80));
110  /// assert!(formatted.split('\n').all(|line| line.len() < 120));
111  /// # Ok(()) }
112  /// ```
113  pub fn format(&self) -> HelpFormat {
114    HelpFormat {
115      help: self,
116      width: WIDTH,
117      padding: PADDING,
118      max_usage: MAX_USAGE,
119    }
120  }
121}
122
123impl fmt::Display for Help {
124  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125    self.format().fmt(f)
126  }
127}
128
129/// A wrapper to format the help message with custom parameters.
130///
131/// Constructed through [Help::format].
132pub struct HelpFormat<'a> {
133  help: &'a Help,
134  width: usize,
135  padding: usize,
136  max_usage: usize,
137}
138
139impl HelpFormat<'_> {
140  /// Configure the width to use for help text.
141  pub fn width(self, width: usize) -> Self {
142    Self { width, ..self }
143  }
144
145  /// Configure the padding to use when formatting help.
146  ///
147  /// This determines the indentation of options and the distances between
148  /// options and help text.
149  pub fn padding(self, padding: usize) -> Self {
150    Self { padding, ..self }
151  }
152
153  /// Configure the max usage width to use when formatting help.
154  ///
155  /// This determines how wide a usage help is allowed to be before it forces
156  /// the associated documentation to flow to the next line.
157  ///
158  /// Usage help is the `--file, -f <path>` part of each switch and argument.
159  pub fn max_usage(self, max_usage: usize) -> Self {
160    Self { max_usage, ..self }
161  }
162}
163
164impl<'a> fmt::Display for HelpFormat<'a> {
165  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166    writeln!(f, "Usage: {name}", name = self.help.usage)?;
167
168    if !self.help.docs.is_empty() {
169      writeln!(f, "{}", TextWrap::new("", self.help.docs, self.width, 0))?;
170    }
171
172    writeln!(f)?;
173
174    let usage_len = self
175      .help
176      .switches
177      .iter()
178      .map(|s| {
179        usize::min(
180          self.max_usage,
181          if s.docs.is_empty() {
182            s.usage.len()
183          } else {
184            s.usage.len() + self.padding
185          },
186        )
187      })
188      .max()
189      .unwrap_or(self.max_usage);
190
191    if !self.help.switches.is_empty() {
192      writeln!(f, "Options:")?;
193      let mut first = true;
194
195      let mut it = self.help.switches.iter().peekable();
196
197      while let Some(d) = it.next() {
198        let first = core::mem::take(&mut first);
199        let more = it.peek().is_some();
200
201        let wrap = TextWrap {
202          init: d.usage,
203          docs: d.docs,
204          width: self.width,
205          padding: self.padding,
206          init_len: Some(usage_len),
207          first,
208          more,
209        };
210
211        writeln!(f, "{}", wrap)?;
212      }
213    }
214
215    Ok(())
216  }
217}
218
219/// Helper to wrap documentation text.
220struct TextWrap<'a> {
221  init: &'a str,
222  docs: &'a [&'static str],
223  width: usize,
224  padding: usize,
225  /// The maximum init length permitted.
226  init_len: Option<usize>,
227  /// If this is the first element.
228  first: bool,
229  /// If there are more elements coming after this.
230  more: bool,
231}
232
233impl<'a> TextWrap<'a> {
234  fn new(init: &'a str, docs: &'a [&'static str], width: usize, padding: usize) -> Self {
235    Self {
236      init,
237      docs,
238      width,
239      padding,
240      init_len: None,
241      first: true,
242      more: false,
243    }
244  }
245
246  fn wrap(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247    let mut it = self.docs.iter().peekable();
248
249    // No documentation lines.
250    if it.peek().is_none() {
251      fill_spaces(f, self.padding)?;
252      f.write_str(self.init)?;
253      return Ok(());
254    }
255
256    let init_len = self.init_len.unwrap_or(self.init.len());
257
258    let (long, mut init) = if self.init.len() + self.padding > init_len {
259      (true, None)
260    } else {
261      (false, Some(&self.init))
262    };
263
264    // NB: init line is broader than maximum permitted init length.
265    if long {
266      // If we're not the first line, add a newline to visually separate
267      // the line with the long usage.
268      if !self.first {
269        writeln!(f)?;
270      }
271
272      fill_spaces(f, self.padding)?;
273      writeln!(f, "{}", self.init)?;
274    }
275
276    let fill = init_len + self.padding;
277
278    let trim = it.peek().map(|line| chars_count(line, |c| c == ' '));
279
280    while let Some(line) = it.next() {
281      let mut line = *line;
282
283      // Trim the line by skipping the whitespace common to all lines..
284      if let Some(trim) = trim {
285        line = skip_chars(line, trim);
286
287        // Whole line was trimmed.
288        if line.is_empty() {
289          writeln!(f)?;
290          continue;
291        }
292      }
293
294      // Whitespace prefix currently in use.
295      let ws_fill = next_index(line, char::is_alphanumeric).unwrap_or_default();
296      let mut line_first = true;
297
298      loop {
299        let fill = if !core::mem::take(&mut line_first) {
300          fill + ws_fill
301        } else {
302          fill
303        };
304
305        let mut space_span = None;
306
307        loop {
308          let c = space_span.map(|(_, e)| e).unwrap_or_default();
309
310          let (start, leap) = match line[c..].find(' ') {
311            Some(i) => {
312              let leap = next_index(&line[c + i..], |c| c != ' ').unwrap_or(1);
313              (c + i, leap)
314            }
315            None => {
316              // if the line fits within the current target fill,
317              // include all of it.
318              if line.len() + fill <= self.width {
319                space_span = None;
320              }
321
322              break;
323            }
324          };
325
326          if start + fill > self.width {
327            break;
328          }
329
330          space_span = Some((start, start + leap));
331        }
332
333        let init_len = if let Some(init) = init.take() {
334          fill_spaces(f, self.padding)?;
335          f.write_str(init)?;
336          self.padding + init.len()
337        } else {
338          0
339        };
340
341        fill_spaces(f, fill.saturating_sub(init_len))?;
342
343        if let Some((start, end)) = space_span {
344          writeln!(f, "{}", &line[..start])?;
345          line = &line[end..];
346          continue;
347        }
348
349        f.write_str(line)?;
350        break;
351      }
352
353      if it.peek().is_some() {
354        writeln!(f)?;
355      }
356    }
357
358    // If we're not the first line, add a newline to visually separate the
359    // line with the long usage.
360    if long && !self.first && self.more {
361      writeln!(f)?;
362    }
363
364    return Ok(());
365
366    /// Get the next index that is alphanumeric.
367    fn next_index(s: &str, p: fn(char) -> bool) -> Option<usize> {
368      Some(s.char_indices().find(|&(_, c)| p(c))?.0)
369    }
370
371    /// Count the number of spaces in the string, and return the first index that is not a space.
372    fn chars_count(s: &str, p: fn(char) -> bool) -> usize {
373      s.chars().take_while(|c| p(*c)).count()
374    }
375
376    /// Skip the given number of characters.
377    fn skip_chars(s: &str, count: usize) -> &str {
378      let e = s
379        .char_indices()
380        .skip(count)
381        .map(|(i, _)| i)
382        .next()
383        .unwrap_or(s.len());
384
385      &s[e..]
386    }
387
388    fn fill_spaces(f: &mut fmt::Formatter<'_>, mut count: usize) -> fmt::Result {
389      // Static buffer for quicker whitespace filling.
390      static BUF: &str = "                                                                ";
391
392      while count > 0 {
393        f.write_str(&BUF[..usize::min(count, BUF.len())])?;
394        count = count.saturating_sub(BUF.len());
395      }
396
397      Ok(())
398    }
399  }
400}
401
402impl fmt::Display for TextWrap<'_> {
403  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404    self.wrap(f)
405  }
406}
407
408/// Helpers around an iterator.
409pub struct Input<I>
410where
411  I: Iterator,
412{
413  it: I,
414  buf: Option<I::Item>,
415}
416
417impl<I> Input<I>
418where
419  I: Iterator,
420{
421  /// Construct a new input wrapper.
422  pub fn new(it: I) -> Self {
423    Self { it, buf: None }
424  }
425}
426
427impl<I> Input<I>
428where
429  I: Iterator,
430  I::Item: TryIntoInput,
431{
432  /// Get the next item in the parser.
433  // XXX For now, shut up Clippy. Eventually,
434  // change the public interface or impl
435  // iterator.
436  #[allow(clippy::should_implement_trait)]
437  pub fn next(&mut self) -> Result<Option<String>, InputError> {
438    if let Some(item) = self.buf.take() {
439      return Ok(Some(item.try_into_string()?));
440    }
441
442    let item = match self.it.next() {
443      Some(item) => item,
444      None => return Ok(None),
445    };
446
447    Ok(Some(item.try_into_string()?))
448  }
449
450  /// Take the next argument unless it looks like a switch.
451  pub fn next_unless_switch(&mut self) -> Result<Option<String>, InputError> {
452    match self.peek() {
453      Some(s) if s.starts_with('-') => Ok(None),
454      _ => self.next(),
455    }
456  }
457
458  /// Get the rest of available items.
459  pub fn rest(&mut self) -> Result<Vec<String>, InputError> {
460    let mut buf = Vec::new();
461
462    if let Some(item) = self.buf.take() {
463      buf.push(item.try_into_string()?);
464    }
465
466    for item in &mut self.it {
467      buf.push(item.try_into_string()?);
468    }
469
470    Ok(buf)
471  }
472
473  fn peek(&mut self) -> Option<&str> {
474    if self.buf.is_none() {
475      self.buf = self.it.next();
476    }
477
478    let item = match self.buf.as_ref() {
479      Some(item) => item,
480      None => return None,
481    };
482
483    item.try_as_str().ok()
484  }
485}
486
487#[derive(Debug)]
488pub struct InputError(());
489
490impl fmt::Display for InputError {
491  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492    write!(f, "encounted non-valid unicode in input")
493  }
494}
495
496impl core_error::Error for InputError {}
497
498/// Trait implemented by types that can be parsed to the `parse` function of an
499/// arguments structure.
500///
501/// This trait is sealed, so that it cannot be implemented outside of the
502/// argwerk crate.
503///
504/// See [define][crate::define] for how its used.
505pub trait TryIntoInput: self::internal::Sealed {
506  #[doc(hidden)]
507  fn try_as_str(&self) -> Result<&str, InputError>;
508
509  #[doc(hidden)]
510  fn try_into_string(self) -> Result<String, InputError>;
511}
512
513impl TryIntoInput for String {
514  #[inline]
515  fn try_as_str(&self) -> Result<&str, InputError> {
516    Ok(self.as_str())
517  }
518
519  #[inline]
520  fn try_into_string(self) -> Result<String, InputError> {
521    Ok(self)
522  }
523}
524
525impl TryIntoInput for &str {
526  #[inline]
527  fn try_as_str(&self) -> Result<&str, InputError> {
528    Ok(*self)
529  }
530
531  #[inline]
532  fn try_into_string(self) -> Result<String, InputError> {
533    Ok(String::from(self))
534  }
535}
536
537mod internal {
538  pub trait Sealed {}
539
540  impl<T> Sealed for T where T: super::TryIntoInput {}
541}