Skip to main content

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