1use alloc::string::String;
7use alloc::vec::Vec;
8use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType, Variant};
9use heck::ToKebabCase;
10use owo_colors::OwoColorize;
11
12#[derive(Debug, Clone)]
14pub struct HelpConfig {
15 pub program_name: Option<String>,
17 pub version: Option<String>,
19 pub description: Option<String>,
21 pub width: usize,
23}
24
25impl Default for HelpConfig {
26 fn default() -> Self {
27 Self {
28 program_name: None,
29 version: None,
30 description: None,
31 width: 80,
32 }
33 }
34}
35
36pub fn generate_help<T: facet_core::Facet<'static>>(config: &HelpConfig) -> String {
38 generate_help_for_shape(T::SHAPE, config)
39}
40
41pub fn generate_help_for_shape(shape: &'static Shape, config: &HelpConfig) -> String {
43 let mut out = String::new();
44
45 let program_name = config
47 .program_name
48 .clone()
49 .or_else(|| std::env::args().next())
50 .unwrap_or_else(|| "program".to_string());
51
52 if let Some(version) = &config.version {
53 out.push_str(&format!("{program_name} {version}\n"));
54 } else {
55 out.push_str(&format!("{program_name}\n"));
56 }
57
58 if !shape.doc.is_empty() {
60 out.push('\n');
61 for line in shape.doc {
62 out.push_str(line.trim());
63 out.push('\n');
64 }
65 }
66
67 if let Some(desc) = &config.description {
69 out.push('\n');
70 out.push_str(desc);
71 out.push('\n');
72 }
73
74 out.push('\n');
75
76 match &shape.ty {
78 Type::User(UserType::Struct(struct_type)) => {
79 generate_struct_help(&mut out, &program_name, struct_type.fields);
80 }
81 Type::User(UserType::Enum(enum_type)) => {
82 generate_enum_help(&mut out, &program_name, enum_type.variants);
83 }
84 _ => {
85 out.push_str("(No help available for this type)\n");
86 }
87 }
88
89 out
90}
91
92fn generate_struct_help(out: &mut String, program_name: &str, fields: &'static [Field]) {
93 let mut flags: Vec<&Field> = Vec::new();
95 let mut positionals: Vec<&Field> = Vec::new();
96 let mut subcommand: Option<&Field> = None;
97
98 for field in fields {
99 if field.has_attr(Some("args"), "subcommand") {
100 subcommand = Some(field);
101 } else if field.has_attr(Some("args"), "positional") {
102 positionals.push(field);
103 } else {
104 flags.push(field);
105 }
106 }
107
108 out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
110 out.push_str(program_name);
111
112 if !flags.is_empty() {
113 out.push_str(" [OPTIONS]");
114 }
115
116 for pos in &positionals {
117 let name = pos.name.to_kebab_case().to_uppercase();
118 let is_optional = matches!(pos.shape().def, Def::Option(_)) || pos.has_default();
119 if is_optional {
120 out.push_str(&format!(" [{name}]"));
121 } else {
122 out.push_str(&format!(" <{name}>"));
123 }
124 }
125
126 if let Some(sub) = subcommand {
127 let is_optional = matches!(sub.shape().def, Def::Option(_));
128 if is_optional {
129 out.push_str(" [COMMAND]");
130 } else {
131 out.push_str(" <COMMAND>");
132 }
133 }
134
135 out.push_str("\n\n");
136
137 if !positionals.is_empty() {
139 out.push_str(&format!("{}:\n", "ARGUMENTS".yellow().bold()));
140 for field in &positionals {
141 write_field_help(out, field, true);
142 }
143 out.push('\n');
144 }
145
146 if !flags.is_empty() {
148 out.push_str(&format!("{}:\n", "OPTIONS".yellow().bold()));
149 for field in &flags {
150 write_field_help(out, field, false);
151 }
152 out.push('\n');
153 }
154
155 if let Some(sub_field) = subcommand {
157 let sub_shape = sub_field.shape();
158 let enum_shape = if let Def::Option(opt) = sub_shape.def {
160 opt.t
161 } else {
162 sub_shape
163 };
164
165 if let Type::User(UserType::Enum(enum_type)) = enum_shape.ty {
166 out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
167 for variant in enum_type.variants {
168 write_variant_help(out, variant);
169 }
170 out.push('\n');
171 }
172 }
173}
174
175fn generate_enum_help(out: &mut String, program_name: &str, variants: &'static [Variant]) {
176 out.push_str(&format!("{}:\n ", "USAGE".yellow().bold()));
178 out.push_str(program_name);
179 out.push_str(" <COMMAND>\n\n");
180
181 out.push_str(&format!("{}:\n", "COMMANDS".yellow().bold()));
182 for variant in variants {
183 write_variant_help(out, variant);
184 }
185 out.push('\n');
186}
187
188fn write_field_help(out: &mut String, field: &Field, is_positional: bool) {
189 out.push_str(" ");
190
191 let short = get_short_flag(field);
193 if let Some(c) = short {
194 out.push_str(&format!("{}, ", format!("-{c}").green()));
195 } else {
196 out.push_str(" ");
197 }
198
199 let kebab_name = field.name.to_kebab_case();
201 if is_positional {
202 out.push_str(&format!(
203 "{}",
204 format!("<{}>", kebab_name.to_uppercase()).green()
205 ));
206 } else {
207 out.push_str(&format!("{}", format!("--{kebab_name}").green()));
208
209 let shape = field.shape();
211 if !shape.is_shape(bool::SHAPE) {
212 out.push_str(&format!(" <{}>", shape.type_identifier.to_uppercase()));
213 }
214 }
215
216 if let Some(doc) = field.doc.first() {
218 out.push_str("\n ");
219 out.push_str(doc.trim());
220 }
221
222 out.push('\n');
223}
224
225fn write_variant_help(out: &mut String, variant: &Variant) {
226 out.push_str(" ");
227
228 let name = variant
230 .get_builtin_attr("rename")
231 .and_then(|attr| attr.get_as::<&str>())
232 .map(|s| (*s).to_string())
233 .unwrap_or_else(|| variant.name.to_kebab_case());
234
235 out.push_str(&format!("{}", name.green()));
236
237 if let Some(doc) = variant.doc.first() {
239 out.push_str("\n ");
240 out.push_str(doc.trim());
241 }
242
243 out.push('\n');
244}
245
246fn get_short_flag(field: &Field) -> Option<char> {
248 field
249 .get_attr(Some("args"), "short")
250 .and_then(|attr| attr.get_as::<crate::Attr>())
251 .and_then(|attr| {
252 if let crate::Attr::Short(c) = attr {
253 c.or_else(|| field.name.chars().next())
255 } else {
256 None
257 }
258 })
259}
260
261pub fn generate_subcommand_help(
263 variant: &'static Variant,
264 parent_program: &str,
265 config: &HelpConfig,
266) -> String {
267 let mut out = String::new();
268
269 let variant_name = variant
270 .get_builtin_attr("rename")
271 .and_then(|attr| attr.get_as::<&str>())
272 .map(|s| (*s).to_string())
273 .unwrap_or_else(|| variant.name.to_kebab_case());
274
275 let full_name = format!("{parent_program} {variant_name}");
276
277 if let Some(version) = &config.version {
279 out.push_str(&format!("{full_name} {version}\n"));
280 } else {
281 out.push_str(&format!("{full_name}\n"));
282 }
283
284 if !variant.doc.is_empty() {
286 out.push('\n');
287 for line in variant.doc {
288 out.push_str(line.trim());
289 out.push('\n');
290 }
291 }
292
293 out.push('\n');
294
295 let fields = variant.data.fields;
300 if variant.data.kind == StructKind::TupleStruct && fields.len() == 1 {
301 let inner_shape = fields[0].shape();
302 if let Type::User(UserType::Struct(struct_type)) = inner_shape.ty {
303 generate_struct_help(&mut out, &full_name, struct_type.fields);
305 return out;
306 }
307 }
308
309 generate_struct_help(&mut out, &full_name, fields);
310
311 out
312}