use crate::{is_counted_field, is_supported_counted_type};
use alloc::string::String;
use alloc::vec::Vec;
use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType, Variant};
use heck::ToKebabCase;
use owo_colors::OwoColorize;
#[derive(Debug, Clone)]
pub struct HelpConfig {
pub program_name: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
pub width: usize,
}
impl Default for HelpConfig {
fn default() -> Self {
Self {
program_name: None,
version: None,
description: None,
width: 80,
}
}
}
pub fn generate_help<T: facet_core::Facet<'static>>(config: &HelpConfig) -> String {
generate_help_for_shape(T::SHAPE, config)
}
pub fn generate_help_for_shape(shape: &'static Shape, config: &HelpConfig) -> String {
let mut out = String::new();
let program_name = config
.program_name
.clone()
.or_else(|| std::env::args().next())
.unwrap_or_else(|| "program".to_string());
if let Some(version) = &config.version {
out.push_str(&format!("{program_name} {version}\n"));
} else {
out.push_str(&format!("{program_name}\n"));
}
if !shape.doc.is_empty() {
out.push('\n');
for line in shape.doc {
out.push_str(line.trim());
out.push('\n');
}
}
if let Some(desc) = &config.description {
out.push('\n');
out.push_str(desc);
out.push('\n');
}
out.push('\n');
match &shape.ty {
Type::User(UserType::Struct(struct_type)) => {
generate_struct_help(&mut out, &program_name, struct_type.fields);
}
Type::User(UserType::Enum(enum_type)) => {
generate_enum_help(&mut out, &program_name, enum_type.variants);
}
_ => {
out.push_str("(No help available for this type)\n");
}
}
out
}
fn generate_struct_help(out: &mut String, program_name: &str, fields: &'static [Field]) {
let mut flags: Vec<&Field> = Vec::new();
let mut positionals: Vec<&Field> = Vec::new();
let mut subcommand: Option<&Field> = None;
for field in fields {
if field.has_attr(Some("args"), "subcommand") {
subcommand = Some(field);
} else if field.has_attr(Some("args"), "positional") {
positionals.push(field);
} else {
flags.push(field);
}
}
out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
out.push_str(program_name);
if !flags.is_empty() {
out.push_str(" [OPTIONS]");
}
for pos in &positionals {
let name = pos.name.to_kebab_case().to_uppercase();
let is_optional = matches!(pos.shape().def, Def::Option(_)) || pos.has_default();
if is_optional {
out.push_str(&format!(" [{name}]"));
} else {
out.push_str(&format!(" <{name}>"));
}
}
if let Some(sub) = subcommand {
let is_optional = matches!(sub.shape().def, Def::Option(_));
if is_optional {
out.push_str(" [COMMAND]");
} else {
out.push_str(" <COMMAND>");
}
}
out.push_str("\n\n");
if !positionals.is_empty() {
out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
for field in &positionals {
write_field_help(out, field, true);
}
out.push('\n');
}
if !flags.is_empty() {
out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
for field in &flags {
write_field_help(out, field, false);
}
out.push('\n');
}
if let Some(sub_field) = subcommand {
let sub_shape = sub_field.shape();
let enum_shape = if let Def::Option(opt) = sub_shape.def {
opt.t
} else {
sub_shape
};
if let Type::User(UserType::Enum(enum_type)) = enum_shape.ty {
out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
for variant in enum_type.variants {
write_variant_help(out, variant);
}
out.push('\n');
}
}
}
fn generate_enum_help(out: &mut String, program_name: &str, variants: &'static [Variant]) {
out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
out.push_str(program_name);
out.push_str(" <COMMAND>\n\n");
out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
for variant in variants {
write_variant_help(out, variant);
}
out.push('\n');
}
fn write_field_help(out: &mut String, field: &Field, is_positional: bool) {
out.push_str(" ");
let short = get_short_flag(field);
if let Some(c) = short {
out.push_str(&format!("{}, ", format!("-{c}").green()));
} else {
out.push_str(" ");
}
let kebab_name = field.name.to_kebab_case();
let is_counted = is_counted_field(field) && is_supported_counted_type(field.shape());
if is_positional {
out.push_str(&format!(
"{}",
format!("<{}>", kebab_name.to_uppercase()).green()
));
} else {
out.push_str(&format!("{}", format!("--{kebab_name}").green()));
let shape = field.shape();
if !is_counted && !shape.is_shape(bool::SHAPE) {
out.push_str(&format!(" <{}>", shape.type_identifier.to_uppercase()));
}
}
if let Some(doc) = field.doc.first() {
out.push_str("\n ");
out.push_str(doc.trim());
}
if is_counted {
out.push_str("\n ");
out.push_str("[can be repeated]");
}
out.push('\n');
}
fn write_variant_help(out: &mut String, variant: &Variant) {
out.push_str(" ");
let name = variant
.get_builtin_attr("rename")
.and_then(|attr| attr.get_as::<&str>())
.map(|s| (*s).to_string())
.unwrap_or_else(|| variant.name.to_kebab_case());
out.push_str(&format!("{}", name.green()));
if let Some(doc) = variant.doc.first() {
out.push_str("\n ");
out.push_str(doc.trim());
}
out.push('\n');
}
fn get_short_flag(field: &Field) -> Option<char> {
field
.get_attr(Some("args"), "short")
.and_then(|attr| attr.get_as::<crate::Attr>())
.and_then(|attr| {
if let crate::Attr::Short(c) = attr {
c.or_else(|| field.name.chars().next())
} else {
None
}
})
}
pub fn generate_subcommand_help(
variant: &'static Variant,
parent_program: &str,
config: &HelpConfig,
) -> String {
let mut out = String::new();
let variant_name = variant
.get_builtin_attr("rename")
.and_then(|attr| attr.get_as::<&str>())
.map(|s| (*s).to_string())
.unwrap_or_else(|| variant.name.to_kebab_case());
let full_name = format!("{parent_program} {variant_name}");
if let Some(version) = &config.version {
out.push_str(&format!("{full_name} {version}\n"));
} else {
out.push_str(&format!("{full_name}\n"));
}
if !variant.doc.is_empty() {
out.push('\n');
for line in variant.doc {
out.push_str(line.trim());
out.push('\n');
}
}
out.push('\n');
let fields = variant.data.fields;
if variant.data.kind == StructKind::TupleStruct && fields.len() == 1 {
let inner_shape = fields[0].shape();
if let Type::User(UserType::Struct(struct_type)) = inner_shape.ty {
generate_struct_help(&mut out, &full_name, struct_type.fields);
return out;
}
}
generate_struct_help(&mut out, &full_name, fields);
out
}