Skip to main content

reformy_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::{ToTokens, format_ident, quote};
3use syn::{DeriveInput, Field, FieldsNamed, Variant, parse_macro_input, parse_str, parse2};
4use syn::{ItemFn, FnArg, Pat};
5
6#[proc_macro_derive(Form, attributes(form))]
7pub fn derive_form(input: TokenStream) -> TokenStream {
8    let input = parse_macro_input!(input as DeriveInput);
9    let name = input.ident;
10
11    let obj = match input.data {
12        syn::Data::Enum(data_enum) => generate_enum_form(&name, data_enum),
13        syn::Data::Struct(data_struct) => generate_struct_form(name, data_struct.fields),
14        _ => {
15            return syn::Error::new_spanned(name, "Only structs and unit enums are supported")
16                .to_compile_error()
17                .into();
18        }
19    };
20
21    obj.generate().into()
22}
23
24fn extract_named(
25    fields_named: FieldsNamed,
26    name: &syn::Ident,
27    v_ident: &syn::Ident,
28) -> VariantInfo {
29    let mut fields: Vec<Field> = vec![];
30
31    for field in fields_named.clone().named {
32        fields.push(field);
33    }
34
35    let mystruct = MyStruct::new(name.clone(), Some(v_ident.clone()), fields);
36
37    VariantInfo {
38        v_ident: v_ident.clone(),
39        titles: Some(mystruct),
40    }
41}
42
43fn extract_variant(name: &syn::Ident, variant: Variant) -> VariantInfo {
44    let v_ident = &variant.ident;
45    match variant.fields {
46        syn::Fields::Unit => VariantInfo {
47            v_ident: v_ident.clone(),
48            titles: None,
49        },
50        syn::Fields::Named(fields_named) => extract_named(fields_named, name, v_ident),
51
52        _ => {
53            panic!()
54            /*
55            return syn::Error::new_spanned(&variant.fields, "Only unit or struct variants are supported")
56                .to_compile_error()
57                .into();
58                */
59        }
60    }
61}
62
63fn generate_enum_form(name: &syn::Ident, data_enum: syn::DataEnum) -> MyObject {
64    let mut fields: Vec<VariantInfo> = vec![];
65
66    for variant in data_enum.variants.into_iter() {
67        fields.push(extract_variant(name, variant));
68    }
69
70    let myenum = MyEnum {
71        name: name.clone(),
72        variants: fields,
73    };
74    MyObject::Enum(myenum)
75}
76
77/// Represents all the info needed to create a Form object
78enum MyObject {
79    Enum(MyEnum),
80    Struct(MyStruct),
81}
82
83impl MyObject {
84    fn form_name(&self) -> syn::Type {
85        match self {
86            MyObject::Enum(obj) => obj.form_name(),
87            MyObject::Struct(obj) => obj.form_name(),
88        }
89    }
90
91    fn name(&self) -> syn::Ident {
92        match self {
93            MyObject::Enum(obj) => obj.name.clone(),
94            MyObject::Struct(obj) => obj.name.clone(),
95        }
96    }
97
98    fn generate(&self) -> proc_macro2::TokenStream {
99        let stream = match self {
100            MyObject::Enum(ob) => ob.generate(),
101            MyObject::Struct(ob) => ob.generate(),
102        };
103
104        let name = self.name();
105        let form_name = self.form_name();
106
107        let widget: proc_macro2::TokenStream = quote! {
108            impl ::reformy::ratatui::widgets::WidgetRef for #form_name {
109                fn render_ref(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer) {
110                    ::reformy::ratatui::widgets::StatefulWidgetRef::render_ref(self, area, buf, &mut true)
111                }
112            }
113
114            impl ::reformy::ratatui::widgets::StatefulWidgetRef for #form_name {
115                type State = bool;
116                fn render_ref(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer, state: &mut Self::State) {
117                    self.render(area, buf, *state);
118                }
119            }
120
121            impl #name {
122                pub fn form() -> #form_name {
123                    #form_name::new()
124                }
125            }
126        };
127
128        quote! { #stream
129        #widget}
130    }
131}
132
133struct MyEnum {
134    name: syn::Ident,
135    variants: Vec<VariantInfo>,
136}
137
138impl MyEnum {
139    fn form_name(&self) -> syn::Type {
140        let ident = format_ident!("{}Form", &self.name);
141        syn::Type::Path(syn::TypePath {
142            qself: None,
143            path: ident.into(),
144        })
145    }
146
147    fn generate(&self) -> proc_macro2::TokenStream {
148        let form_name = self.form_name();
149
150        let variant_fields: Vec<_> = self
151            .variants
152            .iter()
153            .map(|info| {
154                let ident = &info.v_ident;
155                let ty = &info.form_name();
156
157                quote! { pub #ident: #ty  }
158            })
159            .collect();
160        let form_heights: Vec<_> = self
161            .variants
162            .iter()
163            .enumerate()
164            .map(|(idx, info)| {
165                let count = info
166                    .titles
167                    .as_ref()
168                    .map(|x| x.height(true))
169                    .unwrap_or(quote! {0});
170
171                quote! {
172                    #idx => #count,
173                }
174            })
175            .collect();
176
177        let input_matches: Vec<_> = self
178            .variants
179            .iter()
180            .enumerate()
181            .map(|(idx, info)| {
182                let ident = &info.v_ident;
183
184                if info.titles.is_some() {
185                    quote! {
186                        #idx => self.#ident.input(input.clone()),
187                    }
188                } else {
189                    quote! {
190                        #idx => false,
191                    }
192                }
193            })
194            .collect();
195        let build_matches: Vec<_> = self
196            .variants
197            .iter()
198            .enumerate()
199            .map(|(idx, info)| {
200                let ident = &info.v_ident;
201                if info.titles.is_some() {
202                    quote! {
203                        #idx => self.#ident.build(),
204                    }
205                } else {
206                    let name = &self.name;
207                    quote! {
208                        #idx => Some(#name::#ident),
209                    }
210                }
211            })
212            .collect();
213
214        let variant_inits: Vec<_> = self
215            .variants
216            .iter()
217            .map(|info| {
218                let ident = &info.v_ident;
219                match &info.titles {
220                    Some(s) => {
221                        let form = s.form_name();
222                        quote! {
223                            #ident: #form::new()
224                        }
225                    }
226                    None => {
227                        quote! {
228                            #ident: ()
229                        }
230                    }
231                }
232            })
233            .collect();
234        let render_matches: Vec<_> = self
235            .variants
236            .iter()
237            .enumerate()
238            .map(|(idx, info)| {
239                let ident = &info.v_ident;
240
241                if info.titles.is_some() {
242                    quote! {
243                        #idx => self.#ident.render(area, buf, state.clone()),
244                    }
245                } else {
246                    quote! {
247                        #idx => {},
248                    }
249                }
250            })
251            .collect();
252        let variant_titles: Vec<_> = self
253            .variants
254            .iter()
255            .map(|info| {
256                info.titles
257                    .as_ref()
258                    .map(|mys| mys.generate())
259                    .unwrap_or_default()
260            })
261            .collect();
262        let variant_display: Vec<_> = self
263            .variants
264            .iter()
265            .enumerate()
266            .map(|(idx, info)| {
267                let label = info.v_ident.to_string();
268                quote!(#idx => #label,)
269            })
270            .collect();
271
272        let num_variants = variant_display.len();
273        let name = &self.name;
274
275        quote! {
276        #(#variant_titles)*
277
278        pub struct #form_name {
279            pub selected_variant: usize,
280            #(#variant_fields,)*
281        }
282
283        impl #form_name {
284            pub fn new() -> Self {
285                Self {
286                    selected_variant: 0,
287                    #(#variant_inits,)*
288                }
289            }
290            
291            pub fn form_height(&self) -> u16 {
292                let index = self.selected_variant;
293                (match index {
294                    #(#form_heights)*
295                    _ => 0,
296                } + 2) as u16
297            }
298
299            pub fn input(&mut self, input: ::reformy::tui_textarea::Input) -> bool {
300                let key = input.key.clone();
301                (match self.selected_variant {
302                    #(#input_matches)*
303                    _ => false,
304                } ||
305                match key {
306                    ::reformy::tui_textarea::Key::Left if self.selected_variant > 0 => {
307                        self.selected_variant -= 1;
308                        true
309                    }
310                    ::reformy::tui_textarea::Key::Right if self.selected_variant + 1 < #num_variants => {
311                        self.selected_variant += 1;
312                        true
313                    }
314                    _ => false,
315                })
316            }
317
318            pub fn build(&self) -> Option<#name> {
319                match self.selected_variant {
320                    #(#build_matches)*
321                    _ => None,
322                }
323            }
324
325            pub fn render(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer, state: bool) {
326                use ::reformy::ratatui::widgets::WidgetRef;
327                use ::reformy::ratatui::prelude::Constraint;
328
329                let label = match self.selected_variant {
330                    #(#variant_display)*
331                    _ => "???",
332                };
333
334                let title = if state {
335                    format!(">{}: ", label)
336                } else {
337                    format!("{}: ", label)
338                };
339
340                let chunks = ::reformy::ratatui::layout::Layout::default()
341                    .direction(::reformy::ratatui::layout::Direction::Vertical)
342                    .constraints(vec![Constraint::Length(1), Constraint::Min(0)])
343                    .split(area);
344
345                ::reformy::ratatui::widgets::Paragraph::new(format!("[{}]", label)).render_ref(chunks[0], buf);
346
347                let area = chunks[1];
348
349                let chunks = ::reformy::ratatui::layout::Layout::default()
350                    .direction(::reformy::ratatui::layout::Direction::Horizontal)
351                    .constraints(vec![Constraint::Length(2), Constraint::Min(0)])
352                    .split(area);
353
354                let area = chunks[1];
355
356                match self.selected_variant {
357                    #(#render_matches)*
358                    _ => {}
359                };
360            }
361        }
362
363    }.into()
364    }
365}
366
367/// A single variant in an enum
368struct VariantInfo {
369    v_ident: syn::Ident,
370    /// The fields if it's a data enum, none if it's unit
371    titles: Option<MyStruct>,
372}
373
374impl VariantInfo {
375    fn form_name(&self) -> syn::Type {
376        match &self.titles {
377            Some(s) => s.form_name(),
378            None => parse_str("()").unwrap(),
379        }
380    }
381}
382
383#[derive(Clone, Debug)]
384struct FieldType {
385    ty: syn::Type,
386    is_leaf: bool,
387}
388
389/// A single field in a struct-like object.
390struct StructField {
391    field: syn::Ident,
392    field_ty: FieldType,
393    build: proc_macro2::TokenStream,
394    render: proc_macro2::TokenStream,
395    needs_validation: bool,
396}
397
398struct MyStruct {
399    name: syn::Ident,
400    variant: Option<syn::Ident>,
401    fields: Vec<StructField>,
402}
403
404impl MyStruct {
405    fn new(name: syn::Ident, variant: Option<syn::Ident>, fields: Vec<Field>) -> Self {
406        let mut xfields: Vec<StructField> = vec![];
407
408        for (idx, field) in fields.iter().enumerate() {
409            xfields.push(extract_field(idx, field));
410        }
411
412        Self {
413            name,
414            variant,
415            fields: xfields,
416        }
417    }
418
419    fn height_exprs(&self, is_enum: bool) -> Vec<proc_macro2::TokenStream> {
420        self.fields
421            .iter()
422            .map(|f| {
423                if f.field_ty.is_leaf {
424                    quote! { 1 }
425                } else {
426                    //let height = quote! { self.#ident.form_height() };
427                    let ident = f.field.clone();
428                    match &self.variant {
429                        Some(var) if is_enum => quote! {self.#var.#ident.form_height()},
430                        _ => quote! {self.#ident.form_height()},
431                    }
432                }
433            })
434            .collect()
435    }
436
437    fn height(&self, is_enum: bool) -> proc_macro2::TokenStream {
438        let heights = self.height_exprs(is_enum);
439        quote! {
440            0 #( + #heights )*
441        }
442    }
443
444    fn form_name(&self) -> syn::Type {
445        let ident = match &self.variant {
446            Some(var) => format_ident!("{}{}Form", self.name, var),
447            None => format_ident!("{}Form", self.name),
448        };
449        syn::Type::Path(syn::TypePath {
450            qself: None,
451            path: ident.into(),
452        })
453    }
454
455    fn generate(&self) -> proc_macro2::TokenStream {
456        if self.fields.is_empty() {
457            return quote! {}.into();
458        }
459
460        let struct_fields: Vec<_> = self
461            .fields
462            .iter()
463            .map(|i| {
464                let name = i.field.clone();
465                let ty = i.field_ty.ty.clone();
466
467                quote! { pub #name: #ty }
468            })
469            .collect();
470        let height_exprs: Vec<_> = self.height_exprs(false);
471        let field_inits: Vec<_> = self
472            .fields
473            .iter()
474            .map(|i| {
475                let field = i.field.clone();
476                let ty = i.field_ty.ty.clone();
477                if i.needs_validation {
478                    quote! { #field: #ty::new().with_validation(true) }
479                } else {
480                    quote! { #field: #ty::new() }
481                }
482            })
483            .collect();
484        let to_struct_fields: Vec<_> = self.fields.iter().map(|i| i.build.clone()).collect();
485        let selected_matches: Vec<_> = self
486            .fields
487            .iter()
488            .enumerate()
489            .map(|(idx, i)| {
490                let ident = i.field.clone();
491
492                quote! { i if i == #idx => self.#ident.input(theinput.clone()), }
493            })
494            .collect();
495        let render_calls: Vec<_> = self.fields.iter().map(|i| i.render.clone()).collect();
496        let field_count = struct_fields.len();
497        let name = &self.name;
498        let form_name = self.form_name();
499
500        let buildent = if let Some(variant) = &self.variant {
501            quote! { #name::#variant }
502        } else {
503            quote! { #name }
504        };
505
506        quote! {
507            pub struct #form_name {
508                #(#struct_fields,)*
509                pub selected: usize,
510            }
511
512            impl #form_name {
513                pub fn new() -> Self {
514                    Self {
515                        #(#field_inits,)*
516                        selected: 0,
517                    }
518                }
519
520                pub fn form_height(&self) -> u16 {
521                    0 #( + #height_exprs )* + 1
522                }
523
524                pub fn input(&mut self, input: ::reformy::tui_textarea::Input) -> bool {
525                    let theinput = input.clone();
526                    let handled = match self.selected {
527                        #(#selected_matches)*
528                        _ => unreachable!(),
529                    };
530
531                    if handled {
532                        return true;
533                    }
534
535                    match input.key {
536                        ::reformy::tui_textarea::Key::Down if self.selected < #field_count - 1 => {
537                            self.selected += 1;
538                            true
539                        }
540                        ::reformy::tui_textarea::Key::Up if self.selected > 0 => {
541                            self.selected -= 1;
542                            true
543                        }
544                        _ => false,
545                    }
546                }
547
548                fn render(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer, state: bool) {
549                    use ::reformy::ratatui::layout::{Layout, Direction, Constraint};
550                    use ::reformy::ratatui::widgets::WidgetRef;
551
552                    let chunks = Layout::default()
553                        .direction(Direction::Vertical)
554                        .constraints(vec![#(Constraint::Length(#height_exprs)),*])
555                        .split(area);
556
557                    let title = ::reformy::ratatui::widgets::Paragraph::new(stringify!(self.name).to_string() + ":")
558        .style(::reformy::ratatui::style::Style::default().add_modifier(::reformy::ratatui::style::Modifier::BOLD));
559
560                    #(#render_calls)*
561
562                }
563
564                pub fn build(&self) -> Option<#name> {
565                    Some(#buildent {
566                        #(#to_struct_fields,)*
567                    })
568                }
569            }
570        }
571    }
572}
573
574fn extract_field(idx: usize, field: &Field) -> StructField {
575    let ident = field.ident.as_ref().unwrap();
576    let ty = &field.ty;
577
578    if is_nested_field(field) {
579        let ty: syn::Type = parse_str(&format!(
580            "{}Form",
581            ty.to_token_stream().to_string().replace(' ', "")
582        ))
583        .unwrap();
584
585        let to_fields = quote! { #ident: self.#ident.build()? };
586
587        let render = quote! {
588            {
589                let chunk = chunks[#idx];
590                let cols = ::reformy::ratatui::layout::Layout::default()
591                    .direction(::reformy::ratatui::layout::Direction::Vertical)
592                    .constraints([
593                        ::reformy::ratatui::layout::Constraint::Length(1),
594                        ::reformy::ratatui::layout::Constraint::Min(0)
595                    ])
596                    .split(chunk);
597
598                let label = if self.selected == #idx && state {
599                    ::reformy::ratatui::widgets::Paragraph::new(format!("> {}:", stringify!(#ident)))
600                        .style(::reformy::ratatui::style::Style::default().fg(::reformy::ratatui::style::Color::Yellow))
601                } else {
602                    ::reformy::ratatui::widgets::Paragraph::new(format!("{}:", stringify!(#ident)))
603                };
604
605                label.render_ref(cols[0], buf);
606
607                let cols = ::reformy::ratatui::layout::Layout::default()
608                    .direction(::reformy::ratatui::layout::Direction::Horizontal)
609                    .constraints([
610                        ::reformy::ratatui::layout::Constraint::Length(4),
611                        ::reformy::ratatui::layout::Constraint::Min(0)
612                    ])
613                    .split(cols[1]);
614
615                ::reformy::ratatui::widgets::StatefulWidgetRef::render_ref(
616                    &self.#ident,
617                    cols[1],
618                    buf,
619                    &mut (self.selected == #idx && state),
620                );
621            }
622        };
623
624        StructField {
625            field: ident.clone(),
626            field_ty: FieldType { ty, is_leaf: false },
627            build: to_fields,
628            render,
629            needs_validation: false,
630        }
631    } else {
632        let to_fields = quote! { #ident: self.#ident.value()? };
633        let render = quote! {
634            {
635                let chunk = chunks[#idx];
636                let cols = ::reformy::ratatui::layout::Layout::default()
637                    .direction(::reformy::ratatui::layout::Direction::Horizontal)
638                    .constraints([
639                        ::reformy::ratatui::layout::Constraint::Length(12),
640                        ::reformy::ratatui::layout::Constraint::Min(0)
641                    ])
642                    .split(chunk);
643
644                let label = if self.selected == #idx && state {
645                    ::reformy::ratatui::widgets::Paragraph::new(format!("> {}", stringify!(#ident)))
646                        .style(::reformy::ratatui::style::Style::default().fg(::reformy::ratatui::style::Color::Yellow))
647                } else {
648                    ::reformy::ratatui::widgets::Paragraph::new(stringify!(#ident))
649                };
650
651                label.render_ref(cols[0], buf);
652                ::reformy::ratatui::widgets::Widget::render(self.#ident.input.widget(), cols[1], buf);
653            }
654        };
655        StructField {
656            field: ident.clone(),
657            field_ty: FieldType {
658                ty: parse2(quote! {::reformy::Filtext::<#ty>}).unwrap(),
659                is_leaf: true,
660            },
661            build: to_fields,
662            render,
663            needs_validation: is_numeric_type(ty),
664        }
665    }
666}
667
668fn generate_struct_form(name: syn::Ident, fields: syn::Fields) -> MyObject {
669    let named_fields = match fields {
670        syn::Fields::Named(fields) => fields.named,
671        _ => {
672            panic!("only named fields")
673        }
674    };
675
676    let mystruct = MyStruct::new(name.clone(), None, named_fields.into_iter().collect());
677
678    MyObject::Struct(mystruct)
679}
680
681fn is_nested_field(field: &Field) -> bool {
682    field.attrs.iter().any(|attr| {
683        attr.path().is_ident("form")
684    })
685}
686
687fn is_numeric_type(ty: &syn::Type) -> bool {
688    let ty_str = ty.to_token_stream().to_string().replace(' ', "");
689    matches!(
690        ty_str.as_str(),
691        "u8" | "u16" | "u32" | "u64" | "u128" | "usize" |
692        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" |
693        "f32" | "f64"
694    )
695}
696
697/// Convert snake_case function name to PascalCase struct name
698fn snake_to_pascal(s: &str) -> String {
699    s.split('_')
700        .map(|word| {
701            let mut c = word.chars();
702            match c.next() {
703                None => String::new(),
704                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
705            }
706        })
707        .collect()
708}
709
710/// Attribute macro for generating forms from function parameters
711/// Supports #[form] on parameters for nested Form types
712#[proc_macro_attribute]
713pub fn form(_attr: TokenStream, item: TokenStream) -> TokenStream {
714    let input = parse_macro_input!(item as ItemFn);
715    
716    let fn_name = &input.sig.ident;
717    let fn_vis = &input.vis;
718    let fn_attrs = &input.attrs;
719    let fn_sig = &input.sig;
720    let fn_block = &input.block;
721    
722    // Generate struct name from function name (snake_case -> PascalCase)
723    let struct_name = format_ident!("{}", snake_to_pascal(&fn_name.to_string()));
724    
725    // Extract function parameters with their attributes
726    let mut param_names = Vec::new();
727    let mut param_types = Vec::new();
728    let mut param_attrs = Vec::new();
729    let mut new_fn_inputs = Vec::new();
730    
731    for arg in &input.sig.inputs {
732        if let FnArg::Typed(pat_type) = arg {
733            if let Pat::Ident(pat_ident) = &*pat_type.pat {
734                param_names.push(pat_ident.ident.clone());
735                param_types.push((*pat_type.ty).clone());
736                param_attrs.push(pat_type.attrs.clone());
737                
738                // Create cleaned function input without form attributes
739                let mut new_pat_type = pat_type.clone();
740                new_pat_type.attrs.retain(|attr| !attr.path().is_ident("form"));
741                new_fn_inputs.push(FnArg::Typed(new_pat_type));
742            }
743        }
744    }
745    
746    let fn_output = &input.sig.output;
747    let has_params = !param_names.is_empty();
748    
749    let fn_generics = &input.sig.generics;
750    let fn_asyncness = &input.sig.asyncness;
751    let fn_unsafety = &input.sig.unsafety;
752    let fn_abi = &input.sig.abi;
753    
754    let expanded = if has_params {
755        // Function has parameters - generate form
756        quote! {
757            // Keep the original function (without form attributes on params)
758            #(#fn_attrs)*
759            #fn_vis #fn_asyncness #fn_unsafety #fn_abi fn #fn_name #fn_generics(#(#new_fn_inputs),*) #fn_output {
760                #fn_block
761            }
762            
763            // Generate the Args struct with Form
764            #[derive(Debug, Default, ::reformy::Form)]
765            #fn_vis struct #struct_name {
766                #(
767                    #(#param_attrs)*
768                    pub #param_names: #param_types,
769                )*
770            }
771            
772            impl #struct_name {
773                /// Execute the function with these arguments
774                pub fn execute(self) #fn_output {
775                    #fn_name(#(self.#param_names),*)
776                }
777                
778                pub const HAS_PARAMS: bool = true;
779            }
780        }
781    } else {
782        // Zero-arg function - no form needed
783        let form_name = format_ident!("{}Form", struct_name);
784        quote! {
785            // Keep the original function
786            #(#fn_attrs)*
787            #fn_vis #fn_sig {
788                #fn_block
789            }
790            
791            // Generate empty struct (no Form derive)
792            #[derive(Debug, Default)]
793            #fn_vis struct #struct_name;
794            
795            // Generate dummy form type (won't be used but needs to exist)
796            #[derive(Debug)]
797            #fn_vis struct #form_name;
798            
799            impl #form_name {
800                pub fn new() -> Self { Self }
801                
802                pub fn input(&mut self, _input: ::reformy::tui_textarea::Input) -> bool {
803                    false
804                }
805                
806                pub fn build(&self) -> Option<#struct_name> {
807                    Some(#struct_name::default())
808                }
809            }
810            
811            impl ::reformy::ratatui::widgets::StatefulWidgetRef for #form_name {
812                type State = bool;
813                fn render_ref(&self, _area: ::reformy::ratatui::layout::Rect, _buf: &mut ::reformy::ratatui::buffer::Buffer, _state: &mut Self::State) {
814                }
815            }
816            
817            impl #struct_name {
818                /// Execute the function with these arguments
819                pub fn execute(self) #fn_output {
820                    #fn_name()
821                }
822                
823                pub const HAS_PARAMS: bool = false;
824                
825                pub fn form() -> #form_name {
826                    #form_name::new()
827                }
828            }
829        }
830    };
831    
832    expanded.into()
833}
834
835// Menu item structure - either a command or a category with submenus
836#[derive(Clone)]
837enum MenuItem {
838    Command(syn::Ident),
839    Category {
840        name: String,
841        items: Vec<MenuItem>,
842    },
843}
844
845impl MenuItem {
846    fn collect_commands(&self, commands: &mut Vec<syn::Ident>) {
847        match self {
848            MenuItem::Command(ident) => commands.push(ident.clone()),
849            MenuItem::Category { items, .. } => {
850                for item in items {
851                    item.collect_commands(commands);
852                }
853            }
854        }
855    }
856}
857
858// Custom parser for menu items
859fn parse_menu_items(input: syn::parse::ParseStream) -> syn::Result<Vec<MenuItem>> {
860    let mut items = Vec::new();
861    
862    while !input.is_empty() {
863        // Try to parse a string literal (category)
864        if input.peek(syn::LitStr) {
865            let category_name: syn::LitStr = input.parse()?;
866            input.parse::<syn::Token![=>]>()?;
867            
868            let content;
869            syn::braced!(content in input);
870            let subitems = parse_menu_items(&content)?;
871            
872            items.push(MenuItem::Category {
873                name: category_name.value(),
874                items: subitems,
875            });
876        } else {
877            // Parse a command identifier
878            let ident: syn::Ident = input.parse()?;
879            items.push(MenuItem::Command(ident));
880        }
881        
882        // Optional trailing comma
883        if input.peek(syn::Token![,]) {
884            input.parse::<syn::Token![,]>()?;
885        }
886    }
887    
888    Ok(items)
889}
890
891/// Collection macro that generates a complete TUI for multiple commands
892#[proc_macro]
893pub fn menu(input: TokenStream) -> TokenStream {
894    // Parse menu items (flat or nested)
895    let menu_items = parse_macro_input!(input with parse_menu_items);
896    
897    // Collect all commands (flattened from any nesting level)
898    let mut commands = Vec::new();
899    for item in &menu_items {
900        item.collect_commands(&mut commands);
901    }
902    
903    // Generate struct names for each command (snake_case -> PascalCase)
904    let struct_names: Vec<_> = commands.iter().map(|cmd| {
905        format_ident!("{}", snake_to_pascal(&cmd.to_string()))
906    }).collect();
907    
908    let form_names: Vec<_> = struct_names.iter().map(|name| {
909        format_ident!("{}Form", name)
910    }).collect();
911    
912    let command_names: Vec<_> = commands.iter().map(|cmd| cmd.to_string()).collect();
913    let num_commands = commands.len();
914    
915    // Generate indices for match arms
916    let indices: Vec<_> = (0..num_commands).collect();
917    
918    // Generate the runtime menu structure
919    fn generate_menu_structure(items: &[MenuItem]) -> proc_macro2::TokenStream {
920        let mut item_tokens = Vec::new();
921        
922        for item in items {
923            let token = match item {
924                MenuItem::Command(ident) => {
925                    let name = ident.to_string();
926                    quote! { RuntimeMenuItem::Command(#name) }
927                }
928                MenuItem::Category { name, items: subitems } => {
929                    let subitems_tokens = generate_menu_structure(subitems);
930                    quote! {
931                        RuntimeMenuItem::Category {
932                            name: #name.to_string(),
933                            items: vec![#subitems_tokens],
934                        }
935                    }
936                }
937            };
938            item_tokens.push(token);
939        }
940        
941        quote! { #(#item_tokens),* }
942    }
943    
944    let menu_structure = generate_menu_structure(&menu_items);
945    
946    let expanded = quote! {
947        {
948            use ::reformy::ratatui::layout::{Layout, Direction, Constraint};
949            use ::reformy::ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Widget, WidgetRef};
950            use ::reformy::ratatui::style::{Style, Color, Modifier};
951            use ::reformy::ratatui::text::Line;
952            
953            // Runtime menu structure
954            #[derive(Clone)]
955            enum RuntimeMenuItem {
956                Command(&'static str),
957                Category {
958                    name: String,
959                    items: Vec<RuntimeMenuItem>,
960                },
961            }
962            
963            // Enum to hold any of the command forms
964            enum CommandFormState {
965                #(#struct_names(#form_names),)*
966            }
967            
968            impl CommandFormState {
969                fn input(&mut self, input: ::reformy::tui_textarea::Input) -> bool {
970                    match self {
971                        #(CommandFormState::#struct_names(form) => form.input(input),)*
972                    }
973                }
974                
975                fn render(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer) {
976                    match self {
977                        #(CommandFormState::#struct_names(form) => {
978                            ::reformy::ratatui::widgets::StatefulWidgetRef::render_ref(form, area, buf, &mut true)
979                        },)*
980                    }
981                }
982            }
983            
984            // Application state
985            struct AppState {
986                menu_items: Vec<RuntimeMenuItem>,
987                menu_path: Vec<usize>,  // Path through nested menus
988                selected_index: usize,   // Index in current menu level
989                current_form: Option<CommandFormState>,
990                result: Option<String>,
991            }
992            
993            impl AppState {
994                fn new(menu_items: Vec<RuntimeMenuItem>) -> Self {
995                    Self {
996                        menu_items,
997                        menu_path: Vec::new(),
998                        selected_index: 0,
999                        current_form: None,
1000                        result: None,
1001                    }
1002                }
1003                
1004                // Get current menu level based on path
1005                fn current_menu(&self) -> &[RuntimeMenuItem] {
1006                    let mut current = &self.menu_items[..];
1007                    for &index in &self.menu_path {
1008                        if let RuntimeMenuItem::Category { items, .. } = &current[index] {
1009                            current = items;
1010                        }
1011                    }
1012                    current
1013                }
1014                
1015                // Get breadcrumb path
1016                fn breadcrumb(&self) -> Vec<String> {
1017                    let mut breadcrumb = Vec::new();
1018                    let mut current = &self.menu_items[..];
1019                    
1020                    for &index in &self.menu_path {
1021                        if let RuntimeMenuItem::Category { name, items } = &current[index] {
1022                            breadcrumb.push(name.clone());
1023                            current = items;
1024                        }
1025                    }
1026                    
1027                    breadcrumb
1028                }
1029                
1030                // Find command name from any level
1031                fn find_command_name(&self, cmd_name: &str) -> Option<usize> {
1032                    let command_names = &[#(#command_names),*];
1033                    command_names.iter().position(|&name| name == cmd_name)
1034                }
1035                
1036                fn handle_input(&mut self, input: ::reformy::tui_textarea::Input) -> bool {
1037                    use ::reformy::tui_textarea::Key;
1038                    
1039                    // If showing result, any key goes back to menu
1040                    if self.result.is_some() {
1041                        self.result = None;
1042                        self.current_form = None;
1043                        return true;
1044                    }
1045                    
1046                    // If in form view
1047                    if let Some(form) = &mut self.current_form {
1048                        match input.key {
1049                            Key::Esc => {
1050                                self.current_form = None;
1051                                return true;
1052                            }
1053                            Key::Enter => {
1054                                // Try to execute the command
1055                                let result = self.execute_current();
1056                                if let Some(r) = result {
1057                                    self.result = Some(r);
1058                                }
1059                                return true;
1060                            }
1061                            _ => {
1062                                return form.input(input);
1063                            }
1064                        }
1065                    }
1066                    
1067                    // Menu navigation
1068                    let current_menu = self.current_menu();
1069                    let menu_len = current_menu.len();
1070                    
1071                    match input.key {
1072                        Key::Down | Key::Char('j') => {
1073                            self.selected_index = if self.selected_index + 1 >= menu_len {
1074                                0  // Wrap to top
1075                            } else {
1076                                self.selected_index + 1
1077                            };
1078                            true
1079                        }
1080                        Key::Up | Key::Char('k') => {
1081                            self.selected_index = if self.selected_index == 0 {
1082                                menu_len - 1  // Wrap to bottom
1083                            } else {
1084                                self.selected_index - 1
1085                            };
1086                            true
1087                        }
1088                        Key::PageDown | Key::End => {
1089                            self.selected_index = menu_len - 1;
1090                            true
1091                        }
1092                        Key::PageUp | Key::Home => {
1093                            self.selected_index = 0;
1094                            true
1095                        }
1096                        Key::Char('G') => {
1097                            self.selected_index = menu_len - 1;
1098                            true
1099                        }
1100                        Key::Char('g') => {
1101                            self.selected_index = 0;
1102                            true
1103                        }
1104                        Key::Backspace | Key::Char('h') if !self.menu_path.is_empty() => {
1105                            // Go back up one level
1106                            self.menu_path.pop();
1107                            self.selected_index = 0;
1108                            true
1109                        }
1110                        Key::Enter | Key::Char('l') => {
1111                            // Enter category or execute command
1112                            match &current_menu[self.selected_index] {
1113                                RuntimeMenuItem::Category { .. } => {
1114                                    // Enter this category
1115                                    self.menu_path.push(self.selected_index);
1116                                    self.selected_index = 0;
1117                                    true
1118                                }
1119                                RuntimeMenuItem::Command(cmd_name) => {
1120                                    // Execute this command
1121                                    if let Some(cmd_idx) = self.find_command_name(cmd_name) {
1122                                        match cmd_idx {
1123                                            #(#indices => {
1124                                                if #struct_names::HAS_PARAMS {
1125                                                    self.current_form = Some(CommandFormState::#struct_names(#struct_names::form()));
1126                                                } else {
1127                                                    let args = #struct_names::default();
1128                                                    let result = args.execute();
1129                                                    self.result = Some(format!("{:?}", result));
1130                                                }
1131                                            },)*
1132                                            _ => return false,
1133                                        }
1134                                    }
1135                                    true
1136                                }
1137                            }
1138                        }
1139                        _ => false,
1140                    }
1141                }
1142                
1143                fn execute_current(&self) -> Option<String> {
1144                    match &self.current_form {
1145                        #(Some(CommandFormState::#struct_names(form)) => {
1146                            form.build().map(|args| {
1147                                let result = args.execute();
1148                                format!("{:?}", result)
1149                            })
1150                        },)*
1151                        None => None,
1152                    }
1153                }
1154                
1155                fn render(&self, area: ::reformy::ratatui::layout::Rect, buf: &mut ::reformy::ratatui::buffer::Buffer) {
1156                    // If showing result
1157                    if let Some(result) = &self.result {
1158                        let block = Block::default()
1159                            .title("Result (press any key to continue)")
1160                            .borders(Borders::ALL);
1161                        let inner = block.inner(area);
1162                        block.render(area, buf);
1163                        
1164                        Paragraph::new(result.as_str()).render(inner, buf);
1165                        return;
1166                    }
1167                    
1168                    // If in form view
1169                    if let Some(form) = &self.current_form {
1170                        let block = Block::default()
1171                            .title("Enter Parameters (Enter to execute, Esc to cancel)")
1172                            .borders(Borders::ALL);
1173                        let inner = block.inner(area);
1174                        block.render(area, buf);
1175                        
1176                        form.render(inner, buf);
1177                        return;
1178                    }
1179                    
1180                    // Build breadcrumb and title
1181                    let breadcrumb = self.breadcrumb();
1182                    let breadcrumb_str = if breadcrumb.is_empty() {
1183                        "Commands".to_string()
1184                    } else {
1185                        format!("{} >", breadcrumb.join(" > "))
1186                    };
1187                    
1188                    let title = if self.menu_path.is_empty() {
1189                        format!("{} (Enter/l: select, Esc: quit, h: back)", breadcrumb_str)
1190                    } else {
1191                        format!("{} (Enter/l: select, h/Backspace: back)", breadcrumb_str)
1192                    };
1193                    
1194                    let block = Block::default()
1195                        .title(title)
1196                        .borders(Borders::ALL);
1197                    let inner = block.inner(area);
1198                    block.render(area, buf);
1199                    
1200                    // Get current menu items
1201                    let current_menu = self.current_menu();
1202                    let items: Vec<ListItem> = current_menu
1203                        .iter()
1204                        .enumerate()
1205                        .map(|(idx, item)| {
1206                            let (name, is_category) = match item {
1207                                RuntimeMenuItem::Command(name) => (*name, false),
1208                                RuntimeMenuItem::Category { name, .. } => (name.as_str(), true),
1209                            };
1210                            
1211                            let style = if idx == self.selected_index {
1212                                Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
1213                            } else {
1214                                Style::default()
1215                            };
1216                            
1217                            let prefix = if idx == self.selected_index { "> " } else { "  " };
1218                            let suffix = if is_category { " →" } else { "" };
1219                            
1220                            ListItem::new(Line::from(format!("{}{}{}", prefix, name, suffix))).style(style)
1221                        })
1222                        .collect();
1223                    
1224                    let list = List::new(items);
1225                    list.render(inner, buf);
1226                }
1227            }
1228            
1229            // Build menu structure
1230            let menu_items = vec![#menu_structure];
1231            
1232            // Run the TUI
1233            let mut app = AppState::new(menu_items);
1234            let mut terminal = ::reformy::ratatui::init();
1235            
1236            loop {
1237                terminal.draw(|f| {
1238                    app.render(f.area(), f.buffer_mut());
1239                }).unwrap();
1240                
1241                if let ::reformy::crossterm::event::Event::Key(key) = ::reformy::crossterm::event::read().unwrap() {
1242                    match key.code {
1243                        ::reformy::crossterm::event::KeyCode::Esc if app.current_form.is_none() && app.result.is_none() && app.menu_path.is_empty() => break,
1244                        key_code => {
1245                            let input = ::reformy::tui_textarea::Input {
1246                                key: key_code.into(),
1247                                ctrl: key.modifiers.contains(::reformy::crossterm::event::KeyModifiers::CONTROL),
1248                                alt: key.modifiers.contains(::reformy::crossterm::event::KeyModifiers::ALT),
1249                                shift: key.modifiers.contains(::reformy::crossterm::event::KeyModifiers::SHIFT),
1250                            };
1251                            app.handle_input(input);
1252                        }
1253                    }
1254                }
1255            }
1256            
1257            ::reformy::ratatui::restore();
1258        }
1259    };
1260    
1261    expanded.into()
1262}