Skip to main content

facet_args/
help.rs

1//! Help text generation for command-line interfaces.
2//!
3//! This module provides utilities to generate help text from Facet type metadata,
4//! including doc comments, field names, and attribute information.
5
6use crate::{is_counted_field, is_supported_counted_type};
7use alloc::string::String;
8use alloc::vec::Vec;
9use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType, Variant};
10use heck::ToKebabCase;
11use owo_colors::OwoColorize;
12
13/// Configuration for help text generation.
14#[derive(Debug, Clone)]
15pub struct HelpConfig {
16    /// Program name (defaults to executable name)
17    pub program_name: Option<String>,
18    /// Program version
19    pub version: Option<String>,
20    /// Additional description to show after the auto-generated one
21    pub description: Option<String>,
22    /// Width for wrapping text (0 = no wrapping)
23    pub width: usize,
24}
25
26impl Default for HelpConfig {
27    fn default() -> Self {
28        Self {
29            program_name: None,
30            version: None,
31            description: None,
32            width: 80,
33        }
34    }
35}
36
37/// Generate help text for a Facet type.
38pub fn generate_help<T: facet_core::Facet<'static>>(config: &HelpConfig) -> String {
39    generate_help_for_shape(T::SHAPE, config)
40}
41
42/// Generate help text for a shape.
43pub fn generate_help_for_shape(shape: &'static Shape, config: &HelpConfig) -> String {
44    let mut out = String::new();
45
46    // Program name and version
47    let program_name = config
48        .program_name
49        .clone()
50        .or_else(|| std::env::args().next())
51        .unwrap_or_else(|| "program".to_string());
52
53    if let Some(version) = &config.version {
54        out.push_str(&format!("{program_name} {version}\n"));
55    } else {
56        out.push_str(&format!("{program_name}\n"));
57    }
58
59    // Type doc comment
60    if !shape.doc.is_empty() {
61        out.push('\n');
62        for line in shape.doc {
63            out.push_str(line.trim());
64            out.push('\n');
65        }
66    }
67
68    // Additional description
69    if let Some(desc) = &config.description {
70        out.push('\n');
71        out.push_str(desc);
72        out.push('\n');
73    }
74
75    out.push('\n');
76
77    // Generate based on type
78    match &shape.ty {
79        Type::User(UserType::Struct(struct_type)) => {
80            generate_struct_help(&mut out, &program_name, struct_type.fields);
81        }
82        Type::User(UserType::Enum(enum_type)) => {
83            generate_enum_help(&mut out, &program_name, enum_type.variants);
84        }
85        _ => {
86            out.push_str("(No help available for this type)\n");
87        }
88    }
89
90    out
91}
92
93fn generate_struct_help(out: &mut String, program_name: &str, fields: &'static [Field]) {
94    // Collect flags, positionals, and subcommand
95    let mut flags: Vec<&Field> = Vec::new();
96    let mut positionals: Vec<&Field> = Vec::new();
97    let mut subcommand: Option<&Field> = None;
98
99    for field in fields {
100        if field.has_attr(Some("args"), "subcommand") {
101            subcommand = Some(field);
102        } else if field.has_attr(Some("args"), "positional") {
103            positionals.push(field);
104        } else {
105            flags.push(field);
106        }
107    }
108
109    // Usage line
110    out.push_str(&format!("{}:\n    ", "USAGE".yellow().bold()));
111    out.push_str(program_name);
112
113    if !flags.is_empty() {
114        out.push_str(" [OPTIONS]");
115    }
116
117    for pos in &positionals {
118        let name = pos.name.to_kebab_case().to_uppercase();
119        let is_optional = matches!(pos.shape().def, Def::Option(_)) || pos.has_default();
120        if is_optional {
121            out.push_str(&format!(" [{name}]"));
122        } else {
123            out.push_str(&format!(" <{name}>"));
124        }
125    }
126
127    if let Some(sub) = subcommand {
128        let is_optional = matches!(sub.shape().def, Def::Option(_));
129        if is_optional {
130            out.push_str(" [COMMAND]");
131        } else {
132            out.push_str(" <COMMAND>");
133        }
134    }
135
136    out.push_str("\n\n");
137
138    // Positional arguments
139    if !positionals.is_empty() {
140        out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
141        for field in &positionals {
142            write_field_help(out, field, true);
143        }
144        out.push('\n');
145    }
146
147    // Options
148    if !flags.is_empty() {
149        out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
150        for field in &flags {
151            write_field_help(out, field, false);
152        }
153        out.push('\n');
154    }
155
156    // Subcommands
157    if let Some(sub_field) = subcommand {
158        let sub_shape = sub_field.shape();
159        // Handle Option<Enum> or direct Enum
160        let enum_shape = if let Def::Option(opt) = sub_shape.def {
161            opt.t
162        } else {
163            sub_shape
164        };
165
166        if let Type::User(UserType::Enum(enum_type)) = enum_shape.ty {
167            out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
168            for variant in enum_type.variants {
169                write_variant_help(out, variant);
170            }
171            out.push('\n');
172        }
173    }
174}
175
176fn generate_enum_help(out: &mut String, program_name: &str, variants: &'static [Variant]) {
177    // For top-level enum, show subcommands
178    out.push_str(&format!("{}:\n    ", "USAGE".yellow().bold()));
179    out.push_str(program_name);
180    out.push_str(" <COMMAND>\n\n");
181
182    out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
183    for variant in variants {
184        write_variant_help(out, variant);
185    }
186    out.push('\n');
187}
188
189fn write_field_help(out: &mut String, field: &Field, is_positional: bool) {
190    out.push_str("    ");
191
192    // Short flag
193    let short = get_short_flag(field);
194    if let Some(c) = short {
195        out.push_str(&format!("{}, ", format!("-{c}").green()));
196    } else {
197        out.push_str("    ");
198    }
199
200    // Long flag or positional name
201    let kebab_name = field.name.to_kebab_case();
202    let is_counted = is_counted_field(field) && is_supported_counted_type(field.shape());
203
204    if is_positional {
205        out.push_str(&format!(
206            "{}",
207            format!("<{}>", kebab_name.to_uppercase()).green()
208        ));
209    } else {
210        out.push_str(&format!("{}", format!("--{kebab_name}").green()));
211
212        // Show value placeholder for non-bool, non-counted types
213        let shape = field.shape();
214        if !is_counted && !shape.is_shape(bool::SHAPE) {
215            out.push_str(&format!(" <{}>", shape.type_identifier.to_uppercase()));
216        }
217    }
218
219    // Doc comment
220    if let Some(doc) = field.doc.first() {
221        out.push_str("\n            ");
222        out.push_str(doc.trim());
223    }
224
225    if is_counted {
226        out.push_str("\n            ");
227        out.push_str("[can be repeated]");
228    }
229
230    out.push('\n');
231}
232
233fn write_variant_help(out: &mut String, variant: &Variant) {
234    out.push_str("    ");
235
236    // Variant name (check for rename)
237    let name = variant
238        .get_builtin_attr("rename")
239        .and_then(|attr| attr.get_as::<&str>())
240        .map(|s| (*s).to_string())
241        .unwrap_or_else(|| variant.name.to_kebab_case());
242
243    out.push_str(&format!("{}", name.green()));
244
245    // Doc comment
246    if let Some(doc) = variant.doc.first() {
247        out.push_str("\n            ");
248        out.push_str(doc.trim());
249    }
250
251    out.push('\n');
252}
253
254/// Get the short flag character for a field, if any
255fn get_short_flag(field: &Field) -> Option<char> {
256    field
257        .get_attr(Some("args"), "short")
258        .and_then(|attr| attr.get_as::<crate::Attr>())
259        .and_then(|attr| {
260            if let crate::Attr::Short(c) = attr {
261                // If explicit char provided, use it; otherwise use first char of field name
262                c.or_else(|| field.name.chars().next())
263            } else {
264                None
265            }
266        })
267}
268
269/// Generate help for a specific subcommand variant.
270pub fn generate_subcommand_help(
271    variant: &'static Variant,
272    parent_program: &str,
273    config: &HelpConfig,
274) -> String {
275    let mut out = String::new();
276
277    let variant_name = variant
278        .get_builtin_attr("rename")
279        .and_then(|attr| attr.get_as::<&str>())
280        .map(|s| (*s).to_string())
281        .unwrap_or_else(|| variant.name.to_kebab_case());
282
283    let full_name = format!("{parent_program} {variant_name}");
284
285    // Header
286    if let Some(version) = &config.version {
287        out.push_str(&format!("{full_name} {version}\n"));
288    } else {
289        out.push_str(&format!("{full_name}\n"));
290    }
291
292    // Variant doc comment
293    if !variant.doc.is_empty() {
294        out.push('\n');
295        for line in variant.doc {
296            out.push_str(line.trim());
297            out.push('\n');
298        }
299    }
300
301    out.push('\n');
302
303    // Generate help for variant fields
304    // Handle tuple variant with single struct field (newtype pattern)
305    // e.g., `Build(BuildArgs)` should flatten BuildArgs fields
306    // This matches clap's behavior: "automatically flattened with a tuple-variant"
307    let fields = variant.data.fields;
308    if variant.data.kind == StructKind::TupleStruct && fields.len() == 1 {
309        let inner_shape = fields[0].shape();
310        if let Type::User(UserType::Struct(struct_type)) = inner_shape.ty {
311            // Use the inner struct's fields instead of the tuple field
312            generate_struct_help(&mut out, &full_name, struct_type.fields);
313            return out;
314        }
315    }
316
317    generate_struct_help(&mut out, &full_name, fields);
318
319    out
320}