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::Esc => {
729 self.request_close(ctx);
730 InputResult::Consumed
731 }
732 _ => InputResult::Ignored, }
734 }
735
736 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
740 const FOOTER_BUTTON_COUNT: usize = 5;
741
742 match event.code {
743 KeyCode::Left | KeyCode::BackTab => {
744 if self.footer_button_index > 0 {
746 self.footer_button_index -= 1;
747 } else {
748 self.focus.set(FocusPanel::Settings);
749 }
750 InputResult::Consumed
751 }
752 KeyCode::Right => {
753 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
755 self.footer_button_index += 1;
756 }
757 InputResult::Consumed
758 }
759 KeyCode::Tab => {
760 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
762 self.footer_button_index += 1;
763 } else {
764 self.focus.set(FocusPanel::Categories);
765 }
766 InputResult::Consumed
767 }
768 KeyCode::Enter => {
769 match self.footer_button_index {
770 0 => self.cycle_target_layer(), 1 => self.request_reset(),
772 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
773 3 => self.request_close(ctx),
774 4 => ctx.defer(DeferredAction::OpenConfigFile {
775 layer: self.target_layer,
776 }), _ => {}
778 }
779 InputResult::Consumed
780 }
781 KeyCode::Esc => {
782 self.request_close(ctx);
783 InputResult::Consumed
784 }
785 KeyCode::Char('/') => {
786 self.start_search();
787 InputResult::Consumed
788 }
789 KeyCode::Char('?') => {
790 self.toggle_help();
791 InputResult::Consumed
792 }
793 _ => InputResult::Ignored, }
795 }
796
797 fn handle_text_editing_input(
799 &mut self,
800 event: &KeyEvent,
801 ctx: &mut InputContext,
802 ) -> InputResult {
803 let is_json = self.is_editing_json();
804
805 if is_json {
806 return self.handle_json_editing_input(event, ctx);
807 }
808
809 match event.code {
810 KeyCode::Esc => {
811 if !self.can_exit_text_editing() {
813 return InputResult::Consumed;
814 }
815 self.stop_editing();
816 InputResult::Consumed
817 }
818 KeyCode::Enter => {
819 self.text_add_item();
820 InputResult::Consumed
821 }
822 KeyCode::Char(c) => {
823 self.text_insert(c);
824 InputResult::Consumed
825 }
826 KeyCode::Backspace => {
827 self.text_backspace();
828 InputResult::Consumed
829 }
830 KeyCode::Delete => {
831 self.text_remove_focused();
832 InputResult::Consumed
833 }
834 KeyCode::Left => {
835 self.text_move_left();
836 InputResult::Consumed
837 }
838 KeyCode::Right => {
839 self.text_move_right();
840 InputResult::Consumed
841 }
842 KeyCode::Up => {
843 self.text_focus_prev();
844 InputResult::Consumed
845 }
846 KeyCode::Down => {
847 self.text_focus_next();
848 InputResult::Consumed
849 }
850 KeyCode::Tab => {
851 self.stop_editing();
853 self.toggle_focus();
854 InputResult::Consumed
855 }
856 _ => InputResult::Consumed, }
858 }
859
860 fn handle_json_editing_input(
862 &mut self,
863 event: &KeyEvent,
864 ctx: &mut InputContext,
865 ) -> InputResult {
866 match event.code {
867 KeyCode::Esc | KeyCode::Tab => {
868 self.json_exit_editing();
870 }
871 KeyCode::Enter => {
872 self.json_insert_newline();
873 }
874 KeyCode::Char(c) => {
875 if event.modifiers.contains(KeyModifiers::CONTROL) {
876 match c {
877 'a' | 'A' => self.json_select_all(),
878 'c' | 'C' => {
879 if let Some(text) = self.json_selected_text() {
880 ctx.defer(DeferredAction::CopyToClipboard(text));
881 }
882 }
883 'v' | 'V' => {
884 ctx.defer(DeferredAction::PasteToSettings);
885 }
886 _ => {}
887 }
888 } else {
889 self.text_insert(c);
890 }
891 }
892 KeyCode::Backspace => {
893 self.text_backspace();
894 }
895 KeyCode::Delete => {
896 self.json_delete();
897 }
898 KeyCode::Left => {
899 if event.modifiers.contains(KeyModifiers::SHIFT) {
900 self.json_cursor_left_selecting();
901 } else {
902 self.text_move_left();
903 }
904 }
905 KeyCode::Right => {
906 if event.modifiers.contains(KeyModifiers::SHIFT) {
907 self.json_cursor_right_selecting();
908 } else {
909 self.text_move_right();
910 }
911 }
912 KeyCode::Up => {
913 if event.modifiers.contains(KeyModifiers::SHIFT) {
914 self.json_cursor_up_selecting();
915 } else {
916 self.json_cursor_up();
917 }
918 }
919 KeyCode::Down => {
920 if event.modifiers.contains(KeyModifiers::SHIFT) {
921 self.json_cursor_down_selecting();
922 } else {
923 self.json_cursor_down();
924 }
925 }
926 _ => {}
927 }
928 InputResult::Consumed
929 }
930
931 fn handle_number_editing_input(
933 &mut self,
934 event: &KeyEvent,
935 _ctx: &mut InputContext,
936 ) -> InputResult {
937 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
938 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
939
940 match event.code {
941 KeyCode::Esc => {
942 self.number_cancel();
943 }
944 KeyCode::Enter => {
945 self.number_confirm();
946 }
947 KeyCode::Char('a') if ctrl => {
948 self.number_select_all();
949 }
950 KeyCode::Char(c) => {
951 self.number_insert(c);
952 }
953 KeyCode::Backspace if ctrl => {
954 self.number_delete_word_backward();
955 }
956 KeyCode::Backspace => {
957 self.number_backspace();
958 }
959 KeyCode::Delete if ctrl => {
960 self.number_delete_word_forward();
961 }
962 KeyCode::Delete => {
963 self.number_delete();
964 }
965 KeyCode::Left if ctrl && shift => {
966 self.number_move_word_left_selecting();
967 }
968 KeyCode::Left if ctrl => {
969 self.number_move_word_left();
970 }
971 KeyCode::Left if shift => {
972 self.number_move_left_selecting();
973 }
974 KeyCode::Left => {
975 self.number_move_left();
976 }
977 KeyCode::Right if ctrl && shift => {
978 self.number_move_word_right_selecting();
979 }
980 KeyCode::Right if ctrl => {
981 self.number_move_word_right();
982 }
983 KeyCode::Right if shift => {
984 self.number_move_right_selecting();
985 }
986 KeyCode::Right => {
987 self.number_move_right();
988 }
989 KeyCode::Home if shift => {
990 self.number_move_home_selecting();
991 }
992 KeyCode::Home => {
993 self.number_move_home();
994 }
995 KeyCode::End if shift => {
996 self.number_move_end_selecting();
997 }
998 KeyCode::End => {
999 self.number_move_end();
1000 }
1001 _ => {}
1002 }
1003 InputResult::Consumed }
1005
1006 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1008 match event.code {
1009 KeyCode::Up => {
1010 self.dropdown_prev();
1011 InputResult::Consumed
1012 }
1013 KeyCode::Down => {
1014 self.dropdown_next();
1015 InputResult::Consumed
1016 }
1017 KeyCode::Home => {
1018 self.dropdown_home();
1019 InputResult::Consumed
1020 }
1021 KeyCode::End => {
1022 self.dropdown_end();
1023 InputResult::Consumed
1024 }
1025 KeyCode::Enter => {
1026 self.dropdown_confirm();
1027 InputResult::Consumed
1028 }
1029 KeyCode::Esc => {
1030 self.dropdown_cancel();
1031 InputResult::Consumed
1032 }
1033 _ => InputResult::Consumed, }
1035 }
1036
1037 fn request_reset(&mut self) {
1039 if self.has_changes() {
1040 self.showing_reset_dialog = true;
1041 self.reset_dialog_selection = 0;
1042 }
1043 }
1044
1045 fn request_close(&mut self, ctx: &mut InputContext) {
1047 if self.has_changes() {
1048 self.showing_confirm_dialog = true;
1049 self.confirm_dialog_selection = 0;
1050 } else {
1051 ctx.defer(DeferredAction::CloseSettings { save: false });
1052 }
1053 }
1054
1055 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1057 if let Some(item) = self.current_item_mut() {
1058 match &mut item.control {
1059 SettingControl::Toggle(ref mut state) => {
1060 state.checked = !state.checked;
1061 self.on_value_changed();
1062 }
1063 SettingControl::Dropdown(_) => {
1064 self.dropdown_toggle();
1065 }
1066 SettingControl::Number(_) => {
1067 self.start_number_editing();
1068 }
1069 SettingControl::Text(_) => {
1070 self.start_editing();
1071 }
1072 SettingControl::TextList(_) => {
1073 self.start_editing();
1074 }
1075 SettingControl::Map(ref mut state) => {
1076 if state.focused_entry.is_none() {
1077 if state.value_schema.is_some() {
1079 self.open_add_entry_dialog();
1080 }
1081 } else if state.value_schema.is_some() {
1082 self.open_entry_dialog();
1084 } else {
1085 if let Some(idx) = state.focused_entry {
1087 if state.expanded.contains(&idx) {
1088 state.expanded.retain(|&i| i != idx);
1089 } else {
1090 state.expanded.push(idx);
1091 }
1092 }
1093 }
1094 self.on_value_changed();
1095 }
1096 SettingControl::Json(_) => {
1097 self.start_editing();
1098 }
1099 SettingControl::ObjectArray(ref state) => {
1100 if state.focused_index.is_none() {
1101 if state.item_schema.is_some() {
1103 self.open_add_array_item_dialog();
1104 }
1105 } else if state.item_schema.is_some() {
1106 self.open_edit_array_item_dialog();
1108 }
1109 }
1110 SettingControl::Complex { .. } => {
1111 }
1113 }
1114 }
1115 }
1116
1117 fn handle_control_increment(&mut self) {
1119 if let Some(item) = self.current_item_mut() {
1120 if let SettingControl::Number(ref mut state) = &mut item.control {
1121 state.value += 1;
1122 if let Some(max) = state.max {
1123 state.value = state.value.min(max);
1124 }
1125 self.on_value_changed();
1126 }
1127 }
1128 }
1129
1130 fn handle_control_decrement(&mut self) {
1132 if let Some(item) = self.current_item_mut() {
1133 if let SettingControl::Number(ref mut state) = &mut item.control {
1134 if state.value > 0 {
1135 state.value -= 1;
1136 }
1137 if let Some(min) = state.min {
1138 state.value = state.value.max(min);
1139 }
1140 self.on_value_changed();
1141 }
1142 }
1143 }
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use super::*;
1149 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1150
1151 fn key(code: KeyCode) -> KeyEvent {
1152 KeyEvent::new(code, KeyModifiers::NONE)
1153 }
1154
1155 #[test]
1156 fn test_settings_is_modal() {
1157 let schema = include_str!("../../../plugins/config-schema.json");
1159 let config = crate::config::Config::default();
1160 let state = SettingsState::new(schema, &config).unwrap();
1161 assert!(state.is_modal());
1162 }
1163
1164 #[test]
1165 fn test_categories_panel_does_not_leak_to_settings() {
1166 let schema = include_str!("../../../plugins/config-schema.json");
1167 let config = crate::config::Config::default();
1168 let mut state = SettingsState::new(schema, &config).unwrap();
1169 state.visible = true;
1170 state.focus.set(FocusPanel::Categories);
1171
1172 let mut ctx = InputContext::new();
1173
1174 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1177 assert_eq!(result, InputResult::Consumed);
1178 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1179
1180 state.focus.set(FocusPanel::Categories);
1182
1183 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1185 assert_eq!(result, InputResult::Consumed);
1186 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1188 }
1189
1190 #[test]
1191 fn test_tab_cycles_focus_panels() {
1192 let schema = include_str!("../../../plugins/config-schema.json");
1193 let config = crate::config::Config::default();
1194 let mut state = SettingsState::new(schema, &config).unwrap();
1195 state.visible = true;
1196
1197 let mut ctx = InputContext::new();
1198
1199 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1201
1202 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1204 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1205
1206 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1208 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1209 assert_eq!(state.footer_button_index, 0);
1210
1211 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1213 assert_eq!(state.footer_button_index, 1);
1214 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1215 assert_eq!(state.footer_button_index, 2);
1216 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1217 assert_eq!(state.footer_button_index, 3);
1218 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1219 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1221 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1222
1223 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1226 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1227
1228 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1230 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1231 assert_eq!(
1232 state.footer_button_index, 0,
1233 "Footer should reset to Layer button (index 0) on second loop"
1234 );
1235 }
1236
1237 #[test]
1238 fn test_escape_shows_confirm_dialog_with_changes() {
1239 let schema = include_str!("../../../plugins/config-schema.json");
1240 let config = crate::config::Config::default();
1241 let mut state = SettingsState::new(schema, &config).unwrap();
1242 state.visible = true;
1243
1244 state
1246 .pending_changes
1247 .insert("/test".to_string(), serde_json::json!(true));
1248
1249 let mut ctx = InputContext::new();
1250
1251 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1253 assert!(state.showing_confirm_dialog);
1254 assert!(ctx.deferred_actions.is_empty()); }
1256
1257 #[test]
1258 fn test_escape_closes_directly_without_changes() {
1259 let schema = include_str!("../../../plugins/config-schema.json");
1260 let config = crate::config::Config::default();
1261 let mut state = SettingsState::new(schema, &config).unwrap();
1262 state.visible = true;
1263
1264 let mut ctx = InputContext::new();
1265
1266 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1268 assert!(!state.showing_confirm_dialog);
1269 assert_eq!(ctx.deferred_actions.len(), 1);
1270 assert!(matches!(
1271 ctx.deferred_actions[0],
1272 DeferredAction::CloseSettings { save: false }
1273 ));
1274 }
1275
1276 #[test]
1277 fn test_confirm_dialog_navigation() {
1278 let schema = include_str!("../../../plugins/config-schema.json");
1279 let config = crate::config::Config::default();
1280 let mut state = SettingsState::new(schema, &config).unwrap();
1281 state.visible = true;
1282 state.showing_confirm_dialog = true;
1283 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1286
1287 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1289 assert_eq!(state.confirm_dialog_selection, 1);
1290
1291 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1293 assert_eq!(state.confirm_dialog_selection, 2);
1294
1295 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1297 assert_eq!(state.confirm_dialog_selection, 2);
1298
1299 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1301 assert_eq!(state.confirm_dialog_selection, 1);
1302 }
1303
1304 #[test]
1305 fn test_search_mode_captures_typing() {
1306 let schema = include_str!("../../../plugins/config-schema.json");
1307 let config = crate::config::Config::default();
1308 let mut state = SettingsState::new(schema, &config).unwrap();
1309 state.visible = true;
1310
1311 let mut ctx = InputContext::new();
1312
1313 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1315 assert!(state.search_active);
1316
1317 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1319 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1320 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1321 assert_eq!(state.search_query, "tab");
1322
1323 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1325 assert!(!state.search_active);
1326 assert!(state.search_query.is_empty());
1327 }
1328
1329 #[test]
1330 fn test_footer_button_activation() {
1331 let schema = include_str!("../../../plugins/config-schema.json");
1332 let config = crate::config::Config::default();
1333 let mut state = SettingsState::new(schema, &config).unwrap();
1334 state.visible = true;
1335 state.focus.set(FocusPanel::Footer);
1336 state.footer_button_index = 2; let mut ctx = InputContext::new();
1339
1340 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1342 assert_eq!(ctx.deferred_actions.len(), 1);
1343 assert!(matches!(
1344 ctx.deferred_actions[0],
1345 DeferredAction::CloseSettings { save: true }
1346 ));
1347 }
1348}