fresh/view/settings/
input.rs

1//! Input handling for the Settings dialog.
2//!
3//! Implements the InputHandler trait for SettingsState, routing input
4//! through the focus hierarchy: Dialog -> Panel -> Control.
5
6use super::items::SettingControl;
7use super::state::{FocusPanel, SettingsState};
8use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11/// Button action in entry dialog
12enum ButtonAction {
13    Save,
14    Delete,
15    Cancel,
16}
17
18/// Control activation action in entry dialog
19enum ControlAction {
20    ToggleBool,
21    ToggleDropdown,
22    StartEditing,
23    OpenNestedDialog,
24}
25
26impl InputHandler for SettingsState {
27    fn handle_key_event(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
28        // Entry dialog takes priority when open
29        if self.has_entry_dialog() {
30            return self.handle_entry_dialog_input(event, ctx);
31        }
32
33        // Confirmation dialog takes priority
34        if self.showing_confirm_dialog {
35            return self.handle_confirm_dialog_input(event, ctx);
36        }
37
38        // Help overlay takes priority
39        if self.showing_help {
40            return self.handle_help_input(event, ctx);
41        }
42
43        // Search mode takes priority
44        if self.search_active {
45            return self.handle_search_input(event, ctx);
46        }
47
48        // Global shortcut: Ctrl+S to save
49        if event.modifiers.contains(KeyModifiers::CONTROL)
50            && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
51        {
52            ctx.defer(DeferredAction::CloseSettings { save: true });
53            return InputResult::Consumed;
54        }
55
56        // Route to focused panel
57        match self.focus_panel {
58            FocusPanel::Categories => self.handle_categories_input(event, ctx),
59            FocusPanel::Settings => self.handle_settings_input(event, ctx),
60            FocusPanel::Footer => self.handle_footer_input(event, ctx),
61        }
62    }
63
64    fn is_modal(&self) -> bool {
65        true // Settings dialog consumes all unhandled input
66    }
67}
68
69impl SettingsState {
70    /// Handle input when entry dialog is open
71    ///
72    /// Uses the same input flow as the main settings UI:
73    /// 1. If in text editing mode -> handle text input
74    /// 2. If dropdown is open -> handle dropdown navigation
75    /// 3. Otherwise -> handle navigation and control activation
76    fn handle_entry_dialog_input(
77        &mut self,
78        event: &KeyEvent,
79        ctx: &mut InputContext,
80    ) -> InputResult {
81        // Check if we're in a special editing mode
82        let (editing_text, dropdown_open) = if let Some(dialog) = self.entry_dialog() {
83            let dropdown_open = dialog
84                .current_item()
85                .map(|item| matches!(&item.control, SettingControl::Dropdown(s) if s.open))
86                .unwrap_or(false);
87            (dialog.editing_text, dropdown_open)
88        } else {
89            return InputResult::Consumed;
90        };
91
92        // Route to appropriate handler based on mode
93        if editing_text {
94            self.handle_entry_dialog_text_editing(event, ctx)
95        } else if dropdown_open {
96            self.handle_entry_dialog_dropdown(event)
97        } else {
98            self.handle_entry_dialog_navigation(event)
99        }
100    }
101
102    /// Handle text editing input in entry dialog (same pattern as handle_text_editing_input)
103    fn handle_entry_dialog_text_editing(
104        &mut self,
105        event: &KeyEvent,
106        ctx: &mut InputContext,
107    ) -> InputResult {
108        // Check if we're editing JSON
109        let is_editing_json = self
110            .entry_dialog()
111            .map(|d| d.is_editing_json())
112            .unwrap_or(false);
113
114        // Check validation first before borrowing dialog mutably
115        let can_exit = self.entry_dialog_can_exit_text_editing();
116
117        let Some(dialog) = self.entry_dialog_mut() else {
118            return InputResult::Consumed;
119        };
120
121        match event.code {
122            KeyCode::Esc => {
123                // Escape accepts changes (same as Tab) - exit editing mode
124                if !can_exit {
125                    // If validation fails, just stop editing anyway (accept whatever is there)
126                }
127                dialog.stop_editing();
128            }
129            KeyCode::Enter => {
130                if is_editing_json {
131                    // Insert newline in JSON editor
132                    dialog.insert_newline();
133                } else {
134                    // Add item for TextList, or stop editing
135                    if let Some(item) = dialog.current_item_mut() {
136                        if let SettingControl::TextList(state) = &mut item.control {
137                            state.add_item();
138                        }
139                    }
140                }
141            }
142            KeyCode::Char(c) => {
143                if event.modifiers.contains(KeyModifiers::CONTROL) {
144                    match c {
145                        'a' | 'A' => {
146                            // Select all
147                            dialog.select_all();
148                        }
149                        'v' | 'V' => {
150                            // Paste
151                            ctx.defer(DeferredAction::PasteToSettings);
152                        }
153                        _ => {}
154                    }
155                } else {
156                    dialog.insert_char(c);
157                }
158            }
159            KeyCode::Backspace => {
160                dialog.backspace();
161            }
162            KeyCode::Delete => {
163                if is_editing_json {
164                    // Delete character at cursor in JSON editor
165                    dialog.delete();
166                } else {
167                    // Delete item in TextList
168                    dialog.delete_list_item();
169                }
170            }
171            KeyCode::Home => {
172                dialog.cursor_home();
173            }
174            KeyCode::End => {
175                dialog.cursor_end();
176            }
177            KeyCode::Left => {
178                if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
179                    dialog.cursor_left_selecting();
180                } else {
181                    dialog.cursor_left();
182                }
183            }
184            KeyCode::Right => {
185                if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
186                    dialog.cursor_right_selecting();
187                } else {
188                    dialog.cursor_right();
189                }
190            }
191            KeyCode::Up => {
192                if is_editing_json {
193                    // Move cursor up in JSON editor
194                    if event.modifiers.contains(KeyModifiers::SHIFT) {
195                        dialog.cursor_up_selecting();
196                    } else {
197                        dialog.cursor_up();
198                    }
199                } else {
200                    // Move to previous item in TextList
201                    if let Some(item) = dialog.current_item_mut() {
202                        if let SettingControl::TextList(state) = &mut item.control {
203                            state.focus_prev();
204                        }
205                    }
206                }
207            }
208            KeyCode::Down => {
209                if is_editing_json {
210                    // Move cursor down in JSON editor
211                    if event.modifiers.contains(KeyModifiers::SHIFT) {
212                        dialog.cursor_down_selecting();
213                    } else {
214                        dialog.cursor_down();
215                    }
216                } else {
217                    // Move to next item in TextList
218                    if let Some(item) = dialog.current_item_mut() {
219                        if let SettingControl::TextList(state) = &mut item.control {
220                            state.focus_next();
221                        }
222                    }
223                }
224            }
225            KeyCode::Tab => {
226                if is_editing_json {
227                    // Tab exits JSON editor if JSON is valid, otherwise ignored
228                    let is_valid = dialog
229                        .current_item()
230                        .map(|item| {
231                            if let SettingControl::Json(state) = &item.control {
232                                state.is_valid()
233                            } else {
234                                true
235                            }
236                        })
237                        .unwrap_or(true);
238
239                    if is_valid {
240                        // Commit changes and stop editing
241                        if let Some(item) = dialog.current_item_mut() {
242                            if let SettingControl::Json(state) = &mut item.control {
243                                state.commit();
244                            }
245                        }
246                        dialog.stop_editing();
247                    }
248                    // If not valid, Tab is ignored (user must fix or press Esc)
249                }
250            }
251            _ => {}
252        }
253        InputResult::Consumed
254    }
255
256    /// Handle dropdown navigation in entry dialog (same pattern as handle_dropdown_input)
257    fn handle_entry_dialog_dropdown(&mut self, event: &KeyEvent) -> InputResult {
258        let Some(dialog) = self.entry_dialog_mut() else {
259            return InputResult::Consumed;
260        };
261
262        match event.code {
263            KeyCode::Up => {
264                dialog.dropdown_prev();
265            }
266            KeyCode::Down => {
267                dialog.dropdown_next();
268            }
269            KeyCode::Enter => {
270                dialog.dropdown_confirm();
271            }
272            KeyCode::Esc => {
273                dialog.dropdown_confirm(); // Close dropdown
274            }
275            _ => {}
276        }
277        InputResult::Consumed
278    }
279
280    /// Handle navigation and activation in entry dialog (same pattern as handle_settings_input)
281    fn handle_entry_dialog_navigation(&mut self, event: &KeyEvent) -> InputResult {
282        match event.code {
283            KeyCode::Esc => {
284                self.close_entry_dialog();
285            }
286            KeyCode::Up => {
287                if let Some(dialog) = self.entry_dialog_mut() {
288                    dialog.focus_prev();
289                }
290            }
291            KeyCode::Down => {
292                if let Some(dialog) = self.entry_dialog_mut() {
293                    dialog.focus_next();
294                }
295            }
296            KeyCode::Tab => {
297                if let Some(dialog) = self.entry_dialog_mut() {
298                    dialog.focus_next();
299                }
300            }
301            KeyCode::BackTab => {
302                if let Some(dialog) = self.entry_dialog_mut() {
303                    dialog.focus_prev();
304                }
305            }
306            KeyCode::Left => {
307                // Decrement number or navigate within control
308                if let Some(dialog) = self.entry_dialog_mut() {
309                    if !dialog.focus_on_buttons {
310                        dialog.decrement_number();
311                    } else if dialog.focused_button > 0 {
312                        dialog.focused_button -= 1;
313                    }
314                }
315            }
316            KeyCode::Right => {
317                // Increment number or navigate within control
318                if let Some(dialog) = self.entry_dialog_mut() {
319                    if !dialog.focus_on_buttons {
320                        dialog.increment_number();
321                    } else if dialog.focused_button + 1 < dialog.button_count() {
322                        dialog.focused_button += 1;
323                    }
324                }
325            }
326            KeyCode::Enter | KeyCode::Char(' ') => {
327                // Check button state first with immutable borrow
328                let button_action = self.entry_dialog().and_then(|dialog| {
329                    if dialog.focus_on_buttons {
330                        let cancel_idx = dialog.button_count() - 1;
331                        if dialog.focused_button == 0 {
332                            Some(ButtonAction::Save)
333                        } else if !dialog.is_new && dialog.focused_button == 1 {
334                            Some(ButtonAction::Delete)
335                        } else if dialog.focused_button == cancel_idx {
336                            Some(ButtonAction::Cancel)
337                        } else {
338                            None
339                        }
340                    } else {
341                        None
342                    }
343                });
344
345                if let Some(action) = button_action {
346                    match action {
347                        ButtonAction::Save => self.save_entry_dialog(),
348                        ButtonAction::Delete => self.delete_entry_dialog(),
349                        ButtonAction::Cancel => self.close_entry_dialog(),
350                    }
351                } else if event.modifiers.contains(KeyModifiers::CONTROL) {
352                    // Ctrl+Enter always saves
353                    self.save_entry_dialog();
354                } else {
355                    // Activate current control
356                    let control_action = self
357                        .entry_dialog()
358                        .and_then(|dialog| {
359                            dialog.current_item().map(|item| match &item.control {
360                                SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
361                                SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
362                                SettingControl::Text(_)
363                                | SettingControl::TextList(_)
364                                | SettingControl::Number(_)
365                                | SettingControl::Json(_) => Some(ControlAction::StartEditing),
366                                SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
367                                    Some(ControlAction::OpenNestedDialog)
368                                }
369                                _ => None,
370                            })
371                        })
372                        .flatten();
373
374                    if let Some(action) = control_action {
375                        match action {
376                            ControlAction::ToggleBool => {
377                                if let Some(dialog) = self.entry_dialog_mut() {
378                                    dialog.toggle_bool();
379                                }
380                            }
381                            ControlAction::ToggleDropdown => {
382                                if let Some(dialog) = self.entry_dialog_mut() {
383                                    dialog.toggle_dropdown();
384                                }
385                            }
386                            ControlAction::StartEditing => {
387                                if let Some(dialog) = self.entry_dialog_mut() {
388                                    dialog.start_editing();
389                                }
390                            }
391                            ControlAction::OpenNestedDialog => {
392                                // Handle nested Map or ObjectArray - open a nested dialog
393                                self.open_nested_entry_dialog();
394                            }
395                        }
396                    }
397                }
398            }
399            _ => {}
400        }
401        InputResult::Consumed
402    }
403
404    /// Handle input when confirmation dialog is showing
405    fn handle_confirm_dialog_input(
406        &mut self,
407        event: &KeyEvent,
408        ctx: &mut InputContext,
409    ) -> InputResult {
410        match event.code {
411            KeyCode::Left => {
412                if self.confirm_dialog_selection > 0 {
413                    self.confirm_dialog_selection -= 1;
414                }
415                InputResult::Consumed
416            }
417            KeyCode::Right => {
418                if self.confirm_dialog_selection < 2 {
419                    self.confirm_dialog_selection += 1;
420                }
421                InputResult::Consumed
422            }
423            KeyCode::Enter => {
424                match self.confirm_dialog_selection {
425                    0 => ctx.defer(DeferredAction::CloseSettings { save: true }), // Save
426                    1 => ctx.defer(DeferredAction::CloseSettings { save: false }), // Discard
427                    2 => self.showing_confirm_dialog = false, // Cancel - back to settings
428                    _ => {}
429                }
430                InputResult::Consumed
431            }
432            KeyCode::Esc => {
433                self.showing_confirm_dialog = false;
434                InputResult::Consumed
435            }
436            KeyCode::Char('s') | KeyCode::Char('S') => {
437                ctx.defer(DeferredAction::CloseSettings { save: true });
438                InputResult::Consumed
439            }
440            KeyCode::Char('d') | KeyCode::Char('D') => {
441                ctx.defer(DeferredAction::CloseSettings { save: false });
442                InputResult::Consumed
443            }
444            _ => InputResult::Consumed, // Modal: consume all
445        }
446    }
447
448    /// Handle input when help overlay is showing
449    fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
450        // Any key dismisses help
451        self.showing_help = false;
452        InputResult::Consumed
453    }
454
455    /// Handle input when search is active
456    fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
457        match event.code {
458            KeyCode::Esc => {
459                self.cancel_search();
460                InputResult::Consumed
461            }
462            KeyCode::Enter => {
463                self.jump_to_search_result();
464                InputResult::Consumed
465            }
466            KeyCode::Up => {
467                self.search_prev();
468                InputResult::Consumed
469            }
470            KeyCode::Down => {
471                self.search_next();
472                InputResult::Consumed
473            }
474            KeyCode::Char(c) => {
475                self.search_push_char(c);
476                InputResult::Consumed
477            }
478            KeyCode::Backspace => {
479                self.search_pop_char();
480                InputResult::Consumed
481            }
482            _ => InputResult::Consumed, // Modal: consume all
483        }
484    }
485
486    /// Handle input when Categories panel is focused
487    fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
488        match event.code {
489            KeyCode::Up => {
490                self.select_prev();
491                InputResult::Consumed
492            }
493            KeyCode::Down => {
494                self.select_next();
495                InputResult::Consumed
496            }
497            KeyCode::Tab => {
498                self.toggle_focus();
499                InputResult::Consumed
500            }
501            KeyCode::Char('/') => {
502                self.start_search();
503                InputResult::Consumed
504            }
505            KeyCode::Char('?') => {
506                self.toggle_help();
507                InputResult::Consumed
508            }
509            KeyCode::Esc => {
510                self.request_close(ctx);
511                InputResult::Consumed
512            }
513            KeyCode::Enter | KeyCode::Right => {
514                // Enter/Right on categories: move focus to settings panel
515                self.focus_panel = FocusPanel::Settings;
516                InputResult::Consumed
517            }
518            _ => InputResult::Ignored, // Let modal catch it
519        }
520    }
521
522    /// Handle input when Settings panel is focused
523    fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
524        // If editing text, handle text input
525        if self.editing_text {
526            return self.handle_text_editing_input(event, ctx);
527        }
528
529        // If editing number input, handle number input
530        if self.is_number_editing() {
531            return self.handle_number_editing_input(event, ctx);
532        }
533
534        // If dropdown is open, handle dropdown navigation
535        if self.is_dropdown_open() {
536            return self.handle_dropdown_input(event, ctx);
537        }
538
539        match event.code {
540            KeyCode::Up => {
541                self.select_prev();
542                InputResult::Consumed
543            }
544            KeyCode::Down => {
545                self.select_next();
546                InputResult::Consumed
547            }
548            KeyCode::Tab => {
549                self.toggle_focus();
550                InputResult::Consumed
551            }
552            KeyCode::Left => {
553                self.handle_control_decrement();
554                InputResult::Consumed
555            }
556            KeyCode::Right => {
557                self.handle_control_increment();
558                InputResult::Consumed
559            }
560            KeyCode::Enter | KeyCode::Char(' ') => {
561                self.handle_control_activate(ctx);
562                InputResult::Consumed
563            }
564            KeyCode::Char('/') => {
565                self.start_search();
566                InputResult::Consumed
567            }
568            KeyCode::Char('?') => {
569                self.toggle_help();
570                InputResult::Consumed
571            }
572            KeyCode::Esc => {
573                self.request_close(ctx);
574                InputResult::Consumed
575            }
576            _ => InputResult::Ignored, // Let modal catch it
577        }
578    }
579
580    /// Handle input when Footer is focused
581    /// Footer buttons: [Layer] [Reset] [Save] [Cancel] + [Edit] on left for advanced users
582    /// Tab cycles between buttons; after last button, moves to Categories panel
583    fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
584        const FOOTER_BUTTON_COUNT: usize = 5;
585
586        match event.code {
587            KeyCode::Left | KeyCode::BackTab => {
588                // Move to previous button, or wrap to Categories panel
589                if self.footer_button_index > 0 {
590                    self.footer_button_index -= 1;
591                } else {
592                    self.focus_panel = FocusPanel::Settings;
593                }
594                InputResult::Consumed
595            }
596            KeyCode::Right => {
597                // Move to next button
598                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
599                    self.footer_button_index += 1;
600                }
601                InputResult::Consumed
602            }
603            KeyCode::Tab => {
604                // Move to next button, or wrap to Categories panel
605                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
606                    self.footer_button_index += 1;
607                } else {
608                    self.focus_panel = FocusPanel::Categories;
609                }
610                InputResult::Consumed
611            }
612            KeyCode::Enter => {
613                match self.footer_button_index {
614                    0 => self.cycle_target_layer(), // Layer button
615                    1 => self.reset_current_to_default(),
616                    2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
617                    3 => self.request_close(ctx),
618                    4 => ctx.defer(DeferredAction::OpenConfigFile {
619                        layer: self.target_layer,
620                    }), // Edit config file
621                    _ => {}
622                }
623                InputResult::Consumed
624            }
625            KeyCode::Esc => {
626                self.request_close(ctx);
627                InputResult::Consumed
628            }
629            KeyCode::Char('/') => {
630                self.start_search();
631                InputResult::Consumed
632            }
633            KeyCode::Char('?') => {
634                self.toggle_help();
635                InputResult::Consumed
636            }
637            _ => InputResult::Ignored, // Let modal catch it
638        }
639    }
640
641    /// Handle input when editing text in a control
642    fn handle_text_editing_input(
643        &mut self,
644        event: &KeyEvent,
645        _ctx: &mut InputContext,
646    ) -> InputResult {
647        match event.code {
648            KeyCode::Esc => {
649                // Check if current text field requires JSON validation
650                if !self.can_exit_text_editing() {
651                    return InputResult::Consumed;
652                }
653                self.stop_editing();
654                InputResult::Consumed
655            }
656            KeyCode::Enter => {
657                self.text_add_item();
658                InputResult::Consumed
659            }
660            KeyCode::Char(c) => {
661                self.text_insert(c);
662                InputResult::Consumed
663            }
664            KeyCode::Backspace => {
665                self.text_backspace();
666                InputResult::Consumed
667            }
668            KeyCode::Delete => {
669                self.text_remove_focused();
670                InputResult::Consumed
671            }
672            KeyCode::Left => {
673                self.text_move_left();
674                InputResult::Consumed
675            }
676            KeyCode::Right => {
677                self.text_move_right();
678                InputResult::Consumed
679            }
680            KeyCode::Up => {
681                self.text_focus_prev();
682                InputResult::Consumed
683            }
684            KeyCode::Down => {
685                self.text_focus_next();
686                InputResult::Consumed
687            }
688            _ => InputResult::Consumed, // Consume all during text edit
689        }
690    }
691
692    /// Handle input when editing a number input control
693    fn handle_number_editing_input(
694        &mut self,
695        event: &KeyEvent,
696        _ctx: &mut InputContext,
697    ) -> InputResult {
698        let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
699        let shift = event.modifiers.contains(KeyModifiers::SHIFT);
700
701        match event.code {
702            KeyCode::Esc => {
703                self.number_cancel();
704            }
705            KeyCode::Enter => {
706                self.number_confirm();
707            }
708            KeyCode::Char('a') if ctrl => {
709                self.number_select_all();
710            }
711            KeyCode::Char(c) => {
712                self.number_insert(c);
713            }
714            KeyCode::Backspace if ctrl => {
715                self.number_delete_word_backward();
716            }
717            KeyCode::Backspace => {
718                self.number_backspace();
719            }
720            KeyCode::Delete if ctrl => {
721                self.number_delete_word_forward();
722            }
723            KeyCode::Delete => {
724                self.number_delete();
725            }
726            KeyCode::Left if ctrl && shift => {
727                self.number_move_word_left_selecting();
728            }
729            KeyCode::Left if ctrl => {
730                self.number_move_word_left();
731            }
732            KeyCode::Left if shift => {
733                self.number_move_left_selecting();
734            }
735            KeyCode::Left => {
736                self.number_move_left();
737            }
738            KeyCode::Right if ctrl && shift => {
739                self.number_move_word_right_selecting();
740            }
741            KeyCode::Right if ctrl => {
742                self.number_move_word_right();
743            }
744            KeyCode::Right if shift => {
745                self.number_move_right_selecting();
746            }
747            KeyCode::Right => {
748                self.number_move_right();
749            }
750            KeyCode::Home if shift => {
751                self.number_move_home_selecting();
752            }
753            KeyCode::Home => {
754                self.number_move_home();
755            }
756            KeyCode::End if shift => {
757                self.number_move_end_selecting();
758            }
759            KeyCode::End => {
760                self.number_move_end();
761            }
762            _ => {}
763        }
764        InputResult::Consumed // Consume all during number edit
765    }
766
767    /// Handle input when dropdown is open
768    fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
769        match event.code {
770            KeyCode::Up => {
771                self.dropdown_prev();
772                InputResult::Consumed
773            }
774            KeyCode::Down => {
775                self.dropdown_next();
776                InputResult::Consumed
777            }
778            KeyCode::Home => {
779                self.dropdown_home();
780                InputResult::Consumed
781            }
782            KeyCode::End => {
783                self.dropdown_end();
784                InputResult::Consumed
785            }
786            KeyCode::Enter => {
787                self.dropdown_confirm();
788                InputResult::Consumed
789            }
790            KeyCode::Esc => {
791                self.dropdown_cancel();
792                InputResult::Consumed
793            }
794            _ => InputResult::Consumed, // Consume all while dropdown is open
795        }
796    }
797
798    /// Request to close settings (shows confirm dialog if there are changes)
799    fn request_close(&mut self, ctx: &mut InputContext) {
800        if self.has_changes() {
801            self.showing_confirm_dialog = true;
802            self.confirm_dialog_selection = 0;
803        } else {
804            ctx.defer(DeferredAction::CloseSettings { save: false });
805        }
806    }
807
808    /// Handle control activation (Enter/Space on a setting)
809    fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
810        if let Some(item) = self.current_item_mut() {
811            match &mut item.control {
812                SettingControl::Toggle(ref mut state) => {
813                    state.checked = !state.checked;
814                    self.on_value_changed();
815                }
816                SettingControl::Dropdown(_) => {
817                    self.dropdown_toggle();
818                }
819                SettingControl::Number(_) => {
820                    self.start_number_editing();
821                }
822                SettingControl::Text(_) => {
823                    self.start_editing();
824                }
825                SettingControl::TextList(_) => {
826                    self.start_editing();
827                }
828                SettingControl::Map(ref mut state) => {
829                    if state.focused_entry.is_none() {
830                        // On add-new row: open dialog with empty key
831                        if state.value_schema.is_some() {
832                            self.open_add_entry_dialog();
833                        }
834                    } else if state.value_schema.is_some() {
835                        // Has schema: open entry dialog
836                        self.open_entry_dialog();
837                    } else {
838                        // Toggle expanded
839                        if let Some(idx) = state.focused_entry {
840                            if state.expanded.contains(&idx) {
841                                state.expanded.retain(|&i| i != idx);
842                            } else {
843                                state.expanded.push(idx);
844                            }
845                        }
846                    }
847                    self.on_value_changed();
848                }
849                SettingControl::Json(_) => {
850                    self.start_editing();
851                }
852                SettingControl::ObjectArray(ref state) => {
853                    if state.focused_index.is_none() {
854                        // On add-new row: open dialog with empty item
855                        if state.item_schema.is_some() {
856                            self.open_add_array_item_dialog();
857                        }
858                    } else if state.item_schema.is_some() {
859                        // Has schema: open edit dialog
860                        self.open_edit_array_item_dialog();
861                    }
862                }
863                SettingControl::Complex { .. } => {
864                    // Not editable via simple controls
865                }
866            }
867        }
868    }
869
870    /// Handle control increment (Right arrow on numbers/dropdowns)
871    fn handle_control_increment(&mut self) {
872        if let Some(item) = self.current_item_mut() {
873            match &mut item.control {
874                SettingControl::Number(ref mut state) => {
875                    state.value += 1;
876                    if let Some(max) = state.max {
877                        state.value = state.value.min(max);
878                    }
879                    self.on_value_changed();
880                }
881                SettingControl::Dropdown(ref mut state) => {
882                    state.select_next();
883                    self.on_value_changed();
884                }
885                SettingControl::Map(ref mut state) => {
886                    // Navigate within map entries
887                    let entry_count = state.entries.len();
888                    if let Some(idx) = state.focused_entry {
889                        if idx + 1 < entry_count {
890                            state.focused_entry = Some(idx + 1);
891                        }
892                    }
893                }
894                SettingControl::ObjectArray(ref mut state) => {
895                    state.focus_next();
896                }
897                _ => {}
898            }
899        }
900    }
901
902    /// Handle control decrement (Left arrow on numbers/dropdowns)
903    fn handle_control_decrement(&mut self) {
904        if let Some(item) = self.current_item_mut() {
905            match &mut item.control {
906                SettingControl::Number(ref mut state) => {
907                    if state.value > 0 {
908                        state.value -= 1;
909                    }
910                    if let Some(min) = state.min {
911                        state.value = state.value.max(min);
912                    }
913                    self.on_value_changed();
914                }
915                SettingControl::Dropdown(ref mut state) => {
916                    state.select_prev();
917                    self.on_value_changed();
918                }
919                SettingControl::Map(ref mut state) => {
920                    if let Some(idx) = state.focused_entry {
921                        if idx > 0 {
922                            state.focused_entry = Some(idx - 1);
923                        }
924                    }
925                }
926                SettingControl::ObjectArray(ref mut state) => {
927                    state.focus_prev();
928                }
929                _ => {}
930            }
931        }
932    }
933}
934
935#[cfg(test)]
936mod tests {
937    use super::*;
938    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
939
940    fn key(code: KeyCode) -> KeyEvent {
941        KeyEvent::new(code, KeyModifiers::NONE)
942    }
943
944    #[test]
945    fn test_settings_is_modal() {
946        // SettingsState should be modal - consume all unhandled input
947        let schema = include_str!("../../../plugins/config-schema.json");
948        let config = crate::config::Config::default();
949        let state = SettingsState::new(schema, &config).unwrap();
950        assert!(state.is_modal());
951    }
952
953    #[test]
954    fn test_categories_panel_does_not_leak_to_settings() {
955        let schema = include_str!("../../../plugins/config-schema.json");
956        let config = crate::config::Config::default();
957        let mut state = SettingsState::new(schema, &config).unwrap();
958        state.visible = true;
959        state.focus_panel = FocusPanel::Categories;
960
961        let mut ctx = InputContext::new();
962
963        // Enter on categories should NOT affect settings items
964        // It should just move focus to settings panel
965        let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
966        assert_eq!(result, InputResult::Consumed);
967        assert_eq!(state.focus_panel, FocusPanel::Settings);
968
969        // Go back to categories
970        state.focus_panel = FocusPanel::Categories;
971
972        // Left/Right on categories should be consumed but not affect settings
973        let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
974        assert_eq!(result, InputResult::Consumed);
975        // Should have moved to settings panel
976        assert_eq!(state.focus_panel, FocusPanel::Settings);
977    }
978
979    #[test]
980    fn test_tab_cycles_focus_panels() {
981        let schema = include_str!("../../../plugins/config-schema.json");
982        let config = crate::config::Config::default();
983        let mut state = SettingsState::new(schema, &config).unwrap();
984        state.visible = true;
985
986        let mut ctx = InputContext::new();
987
988        // Start at Categories
989        assert_eq!(state.focus_panel, FocusPanel::Categories);
990
991        // Tab -> Settings
992        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
993        assert_eq!(state.focus_panel, FocusPanel::Settings);
994
995        // Tab -> Footer (defaults to Save button, index 2)
996        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
997        assert_eq!(state.focus_panel, FocusPanel::Footer);
998        assert_eq!(state.footer_button_index, 2);
999
1000        // Tab through footer buttons: 2 -> 3 -> 4 -> wrap to Categories
1001        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1002        assert_eq!(state.footer_button_index, 3);
1003        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1004        assert_eq!(state.footer_button_index, 4); // Edit button
1005        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1006        assert_eq!(state.focus_panel, FocusPanel::Categories);
1007
1008        // SECOND LOOP: Tab again should still land on Save button when entering Footer
1009        // Tab -> Settings
1010        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1011        assert_eq!(state.focus_panel, FocusPanel::Settings);
1012
1013        // Tab -> Footer (should reset to Save button, not stay on Edit)
1014        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1015        assert_eq!(state.focus_panel, FocusPanel::Footer);
1016        assert_eq!(
1017            state.footer_button_index, 2,
1018            "Footer should reset to Save button (index 2) on second loop"
1019        );
1020    }
1021
1022    #[test]
1023    fn test_escape_shows_confirm_dialog_with_changes() {
1024        let schema = include_str!("../../../plugins/config-schema.json");
1025        let config = crate::config::Config::default();
1026        let mut state = SettingsState::new(schema, &config).unwrap();
1027        state.visible = true;
1028
1029        // Simulate a change
1030        state
1031            .pending_changes
1032            .insert("/test".to_string(), serde_json::json!(true));
1033
1034        let mut ctx = InputContext::new();
1035
1036        // Escape should show confirm dialog, not close directly
1037        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1038        assert!(state.showing_confirm_dialog);
1039        assert!(ctx.deferred_actions.is_empty()); // No close action yet
1040    }
1041
1042    #[test]
1043    fn test_escape_closes_directly_without_changes() {
1044        let schema = include_str!("../../../plugins/config-schema.json");
1045        let config = crate::config::Config::default();
1046        let mut state = SettingsState::new(schema, &config).unwrap();
1047        state.visible = true;
1048
1049        let mut ctx = InputContext::new();
1050
1051        // Escape without changes should defer close action
1052        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1053        assert!(!state.showing_confirm_dialog);
1054        assert_eq!(ctx.deferred_actions.len(), 1);
1055        assert!(matches!(
1056            ctx.deferred_actions[0],
1057            DeferredAction::CloseSettings { save: false }
1058        ));
1059    }
1060
1061    #[test]
1062    fn test_confirm_dialog_navigation() {
1063        let schema = include_str!("../../../plugins/config-schema.json");
1064        let config = crate::config::Config::default();
1065        let mut state = SettingsState::new(schema, &config).unwrap();
1066        state.visible = true;
1067        state.showing_confirm_dialog = true;
1068        state.confirm_dialog_selection = 0; // Save
1069
1070        let mut ctx = InputContext::new();
1071
1072        // Right -> Discard
1073        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1074        assert_eq!(state.confirm_dialog_selection, 1);
1075
1076        // Right -> Cancel
1077        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1078        assert_eq!(state.confirm_dialog_selection, 2);
1079
1080        // Right again -> stays at Cancel (no wrap)
1081        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1082        assert_eq!(state.confirm_dialog_selection, 2);
1083
1084        // Left -> Discard
1085        state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1086        assert_eq!(state.confirm_dialog_selection, 1);
1087    }
1088
1089    #[test]
1090    fn test_search_mode_captures_typing() {
1091        let schema = include_str!("../../../plugins/config-schema.json");
1092        let config = crate::config::Config::default();
1093        let mut state = SettingsState::new(schema, &config).unwrap();
1094        state.visible = true;
1095
1096        let mut ctx = InputContext::new();
1097
1098        // Start search
1099        state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1100        assert!(state.search_active);
1101
1102        // Type search query
1103        state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1104        state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1105        state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1106        assert_eq!(state.search_query, "tab");
1107
1108        // Escape cancels search
1109        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1110        assert!(!state.search_active);
1111        assert!(state.search_query.is_empty());
1112    }
1113
1114    #[test]
1115    fn test_footer_button_activation() {
1116        let schema = include_str!("../../../plugins/config-schema.json");
1117        let config = crate::config::Config::default();
1118        let mut state = SettingsState::new(schema, &config).unwrap();
1119        state.visible = true;
1120        state.focus_panel = FocusPanel::Footer;
1121        state.footer_button_index = 2; // Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
1122
1123        let mut ctx = InputContext::new();
1124
1125        // Enter on Save button should defer save action
1126        state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1127        assert_eq!(ctx.deferred_actions.len(), 1);
1128        assert!(matches!(
1129            ctx.deferred_actions[0],
1130            DeferredAction::CloseSettings { save: true }
1131        ));
1132    }
1133}