use crate::nanoargs::parser::ArgParser;
use std::fmt::Write as _;
/// Intermediate representation of a single help line item.
/// Built once by the shared assembly logic, then formatted as plain text.
struct HelpEntry {
/// The left column label (e.g. "-v, --verbose" or "--output <FILE>").
label: String,
/// The right column description (e.g. "Enable verbose output (required) [default: x]").
description: String,
}
// ── Leaf text helpers (color styling not supported in this vendored build) ──
fn green(s: &str) -> String {
s.to_string()
}
fn cyan(s: &str) -> String {
s.to_string()
}
fn dim(s: &str) -> String {
s.to_string()
}
fn bold_yellow(s: &str) -> String {
s.to_string()
}
fn bold(s: &str) -> String {
s.to_string()
}
// ── Display width helper ───────────────────────────────────────────────────
/// Returns the visible width of a string.
fn display_width(s: &str) -> usize {
s.len()
}
/// Formats a help section with column-aligned descriptions.
/// Uses `bold_yellow()` for the header and `display_width()` for column alignment.
/// Returns empty string if entries is empty.
fn format_section(header: &str, entries: &[HelpEntry]) -> String {
if entries.is_empty() {
return String::new();
}
let max_width = entries
.iter()
.map(|e| display_width(&e.label))
.max()
.unwrap_or(0);
let mut out = String::new();
write!(out, "\n{}\n", bold_yellow(header)).unwrap();
for entry in entries {
let pad = max_width.saturating_sub(display_width(&entry.label));
writeln!(
out,
" {}{:pad$} {}",
entry.label,
"",
entry.description,
pad = pad
)
.unwrap();
}
out
}
impl ArgParser {
/// Generate formatted help text for this parser.
///
/// Includes the description, usage line, options, positional arguments,
/// and subcommands sections. When the `color` feature is enabled and the
/// terminal supports it, the output includes ANSI color codes.
#[allow(clippy::too_many_lines)]
pub fn help_text(&self) -> String {
let mut out = String::new();
let name = self.program_name.as_deref().unwrap_or("program");
// Description
if let Some(ref desc) = self.program_desc {
out.push_str(desc);
out.push_str("\n\n");
}
// Usage summary line
let has_subcommands = !self.subcommands.is_empty();
let has_visible =
self.flags.iter().any(|f| !f.hidden) || self.options.iter().any(|o| !o.hidden);
write!(out, "{} {}", bold_yellow("Usage:"), bold(name)).unwrap();
if has_visible {
out.push_str(" [OPTIONS]");
}
if has_subcommands {
write!(out, " {}", cyan("<SUBCOMMAND>")).unwrap();
} else {
for pos in &self.positionals {
let suffix = if pos.multi { "..." } else { "" };
if pos.required {
write!(out, " {}{}", cyan(&format!("<{}>", pos.name)), suffix).unwrap();
} else {
write!(out, " {}{}", cyan(&format!("[{}]", pos.name)), suffix).unwrap();
}
}
}
out.push('\n');
// Options (flags + options combined under one header)
{
let mut entries: Vec<HelpEntry> = Vec::new();
for flag in self.flags.iter().filter(|f| !f.hidden) {
let label = match flag.short {
Some(c) => format!(
"{}, {}",
green(&format!("-{c}")),
green(&format!("--{}", flag.long))
),
None => format!(" {}", green(&format!("--{}", flag.long))),
};
entries.push(HelpEntry {
label,
description: flag.description.clone(),
});
}
for opt in self.options.iter().filter(|o| !o.hidden) {
let placeholder_str = cyan(&format!("<{}>", opt.placeholder));
let label = match opt.short {
Some(c) => format!(
"{}, {} {}",
green(&format!("-{c}")),
green(&format!("--{}", opt.long)),
placeholder_str
),
None => format!(
" {} {}",
green(&format!("--{}", opt.long)),
placeholder_str
),
};
let req = if opt.required {
format!(" {}", dim("(required)"))
} else {
String::new()
};
let multi_hint = if opt.multi {
format!(" {}", dim("(multiple)"))
} else {
String::new()
};
let default_hint = match &opt.default {
Some(val) => format!(" {}", dim(&format!("[default: {val}]"))),
None => String::new(),
};
let env_hint = match &opt.env_var {
Some(var) => format!(" {}", dim(&format!("[env: {var}]"))),
None => String::new(),
};
let validator_hint = opt
.validator
.as_ref()
.and_then(|v| v.hint())
.map(|h| format!(" {}", dim(&format!("[{h}]"))))
.unwrap_or_default();
entries.push(HelpEntry {
label,
description: format!(
"{}{req}{multi_hint}{default_hint}{env_hint}{validator_hint}",
opt.description
),
});
}
out.push_str(&format_section("Options:", &entries));
}
// Argument Groups
if !self.groups.is_empty() {
let entries: Vec<HelpEntry> = self
.groups
.iter()
.map(|g| {
let members_str = g
.members
.iter()
.map(|m| green(&format!("--{m}")))
.collect::<Vec<_>>()
.join(", ");
HelpEntry {
label: g.name.clone(),
description: format!("{} {}", members_str, dim("(at least one required)")),
}
})
.collect();
out.push_str(&format_section("Argument Groups:", &entries));
}
// Conflicts
if !self.conflicts.is_empty() {
let entries: Vec<HelpEntry> = self
.conflicts
.iter()
.map(|c| {
let members_str = c
.members
.iter()
.map(|m| green(&format!("--{m}")))
.collect::<Vec<_>>()
.join(", ");
HelpEntry {
label: c.name.clone(),
description: format!("{} {}", members_str, dim("(mutually exclusive)")),
}
})
.collect();
out.push_str(&format_section("Conflicts:", &entries));
}
// Positionals (omitted when subcommands are present)
if !has_subcommands {
let entries: Vec<HelpEntry> = self
.positionals
.iter()
.map(|pos| {
let multi_suffix = if pos.multi { "..." } else { "" };
let label = format!("{}{}", green(&pos.name), multi_suffix);
let req = if pos.required {
format!(" {}", dim("(required)"))
} else {
String::new()
};
let default_hint = match &pos.default {
Some(val) => format!(" {}", dim(&format!("[default: {val}]"))),
None => String::new(),
};
let validator_hint = pos
.validator
.as_ref()
.and_then(|v| v.hint())
.map(|h| format!(" {}", dim(&format!("[{h}]"))))
.unwrap_or_default();
HelpEntry {
label,
description: format!(
"{}{req}{default_hint}{validator_hint}",
pos.description
),
}
})
.collect();
out.push_str(&format_section("Positional arguments:", &entries));
}
// Subcommands
if has_subcommands {
let entries: Vec<HelpEntry> = self
.subcommands
.iter()
.map(|subcmd| HelpEntry {
label: green(&subcmd.name),
description: subcmd.description.clone(),
})
.collect();
out.push_str(&format_section("Subcommands:", &entries));
}
out
}
}