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