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 }
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
77enum 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
367struct VariantInfo {
369 v_ident: syn::Ident,
370 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
389struct 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 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
697fn 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#[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 let struct_name = format_ident!("{}", snake_to_pascal(&fn_name.to_string()));
724
725 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 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 quote! {
757 #(#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 #[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 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 let form_name = format_ident!("{}Form", struct_name);
784 quote! {
785 #(#fn_attrs)*
787 #fn_vis #fn_sig {
788 #fn_block
789 }
790
791 #[derive(Debug, Default)]
793 #fn_vis struct #struct_name;
794
795 #[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 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#[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
858fn parse_menu_items(input: syn::parse::ParseStream) -> syn::Result<Vec<MenuItem>> {
860 let mut items = Vec::new();
861
862 while !input.is_empty() {
863 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 let ident: syn::Ident = input.parse()?;
879 items.push(MenuItem::Command(ident));
880 }
881
882 if input.peek(syn::Token![,]) {
884 input.parse::<syn::Token![,]>()?;
885 }
886 }
887
888 Ok(items)
889}
890
891#[proc_macro]
893pub fn menu(input: TokenStream) -> TokenStream {
894 let menu_items = parse_macro_input!(input with parse_menu_items);
896
897 let mut commands = Vec::new();
899 for item in &menu_items {
900 item.collect_commands(&mut commands);
901 }
902
903 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 let indices: Vec<_> = (0..num_commands).collect();
917
918 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 #[derive(Clone)]
955 enum RuntimeMenuItem {
956 Command(&'static str),
957 Category {
958 name: String,
959 items: Vec<RuntimeMenuItem>,
960 },
961 }
962
963 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 struct AppState {
986 menu_items: Vec<RuntimeMenuItem>,
987 menu_path: Vec<usize>, selected_index: usize, 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 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, .. } = ¤t[index] {
1009 current = items;
1010 }
1011 }
1012 current
1013 }
1014
1015 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 } = ¤t[index] {
1022 breadcrumb.push(name.clone());
1023 current = items;
1024 }
1025 }
1026
1027 breadcrumb
1028 }
1029
1030 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 self.result.is_some() {
1041 self.result = None;
1042 self.current_form = None;
1043 return true;
1044 }
1045
1046 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 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 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 } 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 } 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 self.menu_path.pop();
1107 self.selected_index = 0;
1108 true
1109 }
1110 Key::Enter | Key::Char('l') => {
1111 match ¤t_menu[self.selected_index] {
1113 RuntimeMenuItem::Category { .. } => {
1114 self.menu_path.push(self.selected_index);
1116 self.selected_index = 0;
1117 true
1118 }
1119 RuntimeMenuItem::Command(cmd_name) => {
1120 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 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 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 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 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 let menu_items = vec![#menu_structure];
1231
1232 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}