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::DualList(_)
400                                | SettingControl::Number(_)
401                                | SettingControl::Json(_) => Some(ControlAction::StartEditing),
402                                SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
403                                    Some(ControlAction::OpenNestedDialog)
404                                }
405                                _ => None,
406                            })
407                        })
408                        .flatten();
409
410                    if let Some(action) = control_action {
411                        match action {
412                            ControlAction::ToggleBool => {
413                                if let Some(dialog) = self.entry_dialog_mut() {
414                                    dialog.toggle_bool();
415                                }
416                            }
417                            ControlAction::ToggleDropdown => {
418                                if let Some(dialog) = self.entry_dialog_mut() {
419                                    dialog.toggle_dropdown();
420                                }
421                            }
422                            ControlAction::StartEditing => {
423                                if let Some(dialog) = self.entry_dialog_mut() {
424                                    dialog.start_editing();
425                                }
426                            }
427                            ControlAction::OpenNestedDialog => {
428                                self.open_nested_entry_dialog();
429                            }
430                        }
431                    }
432                }
433            }
434            KeyCode::Char(' ') => {
435                // Space toggles booleans, activates dropdowns (but doesn't submit form)
436                let control_action = self.entry_dialog().and_then(|dialog| {
437                    if dialog.focus_on_buttons {
438                        return None; // Space on buttons does nothing (Enter activates)
439                    }
440                    dialog.current_item().and_then(|item| match &item.control {
441                        SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
442                        SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
443                        _ => None,
444                    })
445                });
446
447                if let Some(action) = control_action {
448                    match action {
449                        ControlAction::ToggleBool => {
450                            if let Some(dialog) = self.entry_dialog_mut() {
451                                dialog.toggle_bool();
452                            }
453                        }
454                        ControlAction::ToggleDropdown => {
455                            if let Some(dialog) = self.entry_dialog_mut() {
456                                dialog.toggle_dropdown();
457                            }
458                        }
459                        _ => {}
460                    }
461                }
462            }
463            KeyCode::Char(c) => {
464                // Auto-enter edit mode when typing on a text or number field
465                let can_auto_edit = self
466                    .entry_dialog()
467                    .and_then(|dialog| {
468                        if dialog.focus_on_buttons {
469                            return None;
470                        }
471                        dialog.current_item().map(|item| match &item.control {
472                            SettingControl::Text(_) | SettingControl::TextList(_) => true,
473                            SettingControl::Number(_) => c.is_ascii_digit() || c == '-' || c == '.',
474                            _ => false,
475                        })
476                    })
477                    .unwrap_or(false);
478
479                if can_auto_edit {
480                    if let Some(dialog) = self.entry_dialog_mut() {
481                        dialog.start_editing();
482                    }
483                    // Now forward the character to the text editing handler
484                    return self.handle_entry_dialog_text_editing(
485                        &KeyEvent::new(KeyCode::Char(c), event.modifiers),
486                        ctx,
487                    );
488                }
489            }
490            _ => {}
491        }
492        InputResult::Consumed
493    }
494
495    /// Handle input when confirmation dialog is showing
496    fn handle_confirm_dialog_input(
497        &mut self,
498        event: &KeyEvent,
499        ctx: &mut InputContext,
500    ) -> InputResult {
501        match event.code {
502            KeyCode::Left | KeyCode::BackTab => {
503                if self.confirm_dialog_selection > 0 {
504                    self.confirm_dialog_selection -= 1;
505                }
506                InputResult::Consumed
507            }
508            KeyCode::Right | KeyCode::Tab => {
509                if self.confirm_dialog_selection < 2 {
510                    self.confirm_dialog_selection += 1;
511                }
512                InputResult::Consumed
513            }
514            KeyCode::Enter => {
515                match self.confirm_dialog_selection {
516                    0 => ctx.defer(DeferredAction::CloseSettings { save: true }), // Save
517                    1 => ctx.defer(DeferredAction::CloseSettings { save: false }), // Discard
518                    2 => self.showing_confirm_dialog = false, // Cancel - back to settings
519                    _ => {}
520                }
521                InputResult::Consumed
522            }
523            KeyCode::Esc => {
524                self.showing_confirm_dialog = false;
525                InputResult::Consumed
526            }
527            KeyCode::Char('s') | KeyCode::Char('S') => {
528                ctx.defer(DeferredAction::CloseSettings { save: true });
529                InputResult::Consumed
530            }
531            KeyCode::Char('d') | KeyCode::Char('D') => {
532                ctx.defer(DeferredAction::CloseSettings { save: false });
533                InputResult::Consumed
534            }
535            _ => InputResult::Consumed, // Modal: consume all
536        }
537    }
538
539    /// Handle input when reset confirmation dialog is showing
540    fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
541        match event.code {
542            KeyCode::Left | KeyCode::BackTab => {
543                if self.reset_dialog_selection > 0 {
544                    self.reset_dialog_selection -= 1;
545                }
546                InputResult::Consumed
547            }
548            KeyCode::Right | KeyCode::Tab => {
549                if self.reset_dialog_selection < 1 {
550                    self.reset_dialog_selection += 1;
551                }
552                InputResult::Consumed
553            }
554            KeyCode::Enter => {
555                match self.reset_dialog_selection {
556                    0 => {
557                        // Reset all changes
558                        self.discard_changes();
559                        self.showing_reset_dialog = false;
560                    }
561                    1 => {
562                        // Cancel - back to settings
563                        self.showing_reset_dialog = false;
564                    }
565                    _ => {}
566                }
567                InputResult::Consumed
568            }
569            KeyCode::Esc => {
570                self.showing_reset_dialog = false;
571                InputResult::Consumed
572            }
573            KeyCode::Char('r') | KeyCode::Char('R') => {
574                self.discard_changes();
575                self.showing_reset_dialog = false;
576                InputResult::Consumed
577            }
578            _ => InputResult::Consumed, // Modal: consume all
579        }
580    }
581
582    /// Handle input when help overlay is showing
583    fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
584        // Any key dismisses help
585        self.showing_help = false;
586        InputResult::Consumed
587    }
588
589    /// Handle input when search is active
590    fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
591        match event.code {
592            KeyCode::Esc => {
593                self.cancel_search();
594                InputResult::Consumed
595            }
596            KeyCode::Enter => {
597                self.jump_to_search_result();
598                InputResult::Consumed
599            }
600            KeyCode::Up => {
601                self.search_prev();
602                InputResult::Consumed
603            }
604            KeyCode::Down => {
605                self.search_next();
606                InputResult::Consumed
607            }
608            KeyCode::Char(c) => {
609                self.search_push_char(c);
610                InputResult::Consumed
611            }
612            KeyCode::Backspace => {
613                self.search_pop_char();
614                InputResult::Consumed
615            }
616            _ => InputResult::Consumed, // Modal: consume all
617        }
618    }
619
620    /// Handle input when Categories panel is focused
621    fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
622        match event.code {
623            KeyCode::Up => {
624                self.select_prev();
625                InputResult::Consumed
626            }
627            KeyCode::Down => {
628                self.select_next();
629                InputResult::Consumed
630            }
631            KeyCode::Tab => {
632                self.toggle_focus();
633                InputResult::Consumed
634            }
635            KeyCode::BackTab => {
636                self.toggle_focus_backward();
637                InputResult::Consumed
638            }
639            KeyCode::Char('/') => {
640                self.start_search();
641                InputResult::Consumed
642            }
643            KeyCode::Char('?') => {
644                self.toggle_help();
645                InputResult::Consumed
646            }
647            KeyCode::Esc => {
648                self.request_close(ctx);
649                InputResult::Consumed
650            }
651            KeyCode::Enter | KeyCode::Right => {
652                // Enter/Right on categories: move focus to settings panel
653                self.focus.set(FocusPanel::Settings);
654                InputResult::Consumed
655            }
656            _ => InputResult::Ignored, // Let modal catch it
657        }
658    }
659
660    /// Handle input when Settings panel is focused
661    fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
662        // If editing text, handle text input
663        if self.editing_text {
664            return self.handle_text_editing_input(event, ctx);
665        }
666
667        // If editing number input, handle number input
668        if self.is_number_editing() {
669            return self.handle_number_editing_input(event, ctx);
670        }
671
672        // If dropdown is open, handle dropdown navigation
673        if self.is_dropdown_open() {
674            return self.handle_dropdown_input(event, ctx);
675        }
676
677        match event.code {
678            KeyCode::Up => {
679                self.select_prev();
680                InputResult::Consumed
681            }
682            KeyCode::Down => {
683                self.select_next();
684                InputResult::Consumed
685            }
686            KeyCode::Tab => {
687                self.toggle_focus();
688                InputResult::Consumed
689            }
690            KeyCode::BackTab => {
691                self.toggle_focus_backward();
692                InputResult::Consumed
693            }
694            KeyCode::Left => {
695                // Left on number controls: decrement value
696                // Left on other controls: navigate back to categories
697                if self.is_number_control() {
698                    self.handle_control_decrement();
699                } else {
700                    self.update_control_focus(false);
701                    self.focus.set(FocusPanel::Categories);
702                }
703                InputResult::Consumed
704            }
705            KeyCode::Right => {
706                self.handle_control_increment();
707                InputResult::Consumed
708            }
709            KeyCode::Enter | KeyCode::Char(' ') => {
710                self.handle_control_activate(ctx);
711                InputResult::Consumed
712            }
713            KeyCode::PageDown => {
714                self.select_next_page();
715                InputResult::Consumed
716            }
717            KeyCode::PageUp => {
718                self.select_prev_page();
719                InputResult::Consumed
720            }
721            KeyCode::Char('/') => {
722                self.start_search();
723                InputResult::Consumed
724            }
725            KeyCode::Char('?') => {
726                self.toggle_help();
727                InputResult::Consumed
728            }
729            KeyCode::Delete => {
730                // Delete key: set nullable setting to null (inherit)
731                self.set_current_to_null();
732                InputResult::Consumed
733            }
734            KeyCode::Esc => {
735                self.request_close(ctx);
736                InputResult::Consumed
737            }
738            _ => InputResult::Ignored, // Let modal catch it
739        }
740    }
741
742    /// Handle input when Footer is focused
743    /// Footer buttons: [Layer] [Reset] [Save] [Cancel] + [Edit] on left for advanced users
744    /// Tab cycles between buttons; after last button, moves to Categories panel
745    fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
746        const FOOTER_BUTTON_COUNT: usize = 5;
747
748        match event.code {
749            KeyCode::Left | KeyCode::BackTab => {
750                // Move to previous button, or wrap to Categories panel
751                if self.footer_button_index > 0 {
752                    self.footer_button_index -= 1;
753                } else {
754                    self.focus.set(FocusPanel::Settings);
755                }
756                InputResult::Consumed
757            }
758            KeyCode::Right => {
759                // Move to next button
760                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
761                    self.footer_button_index += 1;
762                }
763                InputResult::Consumed
764            }
765            KeyCode::Tab => {
766                // Move to next button, or wrap to Categories panel
767                if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
768                    self.footer_button_index += 1;
769                } else {
770                    self.focus.set(FocusPanel::Categories);
771                }
772                InputResult::Consumed
773            }
774            KeyCode::Enter => {
775                match self.footer_button_index {
776                    0 => self.cycle_target_layer(), // Layer button
777                    1 => {
778                        // Reset/Inherit button — for nullable items, set to null (inherit);
779                        // otherwise show reset-all dialog
780                        let is_nullable_set = self
781                            .current_item()
782                            .map(|item| item.nullable && !item.is_null)
783                            .unwrap_or(false);
784                        if is_nullable_set {
785                            self.set_current_to_null();
786                        } else {
787                            self.request_reset();
788                        }
789                    }
790                    2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
791                    3 => self.request_close(ctx),
792                    4 => ctx.defer(DeferredAction::OpenConfigFile {
793                        layer: self.target_layer,
794                    }), // Edit config file
795                    _ => {}
796                }
797                InputResult::Consumed
798            }
799            KeyCode::Esc => {
800                self.request_close(ctx);
801                InputResult::Consumed
802            }
803            KeyCode::Char('/') => {
804                self.start_search();
805                InputResult::Consumed
806            }
807            KeyCode::Char('?') => {
808                self.toggle_help();
809                InputResult::Consumed
810            }
811            _ => InputResult::Ignored, // Let modal catch it
812        }
813    }
814
815    /// Handle input when editing text in a control
816    fn handle_text_editing_input(
817        &mut self,
818        event: &KeyEvent,
819        ctx: &mut InputContext,
820    ) -> InputResult {
821        let is_json = self.is_editing_json();
822
823        if is_json {
824            return self.handle_json_editing_input(event, ctx);
825        }
826
827        // DualList has its own keyboard handling (no text input)
828        if self.is_editing_dual_list() {
829            return self.handle_dual_list_editing_input(event);
830        }
831
832        match event.code {
833            KeyCode::Esc => {
834                // Check if current text field requires JSON validation
835                if !self.can_exit_text_editing() {
836                    return InputResult::Consumed;
837                }
838                self.stop_editing();
839                InputResult::Consumed
840            }
841            KeyCode::Enter => {
842                self.text_add_item();
843                InputResult::Consumed
844            }
845            KeyCode::Char(c) => {
846                self.text_insert(c);
847                InputResult::Consumed
848            }
849            KeyCode::Backspace => {
850                self.text_backspace();
851                InputResult::Consumed
852            }
853            KeyCode::Delete => {
854                self.text_remove_focused();
855                InputResult::Consumed
856            }
857            KeyCode::Left => {
858                self.text_move_left();
859                InputResult::Consumed
860            }
861            KeyCode::Right => {
862                self.text_move_right();
863                InputResult::Consumed
864            }
865            KeyCode::Up => {
866                self.text_focus_prev();
867                InputResult::Consumed
868            }
869            KeyCode::Down => {
870                self.text_focus_next();
871                InputResult::Consumed
872            }
873            KeyCode::Tab => {
874                // Tab exits text editing mode and advances focus to the next panel
875                self.stop_editing();
876                self.toggle_focus();
877                InputResult::Consumed
878            }
879            _ => InputResult::Consumed, // Consume all during text edit
880        }
881    }
882
883    /// Handle input when editing a DualList control
884    fn handle_dual_list_editing_input(&mut self, event: &KeyEvent) -> InputResult {
885        use crate::view::controls::DualListColumn;
886        let shift = event.modifiers.contains(KeyModifiers::SHIFT);
887        match event.code {
888            KeyCode::Esc => {
889                self.stop_editing();
890            }
891            // Tab/BackTab propagate to the settings panel (exit editing)
892            KeyCode::Tab | KeyCode::BackTab => {
893                self.stop_editing();
894                // Return Ignored so the settings panel handles Tab/BackTab
895                return InputResult::Ignored;
896            }
897            KeyCode::Up if shift => {
898                self.with_current_dual_list_mut(|dl| dl.move_up());
899                self.on_value_changed();
900            }
901            KeyCode::Down if shift => {
902                self.with_current_dual_list_mut(|dl| dl.move_down());
903                self.on_value_changed();
904            }
905            KeyCode::Up => {
906                self.with_current_dual_list_mut(|dl| dl.cursor_up());
907            }
908            KeyCode::Down => {
909                self.with_current_dual_list_mut(|dl| dl.cursor_down());
910            }
911            KeyCode::Right if shift => {
912                // Shift+Right: add selected available item to included, follow it
913                let changed = self
914                    .with_current_dual_list_mut(|dl| {
915                        if dl.active_column == DualListColumn::Available {
916                            dl.add_selected();
917                            // Move focus to the Included column, cursor on the newly added item (last)
918                            dl.active_column = DualListColumn::Included;
919                            dl.included_cursor = dl.included.len().saturating_sub(1);
920                            true
921                        } else {
922                            false
923                        }
924                    })
925                    .unwrap_or(false);
926                if changed {
927                    self.on_value_changed();
928                    self.refresh_dual_list_sibling();
929                }
930            }
931            KeyCode::Left if shift => {
932                // Shift+Left: remove selected included item back to available, follow it
933                let changed = self
934                    .with_current_dual_list_mut(|dl| {
935                        if dl.active_column == DualListColumn::Included {
936                            let value = dl.included.get(dl.included_cursor).cloned();
937                            dl.remove_selected();
938                            // Move focus to Available column, find the removed item
939                            dl.active_column = DualListColumn::Available;
940                            if let Some(val) = value {
941                                let avail = dl.available_items();
942                                if let Some(pos) = avail.iter().position(|(v, _)| *v == val) {
943                                    dl.available_cursor = pos;
944                                }
945                            }
946                            true
947                        } else {
948                            false
949                        }
950                    })
951                    .unwrap_or(false);
952                if changed {
953                    self.on_value_changed();
954                    self.refresh_dual_list_sibling();
955                }
956            }
957            KeyCode::Right => {
958                // Plain Right: switch to Included column
959                self.with_current_dual_list_mut(|dl| {
960                    dl.active_column = DualListColumn::Included;
961                });
962            }
963            KeyCode::Left => {
964                // Plain Left: switch to Available column
965                self.with_current_dual_list_mut(|dl| {
966                    dl.active_column = DualListColumn::Available;
967                });
968            }
969            KeyCode::Enter => {
970                // Enter adds/removes based on active column
971                let changed = self
972                    .with_current_dual_list_mut(|dl| match dl.active_column {
973                        DualListColumn::Available => dl.add_selected(),
974                        DualListColumn::Included => dl.remove_selected(),
975                    })
976                    .is_some();
977                if changed {
978                    self.on_value_changed();
979                    self.refresh_dual_list_sibling();
980                }
981            }
982            _ => {}
983        }
984        InputResult::Consumed
985    }
986
987    /// Handle input when editing a JSON control (multiline editor)
988    fn handle_json_editing_input(
989        &mut self,
990        event: &KeyEvent,
991        ctx: &mut InputContext,
992    ) -> InputResult {
993        match event.code {
994            KeyCode::Esc | KeyCode::Tab => {
995                // Accept if valid JSON, revert if invalid, then stop editing
996                self.json_exit_editing();
997            }
998            KeyCode::Enter => {
999                self.json_insert_newline();
1000            }
1001            KeyCode::Char(c) => {
1002                if event.modifiers.contains(KeyModifiers::CONTROL) {
1003                    match c {
1004                        'a' | 'A' => self.json_select_all(),
1005                        'c' | 'C' => {
1006                            if let Some(text) = self.json_selected_text() {
1007                                ctx.defer(DeferredAction::CopyToClipboard(text));
1008                            }
1009                        }
1010                        'v' | 'V' => {
1011                            ctx.defer(DeferredAction::PasteToSettings);
1012                        }
1013                        _ => {}
1014                    }
1015                } else {
1016                    self.text_insert(c);
1017                }
1018            }
1019            KeyCode::Backspace => {
1020                self.text_backspace();
1021            }
1022            KeyCode::Delete => {
1023                self.json_delete();
1024            }
1025            KeyCode::Left => {
1026                if event.modifiers.contains(KeyModifiers::SHIFT) {
1027                    self.json_cursor_left_selecting();
1028                } else {
1029                    self.text_move_left();
1030                }
1031            }
1032            KeyCode::Right => {
1033                if event.modifiers.contains(KeyModifiers::SHIFT) {
1034                    self.json_cursor_right_selecting();
1035                } else {
1036                    self.text_move_right();
1037                }
1038            }
1039            KeyCode::Up => {
1040                if event.modifiers.contains(KeyModifiers::SHIFT) {
1041                    self.json_cursor_up_selecting();
1042                } else {
1043                    self.json_cursor_up();
1044                }
1045            }
1046            KeyCode::Down => {
1047                if event.modifiers.contains(KeyModifiers::SHIFT) {
1048                    self.json_cursor_down_selecting();
1049                } else {
1050                    self.json_cursor_down();
1051                }
1052            }
1053            _ => {}
1054        }
1055        InputResult::Consumed
1056    }
1057
1058    /// Handle input when editing a number input control
1059    fn handle_number_editing_input(
1060        &mut self,
1061        event: &KeyEvent,
1062        _ctx: &mut InputContext,
1063    ) -> InputResult {
1064        let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
1065        let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1066
1067        match event.code {
1068            KeyCode::Esc => {
1069                self.number_cancel();
1070            }
1071            KeyCode::Enter => {
1072                self.number_confirm();
1073            }
1074            KeyCode::Char('a') if ctrl => {
1075                self.number_select_all();
1076            }
1077            KeyCode::Char(c) => {
1078                self.number_insert(c);
1079            }
1080            KeyCode::Backspace if ctrl => {
1081                self.number_delete_word_backward();
1082            }
1083            KeyCode::Backspace => {
1084                self.number_backspace();
1085            }
1086            KeyCode::Delete if ctrl => {
1087                self.number_delete_word_forward();
1088            }
1089            KeyCode::Delete => {
1090                self.number_delete();
1091            }
1092            KeyCode::Left if ctrl && shift => {
1093                self.number_move_word_left_selecting();
1094            }
1095            KeyCode::Left if ctrl => {
1096                self.number_move_word_left();
1097            }
1098            KeyCode::Left if shift => {
1099                self.number_move_left_selecting();
1100            }
1101            KeyCode::Left => {
1102                self.number_move_left();
1103            }
1104            KeyCode::Right if ctrl && shift => {
1105                self.number_move_word_right_selecting();
1106            }
1107            KeyCode::Right if ctrl => {
1108                self.number_move_word_right();
1109            }
1110            KeyCode::Right if shift => {
1111                self.number_move_right_selecting();
1112            }
1113            KeyCode::Right => {
1114                self.number_move_right();
1115            }
1116            KeyCode::Home if shift => {
1117                self.number_move_home_selecting();
1118            }
1119            KeyCode::Home => {
1120                self.number_move_home();
1121            }
1122            KeyCode::End if shift => {
1123                self.number_move_end_selecting();
1124            }
1125            KeyCode::End => {
1126                self.number_move_end();
1127            }
1128            _ => {}
1129        }
1130        InputResult::Consumed // Consume all during number edit
1131    }
1132
1133    /// Handle input when dropdown is open
1134    fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1135        match event.code {
1136            KeyCode::Up => {
1137                self.dropdown_prev();
1138                InputResult::Consumed
1139            }
1140            KeyCode::Down => {
1141                self.dropdown_next();
1142                InputResult::Consumed
1143            }
1144            KeyCode::Home => {
1145                self.dropdown_home();
1146                InputResult::Consumed
1147            }
1148            KeyCode::End => {
1149                self.dropdown_end();
1150                InputResult::Consumed
1151            }
1152            KeyCode::Enter => {
1153                self.dropdown_confirm();
1154                InputResult::Consumed
1155            }
1156            KeyCode::Esc => {
1157                self.dropdown_cancel();
1158                InputResult::Consumed
1159            }
1160            _ => InputResult::Consumed, // Consume all while dropdown is open
1161        }
1162    }
1163
1164    /// Request to reset all changes (shows confirm dialog if there are changes)
1165    fn request_reset(&mut self) {
1166        if self.has_changes() {
1167            self.showing_reset_dialog = true;
1168            self.reset_dialog_selection = 0;
1169        }
1170    }
1171
1172    /// Request to close settings (shows confirm dialog if there are changes)
1173    fn request_close(&mut self, ctx: &mut InputContext) {
1174        if self.has_changes() {
1175            self.showing_confirm_dialog = true;
1176            self.confirm_dialog_selection = 0;
1177        } else {
1178            ctx.defer(DeferredAction::CloseSettings { save: false });
1179        }
1180    }
1181
1182    /// Handle control activation (Enter/Space on a setting)
1183    fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1184        if let Some(item) = self.current_item_mut() {
1185            match &mut item.control {
1186                SettingControl::Toggle(ref mut state) => {
1187                    state.checked = !state.checked;
1188                    self.on_value_changed();
1189                }
1190                SettingControl::Dropdown(_) => {
1191                    self.dropdown_toggle();
1192                }
1193                SettingControl::Number(_) => {
1194                    self.start_number_editing();
1195                }
1196                SettingControl::Text(_) => {
1197                    self.start_editing();
1198                }
1199                SettingControl::TextList(_) | SettingControl::DualList(_) => {
1200                    self.start_editing();
1201                }
1202                SettingControl::Map(ref mut state) => {
1203                    if state.focused_entry.is_none() {
1204                        // On add-new row: open dialog with empty key
1205                        if state.value_schema.is_some() {
1206                            self.open_add_entry_dialog();
1207                        }
1208                    } else if state.value_schema.is_some() {
1209                        // Has schema: open entry dialog
1210                        self.open_entry_dialog();
1211                    } else {
1212                        // Toggle expanded
1213                        if let Some(idx) = state.focused_entry {
1214                            if state.expanded.contains(&idx) {
1215                                state.expanded.retain(|&i| i != idx);
1216                            } else {
1217                                state.expanded.push(idx);
1218                            }
1219                        }
1220                    }
1221                    self.on_value_changed();
1222                }
1223                SettingControl::Json(_) => {
1224                    self.start_editing();
1225                }
1226                SettingControl::ObjectArray(ref state) => {
1227                    if state.focused_index.is_none() {
1228                        // On add-new row: open dialog with empty item
1229                        if state.item_schema.is_some() {
1230                            self.open_add_array_item_dialog();
1231                        }
1232                    } else if state.item_schema.is_some() {
1233                        // Has schema: open edit dialog
1234                        self.open_edit_array_item_dialog();
1235                    }
1236                }
1237                SettingControl::Complex { .. } => {
1238                    // Not editable via simple controls
1239                }
1240            }
1241        }
1242    }
1243
1244    /// Handle control increment (Right arrow on numbers/dropdowns)
1245    fn handle_control_increment(&mut self) {
1246        if let Some(item) = self.current_item_mut() {
1247            if let SettingControl::Number(ref mut state) = &mut item.control {
1248                state.value += 1;
1249                if let Some(max) = state.max {
1250                    state.value = state.value.min(max);
1251                }
1252                self.on_value_changed();
1253            }
1254        }
1255    }
1256
1257    /// Handle control decrement (Left arrow on numbers)
1258    fn handle_control_decrement(&mut self) {
1259        if let Some(item) = self.current_item_mut() {
1260            if let SettingControl::Number(ref mut state) = &mut item.control {
1261                if state.value > 0 {
1262                    state.value -= 1;
1263                }
1264                if let Some(min) = state.min {
1265                    state.value = state.value.max(min);
1266                }
1267                self.on_value_changed();
1268            }
1269        }
1270    }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1277
1278    fn key(code: KeyCode) -> KeyEvent {
1279        KeyEvent::new(code, KeyModifiers::NONE)
1280    }
1281
1282    #[test]
1283    fn test_settings_is_modal() {
1284        // SettingsState should be modal - consume all unhandled input
1285        let schema = include_str!("../../../plugins/config-schema.json");
1286        let config = crate::config::Config::default();
1287        let state = SettingsState::new(schema, &config).unwrap();
1288        assert!(state.is_modal());
1289    }
1290
1291    #[test]
1292    fn test_categories_panel_does_not_leak_to_settings() {
1293        let schema = include_str!("../../../plugins/config-schema.json");
1294        let config = crate::config::Config::default();
1295        let mut state = SettingsState::new(schema, &config).unwrap();
1296        state.visible = true;
1297        state.focus.set(FocusPanel::Categories);
1298
1299        let mut ctx = InputContext::new();
1300
1301        // Enter on categories should NOT affect settings items
1302        // It should just move focus to settings panel
1303        let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1304        assert_eq!(result, InputResult::Consumed);
1305        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1306
1307        // Go back to categories
1308        state.focus.set(FocusPanel::Categories);
1309
1310        // Left/Right on categories should be consumed but not affect settings
1311        let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1312        assert_eq!(result, InputResult::Consumed);
1313        // Should have moved to settings panel
1314        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1315    }
1316
1317    #[test]
1318    fn test_tab_cycles_focus_panels() {
1319        let schema = include_str!("../../../plugins/config-schema.json");
1320        let config = crate::config::Config::default();
1321        let mut state = SettingsState::new(schema, &config).unwrap();
1322        state.visible = true;
1323
1324        let mut ctx = InputContext::new();
1325
1326        // Start at Categories
1327        assert_eq!(state.focus_panel(), FocusPanel::Categories);
1328
1329        // Tab -> Settings
1330        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1331        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1332
1333        // Tab -> Footer (defaults to Layer button, index 0)
1334        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1335        assert_eq!(state.focus_panel(), FocusPanel::Footer);
1336        assert_eq!(state.footer_button_index, 0);
1337
1338        // Tab through footer buttons: 0 -> 1 -> 2 -> 3 -> 4 -> wrap to Categories
1339        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1340        assert_eq!(state.footer_button_index, 1);
1341        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1342        assert_eq!(state.footer_button_index, 2);
1343        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1344        assert_eq!(state.footer_button_index, 3);
1345        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1346        assert_eq!(state.footer_button_index, 4); // Edit button
1347        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1348        assert_eq!(state.focus_panel(), FocusPanel::Categories);
1349
1350        // SECOND LOOP: Tab again should still land on Layer button when entering Footer
1351        // Tab -> Settings
1352        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1353        assert_eq!(state.focus_panel(), FocusPanel::Settings);
1354
1355        // Tab -> Footer (should reset to Layer button, not stay on Edit)
1356        state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1357        assert_eq!(state.focus_panel(), FocusPanel::Footer);
1358        assert_eq!(
1359            state.footer_button_index, 0,
1360            "Footer should reset to Layer button (index 0) on second loop"
1361        );
1362    }
1363
1364    #[test]
1365    fn test_escape_shows_confirm_dialog_with_changes() {
1366        let schema = include_str!("../../../plugins/config-schema.json");
1367        let config = crate::config::Config::default();
1368        let mut state = SettingsState::new(schema, &config).unwrap();
1369        state.visible = true;
1370
1371        // Simulate a change
1372        state
1373            .pending_changes
1374            .insert("/test".to_string(), serde_json::json!(true));
1375
1376        let mut ctx = InputContext::new();
1377
1378        // Escape should show confirm dialog, not close directly
1379        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1380        assert!(state.showing_confirm_dialog);
1381        assert!(ctx.deferred_actions.is_empty()); // No close action yet
1382    }
1383
1384    #[test]
1385    fn test_escape_closes_directly_without_changes() {
1386        let schema = include_str!("../../../plugins/config-schema.json");
1387        let config = crate::config::Config::default();
1388        let mut state = SettingsState::new(schema, &config).unwrap();
1389        state.visible = true;
1390
1391        let mut ctx = InputContext::new();
1392
1393        // Escape without changes should defer close action
1394        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1395        assert!(!state.showing_confirm_dialog);
1396        assert_eq!(ctx.deferred_actions.len(), 1);
1397        assert!(matches!(
1398            ctx.deferred_actions[0],
1399            DeferredAction::CloseSettings { save: false }
1400        ));
1401    }
1402
1403    #[test]
1404    fn test_confirm_dialog_navigation() {
1405        let schema = include_str!("../../../plugins/config-schema.json");
1406        let config = crate::config::Config::default();
1407        let mut state = SettingsState::new(schema, &config).unwrap();
1408        state.visible = true;
1409        state.showing_confirm_dialog = true;
1410        state.confirm_dialog_selection = 0; // Save
1411
1412        let mut ctx = InputContext::new();
1413
1414        // Right -> Discard
1415        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1416        assert_eq!(state.confirm_dialog_selection, 1);
1417
1418        // Right -> Cancel
1419        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1420        assert_eq!(state.confirm_dialog_selection, 2);
1421
1422        // Right again -> stays at Cancel (no wrap)
1423        state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1424        assert_eq!(state.confirm_dialog_selection, 2);
1425
1426        // Left -> Discard
1427        state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1428        assert_eq!(state.confirm_dialog_selection, 1);
1429    }
1430
1431    #[test]
1432    fn test_search_mode_captures_typing() {
1433        let schema = include_str!("../../../plugins/config-schema.json");
1434        let config = crate::config::Config::default();
1435        let mut state = SettingsState::new(schema, &config).unwrap();
1436        state.visible = true;
1437
1438        let mut ctx = InputContext::new();
1439
1440        // Start search
1441        state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1442        assert!(state.search_active);
1443
1444        // Type search query
1445        state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1446        state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1447        state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1448        assert_eq!(state.search_query, "tab");
1449
1450        // Escape cancels search
1451        state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1452        assert!(!state.search_active);
1453        assert!(state.search_query.is_empty());
1454    }
1455
1456    #[test]
1457    fn test_footer_button_activation() {
1458        let schema = include_str!("../../../plugins/config-schema.json");
1459        let config = crate::config::Config::default();
1460        let mut state = SettingsState::new(schema, &config).unwrap();
1461        state.visible = true;
1462        state.focus.set(FocusPanel::Footer);
1463        state.footer_button_index = 2; // Save button (0=Layer, 1=Reset, 2=Save, 3=Cancel)
1464
1465        let mut ctx = InputContext::new();
1466
1467        // Enter on Save button should defer save action
1468        state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1469        assert_eq!(ctx.deferred_actions.len(), 1);
1470        assert!(matches!(
1471            ctx.deferred_actions[0],
1472            DeferredAction::CloseSettings { save: true }
1473        ));
1474    }
1475}