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::PageUp => {
632 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
634 self.tree_step(-viewport);
635 InputResult::Consumed
636 }
637 KeyCode::PageDown => {
638 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
639 self.tree_step(viewport);
640 InputResult::Consumed
641 }
642 KeyCode::Home => {
643 let rows = self.visible_tree();
644 let cur = self.tree_cursor_index(&rows) as i32;
645 if cur > 0 {
646 self.tree_step(-cur);
647 }
648 InputResult::Consumed
649 }
650 KeyCode::End => {
651 let rows = self.visible_tree();
652 let cur = self.tree_cursor_index(&rows) as i32;
653 let last = rows.len() as i32 - 1;
654 if last > cur {
655 self.tree_step(last - cur);
656 }
657 InputResult::Consumed
658 }
659 KeyCode::Tab => {
660 self.toggle_focus();
661 InputResult::Consumed
662 }
663 KeyCode::BackTab => {
664 self.toggle_focus_backward();
665 InputResult::Consumed
666 }
667 KeyCode::Char('/') => {
668 self.start_search();
669 InputResult::Consumed
670 }
671 KeyCode::Char('?') => {
672 self.toggle_help();
673 InputResult::Consumed
674 }
675 KeyCode::Esc => {
676 self.request_close(ctx);
677 InputResult::Consumed
678 }
679 KeyCode::Right => {
680 let cat_idx = self.selected_category;
683 if self.is_category_expandable(cat_idx)
684 && !self.expanded_categories.contains(&cat_idx)
685 {
686 self.expanded_categories.insert(cat_idx);
687 }
688 InputResult::Consumed
689 }
690 KeyCode::Left => {
691 let cat_idx = self.selected_category;
693 if self.expanded_categories.contains(&cat_idx) {
694 self.expanded_categories.remove(&cat_idx);
695 self.tree_cursor_section = None;
700 }
701 InputResult::Consumed
702 }
703 _ => InputResult::Ignored, }
705 }
706
707 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
709 if self.editing_text {
711 return self.handle_text_editing_input(event, ctx);
712 }
713
714 if self.is_number_editing() {
716 return self.handle_number_editing_input(event, ctx);
717 }
718
719 if self.is_dropdown_open() {
721 return self.handle_dropdown_input(event, ctx);
722 }
723
724 match event.code {
725 KeyCode::Up => {
726 self.select_prev();
727 InputResult::Consumed
728 }
729 KeyCode::Down => {
730 self.select_next();
731 InputResult::Consumed
732 }
733 KeyCode::Tab => {
734 self.toggle_focus();
735 InputResult::Consumed
736 }
737 KeyCode::BackTab => {
738 self.toggle_focus_backward();
739 InputResult::Consumed
740 }
741 KeyCode::Left => {
742 if self.is_number_control() {
745 self.handle_control_decrement();
746 } else {
747 self.update_control_focus(false);
748 self.focus.set(FocusPanel::Categories);
749 }
750 InputResult::Consumed
751 }
752 KeyCode::Right => {
753 self.handle_control_increment();
754 InputResult::Consumed
755 }
756 KeyCode::Enter | KeyCode::Char(' ') => {
757 self.handle_control_activate(ctx);
758 InputResult::Consumed
759 }
760 KeyCode::PageDown => {
761 self.select_next_page();
762 InputResult::Consumed
763 }
764 KeyCode::PageUp => {
765 self.select_prev_page();
766 InputResult::Consumed
767 }
768 KeyCode::Char('/') => {
769 self.start_search();
770 InputResult::Consumed
771 }
772 KeyCode::Char('?') => {
773 self.toggle_help();
774 InputResult::Consumed
775 }
776 KeyCode::Delete => {
777 self.set_current_to_null();
779 InputResult::Consumed
780 }
781 KeyCode::Esc => {
782 self.request_close(ctx);
783 InputResult::Consumed
784 }
785 _ => InputResult::Ignored, }
787 }
788
789 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
793 const FOOTER_BUTTON_COUNT: usize = 5;
794
795 match event.code {
796 KeyCode::Left | KeyCode::BackTab => {
797 if self.footer_button_index > 0 {
799 self.footer_button_index -= 1;
800 } else {
801 self.focus.set(FocusPanel::Settings);
802 }
803 InputResult::Consumed
804 }
805 KeyCode::Right => {
806 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
808 self.footer_button_index += 1;
809 }
810 InputResult::Consumed
811 }
812 KeyCode::Tab => {
813 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
815 self.footer_button_index += 1;
816 } else {
817 self.focus.set(FocusPanel::Categories);
818 }
819 InputResult::Consumed
820 }
821 KeyCode::Enter => {
822 match self.footer_button_index {
823 0 => self.cycle_target_layer(), 1 => {
825 let is_nullable_set = self
828 .current_item()
829 .map(|item| item.nullable && !item.is_null)
830 .unwrap_or(false);
831 if is_nullable_set {
832 self.set_current_to_null();
833 } else {
834 self.request_reset();
835 }
836 }
837 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
838 3 => self.request_close(ctx),
839 4 => ctx.defer(DeferredAction::OpenConfigFile {
840 layer: self.target_layer,
841 }), _ => {}
843 }
844 InputResult::Consumed
845 }
846 KeyCode::Esc => {
847 self.request_close(ctx);
848 InputResult::Consumed
849 }
850 KeyCode::Char('/') => {
851 self.start_search();
852 InputResult::Consumed
853 }
854 KeyCode::Char('?') => {
855 self.toggle_help();
856 InputResult::Consumed
857 }
858 _ => InputResult::Ignored, }
860 }
861
862 fn handle_text_editing_input(
864 &mut self,
865 event: &KeyEvent,
866 ctx: &mut InputContext,
867 ) -> InputResult {
868 let is_json = self.is_editing_json();
869
870 if is_json {
871 return self.handle_json_editing_input(event, ctx);
872 }
873
874 if self.is_editing_dual_list() {
876 return self.handle_dual_list_editing_input(event);
877 }
878
879 match event.code {
880 KeyCode::Esc => {
881 if !self.can_exit_text_editing() {
883 return InputResult::Consumed;
884 }
885 self.stop_editing();
886 InputResult::Consumed
887 }
888 KeyCode::Enter => {
889 self.text_add_item();
890 InputResult::Consumed
891 }
892 KeyCode::Char(c) => {
893 self.text_insert(c);
894 InputResult::Consumed
895 }
896 KeyCode::Backspace => {
897 self.text_backspace();
898 InputResult::Consumed
899 }
900 KeyCode::Delete => {
901 self.text_remove_focused();
902 InputResult::Consumed
903 }
904 KeyCode::Left => {
905 self.text_move_left();
906 InputResult::Consumed
907 }
908 KeyCode::Right => {
909 self.text_move_right();
910 InputResult::Consumed
911 }
912 KeyCode::Up => {
913 self.text_focus_prev();
914 InputResult::Consumed
915 }
916 KeyCode::Down => {
917 self.text_focus_next();
918 InputResult::Consumed
919 }
920 KeyCode::Tab => {
921 self.stop_editing();
923 self.toggle_focus();
924 InputResult::Consumed
925 }
926 _ => InputResult::Consumed, }
928 }
929
930 fn handle_dual_list_editing_input(&mut self, event: &KeyEvent) -> InputResult {
932 use crate::view::controls::DualListColumn;
933 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
934 match event.code {
935 KeyCode::Esc => {
936 self.stop_editing();
937 }
938 KeyCode::Tab | KeyCode::BackTab => {
940 self.stop_editing();
941 return InputResult::Ignored;
943 }
944 KeyCode::Up if shift => {
945 self.with_current_dual_list_mut(|dl| dl.move_up());
946 self.on_value_changed();
947 }
948 KeyCode::Down if shift => {
949 self.with_current_dual_list_mut(|dl| dl.move_down());
950 self.on_value_changed();
951 }
952 KeyCode::Up => {
953 self.with_current_dual_list_mut(|dl| dl.cursor_up());
954 }
955 KeyCode::Down => {
956 self.with_current_dual_list_mut(|dl| dl.cursor_down());
957 }
958 KeyCode::Right if shift => {
959 let changed = self
961 .with_current_dual_list_mut(|dl| {
962 if dl.active_column == DualListColumn::Available {
963 dl.add_selected();
964 dl.active_column = DualListColumn::Included;
966 dl.included_cursor = dl.included.len().saturating_sub(1);
967 true
968 } else {
969 false
970 }
971 })
972 .unwrap_or(false);
973 if changed {
974 self.on_value_changed();
975 self.refresh_dual_list_sibling();
976 }
977 }
978 KeyCode::Left if shift => {
979 let changed = self
981 .with_current_dual_list_mut(|dl| {
982 if dl.active_column == DualListColumn::Included {
983 let value = dl.included.get(dl.included_cursor).cloned();
984 dl.remove_selected();
985 dl.active_column = DualListColumn::Available;
987 if let Some(val) = value {
988 let avail = dl.available_items();
989 if let Some(pos) = avail.iter().position(|(v, _)| *v == val) {
990 dl.available_cursor = pos;
991 }
992 }
993 true
994 } else {
995 false
996 }
997 })
998 .unwrap_or(false);
999 if changed {
1000 self.on_value_changed();
1001 self.refresh_dual_list_sibling();
1002 }
1003 }
1004 KeyCode::Right => {
1005 self.with_current_dual_list_mut(|dl| {
1007 dl.active_column = DualListColumn::Included;
1008 });
1009 }
1010 KeyCode::Left => {
1011 self.with_current_dual_list_mut(|dl| {
1013 dl.active_column = DualListColumn::Available;
1014 });
1015 }
1016 KeyCode::Enter => {
1017 let changed = self
1019 .with_current_dual_list_mut(|dl| match dl.active_column {
1020 DualListColumn::Available => dl.add_selected(),
1021 DualListColumn::Included => dl.remove_selected(),
1022 })
1023 .is_some();
1024 if changed {
1025 self.on_value_changed();
1026 self.refresh_dual_list_sibling();
1027 }
1028 }
1029 _ => {}
1030 }
1031 InputResult::Consumed
1032 }
1033
1034 fn handle_json_editing_input(
1036 &mut self,
1037 event: &KeyEvent,
1038 ctx: &mut InputContext,
1039 ) -> InputResult {
1040 match event.code {
1041 KeyCode::Esc | KeyCode::Tab => {
1042 self.json_exit_editing();
1044 }
1045 KeyCode::Enter => {
1046 self.json_insert_newline();
1047 }
1048 KeyCode::Char(c) => {
1049 if event.modifiers.contains(KeyModifiers::CONTROL) {
1050 match c {
1051 'a' | 'A' => self.json_select_all(),
1052 'c' | 'C' => {
1053 if let Some(text) = self.json_selected_text() {
1054 ctx.defer(DeferredAction::CopyToClipboard(text));
1055 }
1056 }
1057 'v' | 'V' => {
1058 ctx.defer(DeferredAction::PasteToSettings);
1059 }
1060 _ => {}
1061 }
1062 } else {
1063 self.text_insert(c);
1064 }
1065 }
1066 KeyCode::Backspace => {
1067 self.text_backspace();
1068 }
1069 KeyCode::Delete => {
1070 self.json_delete();
1071 }
1072 KeyCode::Left => {
1073 if event.modifiers.contains(KeyModifiers::SHIFT) {
1074 self.json_cursor_left_selecting();
1075 } else {
1076 self.text_move_left();
1077 }
1078 }
1079 KeyCode::Right => {
1080 if event.modifiers.contains(KeyModifiers::SHIFT) {
1081 self.json_cursor_right_selecting();
1082 } else {
1083 self.text_move_right();
1084 }
1085 }
1086 KeyCode::Up => {
1087 if event.modifiers.contains(KeyModifiers::SHIFT) {
1088 self.json_cursor_up_selecting();
1089 } else {
1090 self.json_cursor_up();
1091 }
1092 }
1093 KeyCode::Down => {
1094 if event.modifiers.contains(KeyModifiers::SHIFT) {
1095 self.json_cursor_down_selecting();
1096 } else {
1097 self.json_cursor_down();
1098 }
1099 }
1100 _ => {}
1101 }
1102 InputResult::Consumed
1103 }
1104
1105 fn handle_number_editing_input(
1107 &mut self,
1108 event: &KeyEvent,
1109 _ctx: &mut InputContext,
1110 ) -> InputResult {
1111 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
1112 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1113
1114 match event.code {
1115 KeyCode::Esc => {
1116 self.number_cancel();
1117 }
1118 KeyCode::Enter => {
1119 self.number_confirm();
1120 }
1121 KeyCode::Tab | KeyCode::BackTab => {
1122 self.number_confirm();
1127 }
1128 KeyCode::Char('a') if ctrl => {
1129 self.number_select_all();
1130 }
1131 KeyCode::Char(c) => {
1132 self.number_insert(c);
1133 }
1134 KeyCode::Backspace if ctrl => {
1135 self.number_delete_word_backward();
1136 }
1137 KeyCode::Backspace => {
1138 self.number_backspace();
1139 }
1140 KeyCode::Delete if ctrl => {
1141 self.number_delete_word_forward();
1142 }
1143 KeyCode::Delete => {
1144 self.number_delete();
1145 }
1146 KeyCode::Left if ctrl && shift => {
1147 self.number_move_word_left_selecting();
1148 }
1149 KeyCode::Left if ctrl => {
1150 self.number_move_word_left();
1151 }
1152 KeyCode::Left if shift => {
1153 self.number_move_left_selecting();
1154 }
1155 KeyCode::Left => {
1156 self.number_move_left();
1157 }
1158 KeyCode::Right if ctrl && shift => {
1159 self.number_move_word_right_selecting();
1160 }
1161 KeyCode::Right if ctrl => {
1162 self.number_move_word_right();
1163 }
1164 KeyCode::Right if shift => {
1165 self.number_move_right_selecting();
1166 }
1167 KeyCode::Right => {
1168 self.number_move_right();
1169 }
1170 KeyCode::Home if shift => {
1171 self.number_move_home_selecting();
1172 }
1173 KeyCode::Home => {
1174 self.number_move_home();
1175 }
1176 KeyCode::End if shift => {
1177 self.number_move_end_selecting();
1178 }
1179 KeyCode::End => {
1180 self.number_move_end();
1181 }
1182 _ => {}
1183 }
1184 InputResult::Consumed }
1186
1187 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1189 match event.code {
1190 KeyCode::Up => {
1191 self.dropdown_prev();
1192 InputResult::Consumed
1193 }
1194 KeyCode::Down => {
1195 self.dropdown_next();
1196 InputResult::Consumed
1197 }
1198 KeyCode::Home => {
1199 self.dropdown_home();
1200 InputResult::Consumed
1201 }
1202 KeyCode::End => {
1203 self.dropdown_end();
1204 InputResult::Consumed
1205 }
1206 KeyCode::Enter => {
1207 self.dropdown_confirm();
1208 InputResult::Consumed
1209 }
1210 KeyCode::Esc => {
1211 self.dropdown_cancel();
1212 InputResult::Consumed
1213 }
1214 _ => InputResult::Consumed, }
1216 }
1217
1218 fn request_reset(&mut self) {
1220 if self.has_changes() {
1221 self.showing_reset_dialog = true;
1222 self.reset_dialog_selection = 0;
1223 }
1224 }
1225
1226 fn request_close(&mut self, ctx: &mut InputContext) {
1228 if self.has_changes() {
1229 self.showing_confirm_dialog = true;
1230 self.confirm_dialog_selection = 0;
1231 } else {
1232 ctx.defer(DeferredAction::CloseSettings { save: false });
1233 }
1234 }
1235
1236 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1238 if let Some(item) = self.current_item_mut() {
1239 match &mut item.control {
1240 SettingControl::Toggle(ref mut state) => {
1241 state.checked = !state.checked;
1242 self.on_value_changed();
1243 }
1244 SettingControl::Dropdown(_) => {
1245 self.dropdown_toggle();
1246 }
1247 SettingControl::Number(_) => {
1248 self.start_number_editing();
1249 }
1250 SettingControl::Text(_) => {
1251 self.start_editing();
1252 }
1253 SettingControl::TextList(_) | SettingControl::DualList(_) => {
1254 self.start_editing();
1255 }
1256 SettingControl::Map(ref mut state) => {
1257 if state.focused_entry.is_none() {
1258 if state.value_schema.is_some() {
1260 self.open_add_entry_dialog();
1261 }
1262 } else if state.value_schema.is_some() {
1263 self.open_entry_dialog();
1265 } else {
1266 if let Some(idx) = state.focused_entry {
1268 if state.expanded.contains(&idx) {
1269 state.expanded.retain(|&i| i != idx);
1270 } else {
1271 state.expanded.push(idx);
1272 }
1273 }
1274 }
1275 self.on_value_changed();
1276 }
1277 SettingControl::Json(_) => {
1278 self.start_editing();
1279 }
1280 SettingControl::ObjectArray(ref state) => {
1281 if state.focused_index.is_none() {
1282 if state.item_schema.is_some() {
1284 self.open_add_array_item_dialog();
1285 }
1286 } else if state.item_schema.is_some() {
1287 self.open_edit_array_item_dialog();
1289 }
1290 }
1291 SettingControl::Complex { .. } => {
1292 }
1294 }
1295 }
1296 }
1297
1298 fn handle_control_increment(&mut self) {
1300 if let Some(item) = self.current_item_mut() {
1301 if let SettingControl::Number(ref mut state) = &mut item.control {
1302 state.value += 1;
1303 if let Some(max) = state.max {
1304 state.value = state.value.min(max);
1305 }
1306 self.on_value_changed();
1307 }
1308 }
1309 }
1310
1311 fn handle_control_decrement(&mut self) {
1313 if let Some(item) = self.current_item_mut() {
1314 if let SettingControl::Number(ref mut state) = &mut item.control {
1315 if state.value > 0 {
1316 state.value -= 1;
1317 }
1318 if let Some(min) = state.min {
1319 state.value = state.value.max(min);
1320 }
1321 self.on_value_changed();
1322 }
1323 }
1324 }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329 use super::*;
1330 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1331
1332 fn key(code: KeyCode) -> KeyEvent {
1333 KeyEvent::new(code, KeyModifiers::NONE)
1334 }
1335
1336 #[test]
1337 fn test_settings_is_modal() {
1338 let schema = include_str!("../../../plugins/config-schema.json");
1340 let config = crate::config::Config::default();
1341 let state = SettingsState::new(schema, &config).unwrap();
1342 assert!(state.is_modal());
1343 }
1344
1345 #[test]
1346 fn test_categories_panel_does_not_leak_to_settings() {
1347 let schema = include_str!("../../../plugins/config-schema.json");
1348 let config = crate::config::Config::default();
1349 let mut state = SettingsState::new(schema, &config).unwrap();
1350 state.visible = true;
1351 state.focus.set(FocusPanel::Categories);
1352
1353 let mut ctx = InputContext::new();
1354
1355 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1364 assert_eq!(result, InputResult::Ignored);
1365 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1366
1367 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1368 assert_eq!(result, InputResult::Consumed);
1369 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1370
1371 let result = state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1372 assert_eq!(result, InputResult::Consumed);
1373 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1374
1375 let result = state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1377 assert_eq!(result, InputResult::Consumed);
1378 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1379 }
1380
1381 #[test]
1382 fn test_tab_cycles_focus_panels() {
1383 let schema = include_str!("../../../plugins/config-schema.json");
1384 let config = crate::config::Config::default();
1385 let mut state = SettingsState::new(schema, &config).unwrap();
1386 state.visible = true;
1387
1388 let mut ctx = InputContext::new();
1389
1390 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1392
1393 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1395 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1396
1397 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1399 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1400 assert_eq!(state.footer_button_index, 0);
1401
1402 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1404 assert_eq!(state.footer_button_index, 1);
1405 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1406 assert_eq!(state.footer_button_index, 2);
1407 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1408 assert_eq!(state.footer_button_index, 3);
1409 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1410 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1412 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1413
1414 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1417 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1418
1419 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1421 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1422 assert_eq!(
1423 state.footer_button_index, 0,
1424 "Footer should reset to Layer button (index 0) on second loop"
1425 );
1426 }
1427
1428 #[test]
1429 fn test_escape_shows_confirm_dialog_with_changes() {
1430 let schema = include_str!("../../../plugins/config-schema.json");
1431 let config = crate::config::Config::default();
1432 let mut state = SettingsState::new(schema, &config).unwrap();
1433 state.visible = true;
1434
1435 state
1437 .pending_changes
1438 .insert("/test".to_string(), serde_json::json!(true));
1439
1440 let mut ctx = InputContext::new();
1441
1442 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1444 assert!(state.showing_confirm_dialog);
1445 assert!(ctx.deferred_actions.is_empty()); }
1447
1448 #[test]
1449 fn test_escape_closes_directly_without_changes() {
1450 let schema = include_str!("../../../plugins/config-schema.json");
1451 let config = crate::config::Config::default();
1452 let mut state = SettingsState::new(schema, &config).unwrap();
1453 state.visible = true;
1454
1455 let mut ctx = InputContext::new();
1456
1457 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1459 assert!(!state.showing_confirm_dialog);
1460 assert_eq!(ctx.deferred_actions.len(), 1);
1461 assert!(matches!(
1462 ctx.deferred_actions[0],
1463 DeferredAction::CloseSettings { save: false }
1464 ));
1465 }
1466
1467 #[test]
1468 fn test_confirm_dialog_navigation() {
1469 let schema = include_str!("../../../plugins/config-schema.json");
1470 let config = crate::config::Config::default();
1471 let mut state = SettingsState::new(schema, &config).unwrap();
1472 state.visible = true;
1473 state.showing_confirm_dialog = true;
1474 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1477
1478 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1480 assert_eq!(state.confirm_dialog_selection, 1);
1481
1482 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1484 assert_eq!(state.confirm_dialog_selection, 2);
1485
1486 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1488 assert_eq!(state.confirm_dialog_selection, 2);
1489
1490 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1492 assert_eq!(state.confirm_dialog_selection, 1);
1493 }
1494
1495 #[test]
1496 fn test_search_mode_captures_typing() {
1497 let schema = include_str!("../../../plugins/config-schema.json");
1498 let config = crate::config::Config::default();
1499 let mut state = SettingsState::new(schema, &config).unwrap();
1500 state.visible = true;
1501
1502 let mut ctx = InputContext::new();
1503
1504 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1506 assert!(state.search_active);
1507
1508 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1510 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1511 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1512 assert_eq!(state.search_query, "tab");
1513
1514 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1516 assert!(!state.search_active);
1517 assert!(state.search_query.is_empty());
1518 }
1519
1520 #[test]
1521 fn test_footer_button_activation() {
1522 let schema = include_str!("../../../plugins/config-schema.json");
1523 let config = crate::config::Config::default();
1524 let mut state = SettingsState::new(schema, &config).unwrap();
1525 state.visible = true;
1526 state.focus.set(FocusPanel::Footer);
1527 state.footer_button_index = 2; let mut ctx = InputContext::new();
1530
1531 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1533 assert_eq!(ctx.deferred_actions.len(), 1);
1534 assert!(matches!(
1535 ctx.deferred_actions[0],
1536 DeferredAction::CloseSettings { save: true }
1537 ));
1538 }
1539
1540 #[test]
1545 fn test_tab_exits_number_editing() {
1546 use crate::view::settings::items::SettingControl;
1547
1548 let schema = include_str!("../../../plugins/config-schema.json");
1549 let config = crate::config::Config::default();
1550 let mut state = SettingsState::new(schema, &config).unwrap();
1551 state.visible = true;
1552 state.focus.set(FocusPanel::Settings);
1553
1554 let number_idx = state
1556 .pages
1557 .get(state.selected_category)
1558 .and_then(|page| {
1559 page.items
1560 .iter()
1561 .position(|item| matches!(item.control, SettingControl::Number(_)))
1562 })
1563 .expect("expected at least one Number control on the default page");
1564 state.selected_item = number_idx;
1565
1566 state.start_number_editing();
1568 assert!(
1569 state.is_number_editing(),
1570 "precondition: should be in number-editing mode"
1571 );
1572 state.number_insert('7');
1573
1574 let mut ctx = InputContext::new();
1575
1576 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1578 assert!(
1579 !state.is_number_editing(),
1580 "Tab while editing a Number control must exit editing mode"
1581 );
1582 }
1583}