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_help {
40 return self.handle_help_input(event, ctx);
41 }
42
43 if self.search_active {
45 return self.handle_search_input(event, ctx);
46 }
47
48 if event.modifiers.contains(KeyModifiers::CONTROL)
50 && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
51 {
52 ctx.defer(DeferredAction::CloseSettings { save: true });
53 return InputResult::Consumed;
54 }
55
56 match self.focus_panel {
58 FocusPanel::Categories => self.handle_categories_input(event, ctx),
59 FocusPanel::Settings => self.handle_settings_input(event, ctx),
60 FocusPanel::Footer => self.handle_footer_input(event, ctx),
61 }
62 }
63
64 fn is_modal(&self) -> bool {
65 true }
67}
68
69impl SettingsState {
70 fn handle_entry_dialog_input(
77 &mut self,
78 event: &KeyEvent,
79 ctx: &mut InputContext,
80 ) -> InputResult {
81 let (editing_text, dropdown_open) = if let Some(dialog) = self.entry_dialog() {
83 let dropdown_open = dialog
84 .current_item()
85 .map(|item| matches!(&item.control, SettingControl::Dropdown(s) if s.open))
86 .unwrap_or(false);
87 (dialog.editing_text, dropdown_open)
88 } else {
89 return InputResult::Consumed;
90 };
91
92 if editing_text {
94 self.handle_entry_dialog_text_editing(event, ctx)
95 } else if dropdown_open {
96 self.handle_entry_dialog_dropdown(event)
97 } else {
98 self.handle_entry_dialog_navigation(event)
99 }
100 }
101
102 fn handle_entry_dialog_text_editing(
104 &mut self,
105 event: &KeyEvent,
106 ctx: &mut InputContext,
107 ) -> InputResult {
108 let is_editing_json = self
110 .entry_dialog()
111 .map(|d| d.is_editing_json())
112 .unwrap_or(false);
113
114 let can_exit = self.entry_dialog_can_exit_text_editing();
116
117 let Some(dialog) = self.entry_dialog_mut() else {
118 return InputResult::Consumed;
119 };
120
121 match event.code {
122 KeyCode::Esc => {
123 if !can_exit {
125 }
127 dialog.stop_editing();
128 }
129 KeyCode::Enter => {
130 if is_editing_json {
131 dialog.insert_newline();
133 } else {
134 if let Some(item) = dialog.current_item_mut() {
136 if let SettingControl::TextList(state) = &mut item.control {
137 state.add_item();
138 }
139 }
140 }
141 }
142 KeyCode::Char(c) => {
143 if event.modifiers.contains(KeyModifiers::CONTROL) {
144 match c {
145 'a' | 'A' => {
146 dialog.select_all();
148 }
149 'v' | 'V' => {
150 ctx.defer(DeferredAction::PasteToSettings);
152 }
153 _ => {}
154 }
155 } else {
156 dialog.insert_char(c);
157 }
158 }
159 KeyCode::Backspace => {
160 dialog.backspace();
161 }
162 KeyCode::Delete => {
163 if is_editing_json {
164 dialog.delete();
166 } else {
167 dialog.delete_list_item();
169 }
170 }
171 KeyCode::Home => {
172 dialog.cursor_home();
173 }
174 KeyCode::End => {
175 dialog.cursor_end();
176 }
177 KeyCode::Left => {
178 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
179 dialog.cursor_left_selecting();
180 } else {
181 dialog.cursor_left();
182 }
183 }
184 KeyCode::Right => {
185 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
186 dialog.cursor_right_selecting();
187 } else {
188 dialog.cursor_right();
189 }
190 }
191 KeyCode::Up => {
192 if is_editing_json {
193 if event.modifiers.contains(KeyModifiers::SHIFT) {
195 dialog.cursor_up_selecting();
196 } else {
197 dialog.cursor_up();
198 }
199 } else {
200 if let Some(item) = dialog.current_item_mut() {
202 if let SettingControl::TextList(state) = &mut item.control {
203 state.focus_prev();
204 }
205 }
206 }
207 }
208 KeyCode::Down => {
209 if is_editing_json {
210 if event.modifiers.contains(KeyModifiers::SHIFT) {
212 dialog.cursor_down_selecting();
213 } else {
214 dialog.cursor_down();
215 }
216 } else {
217 if let Some(item) = dialog.current_item_mut() {
219 if let SettingControl::TextList(state) = &mut item.control {
220 state.focus_next();
221 }
222 }
223 }
224 }
225 KeyCode::Tab => {
226 if is_editing_json {
227 let is_valid = dialog
229 .current_item()
230 .map(|item| {
231 if let SettingControl::Json(state) = &item.control {
232 state.is_valid()
233 } else {
234 true
235 }
236 })
237 .unwrap_or(true);
238
239 if is_valid {
240 if let Some(item) = dialog.current_item_mut() {
242 if let SettingControl::Json(state) = &mut item.control {
243 state.commit();
244 }
245 }
246 dialog.stop_editing();
247 }
248 }
250 }
251 _ => {}
252 }
253 InputResult::Consumed
254 }
255
256 fn handle_entry_dialog_dropdown(&mut self, event: &KeyEvent) -> InputResult {
258 let Some(dialog) = self.entry_dialog_mut() else {
259 return InputResult::Consumed;
260 };
261
262 match event.code {
263 KeyCode::Up => {
264 dialog.dropdown_prev();
265 }
266 KeyCode::Down => {
267 dialog.dropdown_next();
268 }
269 KeyCode::Enter => {
270 dialog.dropdown_confirm();
271 }
272 KeyCode::Esc => {
273 dialog.dropdown_confirm(); }
275 _ => {}
276 }
277 InputResult::Consumed
278 }
279
280 fn handle_entry_dialog_navigation(&mut self, event: &KeyEvent) -> InputResult {
282 match event.code {
283 KeyCode::Esc => {
284 self.close_entry_dialog();
285 }
286 KeyCode::Up => {
287 if let Some(dialog) = self.entry_dialog_mut() {
288 dialog.focus_prev();
289 }
290 }
291 KeyCode::Down => {
292 if let Some(dialog) = self.entry_dialog_mut() {
293 dialog.focus_next();
294 }
295 }
296 KeyCode::Tab => {
297 if let Some(dialog) = self.entry_dialog_mut() {
298 dialog.focus_next();
299 }
300 }
301 KeyCode::BackTab => {
302 if let Some(dialog) = self.entry_dialog_mut() {
303 dialog.focus_prev();
304 }
305 }
306 KeyCode::Left => {
307 if let Some(dialog) = self.entry_dialog_mut() {
309 if !dialog.focus_on_buttons {
310 dialog.decrement_number();
311 } else if dialog.focused_button > 0 {
312 dialog.focused_button -= 1;
313 }
314 }
315 }
316 KeyCode::Right => {
317 if let Some(dialog) = self.entry_dialog_mut() {
319 if !dialog.focus_on_buttons {
320 dialog.increment_number();
321 } else if dialog.focused_button + 1 < dialog.button_count() {
322 dialog.focused_button += 1;
323 }
324 }
325 }
326 KeyCode::Enter | KeyCode::Char(' ') => {
327 let button_action = self.entry_dialog().and_then(|dialog| {
329 if dialog.focus_on_buttons {
330 let cancel_idx = dialog.button_count() - 1;
331 if dialog.focused_button == 0 {
332 Some(ButtonAction::Save)
333 } else if !dialog.is_new && dialog.focused_button == 1 {
334 Some(ButtonAction::Delete)
335 } else if dialog.focused_button == cancel_idx {
336 Some(ButtonAction::Cancel)
337 } else {
338 None
339 }
340 } else {
341 None
342 }
343 });
344
345 if let Some(action) = button_action {
346 match action {
347 ButtonAction::Save => self.save_entry_dialog(),
348 ButtonAction::Delete => self.delete_entry_dialog(),
349 ButtonAction::Cancel => self.close_entry_dialog(),
350 }
351 } else if event.modifiers.contains(KeyModifiers::CONTROL) {
352 self.save_entry_dialog();
354 } else {
355 let control_action = self
357 .entry_dialog()
358 .and_then(|dialog| {
359 dialog.current_item().map(|item| match &item.control {
360 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
361 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
362 SettingControl::Text(_)
363 | SettingControl::TextList(_)
364 | SettingControl::Number(_)
365 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
366 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
367 Some(ControlAction::OpenNestedDialog)
368 }
369 _ => None,
370 })
371 })
372 .flatten();
373
374 if let Some(action) = control_action {
375 match action {
376 ControlAction::ToggleBool => {
377 if let Some(dialog) = self.entry_dialog_mut() {
378 dialog.toggle_bool();
379 }
380 }
381 ControlAction::ToggleDropdown => {
382 if let Some(dialog) = self.entry_dialog_mut() {
383 dialog.toggle_dropdown();
384 }
385 }
386 ControlAction::StartEditing => {
387 if let Some(dialog) = self.entry_dialog_mut() {
388 dialog.start_editing();
389 }
390 }
391 ControlAction::OpenNestedDialog => {
392 self.open_nested_entry_dialog();
394 }
395 }
396 }
397 }
398 }
399 _ => {}
400 }
401 InputResult::Consumed
402 }
403
404 fn handle_confirm_dialog_input(
406 &mut self,
407 event: &KeyEvent,
408 ctx: &mut InputContext,
409 ) -> InputResult {
410 match event.code {
411 KeyCode::Left => {
412 if self.confirm_dialog_selection > 0 {
413 self.confirm_dialog_selection -= 1;
414 }
415 InputResult::Consumed
416 }
417 KeyCode::Right => {
418 if self.confirm_dialog_selection < 2 {
419 self.confirm_dialog_selection += 1;
420 }
421 InputResult::Consumed
422 }
423 KeyCode::Enter => {
424 match self.confirm_dialog_selection {
425 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
429 }
430 InputResult::Consumed
431 }
432 KeyCode::Esc => {
433 self.showing_confirm_dialog = false;
434 InputResult::Consumed
435 }
436 KeyCode::Char('s') | KeyCode::Char('S') => {
437 ctx.defer(DeferredAction::CloseSettings { save: true });
438 InputResult::Consumed
439 }
440 KeyCode::Char('d') | KeyCode::Char('D') => {
441 ctx.defer(DeferredAction::CloseSettings { save: false });
442 InputResult::Consumed
443 }
444 _ => InputResult::Consumed, }
446 }
447
448 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
450 self.showing_help = false;
452 InputResult::Consumed
453 }
454
455 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
457 match event.code {
458 KeyCode::Esc => {
459 self.cancel_search();
460 InputResult::Consumed
461 }
462 KeyCode::Enter => {
463 self.jump_to_search_result();
464 InputResult::Consumed
465 }
466 KeyCode::Up => {
467 self.search_prev();
468 InputResult::Consumed
469 }
470 KeyCode::Down => {
471 self.search_next();
472 InputResult::Consumed
473 }
474 KeyCode::Char(c) => {
475 self.search_push_char(c);
476 InputResult::Consumed
477 }
478 KeyCode::Backspace => {
479 self.search_pop_char();
480 InputResult::Consumed
481 }
482 _ => InputResult::Consumed, }
484 }
485
486 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
488 match event.code {
489 KeyCode::Up => {
490 self.select_prev();
491 InputResult::Consumed
492 }
493 KeyCode::Down => {
494 self.select_next();
495 InputResult::Consumed
496 }
497 KeyCode::Tab => {
498 self.toggle_focus();
499 InputResult::Consumed
500 }
501 KeyCode::Char('/') => {
502 self.start_search();
503 InputResult::Consumed
504 }
505 KeyCode::Char('?') => {
506 self.toggle_help();
507 InputResult::Consumed
508 }
509 KeyCode::Esc => {
510 self.request_close(ctx);
511 InputResult::Consumed
512 }
513 KeyCode::Enter | KeyCode::Right => {
514 self.focus_panel = FocusPanel::Settings;
516 InputResult::Consumed
517 }
518 _ => InputResult::Ignored, }
520 }
521
522 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
524 if self.editing_text {
526 return self.handle_text_editing_input(event, ctx);
527 }
528
529 if self.is_number_editing() {
531 return self.handle_number_editing_input(event, ctx);
532 }
533
534 if self.is_dropdown_open() {
536 return self.handle_dropdown_input(event, ctx);
537 }
538
539 match event.code {
540 KeyCode::Up => {
541 self.select_prev();
542 InputResult::Consumed
543 }
544 KeyCode::Down => {
545 self.select_next();
546 InputResult::Consumed
547 }
548 KeyCode::Tab => {
549 self.toggle_focus();
550 InputResult::Consumed
551 }
552 KeyCode::Left => {
553 self.handle_control_decrement();
554 InputResult::Consumed
555 }
556 KeyCode::Right => {
557 self.handle_control_increment();
558 InputResult::Consumed
559 }
560 KeyCode::Enter | KeyCode::Char(' ') => {
561 self.handle_control_activate(ctx);
562 InputResult::Consumed
563 }
564 KeyCode::Char('/') => {
565 self.start_search();
566 InputResult::Consumed
567 }
568 KeyCode::Char('?') => {
569 self.toggle_help();
570 InputResult::Consumed
571 }
572 KeyCode::Esc => {
573 self.request_close(ctx);
574 InputResult::Consumed
575 }
576 _ => InputResult::Ignored, }
578 }
579
580 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
584 const FOOTER_BUTTON_COUNT: usize = 5;
585
586 match event.code {
587 KeyCode::Left | KeyCode::BackTab => {
588 if self.footer_button_index > 0 {
590 self.footer_button_index -= 1;
591 } else {
592 self.focus_panel = FocusPanel::Settings;
593 }
594 InputResult::Consumed
595 }
596 KeyCode::Right => {
597 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
599 self.footer_button_index += 1;
600 }
601 InputResult::Consumed
602 }
603 KeyCode::Tab => {
604 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
606 self.footer_button_index += 1;
607 } else {
608 self.focus_panel = FocusPanel::Categories;
609 }
610 InputResult::Consumed
611 }
612 KeyCode::Enter => {
613 match self.footer_button_index {
614 0 => self.cycle_target_layer(), 1 => self.reset_current_to_default(),
616 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
617 3 => self.request_close(ctx),
618 4 => ctx.defer(DeferredAction::OpenConfigFile {
619 layer: self.target_layer,
620 }), _ => {}
622 }
623 InputResult::Consumed
624 }
625 KeyCode::Esc => {
626 self.request_close(ctx);
627 InputResult::Consumed
628 }
629 KeyCode::Char('/') => {
630 self.start_search();
631 InputResult::Consumed
632 }
633 KeyCode::Char('?') => {
634 self.toggle_help();
635 InputResult::Consumed
636 }
637 _ => InputResult::Ignored, }
639 }
640
641 fn handle_text_editing_input(
643 &mut self,
644 event: &KeyEvent,
645 _ctx: &mut InputContext,
646 ) -> InputResult {
647 match event.code {
648 KeyCode::Esc => {
649 if !self.can_exit_text_editing() {
651 return InputResult::Consumed;
652 }
653 self.stop_editing();
654 InputResult::Consumed
655 }
656 KeyCode::Enter => {
657 self.text_add_item();
658 InputResult::Consumed
659 }
660 KeyCode::Char(c) => {
661 self.text_insert(c);
662 InputResult::Consumed
663 }
664 KeyCode::Backspace => {
665 self.text_backspace();
666 InputResult::Consumed
667 }
668 KeyCode::Delete => {
669 self.text_remove_focused();
670 InputResult::Consumed
671 }
672 KeyCode::Left => {
673 self.text_move_left();
674 InputResult::Consumed
675 }
676 KeyCode::Right => {
677 self.text_move_right();
678 InputResult::Consumed
679 }
680 KeyCode::Up => {
681 self.text_focus_prev();
682 InputResult::Consumed
683 }
684 KeyCode::Down => {
685 self.text_focus_next();
686 InputResult::Consumed
687 }
688 _ => InputResult::Consumed, }
690 }
691
692 fn handle_number_editing_input(
694 &mut self,
695 event: &KeyEvent,
696 _ctx: &mut InputContext,
697 ) -> InputResult {
698 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
699 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
700
701 match event.code {
702 KeyCode::Esc => {
703 self.number_cancel();
704 }
705 KeyCode::Enter => {
706 self.number_confirm();
707 }
708 KeyCode::Char('a') if ctrl => {
709 self.number_select_all();
710 }
711 KeyCode::Char(c) => {
712 self.number_insert(c);
713 }
714 KeyCode::Backspace if ctrl => {
715 self.number_delete_word_backward();
716 }
717 KeyCode::Backspace => {
718 self.number_backspace();
719 }
720 KeyCode::Delete if ctrl => {
721 self.number_delete_word_forward();
722 }
723 KeyCode::Delete => {
724 self.number_delete();
725 }
726 KeyCode::Left if ctrl && shift => {
727 self.number_move_word_left_selecting();
728 }
729 KeyCode::Left if ctrl => {
730 self.number_move_word_left();
731 }
732 KeyCode::Left if shift => {
733 self.number_move_left_selecting();
734 }
735 KeyCode::Left => {
736 self.number_move_left();
737 }
738 KeyCode::Right if ctrl && shift => {
739 self.number_move_word_right_selecting();
740 }
741 KeyCode::Right if ctrl => {
742 self.number_move_word_right();
743 }
744 KeyCode::Right if shift => {
745 self.number_move_right_selecting();
746 }
747 KeyCode::Right => {
748 self.number_move_right();
749 }
750 KeyCode::Home if shift => {
751 self.number_move_home_selecting();
752 }
753 KeyCode::Home => {
754 self.number_move_home();
755 }
756 KeyCode::End if shift => {
757 self.number_move_end_selecting();
758 }
759 KeyCode::End => {
760 self.number_move_end();
761 }
762 _ => {}
763 }
764 InputResult::Consumed }
766
767 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
769 match event.code {
770 KeyCode::Up => {
771 self.dropdown_prev();
772 InputResult::Consumed
773 }
774 KeyCode::Down => {
775 self.dropdown_next();
776 InputResult::Consumed
777 }
778 KeyCode::Home => {
779 self.dropdown_home();
780 InputResult::Consumed
781 }
782 KeyCode::End => {
783 self.dropdown_end();
784 InputResult::Consumed
785 }
786 KeyCode::Enter => {
787 self.dropdown_confirm();
788 InputResult::Consumed
789 }
790 KeyCode::Esc => {
791 self.dropdown_cancel();
792 InputResult::Consumed
793 }
794 _ => InputResult::Consumed, }
796 }
797
798 fn request_close(&mut self, ctx: &mut InputContext) {
800 if self.has_changes() {
801 self.showing_confirm_dialog = true;
802 self.confirm_dialog_selection = 0;
803 } else {
804 ctx.defer(DeferredAction::CloseSettings { save: false });
805 }
806 }
807
808 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
810 if let Some(item) = self.current_item_mut() {
811 match &mut item.control {
812 SettingControl::Toggle(ref mut state) => {
813 state.checked = !state.checked;
814 self.on_value_changed();
815 }
816 SettingControl::Dropdown(_) => {
817 self.dropdown_toggle();
818 }
819 SettingControl::Number(_) => {
820 self.start_number_editing();
821 }
822 SettingControl::Text(_) => {
823 self.start_editing();
824 }
825 SettingControl::TextList(_) => {
826 self.start_editing();
827 }
828 SettingControl::Map(ref mut state) => {
829 if state.focused_entry.is_none() {
830 if state.value_schema.is_some() {
832 self.open_add_entry_dialog();
833 }
834 } else if state.value_schema.is_some() {
835 self.open_entry_dialog();
837 } else {
838 if let Some(idx) = state.focused_entry {
840 if state.expanded.contains(&idx) {
841 state.expanded.retain(|&i| i != idx);
842 } else {
843 state.expanded.push(idx);
844 }
845 }
846 }
847 self.on_value_changed();
848 }
849 SettingControl::Json(_) => {
850 self.start_editing();
851 }
852 SettingControl::ObjectArray(ref state) => {
853 if state.focused_index.is_none() {
854 if state.item_schema.is_some() {
856 self.open_add_array_item_dialog();
857 }
858 } else if state.item_schema.is_some() {
859 self.open_edit_array_item_dialog();
861 }
862 }
863 SettingControl::Complex { .. } => {
864 }
866 }
867 }
868 }
869
870 fn handle_control_increment(&mut self) {
872 if let Some(item) = self.current_item_mut() {
873 match &mut item.control {
874 SettingControl::Number(ref mut state) => {
875 state.value += 1;
876 if let Some(max) = state.max {
877 state.value = state.value.min(max);
878 }
879 self.on_value_changed();
880 }
881 SettingControl::Dropdown(ref mut state) => {
882 state.select_next();
883 self.on_value_changed();
884 }
885 SettingControl::Map(ref mut state) => {
886 let entry_count = state.entries.len();
888 if let Some(idx) = state.focused_entry {
889 if idx + 1 < entry_count {
890 state.focused_entry = Some(idx + 1);
891 }
892 }
893 }
894 SettingControl::ObjectArray(ref mut state) => {
895 state.focus_next();
896 }
897 _ => {}
898 }
899 }
900 }
901
902 fn handle_control_decrement(&mut self) {
904 if let Some(item) = self.current_item_mut() {
905 match &mut item.control {
906 SettingControl::Number(ref mut state) => {
907 if state.value > 0 {
908 state.value -= 1;
909 }
910 if let Some(min) = state.min {
911 state.value = state.value.max(min);
912 }
913 self.on_value_changed();
914 }
915 SettingControl::Dropdown(ref mut state) => {
916 state.select_prev();
917 self.on_value_changed();
918 }
919 SettingControl::Map(ref mut state) => {
920 if let Some(idx) = state.focused_entry {
921 if idx > 0 {
922 state.focused_entry = Some(idx - 1);
923 }
924 }
925 }
926 SettingControl::ObjectArray(ref mut state) => {
927 state.focus_prev();
928 }
929 _ => {}
930 }
931 }
932 }
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
939
940 fn key(code: KeyCode) -> KeyEvent {
941 KeyEvent::new(code, KeyModifiers::NONE)
942 }
943
944 #[test]
945 fn test_settings_is_modal() {
946 let schema = include_str!("../../../plugins/config-schema.json");
948 let config = crate::config::Config::default();
949 let state = SettingsState::new(schema, &config).unwrap();
950 assert!(state.is_modal());
951 }
952
953 #[test]
954 fn test_categories_panel_does_not_leak_to_settings() {
955 let schema = include_str!("../../../plugins/config-schema.json");
956 let config = crate::config::Config::default();
957 let mut state = SettingsState::new(schema, &config).unwrap();
958 state.visible = true;
959 state.focus_panel = FocusPanel::Categories;
960
961 let mut ctx = InputContext::new();
962
963 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
966 assert_eq!(result, InputResult::Consumed);
967 assert_eq!(state.focus_panel, FocusPanel::Settings);
968
969 state.focus_panel = FocusPanel::Categories;
971
972 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
974 assert_eq!(result, InputResult::Consumed);
975 assert_eq!(state.focus_panel, FocusPanel::Settings);
977 }
978
979 #[test]
980 fn test_tab_cycles_focus_panels() {
981 let schema = include_str!("../../../plugins/config-schema.json");
982 let config = crate::config::Config::default();
983 let mut state = SettingsState::new(schema, &config).unwrap();
984 state.visible = true;
985
986 let mut ctx = InputContext::new();
987
988 assert_eq!(state.focus_panel, FocusPanel::Categories);
990
991 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
993 assert_eq!(state.focus_panel, FocusPanel::Settings);
994
995 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
997 assert_eq!(state.focus_panel, FocusPanel::Footer);
998 assert_eq!(state.footer_button_index, 2);
999
1000 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1002 assert_eq!(state.footer_button_index, 3);
1003 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1004 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1006 assert_eq!(state.focus_panel, FocusPanel::Categories);
1007
1008 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1011 assert_eq!(state.focus_panel, FocusPanel::Settings);
1012
1013 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1015 assert_eq!(state.focus_panel, FocusPanel::Footer);
1016 assert_eq!(
1017 state.footer_button_index, 2,
1018 "Footer should reset to Save button (index 2) on second loop"
1019 );
1020 }
1021
1022 #[test]
1023 fn test_escape_shows_confirm_dialog_with_changes() {
1024 let schema = include_str!("../../../plugins/config-schema.json");
1025 let config = crate::config::Config::default();
1026 let mut state = SettingsState::new(schema, &config).unwrap();
1027 state.visible = true;
1028
1029 state
1031 .pending_changes
1032 .insert("/test".to_string(), serde_json::json!(true));
1033
1034 let mut ctx = InputContext::new();
1035
1036 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1038 assert!(state.showing_confirm_dialog);
1039 assert!(ctx.deferred_actions.is_empty()); }
1041
1042 #[test]
1043 fn test_escape_closes_directly_without_changes() {
1044 let schema = include_str!("../../../plugins/config-schema.json");
1045 let config = crate::config::Config::default();
1046 let mut state = SettingsState::new(schema, &config).unwrap();
1047 state.visible = true;
1048
1049 let mut ctx = InputContext::new();
1050
1051 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1053 assert!(!state.showing_confirm_dialog);
1054 assert_eq!(ctx.deferred_actions.len(), 1);
1055 assert!(matches!(
1056 ctx.deferred_actions[0],
1057 DeferredAction::CloseSettings { save: false }
1058 ));
1059 }
1060
1061 #[test]
1062 fn test_confirm_dialog_navigation() {
1063 let schema = include_str!("../../../plugins/config-schema.json");
1064 let config = crate::config::Config::default();
1065 let mut state = SettingsState::new(schema, &config).unwrap();
1066 state.visible = true;
1067 state.showing_confirm_dialog = true;
1068 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1071
1072 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1074 assert_eq!(state.confirm_dialog_selection, 1);
1075
1076 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1078 assert_eq!(state.confirm_dialog_selection, 2);
1079
1080 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1082 assert_eq!(state.confirm_dialog_selection, 2);
1083
1084 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1086 assert_eq!(state.confirm_dialog_selection, 1);
1087 }
1088
1089 #[test]
1090 fn test_search_mode_captures_typing() {
1091 let schema = include_str!("../../../plugins/config-schema.json");
1092 let config = crate::config::Config::default();
1093 let mut state = SettingsState::new(schema, &config).unwrap();
1094 state.visible = true;
1095
1096 let mut ctx = InputContext::new();
1097
1098 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1100 assert!(state.search_active);
1101
1102 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1104 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1105 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1106 assert_eq!(state.search_query, "tab");
1107
1108 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1110 assert!(!state.search_active);
1111 assert!(state.search_query.is_empty());
1112 }
1113
1114 #[test]
1115 fn test_footer_button_activation() {
1116 let schema = include_str!("../../../plugins/config-schema.json");
1117 let config = crate::config::Config::default();
1118 let mut state = SettingsState::new(schema, &config).unwrap();
1119 state.visible = true;
1120 state.focus_panel = FocusPanel::Footer;
1121 state.footer_button_index = 2; let mut ctx = InputContext::new();
1124
1125 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1127 assert_eq!(ctx.deferred_actions.len(), 1);
1128 assert!(matches!(
1129 ctx.deferred_actions[0],
1130 DeferredAction::CloseSettings { save: true }
1131 ));
1132 }
1133}