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.has_entry_dialog() {
30 return self.handle_entry_dialog_input(event, ctx);
31 }
32
33 if self.showing_confirm_dialog {
35 return self.handle_confirm_dialog_input(event, ctx);
36 }
37
38 if self.showing_reset_dialog {
40 return self.handle_reset_dialog_input(event);
41 }
42
43 if self.showing_help {
45 return self.handle_help_input(event, ctx);
46 }
47
48 if self.search_active {
50 return self.handle_search_input(event, ctx);
51 }
52
53 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 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 }
72}
73
74impl SettingsState {
75 fn handle_entry_dialog_input(
82 &mut self,
83 event: &KeyEvent,
84 ctx: &mut InputContext,
85 ) -> InputResult {
86 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 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 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 fn handle_entry_dialog_text_editing(
117 &mut self,
118 event: &KeyEvent,
119 ctx: &mut InputContext,
120 ) -> InputResult {
121 let is_editing_json = self
123 .entry_dialog()
124 .map(|d| d.is_editing_json())
125 .unwrap_or(false);
126
127 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 if !can_exit {
138 }
140 dialog.stop_editing();
141 }
142 KeyCode::Enter => {
143 if is_editing_json {
144 dialog.insert_newline();
146 } else {
147 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 dialog.select_all();
161 }
162 'c' | 'C' => {
163 if let Some(text) = dialog.selected_text() {
165 ctx.defer(DeferredAction::CopyToClipboard(text));
166 }
167 }
168 'v' | 'V' => {
169 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 dialog.delete();
185 } else {
186 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 if event.modifiers.contains(KeyModifiers::SHIFT) {
214 dialog.cursor_up_selecting();
215 } else {
216 dialog.cursor_up();
217 }
218 } else {
219 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 if event.modifiers.contains(KeyModifiers::SHIFT) {
232 dialog.cursor_down_selecting();
233 } else {
234 dialog.cursor_down();
235 }
236 } else {
237 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 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 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 } else {
271 if let Some(item) = dialog.current_item_mut() {
273 if let SettingControl::TextList(state) = &mut item.control {
274 state.add_item();
275 }
276 }
277 dialog.stop_editing();
279 }
280 }
281 _ => {}
282 }
283 InputResult::Consumed
284 }
285
286 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(); }
305 _ => {}
306 }
307 InputResult::Consumed
308 }
309
310 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 if let Some(dialog) = self.entry_dialog_mut() {
333 dialog.focus_next();
334 }
335 }
336 KeyCode::BackTab => {
337 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 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 self.save_entry_dialog();
389 } else {
390 let control_action = self
392 .entry_dialog()
393 .and_then(|dialog| {
394 dialog.current_item().map(|item| match &item.control {
395 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
396 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
397 SettingControl::Text(_)
398 | SettingControl::TextList(_)
399 | SettingControl::Number(_)
400 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
401 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
402 Some(ControlAction::OpenNestedDialog)
403 }
404 _ => None,
405 })
406 })
407 .flatten();
408
409 if let Some(action) = control_action {
410 match action {
411 ControlAction::ToggleBool => {
412 if let Some(dialog) = self.entry_dialog_mut() {
413 dialog.toggle_bool();
414 }
415 }
416 ControlAction::ToggleDropdown => {
417 if let Some(dialog) = self.entry_dialog_mut() {
418 dialog.toggle_dropdown();
419 }
420 }
421 ControlAction::StartEditing => {
422 if let Some(dialog) = self.entry_dialog_mut() {
423 dialog.start_editing();
424 }
425 }
426 ControlAction::OpenNestedDialog => {
427 self.open_nested_entry_dialog();
428 }
429 }
430 }
431 }
432 }
433 KeyCode::Char(' ') => {
434 let control_action = self.entry_dialog().and_then(|dialog| {
436 if dialog.focus_on_buttons {
437 return None; }
439 dialog.current_item().and_then(|item| match &item.control {
440 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
441 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
442 _ => None,
443 })
444 });
445
446 if let Some(action) = control_action {
447 match action {
448 ControlAction::ToggleBool => {
449 if let Some(dialog) = self.entry_dialog_mut() {
450 dialog.toggle_bool();
451 }
452 }
453 ControlAction::ToggleDropdown => {
454 if let Some(dialog) = self.entry_dialog_mut() {
455 dialog.toggle_dropdown();
456 }
457 }
458 _ => {}
459 }
460 }
461 }
462 KeyCode::Char(c) => {
463 let can_auto_edit = self
465 .entry_dialog()
466 .and_then(|dialog| {
467 if dialog.focus_on_buttons {
468 return None;
469 }
470 dialog.current_item().map(|item| match &item.control {
471 SettingControl::Text(_) | SettingControl::TextList(_) => true,
472 SettingControl::Number(_) => c.is_ascii_digit() || c == '-' || c == '.',
473 _ => false,
474 })
475 })
476 .unwrap_or(false);
477
478 if can_auto_edit {
479 if let Some(dialog) = self.entry_dialog_mut() {
480 dialog.start_editing();
481 }
482 return self.handle_entry_dialog_text_editing(
484 &KeyEvent::new(KeyCode::Char(c), event.modifiers),
485 ctx,
486 );
487 }
488 }
489 _ => {}
490 }
491 InputResult::Consumed
492 }
493
494 fn handle_confirm_dialog_input(
496 &mut self,
497 event: &KeyEvent,
498 ctx: &mut InputContext,
499 ) -> InputResult {
500 match event.code {
501 KeyCode::Left | KeyCode::BackTab => {
502 if self.confirm_dialog_selection > 0 {
503 self.confirm_dialog_selection -= 1;
504 }
505 InputResult::Consumed
506 }
507 KeyCode::Right | KeyCode::Tab => {
508 if self.confirm_dialog_selection < 2 {
509 self.confirm_dialog_selection += 1;
510 }
511 InputResult::Consumed
512 }
513 KeyCode::Enter => {
514 match self.confirm_dialog_selection {
515 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
519 }
520 InputResult::Consumed
521 }
522 KeyCode::Esc => {
523 self.showing_confirm_dialog = false;
524 InputResult::Consumed
525 }
526 KeyCode::Char('s') | KeyCode::Char('S') => {
527 ctx.defer(DeferredAction::CloseSettings { save: true });
528 InputResult::Consumed
529 }
530 KeyCode::Char('d') | KeyCode::Char('D') => {
531 ctx.defer(DeferredAction::CloseSettings { save: false });
532 InputResult::Consumed
533 }
534 _ => InputResult::Consumed, }
536 }
537
538 fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
540 match event.code {
541 KeyCode::Left | KeyCode::BackTab => {
542 if self.reset_dialog_selection > 0 {
543 self.reset_dialog_selection -= 1;
544 }
545 InputResult::Consumed
546 }
547 KeyCode::Right | KeyCode::Tab => {
548 if self.reset_dialog_selection < 1 {
549 self.reset_dialog_selection += 1;
550 }
551 InputResult::Consumed
552 }
553 KeyCode::Enter => {
554 match self.reset_dialog_selection {
555 0 => {
556 self.discard_changes();
558 self.showing_reset_dialog = false;
559 }
560 1 => {
561 self.showing_reset_dialog = false;
563 }
564 _ => {}
565 }
566 InputResult::Consumed
567 }
568 KeyCode::Esc => {
569 self.showing_reset_dialog = false;
570 InputResult::Consumed
571 }
572 KeyCode::Char('r') | KeyCode::Char('R') => {
573 self.discard_changes();
574 self.showing_reset_dialog = false;
575 InputResult::Consumed
576 }
577 _ => InputResult::Consumed, }
579 }
580
581 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
583 self.showing_help = false;
585 InputResult::Consumed
586 }
587
588 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
590 match event.code {
591 KeyCode::Esc => {
592 self.cancel_search();
593 InputResult::Consumed
594 }
595 KeyCode::Enter => {
596 self.jump_to_search_result();
597 InputResult::Consumed
598 }
599 KeyCode::Up => {
600 self.search_prev();
601 InputResult::Consumed
602 }
603 KeyCode::Down => {
604 self.search_next();
605 InputResult::Consumed
606 }
607 KeyCode::Char(c) => {
608 self.search_push_char(c);
609 InputResult::Consumed
610 }
611 KeyCode::Backspace => {
612 self.search_pop_char();
613 InputResult::Consumed
614 }
615 _ => InputResult::Consumed, }
617 }
618
619 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
621 match event.code {
622 KeyCode::Up => {
623 self.select_prev();
624 InputResult::Consumed
625 }
626 KeyCode::Down => {
627 self.select_next();
628 InputResult::Consumed
629 }
630 KeyCode::Tab => {
631 self.toggle_focus();
632 InputResult::Consumed
633 }
634 KeyCode::BackTab => {
635 self.toggle_focus_backward();
636 InputResult::Consumed
637 }
638 KeyCode::Char('/') => {
639 self.start_search();
640 InputResult::Consumed
641 }
642 KeyCode::Char('?') => {
643 self.toggle_help();
644 InputResult::Consumed
645 }
646 KeyCode::Esc => {
647 self.request_close(ctx);
648 InputResult::Consumed
649 }
650 KeyCode::Enter | KeyCode::Right => {
651 self.focus.set(FocusPanel::Settings);
653 InputResult::Consumed
654 }
655 _ => InputResult::Ignored, }
657 }
658
659 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
661 if self.editing_text {
663 return self.handle_text_editing_input(event, ctx);
664 }
665
666 if self.is_number_editing() {
668 return self.handle_number_editing_input(event, ctx);
669 }
670
671 if self.is_dropdown_open() {
673 return self.handle_dropdown_input(event, ctx);
674 }
675
676 match event.code {
677 KeyCode::Up => {
678 self.select_prev();
679 InputResult::Consumed
680 }
681 KeyCode::Down => {
682 self.select_next();
683 InputResult::Consumed
684 }
685 KeyCode::Tab => {
686 self.toggle_focus();
687 InputResult::Consumed
688 }
689 KeyCode::BackTab => {
690 self.toggle_focus_backward();
691 InputResult::Consumed
692 }
693 KeyCode::Left => {
694 if self.is_number_control() {
697 self.handle_control_decrement();
698 } else {
699 self.update_control_focus(false);
700 self.focus.set(FocusPanel::Categories);
701 }
702 InputResult::Consumed
703 }
704 KeyCode::Right => {
705 self.handle_control_increment();
706 InputResult::Consumed
707 }
708 KeyCode::Enter | KeyCode::Char(' ') => {
709 self.handle_control_activate(ctx);
710 InputResult::Consumed
711 }
712 KeyCode::PageDown => {
713 self.select_next_page();
714 InputResult::Consumed
715 }
716 KeyCode::PageUp => {
717 self.select_prev_page();
718 InputResult::Consumed
719 }
720 KeyCode::Char('/') => {
721 self.start_search();
722 InputResult::Consumed
723 }
724 KeyCode::Char('?') => {
725 self.toggle_help();
726 InputResult::Consumed
727 }
728 KeyCode::Delete => {
729 self.set_current_to_null();
731 InputResult::Consumed
732 }
733 KeyCode::Esc => {
734 self.request_close(ctx);
735 InputResult::Consumed
736 }
737 _ => InputResult::Ignored, }
739 }
740
741 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
745 const FOOTER_BUTTON_COUNT: usize = 5;
746
747 match event.code {
748 KeyCode::Left | KeyCode::BackTab => {
749 if self.footer_button_index > 0 {
751 self.footer_button_index -= 1;
752 } else {
753 self.focus.set(FocusPanel::Settings);
754 }
755 InputResult::Consumed
756 }
757 KeyCode::Right => {
758 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
760 self.footer_button_index += 1;
761 }
762 InputResult::Consumed
763 }
764 KeyCode::Tab => {
765 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
767 self.footer_button_index += 1;
768 } else {
769 self.focus.set(FocusPanel::Categories);
770 }
771 InputResult::Consumed
772 }
773 KeyCode::Enter => {
774 match self.footer_button_index {
775 0 => self.cycle_target_layer(), 1 => {
777 let is_nullable_set = self
780 .current_item()
781 .map(|item| item.nullable && !item.is_null)
782 .unwrap_or(false);
783 if is_nullable_set {
784 self.set_current_to_null();
785 } else {
786 self.request_reset();
787 }
788 }
789 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
790 3 => self.request_close(ctx),
791 4 => ctx.defer(DeferredAction::OpenConfigFile {
792 layer: self.target_layer,
793 }), _ => {}
795 }
796 InputResult::Consumed
797 }
798 KeyCode::Esc => {
799 self.request_close(ctx);
800 InputResult::Consumed
801 }
802 KeyCode::Char('/') => {
803 self.start_search();
804 InputResult::Consumed
805 }
806 KeyCode::Char('?') => {
807 self.toggle_help();
808 InputResult::Consumed
809 }
810 _ => InputResult::Ignored, }
812 }
813
814 fn handle_text_editing_input(
816 &mut self,
817 event: &KeyEvent,
818 ctx: &mut InputContext,
819 ) -> InputResult {
820 let is_json = self.is_editing_json();
821
822 if is_json {
823 return self.handle_json_editing_input(event, ctx);
824 }
825
826 match event.code {
827 KeyCode::Esc => {
828 if !self.can_exit_text_editing() {
830 return InputResult::Consumed;
831 }
832 self.stop_editing();
833 InputResult::Consumed
834 }
835 KeyCode::Enter => {
836 self.text_add_item();
837 InputResult::Consumed
838 }
839 KeyCode::Char(c) => {
840 self.text_insert(c);
841 InputResult::Consumed
842 }
843 KeyCode::Backspace => {
844 self.text_backspace();
845 InputResult::Consumed
846 }
847 KeyCode::Delete => {
848 self.text_remove_focused();
849 InputResult::Consumed
850 }
851 KeyCode::Left => {
852 self.text_move_left();
853 InputResult::Consumed
854 }
855 KeyCode::Right => {
856 self.text_move_right();
857 InputResult::Consumed
858 }
859 KeyCode::Up => {
860 self.text_focus_prev();
861 InputResult::Consumed
862 }
863 KeyCode::Down => {
864 self.text_focus_next();
865 InputResult::Consumed
866 }
867 KeyCode::Tab => {
868 self.stop_editing();
870 self.toggle_focus();
871 InputResult::Consumed
872 }
873 _ => InputResult::Consumed, }
875 }
876
877 fn handle_json_editing_input(
879 &mut self,
880 event: &KeyEvent,
881 ctx: &mut InputContext,
882 ) -> InputResult {
883 match event.code {
884 KeyCode::Esc | KeyCode::Tab => {
885 self.json_exit_editing();
887 }
888 KeyCode::Enter => {
889 self.json_insert_newline();
890 }
891 KeyCode::Char(c) => {
892 if event.modifiers.contains(KeyModifiers::CONTROL) {
893 match c {
894 'a' | 'A' => self.json_select_all(),
895 'c' | 'C' => {
896 if let Some(text) = self.json_selected_text() {
897 ctx.defer(DeferredAction::CopyToClipboard(text));
898 }
899 }
900 'v' | 'V' => {
901 ctx.defer(DeferredAction::PasteToSettings);
902 }
903 _ => {}
904 }
905 } else {
906 self.text_insert(c);
907 }
908 }
909 KeyCode::Backspace => {
910 self.text_backspace();
911 }
912 KeyCode::Delete => {
913 self.json_delete();
914 }
915 KeyCode::Left => {
916 if event.modifiers.contains(KeyModifiers::SHIFT) {
917 self.json_cursor_left_selecting();
918 } else {
919 self.text_move_left();
920 }
921 }
922 KeyCode::Right => {
923 if event.modifiers.contains(KeyModifiers::SHIFT) {
924 self.json_cursor_right_selecting();
925 } else {
926 self.text_move_right();
927 }
928 }
929 KeyCode::Up => {
930 if event.modifiers.contains(KeyModifiers::SHIFT) {
931 self.json_cursor_up_selecting();
932 } else {
933 self.json_cursor_up();
934 }
935 }
936 KeyCode::Down => {
937 if event.modifiers.contains(KeyModifiers::SHIFT) {
938 self.json_cursor_down_selecting();
939 } else {
940 self.json_cursor_down();
941 }
942 }
943 _ => {}
944 }
945 InputResult::Consumed
946 }
947
948 fn handle_number_editing_input(
950 &mut self,
951 event: &KeyEvent,
952 _ctx: &mut InputContext,
953 ) -> InputResult {
954 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
955 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
956
957 match event.code {
958 KeyCode::Esc => {
959 self.number_cancel();
960 }
961 KeyCode::Enter => {
962 self.number_confirm();
963 }
964 KeyCode::Char('a') if ctrl => {
965 self.number_select_all();
966 }
967 KeyCode::Char(c) => {
968 self.number_insert(c);
969 }
970 KeyCode::Backspace if ctrl => {
971 self.number_delete_word_backward();
972 }
973 KeyCode::Backspace => {
974 self.number_backspace();
975 }
976 KeyCode::Delete if ctrl => {
977 self.number_delete_word_forward();
978 }
979 KeyCode::Delete => {
980 self.number_delete();
981 }
982 KeyCode::Left if ctrl && shift => {
983 self.number_move_word_left_selecting();
984 }
985 KeyCode::Left if ctrl => {
986 self.number_move_word_left();
987 }
988 KeyCode::Left if shift => {
989 self.number_move_left_selecting();
990 }
991 KeyCode::Left => {
992 self.number_move_left();
993 }
994 KeyCode::Right if ctrl && shift => {
995 self.number_move_word_right_selecting();
996 }
997 KeyCode::Right if ctrl => {
998 self.number_move_word_right();
999 }
1000 KeyCode::Right if shift => {
1001 self.number_move_right_selecting();
1002 }
1003 KeyCode::Right => {
1004 self.number_move_right();
1005 }
1006 KeyCode::Home if shift => {
1007 self.number_move_home_selecting();
1008 }
1009 KeyCode::Home => {
1010 self.number_move_home();
1011 }
1012 KeyCode::End if shift => {
1013 self.number_move_end_selecting();
1014 }
1015 KeyCode::End => {
1016 self.number_move_end();
1017 }
1018 _ => {}
1019 }
1020 InputResult::Consumed }
1022
1023 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1025 match event.code {
1026 KeyCode::Up => {
1027 self.dropdown_prev();
1028 InputResult::Consumed
1029 }
1030 KeyCode::Down => {
1031 self.dropdown_next();
1032 InputResult::Consumed
1033 }
1034 KeyCode::Home => {
1035 self.dropdown_home();
1036 InputResult::Consumed
1037 }
1038 KeyCode::End => {
1039 self.dropdown_end();
1040 InputResult::Consumed
1041 }
1042 KeyCode::Enter => {
1043 self.dropdown_confirm();
1044 InputResult::Consumed
1045 }
1046 KeyCode::Esc => {
1047 self.dropdown_cancel();
1048 InputResult::Consumed
1049 }
1050 _ => InputResult::Consumed, }
1052 }
1053
1054 fn request_reset(&mut self) {
1056 if self.has_changes() {
1057 self.showing_reset_dialog = true;
1058 self.reset_dialog_selection = 0;
1059 }
1060 }
1061
1062 fn request_close(&mut self, ctx: &mut InputContext) {
1064 if self.has_changes() {
1065 self.showing_confirm_dialog = true;
1066 self.confirm_dialog_selection = 0;
1067 } else {
1068 ctx.defer(DeferredAction::CloseSettings { save: false });
1069 }
1070 }
1071
1072 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1074 if let Some(item) = self.current_item_mut() {
1075 match &mut item.control {
1076 SettingControl::Toggle(ref mut state) => {
1077 state.checked = !state.checked;
1078 self.on_value_changed();
1079 }
1080 SettingControl::Dropdown(_) => {
1081 self.dropdown_toggle();
1082 }
1083 SettingControl::Number(_) => {
1084 self.start_number_editing();
1085 }
1086 SettingControl::Text(_) => {
1087 self.start_editing();
1088 }
1089 SettingControl::TextList(_) => {
1090 self.start_editing();
1091 }
1092 SettingControl::Map(ref mut state) => {
1093 if state.focused_entry.is_none() {
1094 if state.value_schema.is_some() {
1096 self.open_add_entry_dialog();
1097 }
1098 } else if state.value_schema.is_some() {
1099 self.open_entry_dialog();
1101 } else {
1102 if let Some(idx) = state.focused_entry {
1104 if state.expanded.contains(&idx) {
1105 state.expanded.retain(|&i| i != idx);
1106 } else {
1107 state.expanded.push(idx);
1108 }
1109 }
1110 }
1111 self.on_value_changed();
1112 }
1113 SettingControl::Json(_) => {
1114 self.start_editing();
1115 }
1116 SettingControl::ObjectArray(ref state) => {
1117 if state.focused_index.is_none() {
1118 if state.item_schema.is_some() {
1120 self.open_add_array_item_dialog();
1121 }
1122 } else if state.item_schema.is_some() {
1123 self.open_edit_array_item_dialog();
1125 }
1126 }
1127 SettingControl::Complex { .. } => {
1128 }
1130 }
1131 }
1132 }
1133
1134 fn handle_control_increment(&mut self) {
1136 if let Some(item) = self.current_item_mut() {
1137 if let SettingControl::Number(ref mut state) = &mut item.control {
1138 state.value += 1;
1139 if let Some(max) = state.max {
1140 state.value = state.value.min(max);
1141 }
1142 self.on_value_changed();
1143 }
1144 }
1145 }
1146
1147 fn handle_control_decrement(&mut self) {
1149 if let Some(item) = self.current_item_mut() {
1150 if let SettingControl::Number(ref mut state) = &mut item.control {
1151 if state.value > 0 {
1152 state.value -= 1;
1153 }
1154 if let Some(min) = state.min {
1155 state.value = state.value.max(min);
1156 }
1157 self.on_value_changed();
1158 }
1159 }
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1167
1168 fn key(code: KeyCode) -> KeyEvent {
1169 KeyEvent::new(code, KeyModifiers::NONE)
1170 }
1171
1172 #[test]
1173 fn test_settings_is_modal() {
1174 let schema = include_str!("../../../plugins/config-schema.json");
1176 let config = crate::config::Config::default();
1177 let state = SettingsState::new(schema, &config).unwrap();
1178 assert!(state.is_modal());
1179 }
1180
1181 #[test]
1182 fn test_categories_panel_does_not_leak_to_settings() {
1183 let schema = include_str!("../../../plugins/config-schema.json");
1184 let config = crate::config::Config::default();
1185 let mut state = SettingsState::new(schema, &config).unwrap();
1186 state.visible = true;
1187 state.focus.set(FocusPanel::Categories);
1188
1189 let mut ctx = InputContext::new();
1190
1191 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1194 assert_eq!(result, InputResult::Consumed);
1195 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1196
1197 state.focus.set(FocusPanel::Categories);
1199
1200 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1202 assert_eq!(result, InputResult::Consumed);
1203 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1205 }
1206
1207 #[test]
1208 fn test_tab_cycles_focus_panels() {
1209 let schema = include_str!("../../../plugins/config-schema.json");
1210 let config = crate::config::Config::default();
1211 let mut state = SettingsState::new(schema, &config).unwrap();
1212 state.visible = true;
1213
1214 let mut ctx = InputContext::new();
1215
1216 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1218
1219 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1221 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1222
1223 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1225 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1226 assert_eq!(state.footer_button_index, 0);
1227
1228 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1230 assert_eq!(state.footer_button_index, 1);
1231 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1232 assert_eq!(state.footer_button_index, 2);
1233 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1234 assert_eq!(state.footer_button_index, 3);
1235 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1236 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1238 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1239
1240 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1243 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1244
1245 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1247 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1248 assert_eq!(
1249 state.footer_button_index, 0,
1250 "Footer should reset to Layer button (index 0) on second loop"
1251 );
1252 }
1253
1254 #[test]
1255 fn test_escape_shows_confirm_dialog_with_changes() {
1256 let schema = include_str!("../../../plugins/config-schema.json");
1257 let config = crate::config::Config::default();
1258 let mut state = SettingsState::new(schema, &config).unwrap();
1259 state.visible = true;
1260
1261 state
1263 .pending_changes
1264 .insert("/test".to_string(), serde_json::json!(true));
1265
1266 let mut ctx = InputContext::new();
1267
1268 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1270 assert!(state.showing_confirm_dialog);
1271 assert!(ctx.deferred_actions.is_empty()); }
1273
1274 #[test]
1275 fn test_escape_closes_directly_without_changes() {
1276 let schema = include_str!("../../../plugins/config-schema.json");
1277 let config = crate::config::Config::default();
1278 let mut state = SettingsState::new(schema, &config).unwrap();
1279 state.visible = true;
1280
1281 let mut ctx = InputContext::new();
1282
1283 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1285 assert!(!state.showing_confirm_dialog);
1286 assert_eq!(ctx.deferred_actions.len(), 1);
1287 assert!(matches!(
1288 ctx.deferred_actions[0],
1289 DeferredAction::CloseSettings { save: false }
1290 ));
1291 }
1292
1293 #[test]
1294 fn test_confirm_dialog_navigation() {
1295 let schema = include_str!("../../../plugins/config-schema.json");
1296 let config = crate::config::Config::default();
1297 let mut state = SettingsState::new(schema, &config).unwrap();
1298 state.visible = true;
1299 state.showing_confirm_dialog = true;
1300 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1303
1304 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1306 assert_eq!(state.confirm_dialog_selection, 1);
1307
1308 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1310 assert_eq!(state.confirm_dialog_selection, 2);
1311
1312 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1314 assert_eq!(state.confirm_dialog_selection, 2);
1315
1316 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1318 assert_eq!(state.confirm_dialog_selection, 1);
1319 }
1320
1321 #[test]
1322 fn test_search_mode_captures_typing() {
1323 let schema = include_str!("../../../plugins/config-schema.json");
1324 let config = crate::config::Config::default();
1325 let mut state = SettingsState::new(schema, &config).unwrap();
1326 state.visible = true;
1327
1328 let mut ctx = InputContext::new();
1329
1330 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1332 assert!(state.search_active);
1333
1334 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1336 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1337 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1338 assert_eq!(state.search_query, "tab");
1339
1340 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1342 assert!(!state.search_active);
1343 assert!(state.search_query.is_empty());
1344 }
1345
1346 #[test]
1347 fn test_footer_button_activation() {
1348 let schema = include_str!("../../../plugins/config-schema.json");
1349 let config = crate::config::Config::default();
1350 let mut state = SettingsState::new(schema, &config).unwrap();
1351 state.visible = true;
1352 state.focus.set(FocusPanel::Footer);
1353 state.footer_button_index = 2; let mut ctx = InputContext::new();
1356
1357 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1359 assert_eq!(ctx.deferred_actions.len(), 1);
1360 assert!(matches!(
1361 ctx.deferred_actions[0],
1362 DeferredAction::CloseSettings { save: true }
1363 ));
1364 }
1365}