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