1use darling::{FromDeriveInput, FromField, FromVariant};
4use proc_macro::TokenStream;
5use proc_macro2::Ident;
6use quote::{format_ident, quote};
7use std::collections::HashMap;
8use syn::{parse_macro_input, DeriveInput};
9
10#[derive(Debug, FromDeriveInput)]
12#[darling(attributes(action), supports(enum_any))]
13struct ActionOpts {
14 ident: syn::Ident,
15 data: darling::ast::Data<ActionVariant, ()>,
16
17 #[darling(default)]
19 infer_categories: bool,
20
21 #[darling(default)]
23 generate_dispatcher: bool,
24}
25
26#[derive(Debug, FromVariant)]
28#[darling(attributes(action))]
29struct ActionVariant {
30 ident: syn::Ident,
31 fields: darling::ast::Fields<()>,
32
33 #[darling(default)]
35 category: Option<String>,
36
37 #[darling(default)]
39 skip_category: bool,
40}
41
42const ACTION_VERBS: &[&str] = &[
46 "Start", "End", "Open", "Close", "Submit", "Confirm", "Cancel", "Next", "Prev", "Up", "Down", "Left", "Right", "Enter", "Exit", "Escape",
49 "Add", "Remove", "Clear", "Update", "Set", "Get", "Load", "Save", "Delete", "Create",
51 "Show", "Hide", "Enable", "Disable", "Toggle", "Focus", "Blur", "Select", "Move", "Copy", "Cycle", "Reset", "Scroll",
55];
56
57fn split_pascal_case(s: &str) -> Vec<String> {
59 let mut parts = Vec::new();
60 let mut current = String::new();
61
62 for ch in s.chars() {
63 if ch.is_uppercase() && !current.is_empty() {
64 parts.push(current);
65 current = String::new();
66 }
67 current.push(ch);
68 }
69 if !current.is_empty() {
70 parts.push(current);
71 }
72 parts
73}
74
75fn to_snake_case(s: &str) -> String {
77 let mut result = String::new();
78 for (i, ch) in s.chars().enumerate() {
79 if ch.is_uppercase() {
80 if i > 0 {
81 result.push('_');
82 }
83 result.push(ch.to_lowercase().next().unwrap());
84 } else {
85 result.push(ch);
86 }
87 }
88 result
89}
90
91fn to_pascal_case(s: &str) -> String {
93 s.split('_')
94 .map(|part| {
95 let mut chars = part.chars();
96 match chars.next() {
97 None => String::new(),
98 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
99 }
100 })
101 .collect()
102}
103
104fn infer_category(name: &str) -> Option<String> {
106 let parts = split_pascal_case(name);
107 if parts.is_empty() {
108 return None;
109 }
110
111 if parts[0] == "Did" {
113 return Some("async_result".to_string());
114 }
115
116 if parts.len() < 2 {
118 return None;
119 }
120
121 let first_is_verb = ACTION_VERBS.contains(&parts[0].as_str());
127
128 let mut prefix_end = parts.len();
129 let mut found_verb = false;
130 for (i, part) in parts.iter().enumerate().skip(1) {
131 if ACTION_VERBS.contains(&part.as_str()) {
132 prefix_end = i;
133 found_verb = true;
134 break;
135 }
136 }
137
138 if first_is_verb {
142 return None;
143 }
144
145 if !found_verb {
147 return None;
148 }
149
150 if prefix_end == 0 {
151 return None;
152 }
153
154 let prefix_parts: Vec<&str> = parts[..prefix_end].iter().map(|s| s.as_str()).collect();
155 let prefix = prefix_parts.join("");
156
157 Some(to_snake_case(&prefix))
158}
159
160#[proc_macro_derive(Action, attributes(action))]
192pub fn derive_action(input: TokenStream) -> TokenStream {
193 let input = parse_macro_input!(input as DeriveInput);
194
195 let opts = match ActionOpts::from_derive_input(&input) {
197 Ok(opts) => opts,
198 Err(e) => return e.write_errors().into(),
199 };
200
201 let name = &opts.ident;
202
203 let variants = match &opts.data {
204 darling::ast::Data::Enum(variants) => variants,
205 _ => {
206 return syn::Error::new_spanned(&input, "Action can only be derived for enums")
207 .to_compile_error()
208 .into();
209 }
210 };
211
212 let syn_variants = match &input.data {
214 syn::Data::Enum(data) => &data.variants,
215 _ => unreachable!(), };
217
218 let name_arms = variants.iter().map(|v| {
220 let variant_name = &v.ident;
221 let variant_str = variant_name.to_string();
222
223 match &v.fields.style {
224 darling::ast::Style::Unit => quote! {
225 #name::#variant_name => #variant_str
226 },
227 darling::ast::Style::Tuple => quote! {
228 #name::#variant_name(..) => #variant_str
229 },
230 darling::ast::Style::Struct => quote! {
231 #name::#variant_name { .. } => #variant_str
232 },
233 }
234 });
235
236 let params_arms = syn_variants.iter().map(|v| {
238 let variant_name = &v.ident;
239
240 match &v.fields {
241 syn::Fields::Unit => quote! {
242 #name::#variant_name => ::std::string::String::new()
243 },
244 syn::Fields::Unnamed(fields) => {
245 let field_count = fields.unnamed.len();
246 let field_names: Vec<_> = (0..field_count)
247 .map(|i| format_ident!("_{}", i))
248 .collect();
249 let format_str = (0..field_count).map(|_| "{:?}").collect::<Vec<_>>().join(", ");
250 quote! {
251 #name::#variant_name(#(#field_names),*) => ::std::format!(#format_str, #(#field_names),*)
252 }
253 },
254 syn::Fields::Named(fields) => {
255 let field_names: Vec<_> = fields.named.iter()
256 .filter_map(|f| f.ident.as_ref())
257 .collect();
258 if field_names.is_empty() {
259 quote! {
260 #name::#variant_name { .. } => ::std::string::String::new()
261 }
262 } else {
263 let format_str = field_names.iter()
264 .map(|n| format!("{}: {{:?}}", n))
265 .collect::<Vec<_>>()
266 .join(", ");
267 quote! {
268 #name::#variant_name { #(#field_names),*, .. } => ::std::format!(#format_str, #(#field_names),*)
269 }
270 }
271 },
272 }
273 });
274
275 let mut expanded = quote! {
276 impl tui_dispatch::Action for #name {
277 fn name(&self) -> &'static str {
278 match self {
279 #(#name_arms),*
280 }
281 }
282 }
283
284 impl tui_dispatch::ActionParams for #name {
285 fn params(&self) -> ::std::string::String {
286 match self {
287 #(#params_arms),*
288 }
289 }
290 }
291 };
292
293 if opts.infer_categories {
295 let mut categories: HashMap<String, Vec<&Ident>> = HashMap::new();
297 let mut variant_categories: Vec<(&Ident, Option<String>)> = Vec::new();
298
299 for v in variants.iter() {
300 let cat = if v.skip_category {
301 None
302 } else if let Some(ref explicit_cat) = v.category {
303 Some(explicit_cat.clone())
304 } else {
305 infer_category(&v.ident.to_string())
306 };
307
308 variant_categories.push((&v.ident, cat.clone()));
309
310 if let Some(ref category) = cat {
311 categories
312 .entry(category.clone())
313 .or_default()
314 .push(&v.ident);
315 }
316 }
317
318 let mut sorted_categories: Vec<_> = categories.keys().cloned().collect();
320 sorted_categories.sort();
321
322 let category_arms_dedup: Vec<_> = variant_categories
324 .iter()
325 .map(|(variant, cat)| {
326 let cat_expr = match cat {
327 Some(c) => quote! { ::core::option::Option::Some(#c) },
328 None => quote! { ::core::option::Option::None },
329 };
330 quote! { #name::#variant { .. } => #cat_expr }
332 })
333 .collect();
334
335 let category_enum_name = format_ident!("{}Category", name);
337 let category_variants: Vec<_> = sorted_categories
338 .iter()
339 .map(|c| format_ident!("{}", to_pascal_case(c)))
340 .collect();
341 let category_variant_names: Vec<_> = sorted_categories.clone();
342
343 let category_enum_arms: Vec<_> = variant_categories
345 .iter()
346 .map(|(variant, cat)| {
347 let cat_variant = match cat {
348 Some(c) => format_ident!("{}", to_pascal_case(c)),
349 None => format_ident!("Uncategorized"),
350 };
351 quote! { #name::#variant { .. } => #category_enum_name::#cat_variant }
352 })
353 .collect();
354
355 let predicates: Vec<_> = sorted_categories
357 .iter()
358 .map(|cat| {
359 let predicate_name = format_ident!("is_{}", cat);
360 let cat_variants = categories.get(cat).unwrap();
361 let patterns: Vec<_> = cat_variants
362 .iter()
363 .map(|v| quote! { #name::#v { .. } })
364 .collect();
365 let doc = format!(
366 "Returns true if this action belongs to the `{}` category.",
367 cat
368 );
369
370 quote! {
371 #[doc = #doc]
372 pub fn #predicate_name(&self) -> bool {
373 matches!(self, #(#patterns)|*)
374 }
375 }
376 })
377 .collect();
378
379 let category_enum_doc = format!(
381 "Action categories for [`{}`].\n\n\
382 Use [`{}::category_enum()`] to get the category of an action.",
383 name, name
384 );
385
386 expanded = quote! {
387 #expanded
388
389 #[doc = #category_enum_doc]
390 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
391 pub enum #category_enum_name {
392 #(#category_variants,)*
393 Uncategorized,
395 }
396
397 impl #category_enum_name {
398 pub fn all() -> &'static [Self] {
400 &[#(Self::#category_variants,)* Self::Uncategorized]
401 }
402
403 pub fn name(&self) -> &'static str {
405 match self {
406 #(Self::#category_variants => #category_variant_names,)*
407 Self::Uncategorized => "uncategorized",
408 }
409 }
410 }
411
412 impl #name {
413 pub fn category(&self) -> ::core::option::Option<&'static str> {
415 match self {
416 #(#category_arms_dedup,)*
417 }
418 }
419
420 pub fn category_enum(&self) -> #category_enum_name {
422 match self {
423 #(#category_enum_arms,)*
424 }
425 }
426
427 #(#predicates)*
428 }
429
430 impl tui_dispatch::ActionCategory for #name {
431 type Category = #category_enum_name;
432
433 fn category(&self) -> ::core::option::Option<&'static str> {
434 #name::category(self)
435 }
436
437 fn category_enum(&self) -> Self::Category {
438 #name::category_enum(self)
439 }
440 }
441 };
442
443 if opts.generate_dispatcher {
445 let dispatcher_trait_name = format_ident!("{}Dispatcher", name);
446
447 let dispatch_methods: Vec<_> = sorted_categories
448 .iter()
449 .map(|cat| {
450 let method_name = format_ident!("dispatch_{}", cat);
451 let doc = format!("Handle actions in the `{}` category.", cat);
452 quote! {
453 #[doc = #doc]
454 fn #method_name(&mut self, action: &#name) -> bool {
455 false
456 }
457 }
458 })
459 .collect();
460
461 let dispatch_arms: Vec<_> = sorted_categories
462 .iter()
463 .map(|cat| {
464 let method_name = format_ident!("dispatch_{}", cat);
465 let cat_variant = format_ident!("{}", to_pascal_case(cat));
466 quote! {
467 #category_enum_name::#cat_variant => self.#method_name(action)
468 }
469 })
470 .collect();
471
472 let dispatcher_doc = format!(
473 "Dispatcher trait for [`{}`].\n\n\
474 Implement the `dispatch_*` methods for each category you want to handle.\n\
475 The [`dispatch()`](Self::dispatch) method automatically routes to the correct handler.",
476 name
477 );
478
479 expanded = quote! {
480 #expanded
481
482 #[doc = #dispatcher_doc]
483 pub trait #dispatcher_trait_name {
484 #(#dispatch_methods)*
485
486 fn dispatch_uncategorized(&mut self, action: &#name) -> bool {
488 false
489 }
490
491 fn dispatch(&mut self, action: &#name) -> bool {
493 match action.category_enum() {
494 #(#dispatch_arms,)*
495 #category_enum_name::Uncategorized => self.dispatch_uncategorized(action),
496 }
497 }
498 }
499 };
500 }
501 }
502
503 TokenStream::from(expanded)
504}
505
506#[proc_macro_derive(BindingContext)]
525pub fn derive_binding_context(input: TokenStream) -> TokenStream {
526 let input = parse_macro_input!(input as DeriveInput);
527 let name = &input.ident;
528
529 let expanded = match &input.data {
530 syn::Data::Enum(data) => {
531 for variant in &data.variants {
533 if !matches!(variant.fields, syn::Fields::Unit) {
534 return syn::Error::new_spanned(
535 variant,
536 "BindingContext can only be derived for enums with unit variants",
537 )
538 .to_compile_error()
539 .into();
540 }
541 }
542
543 let variant_names: Vec<_> = data.variants.iter().map(|v| &v.ident).collect();
544 let variant_strings: Vec<_> = variant_names
545 .iter()
546 .map(|v| to_snake_case(&v.to_string()))
547 .collect();
548
549 let name_arms = variant_names
550 .iter()
551 .zip(variant_strings.iter())
552 .map(|(v, s)| {
553 quote! { #name::#v => #s }
554 });
555
556 let from_name_arms = variant_names
557 .iter()
558 .zip(variant_strings.iter())
559 .map(|(v, s)| {
560 quote! { #s => ::core::option::Option::Some(#name::#v) }
561 });
562
563 let all_variants = variant_names.iter().map(|v| quote! { #name::#v });
564
565 quote! {
566 impl tui_dispatch::BindingContext for #name {
567 fn name(&self) -> &'static str {
568 match self {
569 #(#name_arms),*
570 }
571 }
572
573 fn from_name(name: &str) -> ::core::option::Option<Self> {
574 match name {
575 #(#from_name_arms,)*
576 _ => ::core::option::Option::None,
577 }
578 }
579
580 fn all() -> &'static [Self] {
581 static ALL: &[#name] = &[#(#all_variants),*];
582 ALL
583 }
584 }
585 }
586 }
587 _ => {
588 return syn::Error::new_spanned(input, "BindingContext can only be derived for enums")
589 .to_compile_error()
590 .into();
591 }
592 };
593
594 TokenStream::from(expanded)
595}
596
597#[proc_macro_derive(ComponentId)]
613pub fn derive_component_id(input: TokenStream) -> TokenStream {
614 let input = parse_macro_input!(input as DeriveInput);
615 let name = &input.ident;
616
617 let expanded = match &input.data {
618 syn::Data::Enum(data) => {
619 for variant in &data.variants {
621 if !matches!(variant.fields, syn::Fields::Unit) {
622 return syn::Error::new_spanned(
623 variant,
624 "ComponentId can only be derived for enums with unit variants",
625 )
626 .to_compile_error()
627 .into();
628 }
629 }
630
631 let variant_names: Vec<_> = data.variants.iter().map(|v| &v.ident).collect();
632 let variant_strings: Vec<_> = variant_names.iter().map(|v| v.to_string()).collect();
633
634 let name_arms = variant_names
635 .iter()
636 .zip(variant_strings.iter())
637 .map(|(v, s)| {
638 quote! { #name::#v => #s }
639 });
640
641 quote! {
642 impl tui_dispatch::ComponentId for #name {
643 fn name(&self) -> &'static str {
644 match self {
645 #(#name_arms),*
646 }
647 }
648 }
649 }
650 }
651 _ => {
652 return syn::Error::new_spanned(input, "ComponentId can only be derived for enums")
653 .to_compile_error()
654 .into();
655 }
656 };
657
658 TokenStream::from(expanded)
659}
660
661#[derive(Debug, FromDeriveInput)]
667#[darling(attributes(debug_state), supports(struct_named))]
668struct DebugStateOpts {
669 ident: syn::Ident,
670 data: darling::ast::Data<(), DebugStateField>,
671}
672
673#[derive(Debug, FromField)]
675#[darling(attributes(debug))]
676struct DebugStateField {
677 ident: Option<syn::Ident>,
678
679 #[darling(default)]
681 section: Option<String>,
682
683 #[darling(default)]
685 skip: bool,
686
687 #[darling(default)]
689 format: Option<String>,
690
691 #[darling(default)]
693 label: Option<String>,
694
695 #[darling(default)]
697 debug_fmt: bool,
698}
699
700#[proc_macro_derive(DebugState, attributes(debug, debug_state))]
738pub fn derive_debug_state(input: TokenStream) -> TokenStream {
739 let input = parse_macro_input!(input as DeriveInput);
740
741 let opts = match DebugStateOpts::from_derive_input(&input) {
742 Ok(opts) => opts,
743 Err(e) => return e.write_errors().into(),
744 };
745
746 let name = &opts.ident;
747 let default_section = name.to_string();
748
749 let fields = match &opts.data {
750 darling::ast::Data::Struct(fields) => fields,
751 _ => {
752 return syn::Error::new_spanned(&input, "DebugState can only be derived for structs")
753 .to_compile_error()
754 .into();
755 }
756 };
757
758 let mut sections: HashMap<String, Vec<&DebugStateField>> = HashMap::new();
760 let mut section_order: Vec<String> = Vec::new();
761
762 for field in fields.iter() {
763 if field.skip {
764 continue;
765 }
766
767 let section_name = field
768 .section
769 .clone()
770 .unwrap_or_else(|| default_section.clone());
771
772 if !section_order.contains(§ion_name) {
773 section_order.push(section_name.clone());
774 }
775
776 sections.entry(section_name).or_default().push(field);
777 }
778
779 let section_code: Vec<_> = section_order
781 .iter()
782 .map(|section_name| {
783 let fields_in_section = sections.get(section_name).unwrap();
784
785 let entry_calls: Vec<_> = fields_in_section
786 .iter()
787 .filter_map(|field| {
788 let field_ident = field.ident.as_ref()?;
789 let label = field
790 .label
791 .clone()
792 .unwrap_or_else(|| field_ident.to_string());
793
794 let value_expr = if let Some(ref fmt) = field.format {
795 quote! { format!(#fmt, self.#field_ident) }
796 } else if field.debug_fmt {
797 quote! { format!("{:?}", self.#field_ident) }
798 } else {
799 quote! { self.#field_ident.to_string() }
800 };
801
802 Some(quote! {
803 .entry(#label, #value_expr)
804 })
805 })
806 .collect();
807
808 quote! {
809 tui_dispatch::debug::DebugSection::new(#section_name)
810 #(#entry_calls)*
811 }
812 })
813 .collect();
814
815 let expanded = quote! {
816 impl tui_dispatch::debug::DebugState for #name {
817 fn debug_sections(&self) -> ::std::vec::Vec<tui_dispatch::debug::DebugSection> {
818 ::std::vec![
819 #(#section_code),*
820 ]
821 }
822 }
823 };
824
825 TokenStream::from(expanded)
826}
827
828#[derive(Debug, FromField)]
834#[darling(attributes(flag))]
835struct FeatureFlagsField {
836 ident: Option<syn::Ident>,
837 ty: syn::Type,
838
839 #[darling(default)]
841 default: Option<bool>,
842}
843
844#[derive(Debug, FromDeriveInput)]
846#[darling(attributes(feature_flags), supports(struct_named))]
847struct FeatureFlagsOpts {
848 ident: syn::Ident,
849 data: darling::ast::Data<(), FeatureFlagsField>,
850}
851
852#[proc_macro_derive(FeatureFlags, attributes(flag, feature_flags))]
883pub fn derive_feature_flags(input: TokenStream) -> TokenStream {
884 let input = parse_macro_input!(input as DeriveInput);
885
886 let opts = match FeatureFlagsOpts::from_derive_input(&input) {
887 Ok(opts) => opts,
888 Err(e) => return e.write_errors().into(),
889 };
890
891 let name = &opts.ident;
892
893 let fields = match &opts.data {
894 darling::ast::Data::Struct(fields) => fields,
895 _ => {
896 return syn::Error::new_spanned(
897 &input,
898 "FeatureFlags can only be derived for structs with named fields",
899 )
900 .to_compile_error()
901 .into();
902 }
903 };
904
905 let bool_fields: Vec<_> = fields
907 .iter()
908 .filter_map(|f| {
909 let ident = f.ident.as_ref()?;
910 if let syn::Type::Path(type_path) = &f.ty {
912 if type_path.path.is_ident("bool") {
913 return Some((ident.clone(), f.default.unwrap_or(false)));
914 }
915 }
916 None
917 })
918 .collect();
919
920 if bool_fields.is_empty() {
921 return syn::Error::new_spanned(
922 &input,
923 "FeatureFlags struct must have at least one bool field",
924 )
925 .to_compile_error()
926 .into();
927 }
928
929 let is_enabled_arms: Vec<_> = bool_fields
931 .iter()
932 .map(|(ident, _)| {
933 let name_str = ident.to_string();
934 quote! { #name_str => ::core::option::Option::Some(self.#ident) }
935 })
936 .collect();
937
938 let set_arms: Vec<_> = bool_fields
940 .iter()
941 .map(|(ident, _)| {
942 let name_str = ident.to_string();
943 quote! {
944 #name_str => {
945 self.#ident = enabled;
946 true
947 }
948 }
949 })
950 .collect();
951
952 let flag_names: Vec<_> = bool_fields
954 .iter()
955 .map(|(ident, _)| ident.to_string())
956 .collect();
957
958 let default_fields: Vec<_> = bool_fields
960 .iter()
961 .map(|(ident, default)| {
962 quote! { #ident: #default }
963 })
964 .collect();
965
966 let expanded = quote! {
967 impl tui_dispatch::FeatureFlags for #name {
968 fn is_enabled(&self, name: &str) -> ::core::option::Option<bool> {
969 match name {
970 #(#is_enabled_arms,)*
971 _ => ::core::option::Option::None,
972 }
973 }
974
975 fn set(&mut self, name: &str, enabled: bool) -> bool {
976 match name {
977 #(#set_arms)*
978 _ => false,
979 }
980 }
981
982 fn all_flags() -> &'static [&'static str] {
983 &[#(#flag_names),*]
984 }
985 }
986
987 impl ::core::default::Default for #name {
988 fn default() -> Self {
989 Self {
990 #(#default_fields,)*
991 }
992 }
993 }
994 };
995
996 TokenStream::from(expanded)
997}