tui_dispatch_macros/
lib.rs

1//! Procedural macros for tui-dispatch
2
3use 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/// Container-level attributes for #[derive(Action)]
11#[derive(Debug, FromDeriveInput)]
12#[darling(attributes(action), supports(enum_any))]
13struct ActionOpts {
14    ident: syn::Ident,
15    data: darling::ast::Data<ActionVariant, ()>,
16
17    /// Enable automatic category inference from variant name prefixes
18    #[darling(default)]
19    infer_categories: bool,
20
21    /// Generate dispatcher trait
22    #[darling(default)]
23    generate_dispatcher: bool,
24}
25
26/// Variant-level attributes
27#[derive(Debug, FromVariant)]
28#[darling(attributes(action))]
29struct ActionVariant {
30    ident: syn::Ident,
31    fields: darling::ast::Fields<()>,
32
33    /// Explicit category override
34    #[darling(default)]
35    category: Option<String>,
36
37    /// Exclude from category inference
38    #[darling(default)]
39    skip_category: bool,
40}
41
42/// Common action verbs that typically appear as the last part of a variant name
43// Action verbs that typically END an action name (the actual verb part)
44// Things like "Form", "Panel", "Field" are nouns, not verbs - they should NOT be here
45const ACTION_VERBS: &[&str] = &[
46    // State transitions
47    "Start", "End", "Open", "Close", "Submit", "Confirm", "Cancel", // Navigation
48    "Next", "Prev", "Up", "Down", "Left", "Right", "Enter", "Exit", "Escape",
49    // CRUD operations
50    "Add", "Remove", "Clear", "Update", "Set", "Get", "Load", "Save", "Delete", "Create",
51    // Visibility
52    "Show", "Hide", "Enable", "Disable", "Toggle", // Focus
53    "Focus", "Blur", "Select", // Movement
54    "Move", "Copy", "Cycle", "Reset", "Scroll",
55];
56
57/// Split a PascalCase string into parts
58fn 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
75/// Convert PascalCase to snake_case
76fn 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
91/// Convert snake_case to PascalCase
92fn 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
104/// Infer category from a variant name using naming patterns
105fn infer_category(name: &str) -> Option<String> {
106    let parts = split_pascal_case(name);
107    if parts.is_empty() {
108        return None;
109    }
110
111    // Check for "Did" prefix (async results)
112    if parts[0] == "Did" {
113        return Some("async_result".to_string());
114    }
115
116    // If only one part, no category
117    if parts.len() < 2 {
118        return None;
119    }
120
121    // Find the longest prefix that ends before an action verb
122    // e.g., ["Connection", "Form", "Submit"] -> "connection_form"
123    // e.g., ["Search", "Add", "Char"] -> "search"
124    // e.g., ["Value", "Viewer", "Scroll", "Up"] -> "value_viewer"
125
126    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    // Skip if first part is an action verb - these are primary actions, not categorized
139    // e.g., "OpenConnectionForm" → "Open" is the verb, "ConnectionForm" is the object
140    // e.g., "NextItem" → "Next" is the verb, "Item" is the object
141    if first_is_verb {
142        return None;
143    }
144
145    // Skip if no verb found in the name - can't determine meaningful category
146    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/// Derive macro for the Action trait
161///
162/// Generates a `name()` method that returns the variant name as a static string.
163///
164/// With `#[action(infer_categories)]`, also generates:
165/// - `category() -> Option<&'static str>` - Get action's category
166/// - `category_enum() -> {Name}Category` - Get category as enum
167/// - `is_{category}()` predicates for each category
168/// - `{Name}Category` enum with all discovered categories
169///
170/// With `#[action(generate_dispatcher)]`, also generates:
171/// - `{Name}Dispatcher` trait with category-based dispatch methods
172///
173/// # Example
174/// ```ignore
175/// #[derive(Action, Clone, Debug)]
176/// #[action(infer_categories, generate_dispatcher)]
177/// enum MyAction {
178///     SearchStart,
179///     SearchClear,
180///     ConnectionFormOpen,
181///     ConnectionFormSubmit,
182///     DidConnect,
183///     Tick,  // uncategorized
184/// }
185///
186/// let action = MyAction::SearchStart;
187/// assert_eq!(action.name(), "SearchStart");
188/// assert_eq!(action.category(), Some("search"));
189/// assert!(action.is_search());
190/// ```
191#[proc_macro_derive(Action, attributes(action))]
192pub fn derive_action(input: TokenStream) -> TokenStream {
193    let input = parse_macro_input!(input as DeriveInput);
194
195    // Try to parse with darling for attributes
196    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    // Get the original syn variants for field info (darling loses field names)
213    let syn_variants = match &input.data {
214        syn::Data::Enum(data) => &data.variants,
215        _ => unreachable!(), // Already checked above
216    };
217
218    // Generate basic name() implementation
219    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    // Generate params() implementation - outputs field values without variant name
237    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 category inference is enabled, generate category-related code
294    if opts.infer_categories {
295        // Collect categories and their variants
296        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        // Sort categories for deterministic output
319        let mut sorted_categories: Vec<_> = categories.keys().cloned().collect();
320        sorted_categories.sort();
321
322        // Create deduplicated category match arms
323        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                // Use wildcard pattern to handle all field types
331                quote! { #name::#variant { .. } => #cat_expr }
332            })
333            .collect();
334
335        // Generate category enum
336        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        // Generate category_enum() method arms
344        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        // Generate is_* predicates
356        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        // Add category-related implementations
380        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                /// Actions that don't belong to any specific category.
394                Uncategorized,
395            }
396
397            impl #category_enum_name {
398                /// Get all category values
399                pub fn all() -> &'static [Self] {
400                    &[#(Self::#category_variants,)* Self::Uncategorized]
401                }
402
403                /// Get category name as string
404                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                /// Get the action's category (if categorized)
414                pub fn category(&self) -> ::core::option::Option<&'static str> {
415                    match self {
416                        #(#category_arms_dedup,)*
417                    }
418                }
419
420                /// Get the category as an enum value
421                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        // Generate dispatcher trait if requested
444        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                    /// Handle uncategorized actions.
487                    fn dispatch_uncategorized(&mut self, action: &#name) -> bool {
488                        false
489                    }
490
491                    /// Main dispatch entry point - routes to category-specific handlers.
492                    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/// Derive macro for the BindingContext trait
507///
508/// Generates implementations for `name()`, `from_name()`, and `all()` methods.
509/// The context name is derived from the variant name converted to snake_case.
510///
511/// # Example
512/// ```ignore
513/// #[derive(BindingContext, Clone, Copy, PartialEq, Eq, Hash)]
514/// enum MyContext {
515///     Default,
516///     Search,
517///     ConnectionForm,
518/// }
519///
520/// // Generated names: "default", "search", "connection_form"
521/// assert_eq!(MyContext::Default.name(), "default");
522/// assert_eq!(MyContext::from_name("search"), Some(MyContext::Search));
523/// ```
524#[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            // Check that all variants are unit variants
532            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/// Derive macro for the ComponentId trait
598///
599/// Generates implementations for `name()` method that returns the variant name.
600///
601/// # Example
602/// ```ignore
603/// #[derive(ComponentId, Clone, Copy, PartialEq, Eq, Hash, Debug)]
604/// enum MyComponentId {
605///     Sidebar,
606///     MainContent,
607///     StatusBar,
608/// }
609///
610/// assert_eq!(MyComponentId::Sidebar.name(), "Sidebar");
611/// ```
612#[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            // Check that all variants are unit variants
620            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// ============================================================================
662// DebugState derive macro
663// ============================================================================
664
665/// Container-level attributes for #[derive(DebugState)]
666#[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/// Field-level attributes for DebugState
674#[derive(Debug, FromField)]
675#[darling(attributes(debug))]
676struct DebugStateField {
677    ident: Option<syn::Ident>,
678
679    /// Section name for this field (groups fields together)
680    #[darling(default)]
681    section: Option<String>,
682
683    /// Skip this field in debug output
684    #[darling(default)]
685    skip: bool,
686
687    /// Custom display format (e.g., "{:?}" for Debug, "{:#?}" for pretty Debug)
688    #[darling(default)]
689    format: Option<String>,
690
691    /// Custom label for this field (defaults to field name)
692    #[darling(default)]
693    label: Option<String>,
694
695    /// Use Debug trait instead of Display
696    #[darling(default)]
697    debug_fmt: bool,
698}
699
700/// Derive macro for the DebugState trait
701///
702/// Automatically generates `debug_sections()` implementation from struct fields.
703///
704/// # Attributes
705///
706/// - `#[debug(section = "Name")]` - Group field under a section
707/// - `#[debug(skip)]` - Exclude field from debug output
708/// - `#[debug(label = "Custom Label")]` - Use custom label instead of field name
709/// - `#[debug(debug_fmt)]` - Use `{:?}` format instead of `Display`
710/// - `#[debug(format = "{:#?}")]` - Use custom format string
711///
712/// # Example
713///
714/// ```ignore
715/// use tui_dispatch::DebugState;
716///
717/// #[derive(DebugState)]
718/// struct AppState {
719///     #[debug(section = "Connection")]
720///     host: String,
721///     #[debug(section = "Connection")]
722///     port: u16,
723///
724///     #[debug(section = "UI")]
725///     scroll_offset: usize,
726///
727///     #[debug(skip)]
728///     internal_cache: HashMap<String, Data>,
729///
730///     #[debug(section = "Stats", debug_fmt)]
731///     status: ConnectionStatus,
732/// }
733/// ```
734///
735/// Fields without a section attribute are grouped under a section named after
736/// the struct (e.g., "AppState").
737#[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    // Group fields by section
759    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(&section_name) {
773            section_order.push(section_name.clone());
774        }
775
776        sections.entry(section_name).or_default().push(field);
777    }
778
779    // Generate code for each section
780    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// ============================================================================
829// FeatureFlags derive macro
830// ============================================================================
831
832/// Field-level attributes for FeatureFlags
833#[derive(Debug, FromField)]
834#[darling(attributes(flag))]
835struct FeatureFlagsField {
836    ident: Option<syn::Ident>,
837    ty: syn::Type,
838
839    /// Default value for this feature (defaults to false)
840    #[darling(default)]
841    default: Option<bool>,
842}
843
844/// Container-level attributes for #[derive(FeatureFlags)]
845#[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/// Derive macro for the FeatureFlags trait
853///
854/// Generates implementations for `is_enabled()`, `set()`, and `all_flags()` methods.
855/// Also generates a `Default` implementation using the specified defaults.
856///
857/// # Attributes
858///
859/// - `#[flag(default = true)]` - Set default value (defaults to false)
860///
861/// # Example
862///
863/// ```ignore
864/// use tui_dispatch::FeatureFlags;
865///
866/// #[derive(FeatureFlags)]
867/// struct Features {
868///     #[flag(default = false)]
869///     new_search_ui: bool,
870///
871///     #[flag(default = true)]
872///     vim_bindings: bool,
873/// }
874///
875/// let mut features = Features::default();
876/// assert!(!features.new_search_ui);
877/// assert!(features.vim_bindings);
878///
879/// features.enable("new_search_ui");
880/// assert!(features.new_search_ui);
881/// ```
882#[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    // Collect bool fields only
906    let bool_fields: Vec<_> = fields
907        .iter()
908        .filter_map(|f| {
909            let ident = f.ident.as_ref()?;
910            // Check if type is bool
911            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    // Generate is_enabled match arms
930    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    // Generate set match arms
939    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    // Generate all_flags array
953    let flag_names: Vec<_> = bool_fields
954        .iter()
955        .map(|(ident, _)| ident.to_string())
956        .collect();
957
958    // Generate Default impl with proper defaults
959    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}