reovim_plugin_settings_menu/
lib.rs

1//! Settings menu plugin for reovim
2//!
3//! This plugin provides an interactive settings menu for configuring editor settings:
4//! - Toggle options for booleans (checkbox style)
5//! - Selection/cycle options for enums (dropdown style)
6//! - Number input for numeric values
7//! - Organized sections
8//! - Vim-style navigation (j/k/h/l)
9//! - Live preview of changes
10//!
11//! # Architecture
12//!
13//! This plugin is fully self-contained:
14//! - Defines its own command IDs
15//! - Manages its own state via `PluginStateRegistry`
16//! - Renders via `PluginWindow` trait
17//! - Communicates via `EventBus` events
18
19mod commands;
20mod settings_menu;
21
22use std::{any::TypeId, sync::Arc};
23
24// Import unified command-event types
25pub use commands::{
26    SettingsMenuClose, SettingsMenuCycleNext, SettingsMenuCyclePrev, SettingsMenuDecrement,
27    SettingsMenuExecuteAction, SettingsMenuIncrement, SettingsMenuOpen, SettingsMenuQuick1,
28    SettingsMenuQuick2, SettingsMenuQuick3, SettingsMenuQuick4, SettingsMenuQuick5,
29    SettingsMenuQuick6, SettingsMenuQuick7, SettingsMenuQuick8, SettingsMenuQuick9,
30    SettingsMenuQuickSelect, SettingsMenuSelectNext, SettingsMenuSelectPrev, SettingsMenuToggle,
31};
32
33use reovim_core::{
34    bind::{CommandRef, EditModeKind, KeymapScope},
35    command::id::CommandId,
36    config::ProfileConfig,
37    display::DisplayInfo,
38    event_bus::{EventBus, EventResult, core_events::RequestFocusChange},
39    frame::FrameBuffer,
40    highlight::Theme,
41    keys,
42    modd::ComponentId,
43    option::{ChangeSource, OptionChanged, RegisterOption, RegisterSettingSection},
44    plugin::{
45        EditorContext, Plugin, PluginContext, PluginId, PluginStateRegistry, PluginWindow, Rect,
46        WindowConfig,
47    },
48    screen::border::{BorderConfig, BorderStyle, WindowAdjacency, render_border_to_buffer},
49};
50
51// Re-export key types for external use (non-command/event types)
52pub use settings_menu::{
53    ActionType, FlatItem, MenuLayout, MessageKind, RegisteredOption, SectionMeta, SettingChange,
54    SettingItem, SettingSection, SettingValue, SettingsInputMode, SettingsMenuState,
55};
56
57/// Plugin-local command IDs
58pub mod command_id {
59    use super::CommandId;
60
61    pub const SETTINGS_MENU_OPEN: CommandId = CommandId::new("settings_menu_open");
62    pub const SETTINGS_MENU_CLOSE: CommandId = CommandId::new("settings_menu_close");
63    pub const SETTINGS_MENU_NEXT: CommandId = CommandId::new("settings_menu_next");
64    pub const SETTINGS_MENU_PREV: CommandId = CommandId::new("settings_menu_prev");
65    pub const SETTINGS_MENU_TOGGLE: CommandId = CommandId::new("settings_menu_toggle");
66    pub const SETTINGS_MENU_CYCLE_NEXT: CommandId = CommandId::new("settings_menu_cycle_next");
67    pub const SETTINGS_MENU_CYCLE_PREV: CommandId = CommandId::new("settings_menu_cycle_prev");
68    pub const SETTINGS_MENU_INCREMENT: CommandId = CommandId::new("settings_menu_increment");
69    pub const SETTINGS_MENU_DECREMENT: CommandId = CommandId::new("settings_menu_decrement");
70    pub const SETTINGS_MENU_EXECUTE: CommandId = CommandId::new("settings_menu_execute");
71    pub const SETTINGS_MENU_QUICK_1: CommandId = CommandId::new("settings_menu_quick_1");
72    pub const SETTINGS_MENU_QUICK_2: CommandId = CommandId::new("settings_menu_quick_2");
73    pub const SETTINGS_MENU_QUICK_3: CommandId = CommandId::new("settings_menu_quick_3");
74    pub const SETTINGS_MENU_QUICK_4: CommandId = CommandId::new("settings_menu_quick_4");
75    pub const SETTINGS_MENU_QUICK_5: CommandId = CommandId::new("settings_menu_quick_5");
76    pub const SETTINGS_MENU_QUICK_6: CommandId = CommandId::new("settings_menu_quick_6");
77    pub const SETTINGS_MENU_QUICK_7: CommandId = CommandId::new("settings_menu_quick_7");
78    pub const SETTINGS_MENU_QUICK_8: CommandId = CommandId::new("settings_menu_quick_8");
79    pub const SETTINGS_MENU_QUICK_9: CommandId = CommandId::new("settings_menu_quick_9");
80}
81
82/// Plugin window for settings menu
83pub struct SettingsPluginWindow;
84
85impl PluginWindow for SettingsPluginWindow {
86    fn window_config(
87        &self,
88        state: &Arc<PluginStateRegistry>,
89        _ctx: &EditorContext,
90    ) -> Option<WindowConfig> {
91        state.with::<SettingsMenuState, _, _>(|settings| {
92            if !settings.visible {
93                return None;
94            }
95
96            let layout = &settings.layout;
97            Some(WindowConfig {
98                bounds: Rect::new(layout.x, layout.y, layout.width, layout.height),
99                z_order: 400, // Modal settings
100                visible: true,
101            })
102        })?
103    }
104
105    #[allow(clippy::cast_possible_truncation)]
106    fn render(
107        &self,
108        state: &Arc<PluginStateRegistry>,
109        _ctx: &EditorContext,
110        buffer: &mut FrameBuffer,
111        bounds: Rect,
112        theme: &Theme,
113    ) {
114        let Some(settings) = state.with::<SettingsMenuState, _, _>(Clone::clone) else {
115            tracing::debug!("Settings render: no state available");
116            return;
117        };
118
119        tracing::debug!(
120            "Settings render: visible={}, items={}, selected={}, scroll={}",
121            settings.visible,
122            settings.flat_items.len(),
123            settings.selected_index,
124            settings.scroll_offset
125        );
126
127        // Log the value of the selected item
128        if let Some(item) = settings.selected_item() {
129            tracing::debug!(
130                "Settings render: selected item key={}, value={:?}",
131                item.key,
132                item.value
133            );
134        }
135
136        let normal_style = &theme.popup.normal;
137
138        // Render border using the plugin window system's border utilities
139        let border_config = BorderConfig::new(BorderStyle::Rounded).with_title("Settings");
140        let adjacency = WindowAdjacency::default();
141        render_border_to_buffer(
142            buffer,
143            bounds.x,
144            bounds.y,
145            bounds.width,
146            bounds.height,
147            &border_config,
148            &theme.popup.border,
149            &adjacency,
150            true, // floating window
151        );
152
153        // Content rows (inside the border)
154        let content_height = bounds.height.saturating_sub(2) as usize;
155        for row in 0..content_height {
156            let y = bounds.y + 1 + row as u16;
157
158            // Get item for this row, accounting for scroll offset
159            let item_idx = row + settings.scroll_offset;
160            let item = settings.flat_items.get(item_idx);
161            let is_selected = item_idx == settings.selected_index;
162            let style = if is_selected {
163                &theme.popup.selected
164            } else {
165                normal_style
166            };
167
168            let item_text = match item {
169                Some(FlatItem::SectionHeader(name)) => format!("[{name}]"),
170                Some(FlatItem::Setting {
171                    section_idx,
172                    item_idx,
173                }) => {
174                    if let Some(section) = settings.sections.get(*section_idx)
175                        && let Some(setting) = section.items.get(*item_idx)
176                    {
177                        // Log the value for selected items
178                        if is_selected {
179                            tracing::debug!(
180                                "Settings render selected: key={}, value={:?}",
181                                setting.key,
182                                setting.value
183                            );
184                        }
185                        // Format: " Label: value" or " [x] Label" for booleans
186                        let value_str = setting.value.display_value();
187                        match &setting.value {
188                            SettingValue::Bool(b) => {
189                                let checkbox = if *b { "[x]" } else { "[ ]" };
190                                format!(" {} {}", checkbox, setting.label)
191                            }
192                            SettingValue::Action(_) => format!(" {}", setting.label),
193                            _ => format!(" {}: {}", setting.label, value_str),
194                        }
195                    } else {
196                        " ???".to_string()
197                    }
198                }
199                None => String::new(), // Empty row
200            };
201
202            // Draw content (inside border, starting at x+1)
203            for (i, ch) in item_text.chars().enumerate() {
204                let x = bounds.x + 1 + i as u16;
205                if x < bounds.x + bounds.width - 1 {
206                    buffer.put_char(x, y, ch, style);
207                }
208            }
209
210            // Fill remaining space with background
211            for x in (bounds.x + 1 + item_text.len() as u16)..(bounds.x + bounds.width - 1) {
212                buffer.put_char(x, y, ' ', if item.is_some() { style } else { normal_style });
213            }
214        }
215    }
216}
217
218/// Component ID for settings menu
219pub const COMPONENT_ID: ComponentId = ComponentId("settings");
220
221/// Settings menu plugin
222///
223/// Provides in-editor settings UI:
224/// - Toggle settings
225/// - Cycle through options
226/// - Increment/decrement values
227/// - Quick access keys (1-9)
228pub struct SettingsMenuPlugin;
229
230impl Plugin for SettingsMenuPlugin {
231    fn id(&self) -> PluginId {
232        PluginId::new("reovim:settings-menu")
233    }
234
235    fn name(&self) -> &'static str {
236        "Settings Menu"
237    }
238
239    fn description(&self) -> &'static str {
240        "In-editor settings menu with live preview"
241    }
242
243    fn dependencies(&self) -> Vec<TypeId> {
244        vec![]
245    }
246
247    fn build(&self, ctx: &mut PluginContext) {
248        // Register display info for status line
249        use reovim_core::highlight::{Color, Style};
250
251        // Gray background for settings/configuration
252        let gray = Color::Rgb {
253            r: 92,
254            g: 99,
255            b: 112,
256        };
257        let fg = Color::Rgb {
258            r: 171,
259            g: 178,
260            b: 191,
261        };
262        let style = Style::new().fg(fg).bg(gray).bold();
263
264        ctx.register_display(COMPONENT_ID, DisplayInfo::new(" SETTINGS ", " ", style));
265
266        // Register navigation commands (unified types)
267        let _ = ctx.register_command(SettingsMenuOpen);
268        let _ = ctx.register_command(SettingsMenuClose);
269        let _ = ctx.register_command(SettingsMenuSelectNext);
270        let _ = ctx.register_command(SettingsMenuSelectPrev);
271
272        // Register action commands (unified types)
273        let _ = ctx.register_command(SettingsMenuToggle);
274        let _ = ctx.register_command(SettingsMenuCycleNext);
275        let _ = ctx.register_command(SettingsMenuCyclePrev);
276        let _ = ctx.register_command(SettingsMenuIncrement);
277        let _ = ctx.register_command(SettingsMenuDecrement);
278        let _ = ctx.register_command(SettingsMenuExecuteAction);
279
280        // Register quick select commands (1-9, unified types)
281        let _ = ctx.register_command(SettingsMenuQuick1);
282        let _ = ctx.register_command(SettingsMenuQuick2);
283        let _ = ctx.register_command(SettingsMenuQuick3);
284        let _ = ctx.register_command(SettingsMenuQuick4);
285        let _ = ctx.register_command(SettingsMenuQuick5);
286        let _ = ctx.register_command(SettingsMenuQuick6);
287        let _ = ctx.register_command(SettingsMenuQuick7);
288        let _ = ctx.register_command(SettingsMenuQuick8);
289        let _ = ctx.register_command(SettingsMenuQuick9);
290
291        // Register keybindings
292        let editor_normal = KeymapScope::editor_normal();
293        let settings_normal = KeymapScope::Component {
294            id: COMPONENT_ID,
295            mode: EditModeKind::Normal,
296        };
297
298        // Space+s to open settings menu (from editor)
299        ctx.bind_key_scoped(
300            editor_normal,
301            keys![Space 's'],
302            CommandRef::Registered(command_id::SETTINGS_MENU_OPEN),
303        );
304
305        // Settings-specific keybindings (when settings menu is focused)
306
307        // Navigation
308        ctx.bind_key_scoped(
309            settings_normal.clone(),
310            keys!['j'],
311            CommandRef::Registered(command_id::SETTINGS_MENU_NEXT),
312        );
313        ctx.bind_key_scoped(
314            settings_normal.clone(),
315            keys!['k'],
316            CommandRef::Registered(command_id::SETTINGS_MENU_PREV),
317        );
318        ctx.bind_key_scoped(
319            settings_normal.clone(),
320            keys![Down],
321            CommandRef::Registered(command_id::SETTINGS_MENU_NEXT),
322        );
323        ctx.bind_key_scoped(
324            settings_normal.clone(),
325            keys![Up],
326            CommandRef::Registered(command_id::SETTINGS_MENU_PREV),
327        );
328
329        // Actions
330        ctx.bind_key_scoped(
331            settings_normal.clone(),
332            keys![Space],
333            CommandRef::Registered(command_id::SETTINGS_MENU_TOGGLE),
334        );
335        ctx.bind_key_scoped(
336            settings_normal.clone(),
337            keys![Enter],
338            CommandRef::Registered(command_id::SETTINGS_MENU_EXECUTE),
339        );
340        ctx.bind_key_scoped(
341            settings_normal.clone(),
342            keys!['l'],
343            CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_NEXT),
344        );
345        ctx.bind_key_scoped(
346            settings_normal.clone(),
347            keys!['h'],
348            CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_PREV),
349        );
350        ctx.bind_key_scoped(
351            settings_normal.clone(),
352            keys![Tab],
353            CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_NEXT),
354        );
355        ctx.bind_key_scoped(
356            settings_normal.clone(),
357            keys![(Shift Tab)],
358            CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_PREV),
359        );
360
361        // Number increment/decrement
362        ctx.bind_key_scoped(
363            settings_normal.clone(),
364            keys!['+'],
365            CommandRef::Registered(command_id::SETTINGS_MENU_INCREMENT),
366        );
367        ctx.bind_key_scoped(
368            settings_normal.clone(),
369            keys!['-'],
370            CommandRef::Registered(command_id::SETTINGS_MENU_DECREMENT),
371        );
372
373        // Quick select (1-9)
374        ctx.bind_key_scoped(
375            settings_normal.clone(),
376            keys!['1'],
377            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_1),
378        );
379        ctx.bind_key_scoped(
380            settings_normal.clone(),
381            keys!['2'],
382            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_2),
383        );
384        ctx.bind_key_scoped(
385            settings_normal.clone(),
386            keys!['3'],
387            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_3),
388        );
389        ctx.bind_key_scoped(
390            settings_normal.clone(),
391            keys!['4'],
392            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_4),
393        );
394        ctx.bind_key_scoped(
395            settings_normal.clone(),
396            keys!['5'],
397            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_5),
398        );
399        ctx.bind_key_scoped(
400            settings_normal.clone(),
401            keys!['6'],
402            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_6),
403        );
404        ctx.bind_key_scoped(
405            settings_normal.clone(),
406            keys!['7'],
407            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_7),
408        );
409        ctx.bind_key_scoped(
410            settings_normal.clone(),
411            keys!['8'],
412            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_8),
413        );
414        ctx.bind_key_scoped(
415            settings_normal.clone(),
416            keys!['9'],
417            CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_9),
418        );
419
420        // Close
421        ctx.bind_key_scoped(
422            settings_normal.clone(),
423            keys![Escape],
424            CommandRef::Registered(command_id::SETTINGS_MENU_CLOSE),
425        );
426        ctx.bind_key_scoped(
427            settings_normal,
428            keys!['q'],
429            CommandRef::Registered(command_id::SETTINGS_MENU_CLOSE),
430        );
431    }
432
433    fn init_state(&self, registry: &PluginStateRegistry) {
434        // Register the settings menu state
435        registry.register(SettingsMenuState::new());
436
437        // Register the plugin window
438        registry.register_plugin_window(Arc::new(SettingsPluginWindow));
439    }
440
441    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
442        // Register :settings ex-command
443        {
444            use {
445                crate::SettingsMenuOpen,
446                reovim_core::{
447                    command_line::ExCommandHandler,
448                    event_bus::{DynEvent, core_events::RegisterExCommand},
449                },
450            };
451
452            bus.emit(RegisterExCommand::new(
453                "settings",
454                ExCommandHandler::ZeroArg {
455                    event_constructor: || DynEvent::new(SettingsMenuOpen),
456                    description: "Open the settings menu",
457                },
458            ));
459        }
460
461        // Handle RegisterSettingSection events
462        {
463            let state = Arc::clone(&state);
464            bus.subscribe::<RegisterSettingSection, _>(100, move |event, _ctx| {
465                state.with_mut::<SettingsMenuState, _, _>(|menu| {
466                    menu.register_section(
467                        event.id.to_string(),
468                        SectionMeta {
469                            display_name: event.display_name.to_string(),
470                            order: event.order,
471                            description: event.description.as_ref().map(|s| s.to_string()),
472                        },
473                    );
474                });
475                EventResult::Handled
476            });
477        }
478
479        // Handle RegisterOption events
480        {
481            let state = Arc::clone(&state);
482            bus.subscribe::<RegisterOption, _>(100, move |event, _ctx| {
483                state.with_mut::<SettingsMenuState, _, _>(|menu| {
484                    menu.register_option(&event.spec, &event.spec.default);
485                });
486                EventResult::Handled
487            });
488        }
489
490        // Handle OptionChanged events - update stored values when options change
491        // from other sources (e.g., :set command, profile load)
492        {
493            let state = Arc::clone(&state);
494            bus.subscribe::<OptionChanged, _>(100, move |event, ctx| {
495                // Skip changes from settings-menu itself to avoid loops
496                if event.source == ChangeSource::SettingsMenu {
497                    return EventResult::Handled;
498                }
499
500                state.with_mut::<SettingsMenuState, _, _>(|menu| {
501                    menu.update_option_value(&event.name, &event.new_value);
502                });
503                ctx.request_render();
504                EventResult::Handled
505            });
506        }
507
508        // Handle SettingsMenuOpen - open the settings menu
509        {
510            let state = Arc::clone(&state);
511            bus.subscribe::<SettingsMenuOpen, _>(100, move |_event, ctx| {
512                state.with_mut::<SettingsMenuState, _, _>(|menu| {
513                    // Use default profile config (profile is unused in open())
514                    let profile = ProfileConfig::default();
515                    menu.open(&profile, "default");
516                    // Use reasonable default dimensions; actual bounds come from window_config()
517                    menu.calculate_layout(120, 40);
518                });
519                // Request focus change to settings component
520                ctx.emit(RequestFocusChange {
521                    target: COMPONENT_ID,
522                });
523                ctx.request_render();
524                EventResult::Handled
525            });
526        }
527
528        // Handle SettingsMenuClose - close the settings menu
529        {
530            let state = Arc::clone(&state);
531            bus.subscribe::<SettingsMenuClose, _>(100, move |_event, ctx| {
532                state.with_mut::<SettingsMenuState, _, _>(|menu| {
533                    menu.close();
534                });
535                // Return focus to editor
536                ctx.emit(RequestFocusChange {
537                    target: ComponentId::EDITOR,
538                });
539                ctx.request_render();
540                EventResult::Handled
541            });
542        }
543
544        // Handle SettingsMenuSelectNext - move to next item
545        {
546            let state = Arc::clone(&state);
547            bus.subscribe::<SettingsMenuSelectNext, _>(100, move |_event, ctx| {
548                state.with_mut::<SettingsMenuState, _, _>(|menu| {
549                    menu.select_next();
550                });
551                ctx.request_render();
552                EventResult::Handled
553            });
554        }
555
556        // Handle SettingsMenuSelectPrev - move to previous item
557        {
558            let state = Arc::clone(&state);
559            bus.subscribe::<SettingsMenuSelectPrev, _>(100, move |_event, ctx| {
560                state.with_mut::<SettingsMenuState, _, _>(|menu| {
561                    menu.select_prev();
562                });
563                ctx.request_render();
564                EventResult::Handled
565            });
566        }
567
568        // Handle SettingsMenuToggle - toggle boolean settings
569        {
570            let state = Arc::clone(&state);
571            bus.subscribe::<SettingsMenuToggle, _>(100, move |_event, ctx| {
572                tracing::debug!("SettingsMenuToggle event received");
573                let change = state.with_mut::<SettingsMenuState, _, _>(|menu| {
574                    tracing::debug!("Before toggle - selected_index: {}", menu.selected_index);
575                    if let Some(item) = menu.selected_item() {
576                        tracing::debug!("Selected item: {} = {:?}", item.key, item.value);
577                    }
578                    let result = menu.toggle_selected();
579                    if let Some(item) = menu.selected_item() {
580                        tracing::debug!("After toggle: {} = {:?}", item.key, item.value);
581                    }
582                    result
583                });
584                if let Some(Some(change)) = change {
585                    tracing::info!(
586                        "Settings toggle: {} changed from {:?} to {:?}",
587                        change.key,
588                        change.old_value,
589                        change.new_value
590                    );
591                    ctx.emit(OptionChanged::new(
592                        change.key,
593                        change.old_value,
594                        change.new_value,
595                        ChangeSource::SettingsMenu,
596                    ));
597                } else {
598                    tracing::debug!(
599                        "Settings toggle: no change (not a boolean or no item selected)"
600                    );
601                }
602                ctx.request_render();
603                EventResult::Handled
604            });
605        }
606
607        // Handle SettingsMenuCycleNext - cycle to next value
608        {
609            let state = Arc::clone(&state);
610            bus.subscribe::<SettingsMenuCycleNext, _>(100, move |_event, ctx| {
611                let change =
612                    state.with_mut::<SettingsMenuState, _, _>(|menu| menu.cycle_next_selected());
613                if let Some(Some(change)) = change {
614                    tracing::debug!("Settings cycle next: {} changed", change.key);
615                    ctx.emit(OptionChanged::new(
616                        change.key,
617                        change.old_value,
618                        change.new_value,
619                        ChangeSource::SettingsMenu,
620                    ));
621                }
622                ctx.request_render();
623                EventResult::Handled
624            });
625        }
626
627        // Handle SettingsMenuCyclePrev - cycle to previous value
628        {
629            let state = Arc::clone(&state);
630            bus.subscribe::<SettingsMenuCyclePrev, _>(100, move |_event, ctx| {
631                let change =
632                    state.with_mut::<SettingsMenuState, _, _>(|menu| menu.cycle_prev_selected());
633                if let Some(Some(change)) = change {
634                    tracing::debug!("Settings cycle prev: {} changed", change.key);
635                    ctx.emit(OptionChanged::new(
636                        change.key,
637                        change.old_value,
638                        change.new_value,
639                        ChangeSource::SettingsMenu,
640                    ));
641                }
642                ctx.request_render();
643                EventResult::Handled
644            });
645        }
646
647        // Handle SettingsMenuIncrement - increment number value
648        {
649            let state = Arc::clone(&state);
650            bus.subscribe::<SettingsMenuIncrement, _>(100, move |_event, ctx| {
651                let change =
652                    state.with_mut::<SettingsMenuState, _, _>(|menu| menu.increment_selected());
653                if let Some(Some(change)) = change {
654                    tracing::debug!("Settings increment: {} changed", change.key);
655                    ctx.emit(OptionChanged::new(
656                        change.key,
657                        change.old_value,
658                        change.new_value,
659                        ChangeSource::SettingsMenu,
660                    ));
661                }
662                ctx.request_render();
663                EventResult::Handled
664            });
665        }
666
667        // Handle SettingsMenuDecrement - decrement number value
668        {
669            let state = Arc::clone(&state);
670            bus.subscribe::<SettingsMenuDecrement, _>(100, move |_event, ctx| {
671                let change =
672                    state.with_mut::<SettingsMenuState, _, _>(|menu| menu.decrement_selected());
673                if let Some(Some(change)) = change {
674                    tracing::debug!("Settings decrement: {} changed", change.key);
675                    ctx.emit(OptionChanged::new(
676                        change.key,
677                        change.old_value,
678                        change.new_value,
679                        ChangeSource::SettingsMenu,
680                    ));
681                }
682                ctx.request_render();
683                EventResult::Handled
684            });
685        }
686
687        // Handle SettingsMenuExecuteAction - execute selected action or toggle
688        {
689            let state = Arc::clone(&state);
690            bus.subscribe::<SettingsMenuExecuteAction, _>(100, move |_event, ctx| {
691                // First, try to get the action type
692                let action = state
693                    .with::<SettingsMenuState, _, _>(|menu| menu.get_selected_action())
694                    .flatten();
695
696                if let Some(action_type) = action {
697                    // Handle profile actions
698                    match action_type {
699                        ActionType::SaveProfile => {
700                            tracing::info!("Settings: Save profile action triggered");
701                            // TODO: Emit profile save event
702                            state.with_mut::<SettingsMenuState, _, _>(|menu| {
703                                menu.set_message(
704                                    "Profile save not yet implemented".to_string(),
705                                    MessageKind::Info,
706                                );
707                            });
708                        }
709                        ActionType::LoadProfile => {
710                            tracing::info!("Settings: Load profile action triggered");
711                            // TODO: Emit profile load event
712                            state.with_mut::<SettingsMenuState, _, _>(|menu| {
713                                menu.set_message(
714                                    "Profile load not yet implemented".to_string(),
715                                    MessageKind::Info,
716                                );
717                            });
718                        }
719                        ActionType::ResetToDefault => {
720                            tracing::info!("Settings: Reset to default action triggered");
721                            // TODO: Emit reset event
722                            state.with_mut::<SettingsMenuState, _, _>(|menu| {
723                                menu.set_message(
724                                    "Reset to default not yet implemented".to_string(),
725                                    MessageKind::Info,
726                                );
727                            });
728                        }
729                    }
730                    ctx.request_render();
731                } else {
732                    // Not an action - try toggle for booleans
733                    let change =
734                        state.with_mut::<SettingsMenuState, _, _>(|menu| menu.toggle_selected());
735                    if let Some(Some(change)) = change {
736                        ctx.emit(OptionChanged::new(
737                            change.key,
738                            change.old_value,
739                            change.new_value,
740                            ChangeSource::SettingsMenu,
741                        ));
742                        ctx.request_render();
743                    }
744                }
745                EventResult::Handled
746            });
747        }
748    }
749}