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