1use super::items::SettingControl;
7use super::state::{FocusPanel, SettingsState};
8use crate::input::handler::{DeferredAction, InputContext, InputHandler, InputResult};
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10
11enum ButtonAction {
13 Save,
14 Delete,
15 Cancel,
16}
17
18enum 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 if self.showing_entry_delete_confirm {
32 return self.handle_entry_delete_confirm_input(event);
33 }
34
35 if self.showing_entry_discard_confirm {
38 return self.handle_entry_discard_confirm_input(event);
39 }
40
41 if self.has_entry_dialog() {
43 return self.handle_entry_dialog_input(event, ctx);
44 }
45
46 if self.showing_confirm_dialog {
48 return self.handle_confirm_dialog_input(event, ctx);
49 }
50
51 if self.showing_reset_dialog {
53 return self.handle_reset_dialog_input(event);
54 }
55
56 if self.showing_help {
58 return self.handle_help_input(event, ctx);
59 }
60
61 if self.search_active {
63 return self.handle_search_input(event, ctx);
64 }
65
66 if event.modifiers.contains(KeyModifiers::CONTROL)
68 && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
69 {
70 ctx.defer(DeferredAction::CloseSettings { save: true });
71 return InputResult::Consumed;
72 }
73
74 match self.focus_panel() {
76 FocusPanel::Categories => self.handle_categories_input(event, ctx),
77 FocusPanel::Settings => self.handle_settings_input(event, ctx),
78 FocusPanel::Footer => self.handle_footer_input(event, ctx),
79 }
80 }
81
82 fn is_modal(&self) -> bool {
83 true }
85}
86
87impl SettingsState {
88 fn handle_entry_dialog_input(
95 &mut self,
96 event: &KeyEvent,
97 ctx: &mut InputContext,
98 ) -> InputResult {
99 if event.modifiers.contains(KeyModifiers::CONTROL)
101 && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
102 {
103 self.save_entry_dialog();
104 return InputResult::Consumed;
105 }
106
107 let (editing_text, dropdown_open) = if let Some(dialog) = self.entry_dialog() {
109 let dropdown_open = dialog
110 .current_item()
111 .map(|item| matches!(&item.control, SettingControl::Dropdown(s) if s.open))
112 .unwrap_or(false);
113 (dialog.editing_text, dropdown_open)
114 } else {
115 return InputResult::Consumed;
116 };
117
118 if editing_text {
120 self.handle_entry_dialog_text_editing(event, ctx)
121 } else if dropdown_open {
122 self.handle_entry_dialog_dropdown(event)
123 } else {
124 self.handle_entry_dialog_navigation(event, ctx)
125 }
126 }
127
128 fn handle_entry_dialog_text_editing(
130 &mut self,
131 event: &KeyEvent,
132 ctx: &mut InputContext,
133 ) -> InputResult {
134 let is_editing_json = self
136 .entry_dialog()
137 .map(|d| d.is_editing_json())
138 .unwrap_or(false);
139
140 let can_exit = self.entry_dialog_can_exit_text_editing();
142
143 let Some(dialog) = self.entry_dialog_mut() else {
144 return InputResult::Consumed;
145 };
146
147 match event.code {
148 KeyCode::Esc => {
149 if !can_exit {
151 }
153 dialog.stop_editing();
154 }
155 KeyCode::Enter => {
156 if is_editing_json {
157 dialog.insert_newline();
159 } else {
160 let is_text_list = matches!(
169 dialog.current_item().map(|i| &i.control),
170 Some(SettingControl::TextList(_))
171 );
172 if is_text_list {
173 if let Some(item) = dialog.current_item_mut() {
174 if let SettingControl::TextList(state) = &mut item.control {
175 state.add_item();
176 }
177 }
178 } else {
179 dialog.stop_editing();
180 dialog.focus_next_field();
181 }
182 }
183 }
184 KeyCode::Char(c) => {
185 if event.modifiers.contains(KeyModifiers::CONTROL) {
186 match c {
187 'a' | 'A' => {
188 dialog.select_all();
190 }
191 'c' | 'C' => {
192 if let Some(text) = dialog.selected_text() {
194 ctx.defer(DeferredAction::CopyToClipboard(text));
195 }
196 }
197 'v' | 'V' => {
198 ctx.defer(DeferredAction::PasteToSettings);
200 }
201 _ => {}
202 }
203 } else {
204 dialog.insert_char(c);
205 }
206 }
207 KeyCode::Backspace => {
208 dialog.backspace();
209 }
210 KeyCode::Delete => {
211 if is_editing_json {
212 dialog.delete();
214 } else {
215 dialog.delete_list_item();
217 }
218 }
219 KeyCode::Home => {
220 dialog.cursor_home();
221 }
222 KeyCode::End => {
223 dialog.cursor_end();
224 }
225 KeyCode::Left => {
226 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
227 dialog.cursor_left_selecting();
228 } else {
229 dialog.cursor_left();
230 }
231 }
232 KeyCode::Right => {
233 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
234 dialog.cursor_right_selecting();
235 } else {
236 dialog.cursor_right();
237 }
238 }
239 KeyCode::Up => {
240 if is_editing_json {
241 if event.modifiers.contains(KeyModifiers::SHIFT) {
243 dialog.cursor_up_selecting();
244 } else {
245 dialog.cursor_up();
246 }
247 } else {
248 let escape = if let Some(item) = dialog.current_item_mut() {
255 if let SettingControl::TextList(state) = &mut item.control {
256 let was_on_addnew = state.focused_item.is_none();
257 let had_pending = !state.new_item_text.is_empty();
258 state.add_item();
259 if was_on_addnew && !had_pending {
260 true
261 } else {
262 state.focus_prev();
263 false
264 }
265 } else {
266 false
267 }
268 } else {
269 false
270 };
271 if escape {
272 dialog.stop_editing();
273 dialog.focus_prev_field();
274 }
275 }
276 }
277 KeyCode::Down => {
278 if is_editing_json {
279 if event.modifiers.contains(KeyModifiers::SHIFT) {
281 dialog.cursor_down_selecting();
282 } else {
283 dialog.cursor_down();
284 }
285 } else {
286 let escape = if let Some(item) = dialog.current_item_mut() {
290 if let SettingControl::TextList(state) = &mut item.control {
291 let was_on_addnew = state.focused_item.is_none();
292 let had_pending = !state.new_item_text.is_empty();
293 state.add_item();
294 if was_on_addnew && !had_pending {
295 true
296 } else {
297 state.focus_next();
298 false
299 }
300 } else {
301 false
302 }
303 } else {
304 false
305 };
306 if escape {
307 dialog.stop_editing();
308 dialog.focus_next_field();
309 }
310 }
311 }
312 KeyCode::Tab => {
313 if is_editing_json {
314 let is_valid = dialog
316 .current_item()
317 .map(|item| {
318 if let SettingControl::Json(state) = &item.control {
319 state.is_valid()
320 } else {
321 true
322 }
323 })
324 .unwrap_or(true);
325
326 if is_valid {
327 if let Some(item) = dialog.current_item_mut() {
329 if let SettingControl::Json(state) = &mut item.control {
330 state.commit();
331 }
332 }
333 dialog.stop_editing();
334 }
335 } else {
337 let escape_forward = if let Some(item) = dialog.current_item_mut() {
342 if let SettingControl::TextList(state) = &mut item.control {
343 state.add_item();
344 true
345 } else {
346 false
347 }
348 } else {
349 false
350 };
351 dialog.stop_editing();
352 if escape_forward {
353 dialog.focus_next_field();
354 }
355 }
356 }
357 _ => {}
358 }
359 InputResult::Consumed
360 }
361
362 fn handle_entry_dialog_dropdown(&mut self, event: &KeyEvent) -> InputResult {
364 let Some(dialog) = self.entry_dialog_mut() else {
365 return InputResult::Consumed;
366 };
367
368 match event.code {
369 KeyCode::Up => {
370 dialog.dropdown_prev();
371 }
372 KeyCode::Down => {
373 dialog.dropdown_next();
374 }
375 KeyCode::Enter => {
376 dialog.dropdown_confirm();
377 }
378 KeyCode::Esc => {
379 dialog.dropdown_confirm(); }
381 _ => {}
382 }
383 InputResult::Consumed
384 }
385
386 fn handle_entry_dialog_navigation(
388 &mut self,
389 event: &KeyEvent,
390 ctx: &mut InputContext,
391 ) -> InputResult {
392 match event.code {
393 KeyCode::Esc => {
394 let dirty = self.entry_dialog().map(|d| d.is_dirty()).unwrap_or(false);
399 if dirty {
400 self.showing_entry_discard_confirm = true;
401 self.entry_discard_confirm_selection = 0;
402 } else {
403 self.close_entry_dialog();
404 }
405 }
406 KeyCode::Up => {
407 if let Some(dialog) = self.entry_dialog_mut() {
408 dialog.focus_prev();
409 }
410 }
411 KeyCode::Down => {
412 if let Some(dialog) = self.entry_dialog_mut() {
413 dialog.focus_next();
414 }
415 }
416 KeyCode::Tab => {
417 if let Some(dialog) = self.entry_dialog_mut() {
419 dialog.focus_next();
420 }
421 }
422 KeyCode::BackTab => {
423 if let Some(dialog) = self.entry_dialog_mut() {
425 dialog.focus_prev();
426 }
427 }
428 KeyCode::Delete => {
429 let removed = self
434 .entry_dialog_mut()
435 .map(|dialog| {
436 if dialog.focus_on_buttons {
437 return false;
438 }
439 if let Some(item) = dialog.current_item_mut() {
440 if let SettingControl::TextList(state) = &mut item.control {
441 if let Some(idx) = state.focused_item {
442 state.remove_item(idx);
443 dialog.user_edited = true;
444 return true;
445 }
446 }
447 }
448 false
449 })
450 .unwrap_or(false);
451 if !removed {
452 }
455 }
456 KeyCode::Left => {
457 if let Some(dialog) = self.entry_dialog_mut() {
458 if dialog.focus_on_buttons && dialog.focused_button > 0 {
459 dialog.focused_button -= 1;
460 }
461 }
462 }
463 KeyCode::Right => {
464 if let Some(dialog) = self.entry_dialog_mut() {
465 if dialog.focus_on_buttons && dialog.focused_button + 1 < dialog.button_count()
466 {
467 dialog.focused_button += 1;
468 }
469 }
470 }
471 KeyCode::Enter => {
472 if self.entry_dialog_activate_focused_field_button() {
474 return InputResult::Consumed;
475 }
476
477 let button_action = self.entry_dialog().and_then(|dialog| {
481 if dialog.focus_on_buttons {
482 let has_delete = !dialog.is_new && !dialog.no_delete;
483 match dialog.focused_button {
484 0 => Some(ButtonAction::Save),
485 1 => Some(ButtonAction::Cancel),
486 2 if has_delete => Some(ButtonAction::Delete),
487 _ => None,
488 }
489 } else {
490 None
491 }
492 });
493
494 if let Some(action) = button_action {
495 match action {
496 ButtonAction::Save => self.save_entry_dialog(),
497 ButtonAction::Delete => self.request_entry_delete_confirm(),
498 ButtonAction::Cancel => self.close_entry_dialog(),
499 }
500 } else if event.modifiers.contains(KeyModifiers::CONTROL) {
501 self.save_entry_dialog();
503 } else {
504 let control_action = self
506 .entry_dialog()
507 .and_then(|dialog| {
508 dialog.current_item().map(|item| match &item.control {
509 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
510 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
511 SettingControl::Text(_)
512 | SettingControl::TextList(_)
513 | SettingControl::DualList(_)
514 | SettingControl::Number(_)
515 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
516 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
517 Some(ControlAction::OpenNestedDialog)
518 }
519 _ => None,
520 })
521 })
522 .flatten();
523
524 if let Some(action) = control_action {
525 match action {
526 ControlAction::ToggleBool => {
527 if let Some(dialog) = self.entry_dialog_mut() {
528 dialog.toggle_bool();
529 }
530 }
531 ControlAction::ToggleDropdown => {
532 if let Some(dialog) = self.entry_dialog_mut() {
533 dialog.toggle_dropdown();
534 }
535 }
536 ControlAction::StartEditing => {
537 if let Some(dialog) = self.entry_dialog_mut() {
538 dialog.start_editing();
539 }
540 }
541 ControlAction::OpenNestedDialog => {
542 self.open_nested_entry_dialog();
543 }
544 }
545 }
546 }
547 }
548 KeyCode::Char(' ') => {
549 if self.entry_dialog_activate_focused_field_button() {
551 return InputResult::Consumed;
552 }
553
554 let control_action = self.entry_dialog().and_then(|dialog| {
556 if dialog.focus_on_buttons {
557 return None; }
559 dialog.current_item().and_then(|item| match &item.control {
560 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
561 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
562 _ => None,
563 })
564 });
565
566 if let Some(action) = control_action {
567 match action {
568 ControlAction::ToggleBool => {
569 if let Some(dialog) = self.entry_dialog_mut() {
570 dialog.toggle_bool();
571 }
572 }
573 ControlAction::ToggleDropdown => {
574 if let Some(dialog) = self.entry_dialog_mut() {
575 dialog.toggle_dropdown();
576 }
577 }
578 _ => {}
579 }
580 }
581 }
582 KeyCode::Char(c) => {
583 let can_auto_edit = self
585 .entry_dialog()
586 .and_then(|dialog| {
587 if dialog.focus_on_buttons {
588 return None;
589 }
590 dialog.current_item().map(|item| match &item.control {
591 SettingControl::Text(_) | SettingControl::TextList(_) => true,
592 SettingControl::Number(_) => c.is_ascii_digit() || c == '-' || c == '.',
593 _ => false,
594 })
595 })
596 .unwrap_or(false);
597
598 if can_auto_edit {
599 if let Some(dialog) = self.entry_dialog_mut() {
600 dialog.start_editing();
601 }
602 return self.handle_entry_dialog_text_editing(
604 &KeyEvent::new(KeyCode::Char(c), event.modifiers),
605 ctx,
606 );
607 }
608 }
609 _ => {}
610 }
611 InputResult::Consumed
612 }
613
614 fn handle_confirm_dialog_input(
616 &mut self,
617 event: &KeyEvent,
618 ctx: &mut InputContext,
619 ) -> InputResult {
620 match event.code {
621 KeyCode::Left | KeyCode::BackTab => {
622 if self.confirm_dialog_selection > 0 {
623 self.confirm_dialog_selection -= 1;
624 }
625 InputResult::Consumed
626 }
627 KeyCode::Right | KeyCode::Tab => {
628 if self.confirm_dialog_selection < 2 {
629 self.confirm_dialog_selection += 1;
630 }
631 InputResult::Consumed
632 }
633 KeyCode::Enter => {
634 match self.confirm_dialog_selection {
635 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
639 }
640 InputResult::Consumed
641 }
642 KeyCode::Esc => {
643 self.showing_confirm_dialog = false;
644 InputResult::Consumed
645 }
646 KeyCode::Char('s') | KeyCode::Char('S') => {
647 ctx.defer(DeferredAction::CloseSettings { save: true });
648 InputResult::Consumed
649 }
650 KeyCode::Char('d') | KeyCode::Char('D') => {
651 ctx.defer(DeferredAction::CloseSettings { save: false });
652 InputResult::Consumed
653 }
654 _ => InputResult::Consumed, }
656 }
657
658 fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
660 match event.code {
661 KeyCode::Left | KeyCode::BackTab => {
662 if self.reset_dialog_selection > 0 {
663 self.reset_dialog_selection -= 1;
664 }
665 InputResult::Consumed
666 }
667 KeyCode::Right | KeyCode::Tab => {
668 if self.reset_dialog_selection < 1 {
669 self.reset_dialog_selection += 1;
670 }
671 InputResult::Consumed
672 }
673 KeyCode::Enter => {
674 match self.reset_dialog_selection {
675 0 => {
676 self.discard_changes();
678 self.showing_reset_dialog = false;
679 }
680 1 => {
681 self.showing_reset_dialog = false;
683 }
684 _ => {}
685 }
686 InputResult::Consumed
687 }
688 KeyCode::Esc => {
689 self.showing_reset_dialog = false;
690 InputResult::Consumed
691 }
692 KeyCode::Char('r') | KeyCode::Char('R') => {
693 self.discard_changes();
694 self.showing_reset_dialog = false;
695 InputResult::Consumed
696 }
697 _ => InputResult::Consumed, }
699 }
700
701 fn handle_entry_discard_confirm_input(&mut self, event: &KeyEvent) -> InputResult {
704 match event.code {
705 KeyCode::Left | KeyCode::BackTab => {
706 if self.entry_discard_confirm_selection > 0 {
707 self.entry_discard_confirm_selection -= 1;
708 }
709 }
710 KeyCode::Right | KeyCode::Tab => {
711 if self.entry_discard_confirm_selection < 1 {
712 self.entry_discard_confirm_selection += 1;
713 }
714 }
715 KeyCode::Enter => {
716 match self.entry_discard_confirm_selection {
717 0 => {
718 self.showing_entry_discard_confirm = false;
720 }
721 1 => {
722 self.showing_entry_discard_confirm = false;
724 self.close_entry_dialog();
725 }
726 _ => {}
727 }
728 }
729 KeyCode::Esc => {
730 self.showing_entry_discard_confirm = false;
732 }
733 KeyCode::Char('d') | KeyCode::Char('D') => {
734 self.showing_entry_discard_confirm = false;
735 self.close_entry_dialog();
736 }
737 _ => {}
738 }
739 InputResult::Consumed
740 }
741
742 fn handle_entry_delete_confirm_input(&mut self, event: &KeyEvent) -> InputResult {
745 match event.code {
746 KeyCode::Left | KeyCode::BackTab => {
747 if self.entry_delete_confirm_selection > 0 {
748 self.entry_delete_confirm_selection -= 1;
749 }
750 }
751 KeyCode::Right | KeyCode::Tab => {
752 if self.entry_delete_confirm_selection < 1 {
753 self.entry_delete_confirm_selection += 1;
754 }
755 }
756 KeyCode::Enter => match self.entry_delete_confirm_selection {
757 0 => {
758 self.showing_entry_delete_confirm = false;
759 }
760 1 => {
761 self.showing_entry_delete_confirm = false;
762 self.delete_entry_dialog();
763 }
764 _ => {}
765 },
766 KeyCode::Esc => {
767 self.showing_entry_delete_confirm = false;
768 }
769 _ => {}
770 }
771 InputResult::Consumed
772 }
773
774 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
776 self.showing_help = false;
778 InputResult::Consumed
779 }
780
781 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
783 match event.code {
784 KeyCode::Esc => {
785 self.cancel_search();
786 InputResult::Consumed
787 }
788 KeyCode::Enter => {
789 self.jump_to_search_result();
790 InputResult::Consumed
791 }
792 KeyCode::Up => {
793 self.search_prev();
794 InputResult::Consumed
795 }
796 KeyCode::Down => {
797 self.search_next();
798 InputResult::Consumed
799 }
800 KeyCode::Char(c) => {
801 self.search_push_char(c);
802 InputResult::Consumed
803 }
804 KeyCode::Backspace => {
805 self.search_pop_char();
806 InputResult::Consumed
807 }
808 _ => InputResult::Consumed, }
810 }
811
812 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
814 match event.code {
815 KeyCode::Up => {
816 self.select_prev();
817 InputResult::Consumed
818 }
819 KeyCode::Down => {
820 self.select_next();
821 InputResult::Consumed
822 }
823 KeyCode::PageUp => {
824 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
826 self.tree_step(-viewport);
827 InputResult::Consumed
828 }
829 KeyCode::PageDown => {
830 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
831 self.tree_step(viewport);
832 InputResult::Consumed
833 }
834 KeyCode::Home => {
835 let rows = self.visible_tree();
836 let cur = self.tree_cursor_index(&rows) as i32;
837 if cur > 0 {
838 self.tree_step(-cur);
839 }
840 InputResult::Consumed
841 }
842 KeyCode::End => {
843 let rows = self.visible_tree();
844 let cur = self.tree_cursor_index(&rows) as i32;
845 let last = rows.len() as i32 - 1;
846 if last > cur {
847 self.tree_step(last - cur);
848 }
849 InputResult::Consumed
850 }
851 KeyCode::Tab => {
852 self.toggle_focus();
853 InputResult::Consumed
854 }
855 KeyCode::BackTab => {
856 self.toggle_focus_backward();
857 InputResult::Consumed
858 }
859 KeyCode::Char('/') => {
860 self.start_search();
861 InputResult::Consumed
862 }
863 KeyCode::Char('?') => {
864 self.toggle_help();
865 InputResult::Consumed
866 }
867 KeyCode::Esc => {
868 self.request_close(ctx);
869 InputResult::Consumed
870 }
871 KeyCode::Right => {
872 let cat_idx = self.selected_category;
875 if self.is_category_expandable(cat_idx)
876 && !self.expanded_categories.contains(&cat_idx)
877 {
878 self.expanded_categories.insert(cat_idx);
879 }
880 InputResult::Consumed
881 }
882 KeyCode::Left => {
883 let cat_idx = self.selected_category;
885 if self.expanded_categories.contains(&cat_idx) {
886 self.expanded_categories.remove(&cat_idx);
887 self.tree_cursor_section = None;
892 }
893 InputResult::Consumed
894 }
895 _ => InputResult::Ignored, }
897 }
898
899 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
901 if self.editing_text {
903 return self.handle_text_editing_input(event, ctx);
904 }
905
906 if self.is_number_editing() {
908 return self.handle_number_editing_input(event, ctx);
909 }
910
911 if self.is_dropdown_open() {
913 return self.handle_dropdown_input(event, ctx);
914 }
915
916 match event.code {
917 KeyCode::Up => {
918 self.select_prev();
919 InputResult::Consumed
920 }
921 KeyCode::Down => {
922 self.select_next();
923 InputResult::Consumed
924 }
925 KeyCode::Tab => {
926 self.toggle_focus();
927 InputResult::Consumed
928 }
929 KeyCode::BackTab => {
930 self.toggle_focus_backward();
931 InputResult::Consumed
932 }
933 KeyCode::Left => {
934 self.update_control_focus(false);
937 self.focus.set(FocusPanel::Categories);
938 InputResult::Consumed
939 }
940 KeyCode::Enter | KeyCode::Char(' ') => {
941 self.handle_control_activate(ctx);
942 InputResult::Consumed
943 }
944 KeyCode::Char(c)
947 if self.is_number_control() && (c.is_ascii_digit() || c == '-' || c == '.') =>
948 {
949 self.start_number_editing();
950 self.number_insert(c);
951 self.on_value_changed();
952 InputResult::Consumed
953 }
954 KeyCode::PageDown => {
955 self.select_next_page();
956 InputResult::Consumed
957 }
958 KeyCode::PageUp => {
959 self.select_prev_page();
960 InputResult::Consumed
961 }
962 KeyCode::Char('/') => {
963 self.start_search();
964 InputResult::Consumed
965 }
966 KeyCode::Char('?') => {
967 self.toggle_help();
968 InputResult::Consumed
969 }
970 KeyCode::Delete => {
971 self.set_current_to_null();
973 InputResult::Consumed
974 }
975 KeyCode::Esc => {
976 self.request_close(ctx);
977 InputResult::Consumed
978 }
979 _ => InputResult::Ignored, }
981 }
982
983 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
987 const FOOTER_BUTTON_COUNT: usize = 5;
988
989 match event.code {
990 KeyCode::Left | KeyCode::BackTab => {
991 if self.footer_button_index > 0 {
993 self.footer_button_index -= 1;
994 } else {
995 self.focus.set(FocusPanel::Settings);
996 }
997 InputResult::Consumed
998 }
999 KeyCode::Right => {
1000 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
1002 self.footer_button_index += 1;
1003 }
1004 InputResult::Consumed
1005 }
1006 KeyCode::Tab => {
1007 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
1009 self.footer_button_index += 1;
1010 } else {
1011 self.focus.set(FocusPanel::Categories);
1012 }
1013 InputResult::Consumed
1014 }
1015 KeyCode::Enter => {
1016 match self.footer_button_index {
1017 0 => self.cycle_target_layer(), 1 => {
1019 let is_nullable_set = self
1022 .current_item()
1023 .map(|item| item.nullable && !item.is_null)
1024 .unwrap_or(false);
1025 if is_nullable_set {
1026 self.set_current_to_null();
1027 } else {
1028 self.request_reset();
1029 }
1030 }
1031 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
1032 3 => self.request_close(ctx),
1033 4 => ctx.defer(DeferredAction::OpenConfigFile {
1034 layer: self.target_layer,
1035 }), _ => {}
1037 }
1038 InputResult::Consumed
1039 }
1040 KeyCode::Esc => {
1041 self.request_close(ctx);
1042 InputResult::Consumed
1043 }
1044 KeyCode::Char('/') => {
1045 self.start_search();
1046 InputResult::Consumed
1047 }
1048 KeyCode::Char('?') => {
1049 self.toggle_help();
1050 InputResult::Consumed
1051 }
1052 _ => InputResult::Ignored, }
1054 }
1055
1056 fn handle_text_editing_input(
1058 &mut self,
1059 event: &KeyEvent,
1060 ctx: &mut InputContext,
1061 ) -> InputResult {
1062 let is_json = self.is_editing_json();
1063
1064 if is_json {
1065 return self.handle_json_editing_input(event, ctx);
1066 }
1067
1068 if self.is_editing_dual_list() {
1070 return self.handle_dual_list_editing_input(event);
1071 }
1072
1073 match event.code {
1074 KeyCode::Esc => {
1075 if !self.can_exit_text_editing() {
1077 return InputResult::Consumed;
1078 }
1079 self.stop_editing();
1080 InputResult::Consumed
1081 }
1082 KeyCode::Enter => {
1083 self.text_add_item();
1084 InputResult::Consumed
1085 }
1086 KeyCode::Char(c) => {
1087 self.text_insert(c);
1088 InputResult::Consumed
1089 }
1090 KeyCode::Backspace => {
1091 self.text_backspace();
1092 InputResult::Consumed
1093 }
1094 KeyCode::Delete => {
1095 self.text_remove_focused();
1096 InputResult::Consumed
1097 }
1098 KeyCode::Left => {
1099 self.text_move_left();
1100 InputResult::Consumed
1101 }
1102 KeyCode::Right => {
1103 self.text_move_right();
1104 InputResult::Consumed
1105 }
1106 KeyCode::Up => {
1107 self.text_focus_prev();
1108 InputResult::Consumed
1109 }
1110 KeyCode::Down => {
1111 self.text_focus_next();
1112 InputResult::Consumed
1113 }
1114 KeyCode::Tab => {
1115 self.stop_editing();
1117 self.toggle_focus();
1118 InputResult::Consumed
1119 }
1120 _ => InputResult::Consumed, }
1122 }
1123
1124 fn handle_dual_list_editing_input(&mut self, event: &KeyEvent) -> InputResult {
1126 use crate::view::controls::DualListColumn;
1127 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1128 match event.code {
1129 KeyCode::Esc => {
1130 self.stop_editing();
1131 }
1132 KeyCode::Tab | KeyCode::BackTab => {
1134 self.stop_editing();
1135 return InputResult::Ignored;
1137 }
1138 KeyCode::Up if shift => {
1139 self.with_current_dual_list_mut(|dl| dl.move_up());
1140 self.on_value_changed();
1141 }
1142 KeyCode::Down if shift => {
1143 self.with_current_dual_list_mut(|dl| dl.move_down());
1144 self.on_value_changed();
1145 }
1146 KeyCode::Up => {
1147 self.with_current_dual_list_mut(|dl| dl.cursor_up());
1148 }
1149 KeyCode::Down => {
1150 self.with_current_dual_list_mut(|dl| dl.cursor_down());
1151 }
1152 KeyCode::Right if shift => {
1153 let changed = self
1155 .with_current_dual_list_mut(|dl| {
1156 if dl.active_column == DualListColumn::Available {
1157 dl.add_selected();
1158 dl.active_column = DualListColumn::Included;
1160 dl.included_cursor = dl.included.len().saturating_sub(1);
1161 true
1162 } else {
1163 false
1164 }
1165 })
1166 .unwrap_or(false);
1167 if changed {
1168 self.on_value_changed();
1169 self.refresh_dual_list_sibling();
1170 }
1171 }
1172 KeyCode::Left if shift => {
1173 let changed = self
1175 .with_current_dual_list_mut(|dl| {
1176 if dl.active_column == DualListColumn::Included {
1177 let value = dl.included.get(dl.included_cursor).cloned();
1178 dl.remove_selected();
1179 dl.active_column = DualListColumn::Available;
1181 if let Some(val) = value {
1182 let avail = dl.available_items();
1183 if let Some(pos) = avail.iter().position(|(v, _)| *v == val) {
1184 dl.available_cursor = pos;
1185 }
1186 }
1187 true
1188 } else {
1189 false
1190 }
1191 })
1192 .unwrap_or(false);
1193 if changed {
1194 self.on_value_changed();
1195 self.refresh_dual_list_sibling();
1196 }
1197 }
1198 KeyCode::Right => {
1199 self.with_current_dual_list_mut(|dl| {
1201 dl.active_column = DualListColumn::Included;
1202 });
1203 }
1204 KeyCode::Left => {
1205 self.with_current_dual_list_mut(|dl| {
1207 dl.active_column = DualListColumn::Available;
1208 });
1209 }
1210 KeyCode::Enter => {
1211 let changed = self
1213 .with_current_dual_list_mut(|dl| match dl.active_column {
1214 DualListColumn::Available => dl.add_selected(),
1215 DualListColumn::Included => dl.remove_selected(),
1216 })
1217 .is_some();
1218 if changed {
1219 self.on_value_changed();
1220 self.refresh_dual_list_sibling();
1221 }
1222 }
1223 _ => {}
1224 }
1225 InputResult::Consumed
1226 }
1227
1228 fn handle_json_editing_input(
1230 &mut self,
1231 event: &KeyEvent,
1232 ctx: &mut InputContext,
1233 ) -> InputResult {
1234 match event.code {
1235 KeyCode::Esc | KeyCode::Tab => {
1236 self.json_exit_editing();
1238 }
1239 KeyCode::Enter => {
1240 self.json_insert_newline();
1241 }
1242 KeyCode::Char(c) => {
1243 if event.modifiers.contains(KeyModifiers::CONTROL) {
1244 match c {
1245 'a' | 'A' => self.json_select_all(),
1246 'c' | 'C' => {
1247 if let Some(text) = self.json_selected_text() {
1248 ctx.defer(DeferredAction::CopyToClipboard(text));
1249 }
1250 }
1251 'v' | 'V' => {
1252 ctx.defer(DeferredAction::PasteToSettings);
1253 }
1254 _ => {}
1255 }
1256 } else {
1257 self.text_insert(c);
1258 }
1259 }
1260 KeyCode::Backspace => {
1261 self.text_backspace();
1262 }
1263 KeyCode::Delete => {
1264 self.json_delete();
1265 }
1266 KeyCode::Left => {
1267 if event.modifiers.contains(KeyModifiers::SHIFT) {
1268 self.json_cursor_left_selecting();
1269 } else {
1270 self.text_move_left();
1271 }
1272 }
1273 KeyCode::Right => {
1274 if event.modifiers.contains(KeyModifiers::SHIFT) {
1275 self.json_cursor_right_selecting();
1276 } else {
1277 self.text_move_right();
1278 }
1279 }
1280 KeyCode::Up => {
1281 if event.modifiers.contains(KeyModifiers::SHIFT) {
1282 self.json_cursor_up_selecting();
1283 } else {
1284 self.json_cursor_up();
1285 }
1286 }
1287 KeyCode::Down => {
1288 if event.modifiers.contains(KeyModifiers::SHIFT) {
1289 self.json_cursor_down_selecting();
1290 } else {
1291 self.json_cursor_down();
1292 }
1293 }
1294 _ => {}
1295 }
1296 InputResult::Consumed
1297 }
1298
1299 fn handle_number_editing_input(
1301 &mut self,
1302 event: &KeyEvent,
1303 _ctx: &mut InputContext,
1304 ) -> InputResult {
1305 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
1306 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1307
1308 match event.code {
1309 KeyCode::Esc => {
1310 self.number_cancel();
1311 }
1312 KeyCode::Enter => {
1313 self.number_confirm();
1314 }
1315 KeyCode::Tab | KeyCode::BackTab => {
1316 self.number_confirm();
1321 }
1322 KeyCode::Char('a') if ctrl => {
1323 self.number_select_all();
1324 }
1325 KeyCode::Char(c) => {
1326 self.number_insert(c);
1327 }
1328 KeyCode::Backspace if ctrl => {
1329 self.number_delete_word_backward();
1330 }
1331 KeyCode::Backspace => {
1332 self.number_backspace();
1333 }
1334 KeyCode::Delete if ctrl => {
1335 self.number_delete_word_forward();
1336 }
1337 KeyCode::Delete => {
1338 self.number_delete();
1339 }
1340 KeyCode::Left if ctrl && shift => {
1341 self.number_move_word_left_selecting();
1342 }
1343 KeyCode::Left if ctrl => {
1344 self.number_move_word_left();
1345 }
1346 KeyCode::Left if shift => {
1347 self.number_move_left_selecting();
1348 }
1349 KeyCode::Left => {
1350 self.number_move_left();
1351 }
1352 KeyCode::Right if ctrl && shift => {
1353 self.number_move_word_right_selecting();
1354 }
1355 KeyCode::Right if ctrl => {
1356 self.number_move_word_right();
1357 }
1358 KeyCode::Right if shift => {
1359 self.number_move_right_selecting();
1360 }
1361 KeyCode::Right => {
1362 self.number_move_right();
1363 }
1364 KeyCode::Home if shift => {
1365 self.number_move_home_selecting();
1366 }
1367 KeyCode::Home => {
1368 self.number_move_home();
1369 }
1370 KeyCode::End if shift => {
1371 self.number_move_end_selecting();
1372 }
1373 KeyCode::End => {
1374 self.number_move_end();
1375 }
1376 _ => {}
1377 }
1378 InputResult::Consumed }
1380
1381 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1383 match event.code {
1384 KeyCode::Up => {
1385 self.dropdown_prev();
1386 InputResult::Consumed
1387 }
1388 KeyCode::Down => {
1389 self.dropdown_next();
1390 InputResult::Consumed
1391 }
1392 KeyCode::Home => {
1393 self.dropdown_home();
1394 InputResult::Consumed
1395 }
1396 KeyCode::End => {
1397 self.dropdown_end();
1398 InputResult::Consumed
1399 }
1400 KeyCode::Enter => {
1401 self.dropdown_confirm();
1402 InputResult::Consumed
1403 }
1404 KeyCode::Esc => {
1405 self.dropdown_cancel();
1406 InputResult::Consumed
1407 }
1408 _ => InputResult::Consumed, }
1410 }
1411
1412 fn request_reset(&mut self) {
1414 if self.has_changes() {
1415 self.showing_reset_dialog = true;
1416 self.reset_dialog_selection = 0;
1417 }
1418 }
1419
1420 fn request_close(&mut self, ctx: &mut InputContext) {
1422 if self.has_changes() {
1423 self.showing_confirm_dialog = true;
1424 self.confirm_dialog_selection = 0;
1425 } else {
1426 ctx.defer(DeferredAction::CloseSettings { save: false });
1427 }
1428 }
1429
1430 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1432 if let Some(item) = self.current_item_mut() {
1433 match &mut item.control {
1434 SettingControl::Toggle(ref mut state) => {
1435 state.checked = !state.checked;
1436 self.on_value_changed();
1437 }
1438 SettingControl::Dropdown(_) => {
1439 self.dropdown_toggle();
1440 }
1441 SettingControl::Number(_) => {
1442 self.start_number_editing();
1443 }
1444 SettingControl::Text(_) => {
1445 self.start_editing();
1446 }
1447 SettingControl::TextList(_) | SettingControl::DualList(_) => {
1448 self.start_editing();
1449 }
1450 SettingControl::Map(ref mut state) => {
1451 if state.focused_entry.is_none() {
1452 if state.value_schema.is_some() {
1454 self.open_add_entry_dialog();
1455 }
1456 } else if state.value_schema.is_some() {
1457 self.open_entry_dialog();
1459 } else {
1460 if let Some(idx) = state.focused_entry {
1462 if state.expanded.contains(&idx) {
1463 state.expanded.retain(|&i| i != idx);
1464 } else {
1465 state.expanded.push(idx);
1466 }
1467 }
1468 }
1469 self.on_value_changed();
1470 }
1471 SettingControl::Json(_) => {
1472 self.start_editing();
1473 }
1474 SettingControl::ObjectArray(ref state) => {
1475 if state.focused_index.is_none() {
1476 if state.item_schema.is_some() {
1478 self.open_add_array_item_dialog();
1479 }
1480 } else if state.item_schema.is_some() {
1481 self.open_edit_array_item_dialog();
1483 }
1484 }
1485 SettingControl::Complex { .. } => {
1486 }
1488 }
1489 }
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1497
1498 fn key(code: KeyCode) -> KeyEvent {
1499 KeyEvent::new(code, KeyModifiers::NONE)
1500 }
1501
1502 #[test]
1503 fn test_settings_is_modal() {
1504 let schema = include_str!("../../../plugins/config-schema.json");
1506 let config = crate::config::Config::default();
1507 let state = SettingsState::new(schema, &config).unwrap();
1508 assert!(state.is_modal());
1509 }
1510
1511 #[test]
1512 fn test_categories_panel_does_not_leak_to_settings() {
1513 let schema = include_str!("../../../plugins/config-schema.json");
1514 let config = crate::config::Config::default();
1515 let mut state = SettingsState::new(schema, &config).unwrap();
1516 state.visible = true;
1517 state.focus.set(FocusPanel::Categories);
1518
1519 let mut ctx = InputContext::new();
1520
1521 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1530 assert_eq!(result, InputResult::Ignored);
1531 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1532
1533 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1534 assert_eq!(result, InputResult::Consumed);
1535 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1536
1537 let result = state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1538 assert_eq!(result, InputResult::Consumed);
1539 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1540
1541 let result = state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1543 assert_eq!(result, InputResult::Consumed);
1544 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1545 }
1546
1547 #[test]
1548 fn test_tab_cycles_focus_panels() {
1549 let schema = include_str!("../../../plugins/config-schema.json");
1550 let config = crate::config::Config::default();
1551 let mut state = SettingsState::new(schema, &config).unwrap();
1552 state.visible = true;
1553
1554 let mut ctx = InputContext::new();
1555
1556 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1558
1559 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1561 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1562
1563 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1565 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1566 assert_eq!(state.footer_button_index, 0);
1567
1568 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1570 assert_eq!(state.footer_button_index, 1);
1571 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1572 assert_eq!(state.footer_button_index, 2);
1573 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1574 assert_eq!(state.footer_button_index, 3);
1575 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1576 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1578 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1579
1580 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1583 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1584
1585 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1587 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1588 assert_eq!(
1589 state.footer_button_index, 0,
1590 "Footer should reset to Layer button (index 0) on second loop"
1591 );
1592 }
1593
1594 #[test]
1595 fn test_escape_shows_confirm_dialog_with_changes() {
1596 let schema = include_str!("../../../plugins/config-schema.json");
1597 let config = crate::config::Config::default();
1598 let mut state = SettingsState::new(schema, &config).unwrap();
1599 state.visible = true;
1600
1601 state
1603 .pending_changes
1604 .insert("/test".to_string(), serde_json::json!(true));
1605
1606 let mut ctx = InputContext::new();
1607
1608 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1610 assert!(state.showing_confirm_dialog);
1611 assert!(ctx.deferred_actions.is_empty()); }
1613
1614 #[test]
1615 fn test_escape_closes_directly_without_changes() {
1616 let schema = include_str!("../../../plugins/config-schema.json");
1617 let config = crate::config::Config::default();
1618 let mut state = SettingsState::new(schema, &config).unwrap();
1619 state.visible = true;
1620
1621 let mut ctx = InputContext::new();
1622
1623 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1625 assert!(!state.showing_confirm_dialog);
1626 assert_eq!(ctx.deferred_actions.len(), 1);
1627 assert!(matches!(
1628 ctx.deferred_actions[0],
1629 DeferredAction::CloseSettings { save: false }
1630 ));
1631 }
1632
1633 #[test]
1634 fn test_confirm_dialog_navigation() {
1635 let schema = include_str!("../../../plugins/config-schema.json");
1636 let config = crate::config::Config::default();
1637 let mut state = SettingsState::new(schema, &config).unwrap();
1638 state.visible = true;
1639 state.showing_confirm_dialog = true;
1640 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1643
1644 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1646 assert_eq!(state.confirm_dialog_selection, 1);
1647
1648 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1650 assert_eq!(state.confirm_dialog_selection, 2);
1651
1652 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1654 assert_eq!(state.confirm_dialog_selection, 2);
1655
1656 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1658 assert_eq!(state.confirm_dialog_selection, 1);
1659 }
1660
1661 #[test]
1662 fn test_search_mode_captures_typing() {
1663 let schema = include_str!("../../../plugins/config-schema.json");
1664 let config = crate::config::Config::default();
1665 let mut state = SettingsState::new(schema, &config).unwrap();
1666 state.visible = true;
1667
1668 let mut ctx = InputContext::new();
1669
1670 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1672 assert!(state.search_active);
1673
1674 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1676 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1677 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1678 assert_eq!(state.search_query, "tab");
1679
1680 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1682 assert!(!state.search_active);
1683 assert!(state.search_query.is_empty());
1684 }
1685
1686 #[test]
1687 fn test_footer_button_activation() {
1688 let schema = include_str!("../../../plugins/config-schema.json");
1689 let config = crate::config::Config::default();
1690 let mut state = SettingsState::new(schema, &config).unwrap();
1691 state.visible = true;
1692 state.focus.set(FocusPanel::Footer);
1693 state.footer_button_index = 2; let mut ctx = InputContext::new();
1696
1697 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1699 assert_eq!(ctx.deferred_actions.len(), 1);
1700 assert!(matches!(
1701 ctx.deferred_actions[0],
1702 DeferredAction::CloseSettings { save: true }
1703 ));
1704 }
1705
1706 #[test]
1711 fn test_tab_exits_number_editing() {
1712 use crate::view::settings::items::SettingControl;
1713
1714 let schema = include_str!("../../../plugins/config-schema.json");
1715 let config = crate::config::Config::default();
1716 let mut state = SettingsState::new(schema, &config).unwrap();
1717 state.visible = true;
1718 state.focus.set(FocusPanel::Settings);
1719
1720 let number_idx = state
1722 .pages
1723 .get(state.selected_category)
1724 .and_then(|page| {
1725 page.items
1726 .iter()
1727 .position(|item| matches!(item.control, SettingControl::Number(_)))
1728 })
1729 .expect("expected at least one Number control on the default page");
1730 state.selected_item = number_idx;
1731
1732 state.start_number_editing();
1734 assert!(
1735 state.is_number_editing(),
1736 "precondition: should be in number-editing mode"
1737 );
1738 state.number_insert('7');
1739
1740 let mut ctx = InputContext::new();
1741
1742 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1744 assert!(
1745 !state.is_number_editing(),
1746 "Tab while editing a Number control must exit editing mode"
1747 );
1748 }
1749}