use clap::CommandFactory;
use colored::Colorize;
pub const XBP_HELP_TEMPLATE: &str = "\
{about-with-newline}\
Usage: {usage}\n\n\
{all-args}\
{after-help}";
pub const XBP_ROOT_HELP_TEMPLATE: &str = "\
{about-with-newline}\
Usage: {usage}\n\n\
{all-args}\
{after-help}";
pub const XBP_ROOT_AFTER_HELP: &str = "\
Quick start:
xbp diag
xbp services
xbp workers list
xbp workers logs -f
xbp api health
Discover:
xbp <command> -h Command help and examples
xbp <command> <sub> -h Subcommand help
xbp --commands Full alphabetical command tree
xbp install Browse installable targets";
pub const CONFIG_AFTER_HELP: &str = "\
Examples:
xbp config
xbp config --project
xbp config cloudflare
xbp config cloudflare status
xbp config openrouter set-key
xbp config linear select-initiative";
pub const VERSION_AFTER_HELP: &str = "\
Examples:
xbp version
xbp version patch
xbp version 1.2.3
xbp version release
xbp version workspace check
xbp version workspace sync --version 3.16.5 --write";
pub const DIAG_AFTER_HELP: &str = "\
Examples:
xbp diag
xbp diag --nginx
xbp diag --ports 80,443
xbp diag --codetime --cursor";
pub const NGINX_AFTER_HELP: &str = "\
Examples:
xbp nginx list
xbp nginx enable api.example.com
xbp nginx disable api.example.com
xbp nginx upstream list";
pub const COMMIT_AFTER_HELP: &str = "\
Examples:
xbp commit
xbp commit --dry-run
xbp commit --push
xbp commit --scope cli";
pub const LOGS_AFTER_HELP: &str = "\
Examples:
xbp logs
xbp logs my-project
xbp logs --ssh-host bastion.example.com";
pub const PUBLISH_AFTER_HELP: &str = "\
Examples:
xbp publish --dry-run
xbp publish --target npm
xbp publish --allow-dirty";
pub const DOMAINS_AFTER_HELP: &str = "\
Examples:
xbp domains list
xbp domains check --domain example.com
xbp domains search --query myapp --extension com";
pub const LOGIN_AFTER_HELP: &str = "\
Examples:
xbp login
xbp login status
xbp login logout";
pub const SSH_AFTER_HELP: &str = "\
Examples:
xbp ssh
xbp ssh --host bastion.example.com
xbp ssh --host 10.0.0.5 --command \"uptime\"";
pub const GENERATE_AFTER_HELP: &str = "\
Examples:
xbp generate config
xbp generate config --force
xbp generate systemd";
pub const DONE_AFTER_HELP: &str = "\
Examples:
xbp done
xbp done --since \"7 days ago\"
xbp done --output report.md";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HelpScope {
Root,
Subcommand,
Catalog,
Auto,
}
pub fn emit_styled_help(raw: &str, scope: HelpScope) {
crate::cli::ui::configure_color_output();
let resolved_scope = match scope {
HelpScope::Auto => detect_help_scope(raw),
other => other,
};
let mut skip_about_line = None::<String>;
let mut skip_until_usage = resolved_scope == HelpScope::Catalog;
if resolved_scope == HelpScope::Root {
print_root_banner(raw);
} else if resolved_scope != HelpScope::Catalog {
if let Some((_, tagline)) = parse_subcommand_heading(raw) {
skip_about_line = Some(tagline);
print_subcommand_banner(raw);
}
}
let lines: Vec<&str> = raw.lines().collect();
let mut index = 0usize;
while index < lines.len() {
let line = lines[index];
let trimmed = line.trim();
if skip_until_usage {
if trimmed.starts_with("Usage:") {
skip_until_usage = false;
} else {
index += 1;
continue;
}
}
if let Some(about) = skip_about_line.as_deref() {
if trimmed == about {
skip_about_line = None;
index += 1;
continue;
}
}
if is_command_name_line(line)
&& index + 1 < lines.len()
&& is_indented_help_text(lines[index + 1], 4)
{
println!("{}", style_command_pair(line, lines[index + 1]));
index += 2;
continue;
}
if is_option_flags_line(line)
&& index + 1 < lines.len()
&& is_indented_help_text(lines[index + 1], 4)
{
println!("{}", style_option_pair(line, lines[index + 1]));
index += 2;
continue;
}
if is_option_entry(line)
&& index + 1 < lines.len()
&& is_indented_help_text(lines[index + 1], 8)
{
println!("{}", style_option_pair(line, lines[index + 1]));
index += 2;
continue;
}
println!("{}", style_help_line(line));
index += 1;
}
println!();
}
pub fn print_command_catalog() {
crate::cli::ui::configure_color_output();
println!();
println!("{}", "xbp commands".bright_magenta().bold());
println!("{}", "Complete command reference".bright_black());
crate::cli::ui::divider(32);
let help = crate::cli::commands::Cli::command()
.render_long_help()
.to_string();
emit_styled_help(&help, HelpScope::Catalog);
}
pub fn emit_version_line(version: &str) {
crate::cli::ui::configure_color_output();
println!(
"{} {}",
"xbp".bright_magenta().bold(),
version.bright_white().bold()
);
}
pub fn is_root_help_text(raw: &str) -> bool {
raw.lines().any(|line| {
let trimmed = line.trim();
trimmed == "Usage: xbp [OPTIONS] [COMMAND]"
|| trimmed == "Usage: xbp.exe [OPTIONS] [COMMAND]"
|| trimmed.starts_with("Usage: xbp [OPTIONS] [COMMAND]")
|| trimmed.starts_with("Usage: xbp.exe [OPTIONS] [COMMAND]")
})
}
fn detect_help_scope(raw: &str) -> HelpScope {
if is_root_help_text(raw) {
return HelpScope::Root;
}
let first_line = raw.lines().next().unwrap_or_default().trim();
if first_line.starts_with("xbp ") && first_line.chars().any(|ch| ch.is_ascii_digit()) {
return HelpScope::Root;
}
HelpScope::Subcommand
}
fn print_root_banner(raw: &str) {
let version = parse_version_line(raw).unwrap_or(env!("CARGO_PKG_VERSION"));
println!();
println!(
"{} {}",
"XBP".bright_magenta().bold(),
format!("v{version}").bright_white()
);
println!("{}", "Deploy · operate · debug · ship".bright_black());
crate::cli::ui::divider(44);
}
fn print_subcommand_banner(raw: &str) {
let Some((command_path, tagline)) = parse_subcommand_heading(raw) else {
return;
};
println!();
println!("{}", command_path.bright_magenta().bold());
if !tagline.is_empty() {
println!("{}", tagline.bright_black());
}
crate::cli::ui::divider(command_path.len().max(28));
}
fn parse_version_line(raw: &str) -> Option<&str> {
let first = raw.lines().next()?.trim();
let rest = first.strip_prefix("xbp")?.trim();
if rest.is_empty() {
None
} else {
Some(rest)
}
}
fn parse_subcommand_heading(raw: &str) -> Option<(String, String)> {
let about = raw
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.starts_with("Usage:"))?;
let usage = raw
.lines()
.map(str::trim)
.find(|line| line.starts_with("Usage:"))?;
let command_path = extract_command_path_from_usage(usage)?;
Some((command_path, about.to_string()))
}
fn extract_command_path_from_usage(usage: &str) -> Option<String> {
let rest = usage.split_once(':')?.1.trim();
let path = rest.split('[').next()?.trim();
let normalized = path.replace(".exe", "");
if normalized.is_empty() {
None
} else {
Some(normalized)
}
}
fn style_help_line(line: &str) -> String {
let trimmed = line.trim_start();
if trimmed.is_empty() {
return String::new();
}
if matches!(
trimmed,
"Commands:" | "Options:" | "Arguments:" | "Subcommands:"
) {
return format!(
"\n{} {}",
"â–¸".bright_blue().bold(),
trimmed.bright_blue().bold()
);
}
if trimmed.starts_with("Usage:") {
return style_usage_line(line);
}
if trimmed == "Discover:" {
return format!(
"\n{} {}",
"â—‡".bright_green().bold(),
trimmed.bright_green().bold()
);
}
if is_example_section_header(trimmed) {
return format!(
"\n{} {}",
"â—‡".bright_green().bold(),
trimmed.bright_green().bold()
);
}
if is_note_section_header(trimmed) {
return format!(
"\n{} {}",
"â—‡".bright_yellow().bold(),
trimmed.bright_yellow().bold()
);
}
if is_command_entry(line) {
return style_command_entry(line);
}
if is_option_flags_line(line) {
return format!(" {}", line.trim().bright_yellow());
}
if is_option_entry(line) {
return style_option_entry(line);
}
if is_command_name_line(line) {
return format!(" {}", line.trim().bright_cyan().bold());
}
if is_indented_help_text(line, 4) || is_indented_help_text(line, 8) {
return format!(" {}", trimmed.bright_black());
}
if is_example_command_line(trimmed) {
return format!(" {}", highlight_inline_flags(trimmed).dimmed());
}
if trimmed.starts_with("Run `") || trimmed.starts_with("Use `") || trimmed.starts_with("Pass `")
{
return trimmed.bright_black().to_string();
}
line.to_string()
}
fn style_usage_line(line: &str) -> String {
let (prefix, rest) = line.split_once(':').unwrap_or((line, ""));
format!(
"{} {}",
format!("{prefix}:").bright_cyan().bold(),
highlight_inline_flags(rest.trim()).bright_white()
)
}
fn is_example_section_header(line: &str) -> bool {
matches!(
line,
"Examples:"
| "Quick start:"
| "Discover:"
| "Available targets:"
| "List installable targets:"
) || line.starts_with("Quick start")
|| line.starts_with("List installable")
}
fn is_note_section_header(line: &str) -> bool {
line == "Notes:"
|| line.starts_with("Tip:")
|| line.starts_with("More info:")
|| line.starts_with("Hint:")
}
fn is_command_entry(line: &str) -> bool {
if !line.starts_with(" ") || line.starts_with(" ") {
return false;
}
let trimmed = line.trim_start();
let Some((name, _)) = trimmed.split_once(" ") else {
return false;
};
!name.starts_with('-') && !name.contains('<') && name.len() <= 24
}
fn style_command_entry(line: &str) -> String {
let trimmed = line.trim_start();
let mut parts = trimmed.splitn(2, " ");
let name = parts.next().unwrap_or_default();
let description = parts.next().unwrap_or_default().trim();
let alias = extract_alias_suffix(description);
let base_description = description
.split_once('[')
.map(|(left, _)| left.trim())
.unwrap_or(description);
let name = name.bright_cyan().bold();
if alias.is_empty() {
format!(" {name:<22} {}", base_description.bright_black())
} else {
format!(
" {name:<22} {} {}",
base_description.bright_black(),
alias.bright_black()
)
}
}
fn extract_alias_suffix(description: &str) -> String {
let Some(start) = description.find('[') else {
return String::new();
};
description[start..].to_string()
}
fn is_option_flags_line(line: &str) -> bool {
let trimmed = line.trim_start();
line.starts_with(" ")
&& !line.starts_with(" ")
&& (trimmed.starts_with("--") || trimmed.starts_with("-"))
}
fn is_option_entry(line: &str) -> bool {
let trimmed = line.trim_start();
line.starts_with(" ")
&& (trimmed.starts_with("--") || trimmed.starts_with("-"))
&& !trimmed.starts_with("Usage:")
}
fn is_command_name_line(line: &str) -> bool {
if !line.starts_with(" ") || line.starts_with(" ") {
return false;
}
let trimmed = line.trim();
!trimmed.is_empty()
&& !trimmed.ends_with(':')
&& !trimmed.starts_with('-')
&& !trimmed.contains(' ')
&& trimmed.len() <= 24
}
fn is_indented_help_text(line: &str, min_spaces: usize) -> bool {
let leading = line.chars().take_while(|ch| *ch == ' ').count();
leading >= min_spaces && !line.trim_start().starts_with('-') && !line.trim().is_empty()
}
fn style_command_pair(name_line: &str, description_line: &str) -> String {
let name = name_line.trim().bright_cyan().bold();
let description = description_line.trim();
let alias = extract_alias_suffix(description);
let base_description = description
.split_once('[')
.map(|(left, _)| left.trim())
.unwrap_or(description);
if alias.is_empty() {
format!(" {name:<22} {}", base_description.bright_black())
} else {
format!(
" {name:<22} {} {}",
base_description.bright_black(),
alias.bright_black()
)
}
}
fn style_option_pair(flags_line: &str, description_line: &str) -> String {
let flags = flags_line.trim();
let styled_flags = flags
.split(", ")
.map(|flag| flag.bright_yellow().to_string())
.collect::<Vec<_>>()
.join(", ");
format!(
" {styled_flags:<28} {}",
description_line.trim().bright_black()
)
}
fn style_option_entry(line: &str) -> String {
let trimmed = line.trim_start();
let mut parts = trimmed.splitn(2, " ");
let flags = parts.next().unwrap_or_default();
let description = parts.next().unwrap_or_default().trim();
let styled_flags = flags
.split(", ")
.map(|flag| flag.bright_yellow().to_string())
.collect::<Vec<_>>()
.join(", ");
if description.is_empty() {
format!(" {styled_flags}")
} else {
format!(" {styled_flags:<28} {}", description.bright_black())
}
}
fn is_example_command_line(line: &str) -> bool {
line.starts_with("xbp ") || line.starts_with("cargo ") || line.starts_with("git ")
}
fn highlight_inline_flags(text: &str) -> String {
let mut output = String::new();
let mut current = String::new();
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '-' && matches!(chars.peek(), Some('-' | 'f' | 'h' | 'l' | 'p' | 'v' | 'n')) {
if !current.is_empty() {
output.push_str(¤t);
current.clear();
}
let mut flag = String::from('-');
if chars.peek() == Some(&'-') {
flag.push(chars.next().expect("dash"));
}
while let Some(&next) = chars.peek() {
if next.is_ascii_alphanumeric() || next == '-' {
flag.push(chars.next().expect("flag char"));
} else {
break;
}
}
output.push_str(&flag.bright_yellow().to_string());
continue;
}
if ch == '<' {
if !current.is_empty() {
output.push_str(¤t);
current.clear();
}
let mut placeholder = String::from('<');
while let Some(next) = chars.next() {
placeholder.push(next);
if next == '>' {
break;
}
}
output.push_str(&placeholder.bright_green().to_string());
continue;
}
current.push(ch);
}
if !current.is_empty() {
output.push_str(¤t);
}
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_root_help_scope() {
let raw = "xbp 10.30.3\n\nAbout\nUsage: xbp [OPTIONS] [COMMAND]";
assert_eq!(detect_help_scope(raw), HelpScope::Root);
}
#[test]
fn workers_help_is_not_root() {
let raw = "Manage workers\nUsage: xbp.exe workers [OPTIONS] <COMMAND>";
assert!(!is_root_help_text(raw));
}
#[test]
fn styles_usage_line_with_flags() {
let styled = style_help_line("Usage: xbp workers logs [OPTIONS]");
assert!(styled.contains("Usage:"));
assert!(styled.contains("workers"));
}
#[test]
fn styles_command_entry_line() {
let styled = style_help_line(" list List workers [aliases: ls]");
assert!(styled.contains("list"));
}
#[test]
fn catalog_scope_skips_duplicate_about() {
let raw = "Deploy services\nUsage: xbp [OPTIONS] [COMMAND]\n\nCommands:\n diag\n Run diagnostics";
emit_styled_help(raw, HelpScope::Catalog);
}
}