Skip to main content

rust_args_parser/
help.rs

1use crate::util::strip_ansi_len;
2use crate::{CmdSpec, Env};
3use core::fmt::Write;
4
5#[cfg(feature = "color")]
6mod ansi {
7    pub const RESET: &str = "\x1b[0m";
8    pub const BOLD: &str = "\x1b[1m";
9    pub const TITLE: &str = "\x1b[4;37m"; // section titles
10    pub const OPT_LABEL: &str = "\x1b[0;94m"; // option labels
11    pub const POS_LABEL: &str = "\x1b[0;93m"; // positional labels
12    pub const METAVAR: &str = "\x1b[0;96m"; // metavars
13    pub const COMMAND: &str = "\x1b[0;95m"; // command names
14    pub const BRIGHT_WHITE: &str = "\x1b[0;97m";
15}
16
17#[cfg(feature = "color")]
18fn paint_title(s: &str) -> String {
19    format!("{}{}{}{}:", ansi::BOLD, ansi::TITLE, s, ansi::RESET)
20}
21#[cfg(not(feature = "color"))]
22fn paint_title(s: &str) -> String {
23    s.to_string()
24}
25
26#[cfg(feature = "color")]
27fn paint_section(s: &str) -> String {
28    format!("{}{}{}:", ansi::TITLE, s, ansi::RESET)
29}
30#[cfg(not(feature = "color"))]
31fn paint_section(s: &str) -> String {
32    s.to_string()
33}
34
35#[cfg(feature = "color")]
36fn paint_option(s: &str) -> String {
37    format!("{}{}{}", ansi::OPT_LABEL, s, ansi::RESET)
38}
39#[cfg(not(feature = "color"))]
40fn paint_option(s: &str) -> String {
41    s.to_string()
42}
43
44#[cfg(feature = "color")]
45fn paint_positional(s: &str) -> String {
46    format!("{}{}{}", ansi::POS_LABEL, s, ansi::RESET)
47}
48#[cfg(not(feature = "color"))]
49fn paint_positional(s: &str) -> String {
50    s.to_string()
51}
52
53#[cfg(feature = "color")]
54fn paint_metavar(s: &str) -> String {
55    format!("{}{}{}", ansi::METAVAR, s, ansi::RESET)
56}
57#[cfg(not(feature = "color"))]
58fn paint_metavar(s: &str) -> String {
59    s.to_string()
60}
61
62#[cfg(feature = "color")]
63fn paint_command(s: &str) -> String {
64    format!("{}{}{}", ansi::COMMAND, s, ansi::RESET)
65}
66#[cfg(not(feature = "color"))]
67fn paint_command(s: &str) -> String {
68    s.to_string()
69}
70
71#[must_use]
72pub fn render_help<Ctx: ?Sized>(env: &Env, cmd: &CmdSpec<'_, Ctx>) -> String {
73    render_help_with_path(env, &[], cmd)
74}
75
76fn print_usage<Ctx: ?Sized>(out_buf: &mut String, path: &[&str], cmd: &CmdSpec<'_, Ctx>) {
77    use crate::spec::PosCardinality;
78    let mut out = String::new();
79    let is_root = path.len() <= 1;
80    let _ = writeln!(out, "{}", paint_title("Usage"));
81    let bin_name = (*path.first().unwrap_or(&"")).to_string();
82    #[cfg(feature = "color")]
83    let _ = write!(out, "  {}{}{}{}", ansi::BRIGHT_WHITE, ansi::BOLD, bin_name, ansi::RESET);
84    #[cfg(not(feature = "color"))]
85    let _ = write!(out, "  {}", bin_name);
86    for command in path.iter().skip(1) {
87        let _ = write!(out, " {}", paint_command(command));
88    }
89    if !cmd.get_opts().is_empty() || is_root {
90        #[cfg(feature = "color")]
91        let _ = write!(out, " {}[options]{}", ansi::OPT_LABEL, ansi::RESET);
92        #[cfg(not(feature = "color"))]
93        let _ = write!(out, " [options]");
94    }
95    if !cmd.get_subcommands().is_empty() {
96        #[cfg(feature = "color")]
97        let _ = write!(out, " {}<command>{}", ansi::COMMAND, ansi::RESET);
98        #[cfg(not(feature = "color"))]
99        let _ = write!(out, " <command>");
100    }
101    for p in cmd.get_positionals() {
102        let name = p.get_name();
103        let (req, ellip) = match p.get_cardinality() {
104            PosCardinality::One { .. } => (p.is_required(), false),
105            PosCardinality::Many => (p.is_required(), true),
106            PosCardinality::Range { min, max } => (min > 0, max > 1),
107        };
108        let token = if ellip { format!("{name}...") } else { name.to_string() };
109        if req {
110            #[cfg(feature = "color")]
111            let _ = write!(out, " {}<{token}>{}", ansi::POS_LABEL, ansi::RESET);
112            #[cfg(not(feature = "color"))]
113            let _ = write!(out, " <{token}>");
114        } else {
115            #[cfg(feature = "color")]
116            let _ = write!(out, " {}[{token}]{}", ansi::POS_LABEL, ansi::RESET);
117            #[cfg(not(feature = "color"))]
118            let _ = write!(out, " [{token}]");
119        }
120    }
121    let _ = writeln!(out_buf, "{out}\n");
122}
123
124/// Render help with **strict column alignment** based on the *longest* label in the section.
125#[allow(clippy::too_many_lines)]
126#[must_use]
127pub fn render_help_with_path<Ctx: ?Sized>(env: &Env, path: &[&str], cmd: &CmdSpec<'_, Ctx>) -> String {
128    let mut out = String::new();
129    if let Some(h) = cmd.get_help() {
130        let _ = writeln!(out, "{h}\n");
131    }
132
133    print_usage(&mut out, path, cmd);
134    let mut rows: Vec<(Vec<String>, Option<&str>, String)> = Vec::new();
135    let is_root = path.len() <= 1;
136
137    if env.auto_help {
138        rows.push((vec!["-h".into(), "--help".into()], None, String::from("Show this help and exit")));
139    }
140
141    if is_root {
142        if env.version.is_some() {
143            rows.push((vec!["-V".into(), "--version".into()], None, String::from("Show version and exit")));
144        }
145        if env.author.is_some() {
146            rows.push((vec!["-A".into(), "--author".into()], None, String::from("Show author and exit")));
147        }
148    }
149
150    // User‑defined options
151    for o in cmd.get_opts() {
152        let mut lab = vec![];
153        let mut meta: Option<&str> = None;
154        if let Some(s) = o.get_short() {
155            lab.push(format!("-{s}"));
156        }
157        if let Some(l) = o.get_long() {
158            lab.push(format!("--{l}"));
159        }
160        if let Some(mv) = o.get_metavar() {
161            meta = Some(mv);
162        }
163        let mut desc: Vec<String> = vec![];
164        if let Some(h) = o.get_help() {
165            desc.push(h.to_string());
166        }
167        if let Some(env) = o.get_env() {
168            desc.push(format!("Env: {env}"));
169        }
170        if let Some(d) = o.get_default() {
171            desc.push(format!("Default: {d:?}"));
172        }
173        let desc = desc.join("; ");
174        rows.push((lab, meta, desc));
175    }
176
177    if !rows.is_empty() {
178        let _ = writeln!(out, "{}", paint_section("Options"));
179        let max_raw =
180            rows.iter().map(|(opts, pos, _)| opts.join(", ").len() + pos.map_or(0, |s| s.len() + 1)).max().unwrap_or(0);
181        let desc_col = 2 + max_raw + 2; // "  " + label + "  "
182        for (lab, pos, desc) in rows {
183            let mut painted = lab.into_iter().map(|s| paint_option(&s)).collect::<Vec<String>>().join(", ");
184            if let Some(pos) = pos {
185                painted.push_str(format!(" {}", paint_metavar(pos)).as_str());
186            }
187            let raw = strip_ansi_len(&painted);
188            let pad = max_raw + (painted.len() - raw);
189            let _ = write!(out, "  {painted:pad$}  ");
190            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
191        }
192    }
193    // Arguments
194    if !cmd.get_positionals().is_empty() {
195        let _ = writeln!(out, "\n{}", paint_section("Arguments"));
196        let mut prow_labels: Vec<(String, usize, String)> = Vec::new();
197        let mut max_raw = 0usize;
198        for p in cmd.get_positionals() {
199            let lab = paint_positional(p.get_name());
200            let raw = strip_ansi_len(&lab);
201            max_raw = max_raw.max(raw);
202            prow_labels.push((lab, raw, p.get_help().unwrap_or("").to_string()));
203        }
204        let desc_col = 2 + max_raw + 2;
205        for (lab, raw, desc) in prow_labels {
206            let pad = max_raw + (lab.len() - raw);
207            let _ = write!(out, "  {lab:pad$}  ");
208            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
209        }
210    }
211    // Commands
212    if !cmd.get_subcommands().is_empty() {
213        let _ = writeln!(out, "\n{}", paint_section("Commands"));
214        let mut crow_labels: Vec<(String, usize, String)> = Vec::new();
215        let mut max_raw = 0usize;
216        for sc in cmd.get_subcommands() {
217            let name = sc.get_name();
218            let mut lab = vec![paint_command(name)];
219            for alias in sc.get_aliases() {
220                lab.push(paint_command(alias));
221            }
222            let lab = lab.join(", ");
223            let raw = strip_ansi_len(&lab);
224            max_raw = max_raw.max(raw);
225            crow_labels.push((lab, raw, sc.get_help().unwrap_or("").to_string()));
226        }
227        let desc_col = 2 + max_raw + 2;
228        for (lab, raw, desc) in crow_labels {
229            let pad = max_raw + (lab.len() - raw);
230            let _ = write!(out, "  {lab:pad$}  ");
231            wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
232        }
233    }
234    out
235}
236
237/// Wrap `text` after the already‑printed label. Subsequent lines start at `start_col`.
238fn wrap_after(out: &mut String, text: &str, start_col: usize, wrap: usize) {
239    if text.is_empty() {
240        let _ = writeln!(out);
241        return;
242    }
243    if wrap == 0 {
244        let _ = writeln!(out, "{text}");
245        return;
246    }
247    let mut col = start_col;
248    let mut first = true;
249    for word in text.split_whitespace() {
250        let wlen = word.len();
251        let add = usize::from(!first);
252        if col + add + wlen > wrap && col > start_col {
253            let _ = writeln!(out);
254            let _ = write!(out, "{}", " ".repeat(start_col));
255            col = start_col;
256            first = true;
257        }
258        if !first {
259            let _ = write!(out, " ");
260            col += 1;
261        }
262        let _ = write!(out, "{word}");
263        col += wlen;
264        first = false;
265    }
266    let _ = writeln!(out);
267}