use crate::styles::*;
use anstyle::Style;
pub struct HelpSection {
pub title: &'static str,
pub entries: Vec<HelpEntry>,
}
pub enum HelpEntry {
Pair {
key: String,
desc: String,
},
Detail(String),
}
#[allow(clippy::too_many_arguments)]
pub fn render(
name: &str,
version: &str,
about: &str,
tagline: &str,
url: &str,
usage: &str,
sections: &[HelpSection],
examples: &[&str],
hint: &str,
badge: Style,
accent: Style,
) {
let bar = paint(DIM, BAR);
let _ = badge;
eprintln!();
eprintln!(
"{} {} {}",
paint(accent, DIAMOND),
paint(WHITE_BOLD, name),
paint(accent, &format!("v{version}")),
);
let subtitle = match (about.is_empty(), tagline.is_empty()) {
(false, false) => format!("{about} · {tagline}"),
(false, true) => about.to_string(),
(true, false) => tagline.to_string(),
(true, true) => String::new(),
};
if !subtitle.is_empty() {
eprintln!("{} {}", bar, paint(DIM, &subtitle));
}
eprintln!();
print_section_title(accent, "Usage");
eprintln!("{} {}", bar, style_usage(usage, accent));
eprintln!();
for section in sections {
print_section_title(accent, section.title);
let max_key = section
.entries
.iter()
.filter_map(|e| {
if let HelpEntry::Pair { key, .. } = e {
Some(visible_width(key))
} else {
None
}
})
.max()
.unwrap_or(0);
for entry in §ion.entries {
match entry {
HelpEntry::Pair { key, desc } => {
let pad = " ".repeat(max_key - visible_width(key) + 2);
let styled_key = style_key(key, accent);
let styled_desc = style_desc(desc);
eprintln!("{} {}{}{}", bar, styled_key, pad, styled_desc);
}
HelpEntry::Detail(line) => {
eprintln!("{} {}", bar, paint(DIM, line));
}
}
}
eprintln!();
}
if !examples.is_empty() {
print_section_title(accent, "Examples");
for ex in examples {
eprintln!("{} {} {}", bar, paint(DIM, "$"), paint(WHITE_BOLD, ex));
}
eprintln!();
}
if !hint.is_empty() {
print_section_title(accent, "Notes");
eprintln!("{} {} {}", bar, paint(accent, ARROW), paint(WHITE, hint));
eprintln!();
}
if !url.is_empty() {
let link = Style::new()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::BrightCyan)))
.underline();
print_section_title(accent, "Link");
eprintln!("{} {}", bar, paint(link, url));
eprintln!();
}
}
fn print_section_title(accent: Style, title: &str) {
eprintln!("{} {}", paint(accent, DIAMOND), paint(accent, title));
}
fn style_key(key: &str, accent: Style) -> String {
let indent_end = key.len() - key.trim_start().len();
let (indent, body) = key.split_at(indent_end);
if body.starts_with('<') && body.ends_with('>') && !body.contains(' ') {
return format!("{}{}", indent, paint(accent, body));
}
let parts: Vec<String> = body.split(" / ").map(style_flag_part).collect();
let sep = format!(" {} ", paint(DIM, "/"));
format!("{}{}", indent, parts.join(&sep))
}
fn style_flag_part(part: &str) -> String {
let mut out = String::new();
let mut buf = String::new();
let mut chars = part.chars().peekable();
while let Some(c) = chars.next() {
if c == '<' {
if !buf.is_empty() {
out.push_str(&paint(WHITE_BOLD, &buf));
buf.clear();
}
let mut angle = String::from("<");
for c2 in chars.by_ref() {
angle.push(c2);
if c2 == '>' {
break;
}
}
while chars.peek() == Some(&'.') {
angle.push(chars.next().unwrap());
}
out.push_str(&paint(DIM, &angle));
} else {
buf.push(c);
}
}
if !buf.is_empty() {
out.push_str(&paint(WHITE_BOLD, &buf));
}
out
}
fn style_desc(desc: &str) -> String {
if let Some(idx) = desc.rfind("[default:") {
if desc.trim_end().ends_with(']') {
let head = desc[..idx].trim_end();
let tail = &desc[idx..];
return format!("{} {}", paint(WHITE, head), paint(DIM, tail));
}
}
paint(WHITE, desc)
}
fn style_usage(usage: &str, accent: Style) -> String {
let mut out = String::new();
let mut buf = String::new();
let mut chars = usage.chars().peekable();
let mut first_word = true;
let flush = |buf: &mut String, out: &mut String, first_word: &mut bool| {
if buf.is_empty() {
return;
}
if *first_word {
let trimmed = buf.trim_end();
let trailing = &buf[trimmed.len()..].to_string();
out.push_str(&paint(WHITE_BOLD, trimmed));
out.push_str(trailing);
*first_word = false;
} else {
out.push_str(&paint(WHITE, buf));
}
buf.clear();
};
while let Some(c) = chars.next() {
match c {
'<' => {
flush(&mut buf, &mut out, &mut first_word);
let mut token = String::from("<");
for c2 in chars.by_ref() {
token.push(c2);
if c2 == '>' {
break;
}
}
out.push_str(&paint(accent, &token));
}
'[' => {
flush(&mut buf, &mut out, &mut first_word);
let mut token = String::from("[");
for c2 in chars.by_ref() {
token.push(c2);
if c2 == ']' {
break;
}
}
out.push_str(&paint(DIM, &token));
}
_ => buf.push(c),
}
}
flush(&mut buf, &mut out, &mut first_word);
out
}
fn visible_width(s: &str) -> usize {
s.chars().count()
}