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::DualList(_)
400 | SettingControl::Number(_)
401 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
402 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
403 Some(ControlAction::OpenNestedDialog)
404 }
405 _ => None,
406 })
407 })
408 .flatten();
409
410 if let Some(action) = control_action {
411 match action {
412 ControlAction::ToggleBool => {
413 if let Some(dialog) = self.entry_dialog_mut() {
414 dialog.toggle_bool();
415 }
416 }
417 ControlAction::ToggleDropdown => {
418 if let Some(dialog) = self.entry_dialog_mut() {
419 dialog.toggle_dropdown();
420 }
421 }
422 ControlAction::StartEditing => {
423 if let Some(dialog) = self.entry_dialog_mut() {
424 dialog.start_editing();
425 }
426 }
427 ControlAction::OpenNestedDialog => {
428 self.open_nested_entry_dialog();
429 }
430 }
431 }
432 }
433 }
434 KeyCode::Char(' ') => {
435 let control_action = self.entry_dialog().and_then(|dialog| {
437 if dialog.focus_on_buttons {
438 return None; }
440 dialog.current_item().and_then(|item| match &item.control {
441 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
442 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
443 _ => None,
444 })
445 });
446
447 if let Some(action) = control_action {
448 match action {
449 ControlAction::ToggleBool => {
450 if let Some(dialog) = self.entry_dialog_mut() {
451 dialog.toggle_bool();
452 }
453 }
454 ControlAction::ToggleDropdown => {
455 if let Some(dialog) = self.entry_dialog_mut() {
456 dialog.toggle_dropdown();
457 }
458 }
459 _ => {}
460 }
461 }
462 }
463 KeyCode::Char(c) => {
464 let can_auto_edit = self
466 .entry_dialog()
467 .and_then(|dialog| {
468 if dialog.focus_on_buttons {
469 return None;
470 }
471 dialog.current_item().map(|item| match &item.control {
472 SettingControl::Text(_) | SettingControl::TextList(_) => true,
473 SettingControl::Number(_) => c.is_ascii_digit() || c == '-' || c == '.',
474 _ => false,
475 })
476 })
477 .unwrap_or(false);
478
479 if can_auto_edit {
480 if let Some(dialog) = self.entry_dialog_mut() {
481 dialog.start_editing();
482 }
483 return self.handle_entry_dialog_text_editing(
485 &KeyEvent::new(KeyCode::Char(c), event.modifiers),
486 ctx,
487 );
488 }
489 }
490 _ => {}
491 }
492 InputResult::Consumed
493 }
494
495 fn handle_confirm_dialog_input(
497 &mut self,
498 event: &KeyEvent,
499 ctx: &mut InputContext,
500 ) -> InputResult {
501 match event.code {
502 KeyCode::Left | KeyCode::BackTab => {
503 if self.confirm_dialog_selection > 0 {
504 self.confirm_dialog_selection -= 1;
505 }
506 InputResult::Consumed
507 }
508 KeyCode::Right | KeyCode::Tab => {
509 if self.confirm_dialog_selection < 2 {
510 self.confirm_dialog_selection += 1;
511 }
512 InputResult::Consumed
513 }
514 KeyCode::Enter => {
515 match self.confirm_dialog_selection {
516 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
520 }
521 InputResult::Consumed
522 }
523 KeyCode::Esc => {
524 self.showing_confirm_dialog = false;
525 InputResult::Consumed
526 }
527 KeyCode::Char('s') | KeyCode::Char('S') => {
528 ctx.defer(DeferredAction::CloseSettings { save: true });
529 InputResult::Consumed
530 }
531 KeyCode::Char('d') | KeyCode::Char('D') => {
532 ctx.defer(DeferredAction::CloseSettings { save: false });
533 InputResult::Consumed
534 }
535 _ => InputResult::Consumed, }
537 }
538
539 fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
541 match event.code {
542 KeyCode::Left | KeyCode::BackTab => {
543 if self.reset_dialog_selection > 0 {
544 self.reset_dialog_selection -= 1;
545 }
546 InputResult::Consumed
547 }
548 KeyCode::Right | KeyCode::Tab => {
549 if self.reset_dialog_selection < 1 {
550 self.reset_dialog_selection += 1;
551 }
552 InputResult::Consumed
553 }
554 KeyCode::Enter => {
555 match self.reset_dialog_selection {
556 0 => {
557 self.discard_changes();
559 self.showing_reset_dialog = false;
560 }
561 1 => {
562 self.showing_reset_dialog = false;
564 }
565 _ => {}
566 }
567 InputResult::Consumed
568 }
569 KeyCode::Esc => {
570 self.showing_reset_dialog = false;
571 InputResult::Consumed
572 }
573 KeyCode::Char('r') | KeyCode::Char('R') => {
574 self.discard_changes();
575 self.showing_reset_dialog = false;
576 InputResult::Consumed
577 }
578 _ => InputResult::Consumed, }
580 }
581
582 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
584 self.showing_help = false;
586 InputResult::Consumed
587 }
588
589 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
591 match event.code {
592 KeyCode::Esc => {
593 self.cancel_search();
594 InputResult::Consumed
595 }
596 KeyCode::Enter => {
597 self.jump_to_search_result();
598 InputResult::Consumed
599 }
600 KeyCode::Up => {
601 self.search_prev();
602 InputResult::Consumed
603 }
604 KeyCode::Down => {
605 self.search_next();
606 InputResult::Consumed
607 }
608 KeyCode::Char(c) => {
609 self.search_push_char(c);
610 InputResult::Consumed
611 }
612 KeyCode::Backspace => {
613 self.search_pop_char();
614 InputResult::Consumed
615 }
616 _ => InputResult::Consumed, }
618 }
619
620 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
622 match event.code {
623 KeyCode::Up => {
624 self.select_prev();
625 InputResult::Consumed
626 }
627 KeyCode::Down => {
628 self.select_next();
629 InputResult::Consumed
630 }
631 KeyCode::Tab => {
632 self.toggle_focus();
633 InputResult::Consumed
634 }
635 KeyCode::BackTab => {
636 self.toggle_focus_backward();
637 InputResult::Consumed
638 }
639 KeyCode::Char('/') => {
640 self.start_search();
641 InputResult::Consumed
642 }
643 KeyCode::Char('?') => {
644 self.toggle_help();
645 InputResult::Consumed
646 }
647 KeyCode::Esc => {
648 self.request_close(ctx);
649 InputResult::Consumed
650 }
651 KeyCode::Enter | KeyCode::Right => {
652 self.focus.set(FocusPanel::Settings);
654 InputResult::Consumed
655 }
656 _ => InputResult::Ignored, }
658 }
659
660 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
662 if self.editing_text {
664 return self.handle_text_editing_input(event, ctx);
665 }
666
667 if self.is_number_editing() {
669 return self.handle_number_editing_input(event, ctx);
670 }
671
672 if self.is_dropdown_open() {
674 return self.handle_dropdown_input(event, ctx);
675 }
676
677 match event.code {
678 KeyCode::Up => {
679 self.select_prev();
680 InputResult::Consumed
681 }
682 KeyCode::Down => {
683 self.select_next();
684 InputResult::Consumed
685 }
686 KeyCode::Tab => {
687 self.toggle_focus();
688 InputResult::Consumed
689 }
690 KeyCode::BackTab => {
691 self.toggle_focus_backward();
692 InputResult::Consumed
693 }
694 KeyCode::Left => {
695 if self.is_number_control() {
698 self.handle_control_decrement();
699 } else {
700 self.update_control_focus(false);
701 self.focus.set(FocusPanel::Categories);
702 }
703 InputResult::Consumed
704 }
705 KeyCode::Right => {
706 self.handle_control_increment();
707 InputResult::Consumed
708 }
709 KeyCode::Enter | KeyCode::Char(' ') => {
710 self.handle_control_activate(ctx);
711 InputResult::Consumed
712 }
713 KeyCode::PageDown => {
714 self.select_next_page();
715 InputResult::Consumed
716 }
717 KeyCode::PageUp => {
718 self.select_prev_page();
719 InputResult::Consumed
720 }
721 KeyCode::Char('/') => {
722 self.start_search();
723 InputResult::Consumed
724 }
725 KeyCode::Char('?') => {
726 self.toggle_help();
727 InputResult::Consumed
728 }
729 KeyCode::Delete => {
730 self.set_current_to_null();
732 InputResult::Consumed
733 }
734 KeyCode::Esc => {
735 self.request_close(ctx);
736 InputResult::Consumed
737 }
738 _ => InputResult::Ignored, }
740 }
741
742 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
746 const FOOTER_BUTTON_COUNT: usize = 5;
747
748 match event.code {
749 KeyCode::Left | KeyCode::BackTab => {
750 if self.footer_button_index > 0 {
752 self.footer_button_index -= 1;
753 } else {
754 self.focus.set(FocusPanel::Settings);
755 }
756 InputResult::Consumed
757 }
758 KeyCode::Right => {
759 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
761 self.footer_button_index += 1;
762 }
763 InputResult::Consumed
764 }
765 KeyCode::Tab => {
766 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
768 self.footer_button_index += 1;
769 } else {
770 self.focus.set(FocusPanel::Categories);
771 }
772 InputResult::Consumed
773 }
774 KeyCode::Enter => {
775 match self.footer_button_index {
776 0 => self.cycle_target_layer(), 1 => {
778 let is_nullable_set = self
781 .current_item()
782 .map(|item| item.nullable && !item.is_null)
783 .unwrap_or(false);
784 if is_nullable_set {
785 self.set_current_to_null();
786 } else {
787 self.request_reset();
788 }
789 }
790 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
791 3 => self.request_close(ctx),
792 4 => ctx.defer(DeferredAction::OpenConfigFile {
793 layer: self.target_layer,
794 }), _ => {}
796 }
797 InputResult::Consumed
798 }
799 KeyCode::Esc => {
800 self.request_close(ctx);
801 InputResult::Consumed
802 }
803 KeyCode::Char('/') => {
804 self.start_search();
805 InputResult::Consumed
806 }
807 KeyCode::Char('?') => {
808 self.toggle_help();
809 InputResult::Consumed
810 }
811 _ => InputResult::Ignored, }
813 }
814
815 fn handle_text_editing_input(
817 &mut self,
818 event: &KeyEvent,
819 ctx: &mut InputContext,
820 ) -> InputResult {
821 let is_json = self.is_editing_json();
822
823 if is_json {
824 return self.handle_json_editing_input(event, ctx);
825 }
826
827 if self.is_editing_dual_list() {
829 return self.handle_dual_list_editing_input(event);
830 }
831
832 match event.code {
833 KeyCode::Esc => {
834 if !self.can_exit_text_editing() {
836 return InputResult::Consumed;
837 }
838 self.stop_editing();
839 InputResult::Consumed
840 }
841 KeyCode::Enter => {
842 self.text_add_item();
843 InputResult::Consumed
844 }
845 KeyCode::Char(c) => {
846 self.text_insert(c);
847 InputResult::Consumed
848 }
849 KeyCode::Backspace => {
850 self.text_backspace();
851 InputResult::Consumed
852 }
853 KeyCode::Delete => {
854 self.text_remove_focused();
855 InputResult::Consumed
856 }
857 KeyCode::Left => {
858 self.text_move_left();
859 InputResult::Consumed
860 }
861 KeyCode::Right => {
862 self.text_move_right();
863 InputResult::Consumed
864 }
865 KeyCode::Up => {
866 self.text_focus_prev();
867 InputResult::Consumed
868 }
869 KeyCode::Down => {
870 self.text_focus_next();
871 InputResult::Consumed
872 }
873 KeyCode::Tab => {
874 self.stop_editing();
876 self.toggle_focus();
877 InputResult::Consumed
878 }
879 _ => InputResult::Consumed, }
881 }
882
883 fn handle_dual_list_editing_input(&mut self, event: &KeyEvent) -> InputResult {
885 use crate::view::controls::DualListColumn;
886 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
887 match event.code {
888 KeyCode::Esc => {
889 self.stop_editing();
890 }
891 KeyCode::Tab | KeyCode::BackTab => {
893 self.stop_editing();
894 return InputResult::Ignored;
896 }
897 KeyCode::Up if shift => {
898 self.with_current_dual_list_mut(|dl| dl.move_up());
899 self.on_value_changed();
900 }
901 KeyCode::Down if shift => {
902 self.with_current_dual_list_mut(|dl| dl.move_down());
903 self.on_value_changed();
904 }
905 KeyCode::Up => {
906 self.with_current_dual_list_mut(|dl| dl.cursor_up());
907 }
908 KeyCode::Down => {
909 self.with_current_dual_list_mut(|dl| dl.cursor_down());
910 }
911 KeyCode::Right if shift => {
912 let changed = self
914 .with_current_dual_list_mut(|dl| {
915 if dl.active_column == DualListColumn::Available {
916 dl.add_selected();
917 dl.active_column = DualListColumn::Included;
919 dl.included_cursor = dl.included.len().saturating_sub(1);
920 true
921 } else {
922 false
923 }
924 })
925 .unwrap_or(false);
926 if changed {
927 self.on_value_changed();
928 self.refresh_dual_list_sibling();
929 }
930 }
931 KeyCode::Left if shift => {
932 let changed = self
934 .with_current_dual_list_mut(|dl| {
935 if dl.active_column == DualListColumn::Included {
936 let value = dl.included.get(dl.included_cursor).cloned();
937 dl.remove_selected();
938 dl.active_column = DualListColumn::Available;
940 if let Some(val) = value {
941 let avail = dl.available_items();
942 if let Some(pos) = avail.iter().position(|(v, _)| *v == val) {
943 dl.available_cursor = pos;
944 }
945 }
946 true
947 } else {
948 false
949 }
950 })
951 .unwrap_or(false);
952 if changed {
953 self.on_value_changed();
954 self.refresh_dual_list_sibling();
955 }
956 }
957 KeyCode::Right => {
958 self.with_current_dual_list_mut(|dl| {
960 dl.active_column = DualListColumn::Included;
961 });
962 }
963 KeyCode::Left => {
964 self.with_current_dual_list_mut(|dl| {
966 dl.active_column = DualListColumn::Available;
967 });
968 }
969 KeyCode::Enter => {
970 let changed = self
972 .with_current_dual_list_mut(|dl| match dl.active_column {
973 DualListColumn::Available => dl.add_selected(),
974 DualListColumn::Included => dl.remove_selected(),
975 })
976 .is_some();
977 if changed {
978 self.on_value_changed();
979 self.refresh_dual_list_sibling();
980 }
981 }
982 _ => {}
983 }
984 InputResult::Consumed
985 }
986
987 fn handle_json_editing_input(
989 &mut self,
990 event: &KeyEvent,
991 ctx: &mut InputContext,
992 ) -> InputResult {
993 match event.code {
994 KeyCode::Esc | KeyCode::Tab => {
995 self.json_exit_editing();
997 }
998 KeyCode::Enter => {
999 self.json_insert_newline();
1000 }
1001 KeyCode::Char(c) => {
1002 if event.modifiers.contains(KeyModifiers::CONTROL) {
1003 match c {
1004 'a' | 'A' => self.json_select_all(),
1005 'c' | 'C' => {
1006 if let Some(text) = self.json_selected_text() {
1007 ctx.defer(DeferredAction::CopyToClipboard(text));
1008 }
1009 }
1010 'v' | 'V' => {
1011 ctx.defer(DeferredAction::PasteToSettings);
1012 }
1013 _ => {}
1014 }
1015 } else {
1016 self.text_insert(c);
1017 }
1018 }
1019 KeyCode::Backspace => {
1020 self.text_backspace();
1021 }
1022 KeyCode::Delete => {
1023 self.json_delete();
1024 }
1025 KeyCode::Left => {
1026 if event.modifiers.contains(KeyModifiers::SHIFT) {
1027 self.json_cursor_left_selecting();
1028 } else {
1029 self.text_move_left();
1030 }
1031 }
1032 KeyCode::Right => {
1033 if event.modifiers.contains(KeyModifiers::SHIFT) {
1034 self.json_cursor_right_selecting();
1035 } else {
1036 self.text_move_right();
1037 }
1038 }
1039 KeyCode::Up => {
1040 if event.modifiers.contains(KeyModifiers::SHIFT) {
1041 self.json_cursor_up_selecting();
1042 } else {
1043 self.json_cursor_up();
1044 }
1045 }
1046 KeyCode::Down => {
1047 if event.modifiers.contains(KeyModifiers::SHIFT) {
1048 self.json_cursor_down_selecting();
1049 } else {
1050 self.json_cursor_down();
1051 }
1052 }
1053 _ => {}
1054 }
1055 InputResult::Consumed
1056 }
1057
1058 fn handle_number_editing_input(
1060 &mut self,
1061 event: &KeyEvent,
1062 _ctx: &mut InputContext,
1063 ) -> InputResult {
1064 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
1065 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1066
1067 match event.code {
1068 KeyCode::Esc => {
1069 self.number_cancel();
1070 }
1071 KeyCode::Enter => {
1072 self.number_confirm();
1073 }
1074 KeyCode::Char('a') if ctrl => {
1075 self.number_select_all();
1076 }
1077 KeyCode::Char(c) => {
1078 self.number_insert(c);
1079 }
1080 KeyCode::Backspace if ctrl => {
1081 self.number_delete_word_backward();
1082 }
1083 KeyCode::Backspace => {
1084 self.number_backspace();
1085 }
1086 KeyCode::Delete if ctrl => {
1087 self.number_delete_word_forward();
1088 }
1089 KeyCode::Delete => {
1090 self.number_delete();
1091 }
1092 KeyCode::Left if ctrl && shift => {
1093 self.number_move_word_left_selecting();
1094 }
1095 KeyCode::Left if ctrl => {
1096 self.number_move_word_left();
1097 }
1098 KeyCode::Left if shift => {
1099 self.number_move_left_selecting();
1100 }
1101 KeyCode::Left => {
1102 self.number_move_left();
1103 }
1104 KeyCode::Right if ctrl && shift => {
1105 self.number_move_word_right_selecting();
1106 }
1107 KeyCode::Right if ctrl => {
1108 self.number_move_word_right();
1109 }
1110 KeyCode::Right if shift => {
1111 self.number_move_right_selecting();
1112 }
1113 KeyCode::Right => {
1114 self.number_move_right();
1115 }
1116 KeyCode::Home if shift => {
1117 self.number_move_home_selecting();
1118 }
1119 KeyCode::Home => {
1120 self.number_move_home();
1121 }
1122 KeyCode::End if shift => {
1123 self.number_move_end_selecting();
1124 }
1125 KeyCode::End => {
1126 self.number_move_end();
1127 }
1128 _ => {}
1129 }
1130 InputResult::Consumed }
1132
1133 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1135 match event.code {
1136 KeyCode::Up => {
1137 self.dropdown_prev();
1138 InputResult::Consumed
1139 }
1140 KeyCode::Down => {
1141 self.dropdown_next();
1142 InputResult::Consumed
1143 }
1144 KeyCode::Home => {
1145 self.dropdown_home();
1146 InputResult::Consumed
1147 }
1148 KeyCode::End => {
1149 self.dropdown_end();
1150 InputResult::Consumed
1151 }
1152 KeyCode::Enter => {
1153 self.dropdown_confirm();
1154 InputResult::Consumed
1155 }
1156 KeyCode::Esc => {
1157 self.dropdown_cancel();
1158 InputResult::Consumed
1159 }
1160 _ => InputResult::Consumed, }
1162 }
1163
1164 fn request_reset(&mut self) {
1166 if self.has_changes() {
1167 self.showing_reset_dialog = true;
1168 self.reset_dialog_selection = 0;
1169 }
1170 }
1171
1172 fn request_close(&mut self, ctx: &mut InputContext) {
1174 if self.has_changes() {
1175 self.showing_confirm_dialog = true;
1176 self.confirm_dialog_selection = 0;
1177 } else {
1178 ctx.defer(DeferredAction::CloseSettings { save: false });
1179 }
1180 }
1181
1182 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1184 if let Some(item) = self.current_item_mut() {
1185 match &mut item.control {
1186 SettingControl::Toggle(ref mut state) => {
1187 state.checked = !state.checked;
1188 self.on_value_changed();
1189 }
1190 SettingControl::Dropdown(_) => {
1191 self.dropdown_toggle();
1192 }
1193 SettingControl::Number(_) => {
1194 self.start_number_editing();
1195 }
1196 SettingControl::Text(_) => {
1197 self.start_editing();
1198 }
1199 SettingControl::TextList(_) | SettingControl::DualList(_) => {
1200 self.start_editing();
1201 }
1202 SettingControl::Map(ref mut state) => {
1203 if state.focused_entry.is_none() {
1204 if state.value_schema.is_some() {
1206 self.open_add_entry_dialog();
1207 }
1208 } else if state.value_schema.is_some() {
1209 self.open_entry_dialog();
1211 } else {
1212 if let Some(idx) = state.focused_entry {
1214 if state.expanded.contains(&idx) {
1215 state.expanded.retain(|&i| i != idx);
1216 } else {
1217 state.expanded.push(idx);
1218 }
1219 }
1220 }
1221 self.on_value_changed();
1222 }
1223 SettingControl::Json(_) => {
1224 self.start_editing();
1225 }
1226 SettingControl::ObjectArray(ref state) => {
1227 if state.focused_index.is_none() {
1228 if state.item_schema.is_some() {
1230 self.open_add_array_item_dialog();
1231 }
1232 } else if state.item_schema.is_some() {
1233 self.open_edit_array_item_dialog();
1235 }
1236 }
1237 SettingControl::Complex { .. } => {
1238 }
1240 }
1241 }
1242 }
1243
1244 fn handle_control_increment(&mut self) {
1246 if let Some(item) = self.current_item_mut() {
1247 if let SettingControl::Number(ref mut state) = &mut item.control {
1248 state.value += 1;
1249 if let Some(max) = state.max {
1250 state.value = state.value.min(max);
1251 }
1252 self.on_value_changed();
1253 }
1254 }
1255 }
1256
1257 fn handle_control_decrement(&mut self) {
1259 if let Some(item) = self.current_item_mut() {
1260 if let SettingControl::Number(ref mut state) = &mut item.control {
1261 if state.value > 0 {
1262 state.value -= 1;
1263 }
1264 if let Some(min) = state.min {
1265 state.value = state.value.max(min);
1266 }
1267 self.on_value_changed();
1268 }
1269 }
1270 }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275 use super::*;
1276 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1277
1278 fn key(code: KeyCode) -> KeyEvent {
1279 KeyEvent::new(code, KeyModifiers::NONE)
1280 }
1281
1282 #[test]
1283 fn test_settings_is_modal() {
1284 let schema = include_str!("../../../plugins/config-schema.json");
1286 let config = crate::config::Config::default();
1287 let state = SettingsState::new(schema, &config).unwrap();
1288 assert!(state.is_modal());
1289 }
1290
1291 #[test]
1292 fn test_categories_panel_does_not_leak_to_settings() {
1293 let schema = include_str!("../../../plugins/config-schema.json");
1294 let config = crate::config::Config::default();
1295 let mut state = SettingsState::new(schema, &config).unwrap();
1296 state.visible = true;
1297 state.focus.set(FocusPanel::Categories);
1298
1299 let mut ctx = InputContext::new();
1300
1301 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1304 assert_eq!(result, InputResult::Consumed);
1305 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1306
1307 state.focus.set(FocusPanel::Categories);
1309
1310 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1312 assert_eq!(result, InputResult::Consumed);
1313 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1315 }
1316
1317 #[test]
1318 fn test_tab_cycles_focus_panels() {
1319 let schema = include_str!("../../../plugins/config-schema.json");
1320 let config = crate::config::Config::default();
1321 let mut state = SettingsState::new(schema, &config).unwrap();
1322 state.visible = true;
1323
1324 let mut ctx = InputContext::new();
1325
1326 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1328
1329 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1331 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1332
1333 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1335 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1336 assert_eq!(state.footer_button_index, 0);
1337
1338 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1340 assert_eq!(state.footer_button_index, 1);
1341 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1342 assert_eq!(state.footer_button_index, 2);
1343 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1344 assert_eq!(state.footer_button_index, 3);
1345 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1346 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1348 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1349
1350 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1353 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1354
1355 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1357 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1358 assert_eq!(
1359 state.footer_button_index, 0,
1360 "Footer should reset to Layer button (index 0) on second loop"
1361 );
1362 }
1363
1364 #[test]
1365 fn test_escape_shows_confirm_dialog_with_changes() {
1366 let schema = include_str!("../../../plugins/config-schema.json");
1367 let config = crate::config::Config::default();
1368 let mut state = SettingsState::new(schema, &config).unwrap();
1369 state.visible = true;
1370
1371 state
1373 .pending_changes
1374 .insert("/test".to_string(), serde_json::json!(true));
1375
1376 let mut ctx = InputContext::new();
1377
1378 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1380 assert!(state.showing_confirm_dialog);
1381 assert!(ctx.deferred_actions.is_empty()); }
1383
1384 #[test]
1385 fn test_escape_closes_directly_without_changes() {
1386 let schema = include_str!("../../../plugins/config-schema.json");
1387 let config = crate::config::Config::default();
1388 let mut state = SettingsState::new(schema, &config).unwrap();
1389 state.visible = true;
1390
1391 let mut ctx = InputContext::new();
1392
1393 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1395 assert!(!state.showing_confirm_dialog);
1396 assert_eq!(ctx.deferred_actions.len(), 1);
1397 assert!(matches!(
1398 ctx.deferred_actions[0],
1399 DeferredAction::CloseSettings { save: false }
1400 ));
1401 }
1402
1403 #[test]
1404 fn test_confirm_dialog_navigation() {
1405 let schema = include_str!("../../../plugins/config-schema.json");
1406 let config = crate::config::Config::default();
1407 let mut state = SettingsState::new(schema, &config).unwrap();
1408 state.visible = true;
1409 state.showing_confirm_dialog = true;
1410 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1413
1414 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1416 assert_eq!(state.confirm_dialog_selection, 1);
1417
1418 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1420 assert_eq!(state.confirm_dialog_selection, 2);
1421
1422 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1424 assert_eq!(state.confirm_dialog_selection, 2);
1425
1426 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1428 assert_eq!(state.confirm_dialog_selection, 1);
1429 }
1430
1431 #[test]
1432 fn test_search_mode_captures_typing() {
1433 let schema = include_str!("../../../plugins/config-schema.json");
1434 let config = crate::config::Config::default();
1435 let mut state = SettingsState::new(schema, &config).unwrap();
1436 state.visible = true;
1437
1438 let mut ctx = InputContext::new();
1439
1440 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1442 assert!(state.search_active);
1443
1444 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1446 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1447 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1448 assert_eq!(state.search_query, "tab");
1449
1450 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1452 assert!(!state.search_active);
1453 assert!(state.search_query.is_empty());
1454 }
1455
1456 #[test]
1457 fn test_footer_button_activation() {
1458 let schema = include_str!("../../../plugins/config-schema.json");
1459 let config = crate::config::Config::default();
1460 let mut state = SettingsState::new(schema, &config).unwrap();
1461 state.visible = true;
1462 state.focus.set(FocusPanel::Footer);
1463 state.footer_button_index = 2; let mut ctx = InputContext::new();
1466
1467 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1469 assert_eq!(ctx.deferred_actions.len(), 1);
1470 assert!(matches!(
1471 ctx.deferred_actions[0],
1472 DeferredAction::CloseSettings { save: true }
1473 ));
1474 }
1475}