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::Delete => {
729                // Delete key: set nullable setting to null (inherit)
730                self.set_current_to_null();
731                InputResult::Consumed
732            }
733            KeyCode::Esc => {
734                self.request_close(ctx);
735                InputResult::Consumed
736            }
737            _ => InputResult::Ignored, // Let modal catch it
738        }
739    }
740
741    /// Handle input when Footer is focused
742    /// Footer buttons: [Layer] [Reset] [Save] [Cancel] + [Edit] on left for advanced users
743    /// Tab cycles between buttons; after last button, moves to Categories panel
744    fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
745        const FOOTER_BUTTON_COUNT: usize = 5;
746
747        match event.code {
748            KeyCode::Left | KeyCode::BackTab => {
749                // Move to previous button, or wrap to Categories panel
750                if self.footer_button_index > 0 {
751                    self.footer_button_index -= 1;
752                } else {
753                    self.focus.set(FocusPanel::Settings);
754                }
755                InputResult::Consumed
756            }
757            KeyCode::Right => {
758                // Move to next button
759                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
760                    self.footer_button_index += 1;
761                }
762                InputResult::Consumed
763            }
764            KeyCode::Tab => {
765                // Move to next button, or wrap to Categories panel
766                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
767                    self.footer_button_index += 1;
768                } else {
769                    self.focus.set(FocusPanel::Categories);
770                }
771                InputResult::Consumed
772            }
773            KeyCode::Enter => {
774                match self.footer_button_index {
775                    0 => self.cycle_target_layer(), // Layer button
776                    1 => {
777                        // Reset/Inherit button — for nullable items, set to null (inherit);
778                        // otherwise show reset-all dialog
779                        let is_nullable_set = self
780                            .current_item()
781                            .map(|item| item.nullable && !item.is_null)
782                            .unwrap_or(false);
783                        if is_nullable_set {
784                            self.set_current_to_null();
785                        } else {
786                            self.request_reset();
787                        }
788                    }
789                    2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
790                    3 => self.request_close(ctx),
791                    4 => ctx.defer(DeferredAction::OpenConfigFile {
792                        layer: self.target_layer,
793                    }), // Edit config file
794                    _ => {}
795                }
796                InputResult::Consumed
797            }
798            KeyCode::Esc => {
799                self.request_close(ctx);
800                InputResult::Consumed
801            }
802            KeyCode::Char('/') => {
803                self.start_search();
804                InputResult::Consumed
805            }
806            KeyCode::Char('?') => {
807                self.toggle_help();
808                InputResult::Consumed
809            }
810            _ => InputResult::Ignored, // Let modal catch it
811        }
812    }
813
814    /// Handle input when editing text in a control
815    fn handle_text_editing_input(
816        &mut self,
817        event: &KeyEvent,
818        ctx: &mut InputContext,
819    ) -> InputResult {
820        let is_json = self.is_editing_json();
821
822        if is_json {
823            return self.handle_json_editing_input(event, ctx);
824        }
825
826        match event.code {
827            KeyCode::Esc => {
828                // Check if current text field requires JSON validation
829                if !self.can_exit_text_editing() {
830                    return InputResult::Consumed;
831                }
832                self.stop_editing();
833                InputResult::Consumed
834            }
835            KeyCode::Enter => {
836                self.text_add_item();
837                InputResult::Consumed
838            }
839            KeyCode::Char(c) => {
840                self.text_insert(c);
841                InputResult::Consumed
842            }
843            KeyCode::Backspace => {
844                self.text_backspace();
845                InputResult::Consumed
846            }
847            KeyCode::Delete => {
848                self.text_remove_focused();
849                InputResult::Consumed
850            }
851            KeyCode::Left => {
852                self.text_move_left();
853                InputResult::Consumed
854            }
855            KeyCode::Right => {
856                self.text_move_right();
857                InputResult::Consumed
858            }
859            KeyCode::Up => {
860                self.text_focus_prev();
861                InputResult::Consumed
862            }
863            KeyCode::Down => {
864                self.text_focus_next();
865                InputResult::Consumed
866            }
867            KeyCode::Tab => {
868                // Tab exits text editing mode and advances focus to the next panel
869                self.stop_editing();
870                self.toggle_focus();
871                InputResult::Consumed
872            }
873            _ => InputResult::Consumed, // Consume all during text edit
874        }
875    }
876
877    /// Handle input when editing a JSON control (multiline editor)
878    fn handle_json_editing_input(
879        &mut self,
880        event: &KeyEvent,
881        ctx: &mut InputContext,
882    ) -> InputResult {
883        match event.code {
884            KeyCode::Esc | KeyCode::Tab => {
885                // Accept if valid JSON, revert if invalid, then stop editing
886                self.json_exit_editing();
887            }
888            KeyCode::Enter => {
889                self.json_insert_newline();
890            }
891            KeyCode::Char(c) => {
892                if event.modifiers.contains(KeyModifiers::CONTROL) {
893                    match c {
894                        'a' | 'A' => self.json_select_all(),
895                        'c' | 'C' => {
896                            if let Some(text) = self.json_selected_text() {
897                                ctx.defer(DeferredAction::CopyToClipboard(text));
898                            }
899                        }
900                        'v' | 'V' => {
901                            ctx.defer(DeferredAction::PasteToSettings);
902                        }
903                        _ => {}
904                    }
905                } else {
906                    self.text_insert(c);
907                }
908            }
909            KeyCode::Backspace => {
910                self.text_backspace();
911            }
912            KeyCode::Delete => {
913                self.json_delete();
914            }
915            KeyCode::Left => {
916                if event.modifiers.contains(KeyModifiers::SHIFT) {
917                    self.json_cursor_left_selecting();
918                } else {
919                    self.text_move_left();
920                }
921            }
922            KeyCode::Right => {
923                if event.modifiers.contains(KeyModifiers::SHIFT) {
924                    self.json_cursor_right_selecting();
925                } else {
926                    self.text_move_right();
927                }
928            }
929            KeyCode::Up => {
930                if event.modifiers.contains(KeyModifiers::SHIFT) {
931                    self.json_cursor_up_selecting();
932                } else {
933                    self.json_cursor_up();
934                }
935            }
936            KeyCode::Down => {
937                if event.modifiers.contains(KeyModifiers::SHIFT) {
938                    self.json_cursor_down_selecting();
939                } else {
940                    self.json_cursor_down();
941                }
942            }
943            _ => {}
944        }
945        InputResult::Consumed
946    }
947
948    /// Handle input when editing a number input control
949    fn handle_number_editing_input(
950        &mut self,
951        event: &KeyEvent,
952        _ctx: &mut InputContext,
953    ) -> InputResult {
954        let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
955        let shift = event.modifiers.contains(KeyModifiers::SHIFT);
956
957        match event.code {
958            KeyCode::Esc => {
959                self.number_cancel();
960            }
961            KeyCode::Enter => {
962                self.number_confirm();
963            }
964            KeyCode::Char('a') if ctrl => {
965                self.number_select_all();
966            }
967            KeyCode::Char(c) => {
968                self.number_insert(c);
969            }
970            KeyCode::Backspace if ctrl => {
971                self.number_delete_word_backward();
972            }
973            KeyCode::Backspace => {
974                self.number_backspace();
975            }
976            KeyCode::Delete if ctrl => {
977                self.number_delete_word_forward();
978            }
979            KeyCode::Delete => {
980                self.number_delete();
981            }
982            KeyCode::Left if ctrl && shift => {
983                self.number_move_word_left_selecting();
984            }
985            KeyCode::Left if ctrl => {
986                self.number_move_word_left();
987            }
988            KeyCode::Left if shift => {
989                self.number_move_left_selecting();
990            }
991            KeyCode::Left => {
992                self.number_move_left();
993            }
994            KeyCode::Right if ctrl && shift => {
995                self.number_move_word_right_selecting();
996            }
997            KeyCode::Right if ctrl => {
998                self.number_move_word_right();
999            }
1000            KeyCode::Right if shift => {
1001                self.number_move_right_selecting();
1002            }
1003            KeyCode::Right => {
1004                self.number_move_right();
1005            }
1006            KeyCode::Home if shift => {
1007                self.number_move_home_selecting();
1008            }
1009            KeyCode::Home => {
1010                self.number_move_home();
1011            }
1012            KeyCode::End if shift => {
1013                self.number_move_end_selecting();
1014            }
1015            KeyCode::End => {
1016                self.number_move_end();
1017            }
1018            _ => {}
1019        }
1020        InputResult::Consumed // Consume all during number edit
1021    }
1022
1023    /// Handle input when dropdown is open
1024    fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1025        match event.code {
1026            KeyCode::Up => {
1027                self.dropdown_prev();
1028                InputResult::Consumed
1029            }
1030            KeyCode::Down => {
1031                self.dropdown_next();
1032                InputResult::Consumed
1033            }
1034            KeyCode::Home => {
1035                self.dropdown_home();
1036                InputResult::Consumed
1037            }
1038            KeyCode::End => {
1039                self.dropdown_end();
1040                InputResult::Consumed
1041            }
1042            KeyCode::Enter => {
1043                self.dropdown_confirm();
1044                InputResult::Consumed
1045            }
1046            KeyCode::Esc => {
1047                self.dropdown_cancel();
1048                InputResult::Consumed
1049            }
1050            _ => InputResult::Consumed, // Consume all while dropdown is open
1051        }
1052    }
1053
1054    /// Request to reset all changes (shows confirm dialog if there are changes)
1055    fn request_reset(&mut self) {
1056        if self.has_changes() {
1057            self.showing_reset_dialog = true;
1058            self.reset_dialog_selection = 0;
1059        }
1060    }
1061
1062    /// Request to close settings (shows confirm dialog if there are changes)
1063    fn request_close(&mut self, ctx: &mut InputContext) {
1064        if self.has_changes() {
1065            self.showing_confirm_dialog = true;
1066            self.confirm_dialog_selection = 0;
1067        } else {
1068            ctx.defer(DeferredAction::CloseSettings { save: false });
1069        }
1070    }
1071
1072    /// Handle control activation (Enter/Space on a setting)
1073    fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1074        if let Some(item) = self.current_item_mut() {
1075            match &mut item.control {
1076                SettingControl::Toggle(ref mut state) => {
1077                    state.checked = !state.checked;
1078                    self.on_value_changed();
1079                }
1080                SettingControl::Dropdown(_) => {
1081                    self.dropdown_toggle();
1082                }
1083                SettingControl::Number(_) => {
1084                    self.start_number_editing();
1085                }
1086                SettingControl::Text(_) => {
1087                    self.start_editing();
1088                }
1089                SettingControl::TextList(_) => {
1090                    self.start_editing();
1091                }
1092                SettingControl::Map(ref mut state) => {
1093                    if state.focused_entry.is_none() {
1094                        // On add-new row: open dialog with empty key
1095                        if state.value_schema.is_some() {
1096                            self.open_add_entry_dialog();
1097                        }
1098                    } else if state.value_schema.is_some() {
1099                        // Has schema: open entry dialog
1100                        self.open_entry_dialog();
1101                    } else {
1102                        // Toggle expanded
1103                        if let Some(idx) = state.focused_entry {
1104                            if state.expanded.contains(&idx) {
1105                                state.expanded.retain(|&i| i != idx);
1106                            } else {
1107                                state.expanded.push(idx);
1108                            }
1109                        }
1110                    }
1111                    self.on_value_changed();
1112                }
1113                SettingControl::Json(_) => {
1114                    self.start_editing();
1115                }
1116                SettingControl::ObjectArray(ref state) => {
1117                    if state.focused_index.is_none() {
1118                        // On add-new row: open dialog with empty item
1119                        if state.item_schema.is_some() {
1120                            self.open_add_array_item_dialog();
1121                        }
1122                    } else if state.item_schema.is_some() {
1123                        // Has schema: open edit dialog
1124                        self.open_edit_array_item_dialog();
1125                    }
1126                }
1127                SettingControl::Complex { .. } => {
1128                    // Not editable via simple controls
1129                }
1130            }
1131        }
1132    }
1133
1134    /// Handle control increment (Right arrow on numbers/dropdowns)
1135    fn handle_control_increment(&mut self) {
1136        if let Some(item) = self.current_item_mut() {
1137            if let SettingControl::Number(ref mut state) = &mut item.control {
1138                state.value += 1;
1139                if let Some(max) = state.max {
1140                    state.value = state.value.min(max);
1141                }
1142                self.on_value_changed();
1143            }
1144        }
1145    }
1146
1147    /// Handle control decrement (Left arrow on numbers)
1148    fn handle_control_decrement(&mut self) {
1149        if let Some(item) = self.current_item_mut() {
1150            if let SettingControl::Number(ref mut state) = &mut item.control {
1151                if state.value > 0 {
1152                    state.value -= 1;
1153                }
1154                if let Some(min) = state.min {
1155                    state.value = state.value.max(min);
1156                }
1157                self.on_value_changed();
1158            }
1159        }
1160    }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1167
1168    fn key(code: KeyCode) -> KeyEvent {
1169        KeyEvent::new(code, KeyModifiers::NONE)
1170    }
1171
1172    #[test]
1173    fn test_settings_is_modal() {
1174        // SettingsState should be modal - consume all unhandled input
1175        let schema = include_str!("../../../plugins/config-schema.json");
1176        let config = crate::config::Config::default();
1177        let state = SettingsState::new(schema, &config).unwrap();
1178        assert!(state.is_modal());
1179    }
1180
1181    #[test]
1182    fn test_categories_panel_does_not_leak_to_settings() {
1183        let schema = include_str!("../../../plugins/config-schema.json");
1184        let config = crate::config::Config::default();
1185        let mut state = SettingsState::new(schema, &config).unwrap();
1186        state.visible = true;
1187        state.focus.set(FocusPanel::Categories);
1188
1189        let mut ctx = InputContext::new();
1190
1191        // Enter on categories should NOT affect settings items
1192        // It should just move focus to settings panel
1193        let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1194        assert_eq!(result, InputResult::Consumed);
1195        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1196
1197        // Go back to categories
1198        state.focus.set(FocusPanel::Categories);
1199
1200        // Left/Right on categories should be consumed but not affect settings
1201        let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1202        assert_eq!(result, InputResult::Consumed);
1203        // Should have moved to settings panel
1204        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1205    }
1206
1207    #[test]
1208    fn test_tab_cycles_focus_panels() {
1209        let schema = include_str!("../../../plugins/config-schema.json");
1210        let config = crate::config::Config::default();
1211        let mut state = SettingsState::new(schema, &config).unwrap();
1212        state.visible = true;
1213
1214        let mut ctx = InputContext::new();
1215
1216        // Start at Categories
1217        assert_eq!(state.focus_panel(), FocusPanel::Categories);
1218
1219        // Tab -> Settings
1220        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1221        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1222
1223        // Tab -> Footer (defaults to Layer button, index 0)
1224        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1225        assert_eq!(state.focus_panel(), FocusPanel::Footer);
1226        assert_eq!(state.footer_button_index, 0);
1227
1228        // Tab through footer buttons: 0 -> 1 -> 2 -> 3 -> 4 -> wrap to Categories
1229        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1230        assert_eq!(state.footer_button_index, 1);
1231        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1232        assert_eq!(state.footer_button_index, 2);
1233        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1234        assert_eq!(state.footer_button_index, 3);
1235        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1236        assert_eq!(state.footer_button_index, 4); // Edit button
1237        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1238        assert_eq!(state.focus_panel(), FocusPanel::Categories);
1239
1240        // SECOND LOOP: Tab again should still land on Layer button when entering Footer
1241        // Tab -> Settings
1242        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1243        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1244
1245        // Tab -> Footer (should reset to Layer button, not stay on Edit)
1246        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1247        assert_eq!(state.focus_panel(), FocusPanel::Footer);
1248        assert_eq!(
1249            state.footer_button_index, 0,
1250            "Footer should reset to Layer button (index 0) on second loop"
1251        );
1252    }
1253
1254    #[test]
1255    fn test_escape_shows_confirm_dialog_with_changes() {
1256        let schema = include_str!("../../../plugins/config-schema.json");
1257        let config = crate::config::Config::default();
1258        let mut state = SettingsState::new(schema, &config).unwrap();
1259        state.visible = true;
1260
1261        // Simulate a change
1262        state
1263            .pending_changes
1264            .insert("/test".to_string(), serde_json::json!(true));
1265
1266        let mut ctx = InputContext::new();
1267
1268        // Escape should show confirm dialog, not close directly
1269        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1270        assert!(state.showing_confirm_dialog);
1271        assert!(ctx.deferred_actions.is_empty()); // No close action yet
1272    }
1273
1274    #[test]
1275    fn test_escape_closes_directly_without_changes() {
1276        let schema = include_str!("../../../plugins/config-schema.json");
1277        let config = crate::config::Config::default();
1278        let mut state = SettingsState::new(schema, &config).unwrap();
1279        state.visible = true;
1280
1281        let mut ctx = InputContext::new();
1282
1283        // Escape without changes should defer close action
1284        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1285        assert!(!state.showing_confirm_dialog);
1286        assert_eq!(ctx.deferred_actions.len(), 1);
1287        assert!(matches!(
1288            ctx.deferred_actions[0],
1289            DeferredAction::CloseSettings { save: false }
1290        ));
1291    }
1292
1293    #[test]
1294    fn test_confirm_dialog_navigation() {
1295        let schema = include_str!("../../../plugins/config-schema.json");
1296        let config = crate::config::Config::default();
1297        let mut state = SettingsState::new(schema, &config).unwrap();
1298        state.visible = true;
1299        state.showing_confirm_dialog = true;
1300        state.confirm_dialog_selection = 0; // Save
1301
1302        let mut ctx = InputContext::new();
1303
1304        // Right -> Discard
1305        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1306        assert_eq!(state.confirm_dialog_selection, 1);
1307
1308        // Right -> Cancel
1309        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1310        assert_eq!(state.confirm_dialog_selection, 2);
1311
1312        // Right again -> stays at Cancel (no wrap)
1313        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1314        assert_eq!(state.confirm_dialog_selection, 2);
1315
1316        // Left -> Discard
1317        state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1318        assert_eq!(state.confirm_dialog_selection, 1);
1319    }
1320
1321    #[test]
1322    fn test_search_mode_captures_typing() {
1323        let schema = include_str!("../../../plugins/config-schema.json");
1324        let config = crate::config::Config::default();
1325        let mut state = SettingsState::new(schema, &config).unwrap();
1326        state.visible = true;
1327
1328        let mut ctx = InputContext::new();
1329
1330        // Start search
1331        state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1332        assert!(state.search_active);
1333
1334        // Type search query
1335        state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1336        state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1337        state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1338        assert_eq!(state.search_query, "tab");
1339
1340        // Escape cancels search
1341        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1342        assert!(!state.search_active);
1343        assert!(state.search_query.is_empty());
1344    }
1345
1346    #[test]
1347    fn test_footer_button_activation() {
1348        let schema = include_str!("../../../plugins/config-schema.json");
1349        let config = crate::config::Config::default();
1350        let mut state = SettingsState::new(schema, &config).unwrap();
1351        state.visible = true;
1352        state.focus.set(FocusPanel::Footer);
1353        state.footer_button_index = 2; // Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
1354
1355        let mut ctx = InputContext::new();
1356
1357        // Enter on Save button should defer save action
1358        state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1359        assert_eq!(ctx.deferred_actions.len(), 1);
1360        assert!(matches!(
1361            ctx.deferred_actions[0],
1362            DeferredAction::CloseSettings { save: true }
1363        ));
1364    }
1365}