helptext/
help.rs

1use std::io::{self, Write};
2
3use crate::Color;
4
5/// A structured help message.
6///
7/// Refer to the [crate-level documentation](index.html) for help.
8#[derive(Debug, Clone)]
9pub struct Help<'a>(pub &'a [HelpSection<'a>]);
10
11impl Help<'_> {
12    pub fn write(&self, buf: &mut impl Write, long: bool, colored: bool) -> io::Result<()> {
13        for section in self.0 {
14            section.write(buf, long, colored, 0, false)?;
15        }
16        Ok(())
17    }
18}
19
20/// Part of a help message. Should be created with the
21/// [`sections`](crate::sections) macro.
22#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub enum HelpSection<'a> {
25    Short(&'a HelpSection<'a>),
26    Long(&'a HelpSection<'a>),
27
28    Text(&'a [Segment<'a>]),
29    Name(&'a str, &'a [HelpSection<'a>]),
30    Table(TableMode, &'a [(&'a str, &'a [HelpSection<'a>])]),
31}
32
33impl HelpSection<'_> {
34    fn write(
35        &self,
36        buf: &mut impl Write,
37        long: bool,
38        colored: bool,
39        indent: usize,
40        same_line: bool,
41    ) -> io::Result<bool> {
42        match *self {
43            HelpSection::Short(section) => {
44                if !long {
45                    return section.write(buf, long, colored, indent, same_line);
46                }
47            }
48            HelpSection::Long(section) => {
49                if long {
50                    return section.write(buf, long, colored, indent, same_line);
51                }
52            }
53            HelpSection::Text(segments) => {
54                if !same_line {
55                    buf.write_all(
56                        &b"                                                  "[..indent],
57                    )?;
58                }
59                for segment in segments {
60                    segment.write(&mut *buf, colored, indent)?;
61                }
62                buf.write_all(b"\n")?;
63                return Ok(true);
64            }
65            HelpSection::Name(name, sections) => {
66                buf.write_all(
67                    &b"\n                                                  "[..indent + 1],
68                )?;
69
70                if colored {
71                    buf.write_all(Color::ANSI_Y.as_bytes())?;
72                    buf.write_all(name.as_bytes())?;
73                    buf.write_all(Color::ANSI_RESET.as_bytes())?;
74                } else {
75                    buf.write_all(name.as_bytes())?;
76                }
77                let new_indent = indent + 4;
78                buf.write_all(b":\n")?;
79
80                let mut line_written = false;
81                for section in sections {
82                    if line_written {
83                        buf.write_all(
84                            &b"                                                  "[..new_indent],
85                        )?;
86                    }
87                    line_written |= section.write(buf, long, colored, new_indent, false)?;
88                }
89                return Ok(line_written);
90            }
91            HelpSection::Table(style, rows) => {
92                let mut is_small = match style {
93                    TableMode::Compact => true,
94                    TableMode::Auto => !long,
95                };
96                let col_width = if is_small {
97                    // we don't care about Unicode here, help is only available in English
98                    rows.iter().map(|&(col1, _)| col1.len()).max().unwrap_or(0) + 2
99                } else {
100                    0
101                };
102                if col_width + indent > 50 {
103                    is_small = false;
104                }
105                let new_indent = if is_small { indent + col_width } else { indent + 8 };
106
107                for (i, &(key, value)) in rows.iter().enumerate() {
108                    if !long && value.iter().all(|section| matches!(section, HelpSection::Long(_)))
109                    {
110                        continue;
111                    }
112
113                    buf.write_all(
114                        &b"                                                  "[..indent],
115                    )?;
116
117                    if colored {
118                        buf.write_all(Color::ANSI_G.as_bytes())?;
119                        buf.write_all(key.as_bytes())?;
120                        buf.write_all(Color::ANSI_RESET.as_bytes())?;
121                    } else {
122                        buf.write_all(key.as_bytes())?;
123                    }
124
125                    if is_small {
126                        buf.write_all(
127                            &b"                                                  "
128                                [..col_width - key.len()],
129                        )?;
130                    } else {
131                        buf.write_all(b"\n")?;
132                    }
133
134                    let mut line_written = false;
135                    for section in value {
136                        line_written |= section.write(
137                            buf,
138                            long,
139                            colored,
140                            new_indent,
141                            is_small && !line_written,
142                        )?;
143                    }
144
145                    if !is_small && i + 1 < rows.len() {
146                        buf.write_all(b"\n")?;
147                    }
148                }
149                return Ok(true);
150            }
151        }
152        Ok(false)
153    }
154}
155
156#[derive(Debug, Copy, Clone)]
157#[non_exhaustive]
158pub enum TableMode {
159    Compact,
160    Auto,
161}
162
163#[derive(Debug, Clone)]
164pub struct Segment<'a> {
165    pub style: Option<Color>,
166    pub text: &'a str,
167    pub ticks: bool,
168}
169
170impl<'a> Segment<'a> {
171    pub const fn new(text: &'a str) -> Self {
172        Segment { style: None, text, ticks: false }
173    }
174
175    pub fn write(&self, buf: &mut impl Write, colored: bool, indent: usize) -> io::Result<()> {
176        if let Some(color) = &self.style {
177            if colored {
178                buf.write_all(color.ansi_code().as_bytes())?;
179            } else if self.ticks {
180                buf.write_all(b"`")?;
181            }
182        }
183
184        let mut is_first_line = true;
185        for line in self.text.split('\n') {
186            if !is_first_line {
187                buf.write_all(
188                    &b"\n                                                  "[..indent + 1],
189                )?;
190            }
191            buf.write_all(line.as_bytes())?;
192            is_first_line = false;
193        }
194
195        if self.style.is_some() {
196            if colored {
197                buf.write_all(Color::ANSI_RESET.as_bytes())?;
198            } else if self.ticks {
199                buf.write_all(b"`")?;
200            }
201        }
202
203        Ok(())
204    }
205}