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>(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 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; 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 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 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
237fn 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}