1use crate::styles::*;
7use anstyle::Style;
8
9pub struct HelpSection {
11 pub title: &'static str,
13 pub entries: Vec<HelpEntry>,
15}
16
17pub enum HelpEntry {
19 Pair {
21 key: String,
23 desc: String,
25 },
26 Detail(String),
28}
29
30#[allow(clippy::too_many_arguments)]
35pub fn render(
36 name: &str,
37 version: &str,
38 about: &str,
39 tagline: &str,
40 url: &str,
41 usage: &str,
42 sections: &[HelpSection],
43 examples: &[&str],
44 hint: &str,
45 badge: Style,
46 accent: Style,
47) {
48 let bar = paint(DIM, BAR);
49 let _ = badge; eprintln!();
53 eprintln!(
54 "{} {} {}",
55 paint(accent, DIAMOND),
56 paint(WHITE_BOLD, name),
57 paint(accent, &format!("v{version}")),
58 );
59 let subtitle = match (about.is_empty(), tagline.is_empty()) {
60 (false, false) => format!("{about} · {tagline}"),
61 (false, true) => about.to_string(),
62 (true, false) => tagline.to_string(),
63 (true, true) => String::new(),
64 };
65 if !subtitle.is_empty() {
66 eprintln!("{} {}", bar, paint(DIM, &subtitle));
67 }
68 eprintln!();
69
70 print_section_title(accent, "Usage");
72 eprintln!("{} {}", bar, style_usage(usage, accent));
73 eprintln!();
74
75 for section in sections {
77 print_section_title(accent, section.title);
78
79 let max_key = section
80 .entries
81 .iter()
82 .filter_map(|e| {
83 if let HelpEntry::Pair { key, .. } = e {
84 Some(visible_width(key))
85 } else {
86 None
87 }
88 })
89 .max()
90 .unwrap_or(0);
91
92 for entry in §ion.entries {
93 match entry {
94 HelpEntry::Pair { key, desc } => {
95 let pad = " ".repeat(max_key - visible_width(key) + 2);
96 let styled_key = style_key(key, accent);
97 let styled_desc = style_desc(desc);
98 eprintln!("{} {}{}{}", bar, styled_key, pad, styled_desc);
99 }
100 HelpEntry::Detail(line) => {
101 eprintln!("{} {}", bar, paint(DIM, line));
102 }
103 }
104 }
105 eprintln!();
106 }
107
108 if !examples.is_empty() {
110 print_section_title(accent, "Examples");
111 for ex in examples {
112 eprintln!("{} {} {}", bar, paint(DIM, "$"), paint(WHITE_BOLD, ex));
113 }
114 eprintln!();
115 }
116
117 if !hint.is_empty() {
119 print_section_title(accent, "Notes");
120 eprintln!("{} {} {}", bar, paint(accent, ARROW), paint(WHITE, hint));
121 eprintln!();
122 }
123
124 if !url.is_empty() {
126 let link = Style::new()
127 .fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::BrightCyan)))
128 .underline();
129 print_section_title(accent, "Link");
130 eprintln!("{} {}", bar, paint(link, url));
131 eprintln!();
132 }
133}
134
135fn print_section_title(accent: Style, title: &str) {
138 eprintln!("{} {}", paint(accent, DIAMOND), paint(accent, title));
139}
140
141fn style_key(key: &str, accent: Style) -> String {
148 let indent_end = key.len() - key.trim_start().len();
149 let (indent, body) = key.split_at(indent_end);
150
151 if body.starts_with('<') && body.ends_with('>') && !body.contains(' ') {
153 return format!("{}{}", indent, paint(accent, body));
154 }
155
156 let parts: Vec<String> = body.split(" / ").map(style_flag_part).collect();
158 let sep = format!(" {} ", paint(DIM, "/"));
159 format!("{}{}", indent, parts.join(&sep))
160}
161
162fn style_flag_part(part: &str) -> String {
165 let mut out = String::new();
166 let mut buf = String::new();
167 let mut chars = part.chars().peekable();
168 while let Some(c) = chars.next() {
169 if c == '<' {
170 if !buf.is_empty() {
171 out.push_str(&paint(WHITE_BOLD, &buf));
172 buf.clear();
173 }
174 let mut angle = String::from("<");
175 for c2 in chars.by_ref() {
176 angle.push(c2);
177 if c2 == '>' {
178 break;
179 }
180 }
181 while chars.peek() == Some(&'.') {
183 angle.push(chars.next().unwrap());
184 }
185 out.push_str(&paint(DIM, &angle));
186 } else {
187 buf.push(c);
188 }
189 }
190 if !buf.is_empty() {
191 out.push_str(&paint(WHITE_BOLD, &buf));
192 }
193 out
194}
195
196fn style_desc(desc: &str) -> String {
198 if let Some(idx) = desc.rfind("[default:") {
199 if desc.trim_end().ends_with(']') {
200 let head = desc[..idx].trim_end();
201 let tail = &desc[idx..];
202 return format!("{} {}", paint(WHITE, head), paint(DIM, tail));
203 }
204 }
205 paint(WHITE, desc)
206}
207
208fn style_usage(usage: &str, accent: Style) -> String {
210 let mut out = String::new();
211 let mut buf = String::new();
212 let mut chars = usage.chars().peekable();
213 let mut first_word = true;
214
215 let flush = |buf: &mut String, out: &mut String, first_word: &mut bool| {
216 if buf.is_empty() {
217 return;
218 }
219 if *first_word {
221 let trimmed = buf.trim_end();
222 let trailing = &buf[trimmed.len()..].to_string();
223 out.push_str(&paint(WHITE_BOLD, trimmed));
224 out.push_str(trailing);
225 *first_word = false;
226 } else {
227 out.push_str(&paint(WHITE, buf));
228 }
229 buf.clear();
230 };
231
232 while let Some(c) = chars.next() {
233 match c {
234 '<' => {
235 flush(&mut buf, &mut out, &mut first_word);
236 let mut token = String::from("<");
237 for c2 in chars.by_ref() {
238 token.push(c2);
239 if c2 == '>' {
240 break;
241 }
242 }
243 out.push_str(&paint(accent, &token));
244 }
245 '[' => {
246 flush(&mut buf, &mut out, &mut first_word);
247 let mut token = String::from("[");
248 for c2 in chars.by_ref() {
249 token.push(c2);
250 if c2 == ']' {
251 break;
252 }
253 }
254 out.push_str(&paint(DIM, &token));
255 }
256 _ => buf.push(c),
257 }
258 }
259 flush(&mut buf, &mut out, &mut first_word);
260 out
261}
262
263fn visible_width(s: &str) -> usize {
266 s.chars().count()
267}