1use 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#[derive(Debug, Clone)]
15pub struct HelpConfig {
16 pub program_name: Option<String>,
18 pub version: Option<String>,
20 pub description: Option<String>,
22 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
37pub fn generate_help<T: facet_core::Facet<'static>>(config: &HelpConfig) -> String {
39 generate_help_for_shape(T::SHAPE, config)
40}
41
42pub fn generate_help_for_shape(shape: &'static Shape, config: &HelpConfig) -> String {
44 let mut out = String::new();
45
46 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 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 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 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 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 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 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 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 if let Some(sub_field) = subcommand {
158 let sub_shape = sub_field.shape();
159 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 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 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 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 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 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 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 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
254fn 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 c.or_else(|| field.name.chars().next())
263 } else {
264 None
265 }
266 })
267}
268
269pub 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 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 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 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 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}