use crate::missing::normalize_program_name;
use crate::schema::{ArgLevelSchema, ArgSchema, Schema, Subcommand};
use facet_core::Facet;
use heck::ToKebabCase;
use owo_colors::OwoColorize;
use owo_colors::Stream::Stdout;
use std::string::String;
use std::vec::Vec;
pub fn generate_help<T: Facet<'static>>(config: &HelpConfig) -> String {
generate_help_for_shape(T::SHAPE, config)
}
pub fn generate_help_for_shape(shape: &'static facet_core::Shape, config: &HelpConfig) -> String {
let schema = match Schema::from_shape(shape) {
Ok(s) => s,
Err(_) => {
let program_name = config
.program_name
.clone()
.or_else(|| {
std::env::args()
.next()
.map(|path| normalize_program_name(&path))
})
.unwrap_or_else(|| "program".to_string());
return format!(
"{}\n\n(Schema could not be built for this type)\n",
program_name
);
}
};
generate_help_for_subcommand(&schema, &[], config)
}
#[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_for_subcommand(
schema: &Schema,
subcommand_path: &[String],
config: &HelpConfig,
) -> String {
let program_name = config
.program_name
.clone()
.or_else(|| {
std::env::args()
.next()
.map(|path| normalize_program_name(&path))
})
.unwrap_or_else(|| "program".to_string());
if subcommand_path.is_empty() {
return generate_help_from_schema(schema, &program_name, config);
}
let mut current_args = schema.args();
let mut command_path = vec![program_name.clone()];
for name in subcommand_path {
let sub = current_args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
if let Some(sub) = sub {
command_path.push(sub.cli_name().to_string());
current_args = sub.args();
} else {
return generate_help_from_schema(schema, &program_name, config);
}
}
let mut final_sub: Option<&Subcommand> = None;
let mut args = schema.args();
for name in subcommand_path {
let sub = args
.subcommands()
.values()
.find(|s| s.effective_name() == name);
if let Some(sub) = sub {
final_sub = Some(sub);
args = sub.args();
}
}
generate_help_for_subcommand_level(current_args, final_sub, &command_path.join(" "), config)
}
fn generate_help_from_schema(schema: &Schema, program_name: &str, config: &HelpConfig) -> String {
let mut out = String::new();
if let Some(version) = &config.version {
out.push_str(&format!("{program_name} {version}\n"));
} else {
out.push_str(&format!("{program_name}\n"));
}
if let Some(summary) = schema.docs().summary() {
out.push('\n');
out.push_str(summary.trim());
out.push('\n');
}
if let Some(details) = schema.docs().details() {
for line in details.lines() {
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');
generate_arg_level_help(&mut out, schema.args(), program_name);
out
}
fn generate_help_for_subcommand_level(
args: &ArgLevelSchema,
subcommand: Option<&Subcommand>,
full_command: &str,
config: &HelpConfig,
) -> String {
let mut out = String::new();
out.push_str(&format!("{full_command}\n"));
if let Some(sub) = subcommand {
if let Some(summary) = sub.docs().summary() {
out.push('\n');
out.push_str(summary.trim());
out.push('\n');
}
if let Some(details) = sub.docs().details() {
for line in details.lines() {
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');
generate_arg_level_help(&mut out, args, full_command);
out
}
fn generate_arg_level_help(out: &mut String, args: &ArgLevelSchema, program_name: &str) {
let mut positionals: Vec<&ArgSchema> = Vec::new();
let mut flags: Vec<&ArgSchema> = Vec::new();
for (_name, arg) in args.args().iter() {
if arg.kind().is_positional() {
positionals.push(arg);
} else {
flags.push(arg);
}
}
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_uppercase();
if pos.required() {
out.push_str(&format!(" <{name}>"));
} else {
out.push_str(&format!(" [{name}]"));
}
}
if args.has_subcommands() {
if args.subcommand_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 arg in &positionals {
write_arg_help(out, arg);
}
out.push('\n');
}
if !flags.is_empty() {
out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
for arg in &flags {
write_arg_help(out, arg);
}
out.push('\n');
}
if args.has_subcommands() {
out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
for sub in args.subcommands().values() {
write_subcommand_help(out, sub);
}
out.push('\n');
}
}
fn write_arg_help(out: &mut String, arg: &ArgSchema) {
out.push_str(" ");
let is_positional = arg.kind().is_positional();
if let Some(c) = arg.kind().short() {
out.push_str(&format!(
"{}, ",
format!("-{c}").if_supports_color(Stdout, |text| text.green())
));
} else {
out.push_str(" ");
}
let name = arg.name();
let is_counted = arg.kind().is_counted();
if is_positional {
out.push_str(&format!(
"{}",
format!("<{}>", name.to_uppercase()).if_supports_color(Stdout, |text| text.green())
));
} else {
out.push_str(&format!(
"{}",
format!("--{}", name.to_kebab_case()).if_supports_color(Stdout, |text| text.green())
));
if !is_counted && !arg.value().is_bool() {
let placeholder = if let Some(desc) = arg.label() {
desc.to_uppercase()
} else if let Some(variants) = arg.value().inner_if_option().enum_variants() {
variants.join(",")
} else {
arg.value().type_identifier().to_uppercase()
};
out.push_str(&format!(" <{}>", placeholder));
}
}
if let Some(summary) = arg.docs().summary() {
out.push_str("\n ");
out.push_str(summary.trim());
}
if is_counted {
out.push_str("\n ");
out.push_str("[can be repeated]");
}
out.push('\n');
}
fn write_subcommand_help(out: &mut String, sub: &Subcommand) {
out.push_str(" ");
out.push_str(&format!(
"{}",
sub.cli_name()
.if_supports_color(Stdout, |text| text.green())
));
if let Some(summary) = sub.docs().summary() {
out.push_str("\n ");
out.push_str(summary.trim());
}
out.push('\n');
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
use figue_attrs as args;
#[derive(Facet)]
struct CommonArgs {
#[facet(args::named, crate::short = 'v')]
verbose: bool,
#[facet(args::named, crate::short = 'q')]
quiet: bool,
}
#[derive(Facet)]
struct ArgsWithFlatten {
#[facet(args::positional)]
input: String,
#[facet(flatten)]
common: CommonArgs,
}
#[test]
fn test_flatten_args_appear_in_help() {
let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("--verbose"),
"help should contain --verbose from flattened CommonArgs"
);
assert!(help.contains("-v"), "help should contain -v short flag");
assert!(
help.contains("--quiet"),
"help should contain --quiet from flattened CommonArgs"
);
assert!(help.contains("-q"), "help should contain -q short flag");
assert!(
!help.contains("--common"),
"help should not show --common as a flag"
);
}
#[test]
fn test_flatten_docs_preserved() {
let schema = Schema::from_shape(ArgsWithFlatten::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("verbose output"),
"help should contain verbose field doc"
);
assert!(
help.contains("quiet mode"),
"help should contain quiet field doc"
);
}
#[derive(Facet)]
struct ServeArgs {
#[facet(args::named)]
port: u16,
#[facet(args::named)]
host: String,
}
#[derive(Facet)]
struct TupleVariantArgs {
#[facet(args::subcommand)]
command: Option<TupleVariantCommand>,
}
#[derive(Facet)]
#[repr(u8)]
#[allow(dead_code)]
enum TupleVariantCommand {
Serve(ServeArgs),
}
#[test]
fn test_label_overrides_placeholder() {
#[derive(Facet)]
struct TDArgs {
#[facet(args::named, args::label = "PATH")]
input: std::path::PathBuf,
}
let schema = Schema::from_shape(TDArgs::SHAPE).unwrap();
let help = generate_help_for_subcommand(&schema, &[], &HelpConfig::default());
assert!(
help.contains("<PATH>"),
"help should use custom label placeholder"
);
}
#[test]
fn test_tuple_variant_fields_not_shown_as_option() {
let schema = Schema::from_shape(TupleVariantArgs::SHAPE).unwrap();
let help =
generate_help_for_subcommand(&schema, &["Serve".to_string()], &HelpConfig::default());
assert!(
help.contains("--port"),
"help should contain --port from ServeArgs"
);
assert!(
help.contains("--host"),
"help should contain --host from ServeArgs"
);
assert!(
!help.contains("--0"),
"help should NOT show --0 for tuple variant wrapper field"
);
assert!(
!help.contains("SERVEARGS"),
"help should NOT show SERVEARGS as an option value"
);
}
}