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"; pub const OPT_LABEL: &str = "\x1b[0;94m"; pub const POS_LABEL: &str = "\x1b[0;93m"; pub const METAVAR: &str = "\x1b[0;96m"; pub const COMMAND: &str = "\x1b[0;95m"; 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#[allow(clippy::too_many_lines)]
126#[must_use]
127pub fn render_help_with_path<Ctx: ?Sized>(
128 env: &Env,
129 path: &[&str],
130 cmd: &CmdSpec<'_, Ctx>,
131) -> String {
132 let mut out = String::new();
133 if let Some(h) = cmd.get_help() {
134 let _ = writeln!(out, "{h}\n");
135 }
136
137 print_usage(&mut out, path, cmd);
138 let mut rows: Vec<(Vec<String>, Option<&str>, String)> = Vec::new();
139 let is_root = path.len() <= 1;
140
141 if env.auto_help {
142 rows.push((
143 vec!["-h".into(), "--help".into()],
144 None,
145 String::from("Show this help and exit"),
146 ));
147 }
148
149 if is_root {
150 if env.version.is_some() {
151 rows.push((
152 vec!["-V".into(), "--version".into()],
153 None,
154 String::from("Show version and exit"),
155 ));
156 }
157 if env.author.is_some() {
158 rows.push((
159 vec!["-A".into(), "--author".into()],
160 None,
161 String::from("Show author and exit"),
162 ));
163 }
164 }
165
166 for o in cmd.get_opts() {
168 let mut lab = vec![];
169 let mut meta: Option<&str> = None;
170 if let Some(s) = o.get_short() {
171 lab.push(format!("-{s}"));
172 }
173 if let Some(l) = o.get_long() {
174 lab.push(format!("--{l}"));
175 }
176 if let Some(mv) = o.get_metavar() {
177 meta = Some(mv);
178 }
179 let mut desc: Vec<String> = vec![];
180 if let Some(h) = o.get_help() {
181 desc.push(h.to_string());
182 }
183 if let Some(env) = o.get_env() {
184 desc.push(format!("Env: {env}"));
185 }
186 if let Some(d) = o.get_default() {
187 desc.push(format!("Default: {d:?}"));
188 }
189 let desc = desc.join("; ");
190 rows.push((lab, meta, desc));
191 }
192
193 if !rows.is_empty() {
194 let _ = writeln!(out, "{}", paint_section("Options"));
195 let max_raw = rows
196 .iter()
197 .map(|(opts, pos, _)| opts.join(", ").len() + pos.map_or(0, |s| s.len() + 1))
198 .max()
199 .unwrap_or(0);
200 let desc_col = 2 + max_raw + 2; for (lab, pos, desc) in rows {
202 let mut painted =
203 lab.into_iter().map(|s| paint_option(&s)).collect::<Vec<String>>().join(", ");
204 if let Some(pos) = pos {
205 painted.push_str(format!(" {}", paint_metavar(pos)).as_str());
206 }
207 let raw = strip_ansi_len(&painted);
208 let pad = max_raw + (painted.len() - raw);
209 let _ = write!(out, " {painted:pad$} ");
210 wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
211 }
212 }
213 if !cmd.get_positionals().is_empty() {
215 let _ = writeln!(out, "\n{}", paint_section("Arguments"));
216 let mut prow_labels: Vec<(String, usize, String)> = Vec::new();
217 let mut max_raw = 0usize;
218 for p in cmd.get_positionals() {
219 let lab = paint_positional(p.get_name());
220 let raw = strip_ansi_len(&lab);
221 max_raw = max_raw.max(raw);
222 prow_labels.push((lab, raw, p.get_help().unwrap_or("").to_string()));
223 }
224 let desc_col = 2 + max_raw + 2;
225 for (lab, raw, desc) in prow_labels {
226 let pad = max_raw + (lab.len() - raw);
227 let _ = write!(out, " {lab:pad$} ");
228 wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
229 }
230 }
231 if !cmd.get_subcommands().is_empty() {
233 let _ = writeln!(out, "\n{}", paint_section("Commands"));
234 let mut crow_labels: Vec<(String, usize, String)> = Vec::new();
235 let mut max_raw = 0usize;
236 for sc in cmd.get_subcommands() {
237 let name = sc.get_name();
238 let mut lab = vec![paint_command(name)];
239 for alias in sc.get_aliases() {
240 lab.push(paint_command(alias));
241 }
242 let lab = lab.join(", ");
243 let raw = strip_ansi_len(&lab);
244 max_raw = max_raw.max(raw);
245 crow_labels.push((lab, raw, sc.get_help().unwrap_or("").to_string()));
246 }
247 let desc_col = 2 + max_raw + 2;
248 for (lab, raw, desc) in crow_labels {
249 let pad = max_raw + (lab.len() - raw);
250 let _ = write!(out, " {lab:pad$} ");
251 wrap_after(&mut out, &desc, desc_col, env.wrap_cols);
252 }
253 }
254 out
255}
256
257fn wrap_after(out: &mut String, text: &str, start_col: usize, wrap: usize) {
259 if text.is_empty() {
260 let _ = writeln!(out);
261 return;
262 }
263 if wrap == 0 {
264 let _ = writeln!(out, "{text}");
265 return;
266 }
267 let mut col = start_col;
268 let mut first = true;
269 for word in text.split_whitespace() {
270 let wlen = word.len();
271 let add = usize::from(!first);
272 if col + add + wlen > wrap && col > start_col {
273 let _ = writeln!(out);
274 let _ = write!(out, "{}", " ".repeat(start_col));
275 col = start_col;
276 first = true;
277 }
278 if !first {
279 let _ = write!(out, " ");
280 col += 1;
281 }
282 let _ = write!(out, "{word}");
283 col += wlen;
284 first = false;
285 }
286 let _ = writeln!(out);
287}