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 let (editing_text, dropdown_open) = if let Some(dialog) = self.entry_dialog() {
88 let dropdown_open = dialog
89 .current_item()
90 .map(|item| matches!(&item.control, SettingControl::Dropdown(s) if s.open))
91 .unwrap_or(false);
92 (dialog.editing_text, dropdown_open)
93 } else {
94 return InputResult::Consumed;
95 };
96
97 if editing_text {
99 self.handle_entry_dialog_text_editing(event, ctx)
100 } else if dropdown_open {
101 self.handle_entry_dialog_dropdown(event)
102 } else {
103 self.handle_entry_dialog_navigation(event)
104 }
105 }
106
107 fn handle_entry_dialog_text_editing(
109 &mut self,
110 event: &KeyEvent,
111 ctx: &mut InputContext,
112 ) -> InputResult {
113 let is_editing_json = self
115 .entry_dialog()
116 .map(|d| d.is_editing_json())
117 .unwrap_or(false);
118
119 let can_exit = self.entry_dialog_can_exit_text_editing();
121
122 let Some(dialog) = self.entry_dialog_mut() else {
123 return InputResult::Consumed;
124 };
125
126 match event.code {
127 KeyCode::Esc => {
128 if !can_exit {
130 }
132 dialog.stop_editing();
133 }
134 KeyCode::Enter => {
135 if is_editing_json {
136 dialog.insert_newline();
138 } else {
139 if let Some(item) = dialog.current_item_mut() {
141 if let SettingControl::TextList(state) = &mut item.control {
142 state.add_item();
143 }
144 }
145 }
146 }
147 KeyCode::Char(c) => {
148 if event.modifiers.contains(KeyModifiers::CONTROL) {
149 match c {
150 'a' | 'A' => {
151 dialog.select_all();
153 }
154 'c' | 'C' => {
155 if let Some(text) = dialog.selected_text() {
157 ctx.defer(DeferredAction::CopyToClipboard(text));
158 }
159 }
160 'v' | 'V' => {
161 ctx.defer(DeferredAction::PasteToSettings);
163 }
164 _ => {}
165 }
166 } else {
167 dialog.insert_char(c);
168 }
169 }
170 KeyCode::Backspace => {
171 dialog.backspace();
172 }
173 KeyCode::Delete => {
174 if is_editing_json {
175 dialog.delete();
177 } else {
178 dialog.delete_list_item();
180 }
181 }
182 KeyCode::Home => {
183 dialog.cursor_home();
184 }
185 KeyCode::End => {
186 dialog.cursor_end();
187 }
188 KeyCode::Left => {
189 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
190 dialog.cursor_left_selecting();
191 } else {
192 dialog.cursor_left();
193 }
194 }
195 KeyCode::Right => {
196 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
197 dialog.cursor_right_selecting();
198 } else {
199 dialog.cursor_right();
200 }
201 }
202 KeyCode::Up => {
203 if is_editing_json {
204 if event.modifiers.contains(KeyModifiers::SHIFT) {
206 dialog.cursor_up_selecting();
207 } else {
208 dialog.cursor_up();
209 }
210 } else {
211 if let Some(item) = dialog.current_item_mut() {
213 if let SettingControl::TextList(state) = &mut item.control {
214 state.focus_prev();
215 }
216 }
217 }
218 }
219 KeyCode::Down => {
220 if is_editing_json {
221 if event.modifiers.contains(KeyModifiers::SHIFT) {
223 dialog.cursor_down_selecting();
224 } else {
225 dialog.cursor_down();
226 }
227 } else {
228 if let Some(item) = dialog.current_item_mut() {
230 if let SettingControl::TextList(state) = &mut item.control {
231 state.focus_next();
232 }
233 }
234 }
235 }
236 KeyCode::Tab => {
237 if is_editing_json {
238 let is_valid = dialog
240 .current_item()
241 .map(|item| {
242 if let SettingControl::Json(state) = &item.control {
243 state.is_valid()
244 } else {
245 true
246 }
247 })
248 .unwrap_or(true);
249
250 if is_valid {
251 if let Some(item) = dialog.current_item_mut() {
253 if let SettingControl::Json(state) = &mut item.control {
254 state.commit();
255 }
256 }
257 dialog.stop_editing();
258 }
259 }
261 }
262 _ => {}
263 }
264 InputResult::Consumed
265 }
266
267 fn handle_entry_dialog_dropdown(&mut self, event: &KeyEvent) -> InputResult {
269 let Some(dialog) = self.entry_dialog_mut() else {
270 return InputResult::Consumed;
271 };
272
273 match event.code {
274 KeyCode::Up => {
275 dialog.dropdown_prev();
276 }
277 KeyCode::Down => {
278 dialog.dropdown_next();
279 }
280 KeyCode::Enter => {
281 dialog.dropdown_confirm();
282 }
283 KeyCode::Esc => {
284 dialog.dropdown_confirm(); }
286 _ => {}
287 }
288 InputResult::Consumed
289 }
290
291 fn handle_entry_dialog_navigation(&mut self, event: &KeyEvent) -> InputResult {
293 match event.code {
294 KeyCode::Esc => {
295 self.close_entry_dialog();
296 }
297 KeyCode::Up => {
298 if let Some(dialog) = self.entry_dialog_mut() {
299 dialog.focus_prev();
300 }
301 }
302 KeyCode::Down => {
303 if let Some(dialog) = self.entry_dialog_mut() {
304 dialog.focus_next();
305 }
306 }
307 KeyCode::Tab => {
308 if let Some(dialog) = self.entry_dialog_mut() {
309 dialog.focus_next();
310 }
311 }
312 KeyCode::BackTab => {
313 if let Some(dialog) = self.entry_dialog_mut() {
314 dialog.focus_prev();
315 }
316 }
317 KeyCode::Left => {
318 if let Some(dialog) = self.entry_dialog_mut() {
320 if !dialog.focus_on_buttons {
321 dialog.decrement_number();
322 } else if dialog.focused_button > 0 {
323 dialog.focused_button -= 1;
324 }
325 }
326 }
327 KeyCode::Right => {
328 if let Some(dialog) = self.entry_dialog_mut() {
330 if !dialog.focus_on_buttons {
331 dialog.increment_number();
332 } else if dialog.focused_button + 1 < dialog.button_count() {
333 dialog.focused_button += 1;
334 }
335 }
336 }
337 KeyCode::Enter | KeyCode::Char(' ') => {
338 let button_action = self.entry_dialog().and_then(|dialog| {
340 if dialog.focus_on_buttons {
341 let cancel_idx = dialog.button_count() - 1;
342 if dialog.focused_button == 0 {
343 Some(ButtonAction::Save)
344 } else if !dialog.is_new && dialog.focused_button == 1 {
345 Some(ButtonAction::Delete)
346 } else if dialog.focused_button == cancel_idx {
347 Some(ButtonAction::Cancel)
348 } else {
349 None
350 }
351 } else {
352 None
353 }
354 });
355
356 if let Some(action) = button_action {
357 match action {
358 ButtonAction::Save => self.save_entry_dialog(),
359 ButtonAction::Delete => self.delete_entry_dialog(),
360 ButtonAction::Cancel => self.close_entry_dialog(),
361 }
362 } else if event.modifiers.contains(KeyModifiers::CONTROL) {
363 self.save_entry_dialog();
365 } else {
366 let control_action = self
368 .entry_dialog()
369 .and_then(|dialog| {
370 dialog.current_item().map(|item| match &item.control {
371 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
372 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
373 SettingControl::Text(_)
374 | SettingControl::TextList(_)
375 | SettingControl::Number(_)
376 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
377 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
378 Some(ControlAction::OpenNestedDialog)
379 }
380 _ => None,
381 })
382 })
383 .flatten();
384
385 if let Some(action) = control_action {
386 match action {
387 ControlAction::ToggleBool => {
388 if let Some(dialog) = self.entry_dialog_mut() {
389 dialog.toggle_bool();
390 }
391 }
392 ControlAction::ToggleDropdown => {
393 if let Some(dialog) = self.entry_dialog_mut() {
394 dialog.toggle_dropdown();
395 }
396 }
397 ControlAction::StartEditing => {
398 if let Some(dialog) = self.entry_dialog_mut() {
399 dialog.start_editing();
400 }
401 }
402 ControlAction::OpenNestedDialog => {
403 self.open_nested_entry_dialog();
405 }
406 }
407 }
408 }
409 }
410 _ => {}
411 }
412 InputResult::Consumed
413 }
414
415 fn handle_confirm_dialog_input(
417 &mut self,
418 event: &KeyEvent,
419 ctx: &mut InputContext,
420 ) -> InputResult {
421 match event.code {
422 KeyCode::Left | KeyCode::BackTab => {
423 if self.confirm_dialog_selection > 0 {
424 self.confirm_dialog_selection -= 1;
425 }
426 InputResult::Consumed
427 }
428 KeyCode::Right | KeyCode::Tab => {
429 if self.confirm_dialog_selection < 2 {
430 self.confirm_dialog_selection += 1;
431 }
432 InputResult::Consumed
433 }
434 KeyCode::Enter => {
435 match self.confirm_dialog_selection {
436 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
440 }
441 InputResult::Consumed
442 }
443 KeyCode::Esc => {
444 self.showing_confirm_dialog = false;
445 InputResult::Consumed
446 }
447 KeyCode::Char('s') | KeyCode::Char('S') => {
448 ctx.defer(DeferredAction::CloseSettings { save: true });
449 InputResult::Consumed
450 }
451 KeyCode::Char('d') | KeyCode::Char('D') => {
452 ctx.defer(DeferredAction::CloseSettings { save: false });
453 InputResult::Consumed
454 }
455 _ => InputResult::Consumed, }
457 }
458
459 fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
461 match event.code {
462 KeyCode::Left | KeyCode::BackTab => {
463 if self.reset_dialog_selection > 0 {
464 self.reset_dialog_selection -= 1;
465 }
466 InputResult::Consumed
467 }
468 KeyCode::Right | KeyCode::Tab => {
469 if self.reset_dialog_selection < 1 {
470 self.reset_dialog_selection += 1;
471 }
472 InputResult::Consumed
473 }
474 KeyCode::Enter => {
475 match self.reset_dialog_selection {
476 0 => {
477 self.discard_changes();
479 self.showing_reset_dialog = false;
480 }
481 1 => {
482 self.showing_reset_dialog = false;
484 }
485 _ => {}
486 }
487 InputResult::Consumed
488 }
489 KeyCode::Esc => {
490 self.showing_reset_dialog = false;
491 InputResult::Consumed
492 }
493 KeyCode::Char('r') | KeyCode::Char('R') => {
494 self.discard_changes();
495 self.showing_reset_dialog = false;
496 InputResult::Consumed
497 }
498 _ => InputResult::Consumed, }
500 }
501
502 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
504 self.showing_help = false;
506 InputResult::Consumed
507 }
508
509 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
511 match event.code {
512 KeyCode::Esc => {
513 self.cancel_search();
514 InputResult::Consumed
515 }
516 KeyCode::Enter => {
517 self.jump_to_search_result();
518 InputResult::Consumed
519 }
520 KeyCode::Up => {
521 self.search_prev();
522 InputResult::Consumed
523 }
524 KeyCode::Down => {
525 self.search_next();
526 InputResult::Consumed
527 }
528 KeyCode::Char(c) => {
529 self.search_push_char(c);
530 InputResult::Consumed
531 }
532 KeyCode::Backspace => {
533 self.search_pop_char();
534 InputResult::Consumed
535 }
536 _ => InputResult::Consumed, }
538 }
539
540 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
542 match event.code {
543 KeyCode::Up => {
544 self.select_prev();
545 InputResult::Consumed
546 }
547 KeyCode::Down => {
548 self.select_next();
549 InputResult::Consumed
550 }
551 KeyCode::Tab => {
552 self.toggle_focus();
553 InputResult::Consumed
554 }
555 KeyCode::BackTab => {
556 self.toggle_focus_backward();
557 InputResult::Consumed
558 }
559 KeyCode::Char('/') => {
560 self.start_search();
561 InputResult::Consumed
562 }
563 KeyCode::Char('?') => {
564 self.toggle_help();
565 InputResult::Consumed
566 }
567 KeyCode::Esc => {
568 self.request_close(ctx);
569 InputResult::Consumed
570 }
571 KeyCode::Enter | KeyCode::Right => {
572 self.focus.set(FocusPanel::Settings);
574 InputResult::Consumed
575 }
576 _ => InputResult::Ignored, }
578 }
579
580 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
582 if self.editing_text {
584 return self.handle_text_editing_input(event, ctx);
585 }
586
587 if self.is_number_editing() {
589 return self.handle_number_editing_input(event, ctx);
590 }
591
592 if self.is_dropdown_open() {
594 return self.handle_dropdown_input(event, ctx);
595 }
596
597 match event.code {
598 KeyCode::Up => {
599 self.select_prev();
600 InputResult::Consumed
601 }
602 KeyCode::Down => {
603 self.select_next();
604 InputResult::Consumed
605 }
606 KeyCode::Tab => {
607 self.toggle_focus();
608 InputResult::Consumed
609 }
610 KeyCode::BackTab => {
611 self.toggle_focus_backward();
612 InputResult::Consumed
613 }
614 KeyCode::Left => {
615 if self.is_number_control() {
618 self.handle_control_decrement();
619 } else {
620 self.update_control_focus(false);
621 self.focus.set(FocusPanel::Categories);
622 }
623 InputResult::Consumed
624 }
625 KeyCode::Right => {
626 self.handle_control_increment();
627 InputResult::Consumed
628 }
629 KeyCode::Enter | KeyCode::Char(' ') => {
630 self.handle_control_activate(ctx);
631 InputResult::Consumed
632 }
633 KeyCode::Char('/') => {
634 self.start_search();
635 InputResult::Consumed
636 }
637 KeyCode::Char('?') => {
638 self.toggle_help();
639 InputResult::Consumed
640 }
641 KeyCode::Esc => {
642 self.request_close(ctx);
643 InputResult::Consumed
644 }
645 _ => InputResult::Ignored, }
647 }
648
649 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
653 const FOOTER_BUTTON_COUNT: usize = 5;
654
655 match event.code {
656 KeyCode::Left | KeyCode::BackTab => {
657 if self.footer_button_index > 0 {
659 self.footer_button_index -= 1;
660 } else {
661 self.focus.set(FocusPanel::Settings);
662 }
663 InputResult::Consumed
664 }
665 KeyCode::Right => {
666 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
668 self.footer_button_index += 1;
669 }
670 InputResult::Consumed
671 }
672 KeyCode::Tab => {
673 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
675 self.footer_button_index += 1;
676 } else {
677 self.focus.set(FocusPanel::Categories);
678 }
679 InputResult::Consumed
680 }
681 KeyCode::Enter => {
682 match self.footer_button_index {
683 0 => self.cycle_target_layer(), 1 => self.request_reset(),
685 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
686 3 => self.request_close(ctx),
687 4 => ctx.defer(DeferredAction::OpenConfigFile {
688 layer: self.target_layer,
689 }), _ => {}
691 }
692 InputResult::Consumed
693 }
694 KeyCode::Esc => {
695 self.request_close(ctx);
696 InputResult::Consumed
697 }
698 KeyCode::Char('/') => {
699 self.start_search();
700 InputResult::Consumed
701 }
702 KeyCode::Char('?') => {
703 self.toggle_help();
704 InputResult::Consumed
705 }
706 _ => InputResult::Ignored, }
708 }
709
710 fn handle_text_editing_input(
712 &mut self,
713 event: &KeyEvent,
714 ctx: &mut InputContext,
715 ) -> InputResult {
716 let is_json = self.is_editing_json();
717
718 if is_json {
719 return self.handle_json_editing_input(event, ctx);
720 }
721
722 match event.code {
723 KeyCode::Esc => {
724 if !self.can_exit_text_editing() {
726 return InputResult::Consumed;
727 }
728 self.stop_editing();
729 InputResult::Consumed
730 }
731 KeyCode::Enter => {
732 self.text_add_item();
733 InputResult::Consumed
734 }
735 KeyCode::Char(c) => {
736 self.text_insert(c);
737 InputResult::Consumed
738 }
739 KeyCode::Backspace => {
740 self.text_backspace();
741 InputResult::Consumed
742 }
743 KeyCode::Delete => {
744 self.text_remove_focused();
745 InputResult::Consumed
746 }
747 KeyCode::Left => {
748 self.text_move_left();
749 InputResult::Consumed
750 }
751 KeyCode::Right => {
752 self.text_move_right();
753 InputResult::Consumed
754 }
755 KeyCode::Up => {
756 self.text_focus_prev();
757 InputResult::Consumed
758 }
759 KeyCode::Down => {
760 self.text_focus_next();
761 InputResult::Consumed
762 }
763 _ => InputResult::Consumed, }
765 }
766
767 fn handle_json_editing_input(
769 &mut self,
770 event: &KeyEvent,
771 ctx: &mut InputContext,
772 ) -> InputResult {
773 match event.code {
774 KeyCode::Esc | KeyCode::Tab => {
775 self.json_exit_editing();
777 }
778 KeyCode::Enter => {
779 self.json_insert_newline();
780 }
781 KeyCode::Char(c) => {
782 if event.modifiers.contains(KeyModifiers::CONTROL) {
783 match c {
784 'a' | 'A' => self.json_select_all(),
785 'c' | 'C' => {
786 if let Some(text) = self.json_selected_text() {
787 ctx.defer(DeferredAction::CopyToClipboard(text));
788 }
789 }
790 'v' | 'V' => {
791 ctx.defer(DeferredAction::PasteToSettings);
792 }
793 _ => {}
794 }
795 } else {
796 self.text_insert(c);
797 }
798 }
799 KeyCode::Backspace => {
800 self.text_backspace();
801 }
802 KeyCode::Delete => {
803 self.json_delete();
804 }
805 KeyCode::Left => {
806 if event.modifiers.contains(KeyModifiers::SHIFT) {
807 self.json_cursor_left_selecting();
808 } else {
809 self.text_move_left();
810 }
811 }
812 KeyCode::Right => {
813 if event.modifiers.contains(KeyModifiers::SHIFT) {
814 self.json_cursor_right_selecting();
815 } else {
816 self.text_move_right();
817 }
818 }
819 KeyCode::Up => {
820 if event.modifiers.contains(KeyModifiers::SHIFT) {
821 self.json_cursor_up_selecting();
822 } else {
823 self.json_cursor_up();
824 }
825 }
826 KeyCode::Down => {
827 if event.modifiers.contains(KeyModifiers::SHIFT) {
828 self.json_cursor_down_selecting();
829 } else {
830 self.json_cursor_down();
831 }
832 }
833 _ => {}
834 }
835 InputResult::Consumed
836 }
837
838 fn handle_number_editing_input(
840 &mut self,
841 event: &KeyEvent,
842 _ctx: &mut InputContext,
843 ) -> InputResult {
844 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
845 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
846
847 match event.code {
848 KeyCode::Esc => {
849 self.number_cancel();
850 }
851 KeyCode::Enter => {
852 self.number_confirm();
853 }
854 KeyCode::Char('a') if ctrl => {
855 self.number_select_all();
856 }
857 KeyCode::Char(c) => {
858 self.number_insert(c);
859 }
860 KeyCode::Backspace if ctrl => {
861 self.number_delete_word_backward();
862 }
863 KeyCode::Backspace => {
864 self.number_backspace();
865 }
866 KeyCode::Delete if ctrl => {
867 self.number_delete_word_forward();
868 }
869 KeyCode::Delete => {
870 self.number_delete();
871 }
872 KeyCode::Left if ctrl && shift => {
873 self.number_move_word_left_selecting();
874 }
875 KeyCode::Left if ctrl => {
876 self.number_move_word_left();
877 }
878 KeyCode::Left if shift => {
879 self.number_move_left_selecting();
880 }
881 KeyCode::Left => {
882 self.number_move_left();
883 }
884 KeyCode::Right if ctrl && shift => {
885 self.number_move_word_right_selecting();
886 }
887 KeyCode::Right if ctrl => {
888 self.number_move_word_right();
889 }
890 KeyCode::Right if shift => {
891 self.number_move_right_selecting();
892 }
893 KeyCode::Right => {
894 self.number_move_right();
895 }
896 KeyCode::Home if shift => {
897 self.number_move_home_selecting();
898 }
899 KeyCode::Home => {
900 self.number_move_home();
901 }
902 KeyCode::End if shift => {
903 self.number_move_end_selecting();
904 }
905 KeyCode::End => {
906 self.number_move_end();
907 }
908 _ => {}
909 }
910 InputResult::Consumed }
912
913 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
915 match event.code {
916 KeyCode::Up => {
917 self.dropdown_prev();
918 InputResult::Consumed
919 }
920 KeyCode::Down => {
921 self.dropdown_next();
922 InputResult::Consumed
923 }
924 KeyCode::Home => {
925 self.dropdown_home();
926 InputResult::Consumed
927 }
928 KeyCode::End => {
929 self.dropdown_end();
930 InputResult::Consumed
931 }
932 KeyCode::Enter => {
933 self.dropdown_confirm();
934 InputResult::Consumed
935 }
936 KeyCode::Esc => {
937 self.dropdown_cancel();
938 InputResult::Consumed
939 }
940 _ => InputResult::Consumed, }
942 }
943
944 fn request_reset(&mut self) {
946 if self.has_changes() {
947 self.showing_reset_dialog = true;
948 self.reset_dialog_selection = 0;
949 }
950 }
951
952 fn request_close(&mut self, ctx: &mut InputContext) {
954 if self.has_changes() {
955 self.showing_confirm_dialog = true;
956 self.confirm_dialog_selection = 0;
957 } else {
958 ctx.defer(DeferredAction::CloseSettings { save: false });
959 }
960 }
961
962 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
964 if let Some(item) = self.current_item_mut() {
965 match &mut item.control {
966 SettingControl::Toggle(ref mut state) => {
967 state.checked = !state.checked;
968 self.on_value_changed();
969 }
970 SettingControl::Dropdown(_) => {
971 self.dropdown_toggle();
972 }
973 SettingControl::Number(_) => {
974 self.start_number_editing();
975 }
976 SettingControl::Text(_) => {
977 self.start_editing();
978 }
979 SettingControl::TextList(_) => {
980 self.start_editing();
981 }
982 SettingControl::Map(ref mut state) => {
983 if state.focused_entry.is_none() {
984 if state.value_schema.is_some() {
986 self.open_add_entry_dialog();
987 }
988 } else if state.value_schema.is_some() {
989 self.open_entry_dialog();
991 } else {
992 if let Some(idx) = state.focused_entry {
994 if state.expanded.contains(&idx) {
995 state.expanded.retain(|&i| i != idx);
996 } else {
997 state.expanded.push(idx);
998 }
999 }
1000 }
1001 self.on_value_changed();
1002 }
1003 SettingControl::Json(_) => {
1004 self.start_editing();
1005 }
1006 SettingControl::ObjectArray(ref state) => {
1007 if state.focused_index.is_none() {
1008 if state.item_schema.is_some() {
1010 self.open_add_array_item_dialog();
1011 }
1012 } else if state.item_schema.is_some() {
1013 self.open_edit_array_item_dialog();
1015 }
1016 }
1017 SettingControl::Complex { .. } => {
1018 }
1020 }
1021 }
1022 }
1023
1024 fn handle_control_increment(&mut self) {
1026 if let Some(item) = self.current_item_mut() {
1027 match &mut item.control {
1028 SettingControl::Number(ref mut state) => {
1029 state.value += 1;
1030 if let Some(max) = state.max {
1031 state.value = state.value.min(max);
1032 }
1033 self.on_value_changed();
1034 }
1035 _ => {}
1036 }
1037 }
1038 }
1039
1040 fn handle_control_decrement(&mut self) {
1042 if let Some(item) = self.current_item_mut() {
1043 match &mut item.control {
1044 SettingControl::Number(ref mut state) => {
1045 if state.value > 0 {
1046 state.value -= 1;
1047 }
1048 if let Some(min) = state.min {
1049 state.value = state.value.max(min);
1050 }
1051 self.on_value_changed();
1052 }
1053 _ => {}
1054 }
1055 }
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1063
1064 fn key(code: KeyCode) -> KeyEvent {
1065 KeyEvent::new(code, KeyModifiers::NONE)
1066 }
1067
1068 #[test]
1069 fn test_settings_is_modal() {
1070 let schema = include_str!("../../../plugins/config-schema.json");
1072 let config = crate::config::Config::default();
1073 let state = SettingsState::new(schema, &config).unwrap();
1074 assert!(state.is_modal());
1075 }
1076
1077 #[test]
1078 fn test_categories_panel_does_not_leak_to_settings() {
1079 let schema = include_str!("../../../plugins/config-schema.json");
1080 let config = crate::config::Config::default();
1081 let mut state = SettingsState::new(schema, &config).unwrap();
1082 state.visible = true;
1083 state.focus.set(FocusPanel::Categories);
1084
1085 let mut ctx = InputContext::new();
1086
1087 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1090 assert_eq!(result, InputResult::Consumed);
1091 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1092
1093 state.focus.set(FocusPanel::Categories);
1095
1096 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1098 assert_eq!(result, InputResult::Consumed);
1099 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1101 }
1102
1103 #[test]
1104 fn test_tab_cycles_focus_panels() {
1105 let schema = include_str!("../../../plugins/config-schema.json");
1106 let config = crate::config::Config::default();
1107 let mut state = SettingsState::new(schema, &config).unwrap();
1108 state.visible = true;
1109
1110 let mut ctx = InputContext::new();
1111
1112 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1114
1115 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1117 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1118
1119 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1121 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1122 assert_eq!(state.footer_button_index, 0);
1123
1124 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1126 assert_eq!(state.footer_button_index, 1);
1127 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1128 assert_eq!(state.footer_button_index, 2);
1129 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1130 assert_eq!(state.footer_button_index, 3);
1131 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1132 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1134 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1135
1136 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1139 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1140
1141 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1143 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1144 assert_eq!(
1145 state.footer_button_index, 0,
1146 "Footer should reset to Layer button (index 0) on second loop"
1147 );
1148 }
1149
1150 #[test]
1151 fn test_escape_shows_confirm_dialog_with_changes() {
1152 let schema = include_str!("../../../plugins/config-schema.json");
1153 let config = crate::config::Config::default();
1154 let mut state = SettingsState::new(schema, &config).unwrap();
1155 state.visible = true;
1156
1157 state
1159 .pending_changes
1160 .insert("/test".to_string(), serde_json::json!(true));
1161
1162 let mut ctx = InputContext::new();
1163
1164 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1166 assert!(state.showing_confirm_dialog);
1167 assert!(ctx.deferred_actions.is_empty()); }
1169
1170 #[test]
1171 fn test_escape_closes_directly_without_changes() {
1172 let schema = include_str!("../../../plugins/config-schema.json");
1173 let config = crate::config::Config::default();
1174 let mut state = SettingsState::new(schema, &config).unwrap();
1175 state.visible = true;
1176
1177 let mut ctx = InputContext::new();
1178
1179 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1181 assert!(!state.showing_confirm_dialog);
1182 assert_eq!(ctx.deferred_actions.len(), 1);
1183 assert!(matches!(
1184 ctx.deferred_actions[0],
1185 DeferredAction::CloseSettings { save: false }
1186 ));
1187 }
1188
1189 #[test]
1190 fn test_confirm_dialog_navigation() {
1191 let schema = include_str!("../../../plugins/config-schema.json");
1192 let config = crate::config::Config::default();
1193 let mut state = SettingsState::new(schema, &config).unwrap();
1194 state.visible = true;
1195 state.showing_confirm_dialog = true;
1196 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1199
1200 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1202 assert_eq!(state.confirm_dialog_selection, 1);
1203
1204 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1206 assert_eq!(state.confirm_dialog_selection, 2);
1207
1208 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1210 assert_eq!(state.confirm_dialog_selection, 2);
1211
1212 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1214 assert_eq!(state.confirm_dialog_selection, 1);
1215 }
1216
1217 #[test]
1218 fn test_search_mode_captures_typing() {
1219 let schema = include_str!("../../../plugins/config-schema.json");
1220 let config = crate::config::Config::default();
1221 let mut state = SettingsState::new(schema, &config).unwrap();
1222 state.visible = true;
1223
1224 let mut ctx = InputContext::new();
1225
1226 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1228 assert!(state.search_active);
1229
1230 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1232 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1233 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1234 assert_eq!(state.search_query, "tab");
1235
1236 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1238 assert!(!state.search_active);
1239 assert!(state.search_query.is_empty());
1240 }
1241
1242 #[test]
1243 fn test_footer_button_activation() {
1244 let schema = include_str!("../../../plugins/config-schema.json");
1245 let config = crate::config::Config::default();
1246 let mut state = SettingsState::new(schema, &config).unwrap();
1247 state.visible = true;
1248 state.focus.set(FocusPanel::Footer);
1249 state.footer_button_index = 2; let mut ctx = InputContext::new();
1252
1253 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1255 assert_eq!(ctx.deferred_actions.len(), 1);
1256 assert!(matches!(
1257 ctx.deferred_actions[0],
1258 DeferredAction::CloseSettings { save: true }
1259 ));
1260 }
1261}