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.showing_entry_delete_confirm {
32 return self.handle_entry_delete_confirm_input(event);
33 }
34
35 if self.showing_entry_discard_confirm {
38 return self.handle_entry_discard_confirm_input(event);
39 }
40
41 if self.has_entry_dialog() {
43 return self.handle_entry_dialog_input(event, ctx);
44 }
45
46 if self.showing_confirm_dialog {
48 return self.handle_confirm_dialog_input(event, ctx);
49 }
50
51 if self.showing_reset_dialog {
53 return self.handle_reset_dialog_input(event);
54 }
55
56 if self.showing_help {
58 return self.handle_help_input(event, ctx);
59 }
60
61 if self.search_active {
63 return self.handle_search_input(event, ctx);
64 }
65
66 if event.modifiers.contains(KeyModifiers::CONTROL)
68 && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
69 {
70 ctx.defer(DeferredAction::CloseSettings { save: true });
71 return InputResult::Consumed;
72 }
73
74 match self.focus_panel() {
76 FocusPanel::Categories => self.handle_categories_input(event, ctx),
77 FocusPanel::Settings => self.handle_settings_input(event, ctx),
78 FocusPanel::Footer => self.handle_footer_input(event, ctx),
79 }
80 }
81
82 fn is_modal(&self) -> bool {
83 true }
85}
86
87impl SettingsState {
88 fn handle_entry_dialog_input(
95 &mut self,
96 event: &KeyEvent,
97 ctx: &mut InputContext,
98 ) -> InputResult {
99 if event.modifiers.contains(KeyModifiers::CONTROL)
101 && matches!(event.code, KeyCode::Char('s') | KeyCode::Char('S'))
102 {
103 self.save_entry_dialog();
104 return InputResult::Consumed;
105 }
106
107 if event.modifiers.contains(KeyModifiers::CONTROL)
111 && matches!(event.code, KeyCode::Char('r') | KeyCode::Char('R'))
112 {
113 let editing = self.entry_dialog().map(|d| d.editing_text).unwrap_or(false);
114 if !editing {
115 self.reset_focused_entry_field();
116 return InputResult::Consumed;
117 }
118 }
119
120 let (editing_text, dropdown_open) = if let Some(dialog) = self.entry_dialog() {
122 let dropdown_open = dialog
123 .current_item()
124 .map(|item| matches!(&item.control, SettingControl::Dropdown(s) if s.open))
125 .unwrap_or(false);
126 (dialog.editing_text, dropdown_open)
127 } else {
128 return InputResult::Consumed;
129 };
130
131 if editing_text {
133 self.handle_entry_dialog_text_editing(event, ctx)
134 } else if dropdown_open {
135 self.handle_entry_dialog_dropdown(event)
136 } else {
137 self.handle_entry_dialog_navigation(event, ctx)
138 }
139 }
140
141 fn handle_entry_dialog_text_editing(
143 &mut self,
144 event: &KeyEvent,
145 ctx: &mut InputContext,
146 ) -> InputResult {
147 let is_editing_json = self
149 .entry_dialog()
150 .map(|d| d.is_editing_json())
151 .unwrap_or(false);
152
153 let can_exit = self.entry_dialog_can_exit_text_editing();
155
156 let Some(dialog) = self.entry_dialog_mut() else {
157 return InputResult::Consumed;
158 };
159
160 match event.code {
161 KeyCode::Esc => {
162 if !can_exit {
164 }
166 dialog.stop_editing();
167 }
168 KeyCode::Enter => {
169 if is_editing_json {
170 dialog.insert_newline();
172 } else {
173 if let Some(item) = dialog.current_item_mut() {
175 if let SettingControl::TextList(state) = &mut item.control {
176 state.add_item();
177 }
178 }
179 }
180 }
181 KeyCode::Char(c) => {
182 if event.modifiers.contains(KeyModifiers::CONTROL) {
183 match c {
184 'a' | 'A' => {
185 dialog.select_all();
187 }
188 'c' | 'C' => {
189 if let Some(text) = dialog.selected_text() {
191 ctx.defer(DeferredAction::CopyToClipboard(text));
192 }
193 }
194 'v' | 'V' => {
195 ctx.defer(DeferredAction::PasteToSettings);
197 }
198 _ => {}
199 }
200 } else {
201 dialog.insert_char(c);
202 }
203 }
204 KeyCode::Backspace => {
205 dialog.backspace();
206 }
207 KeyCode::Delete => {
208 if is_editing_json {
209 dialog.delete();
211 } else {
212 dialog.delete_list_item();
214 }
215 }
216 KeyCode::Home => {
217 dialog.cursor_home();
218 }
219 KeyCode::End => {
220 dialog.cursor_end();
221 }
222 KeyCode::Left => {
223 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
224 dialog.cursor_left_selecting();
225 } else {
226 dialog.cursor_left();
227 }
228 }
229 KeyCode::Right => {
230 if is_editing_json && event.modifiers.contains(KeyModifiers::SHIFT) {
231 dialog.cursor_right_selecting();
232 } else {
233 dialog.cursor_right();
234 }
235 }
236 KeyCode::Up => {
237 if is_editing_json {
238 if event.modifiers.contains(KeyModifiers::SHIFT) {
240 dialog.cursor_up_selecting();
241 } else {
242 dialog.cursor_up();
243 }
244 } else {
245 let escape = if let Some(item) = dialog.current_item_mut() {
252 if let SettingControl::TextList(state) = &mut item.control {
253 let was_on_addnew = state.focused_item.is_none();
254 let had_pending = !state.new_item_text.is_empty();
255 state.add_item();
256 if was_on_addnew && !had_pending {
257 true
258 } else {
259 state.focus_prev();
260 false
261 }
262 } else {
263 false
264 }
265 } else {
266 false
267 };
268 if escape {
269 dialog.stop_editing();
270 dialog.focus_prev();
271 }
272 }
273 }
274 KeyCode::Down => {
275 if is_editing_json {
276 if event.modifiers.contains(KeyModifiers::SHIFT) {
278 dialog.cursor_down_selecting();
279 } else {
280 dialog.cursor_down();
281 }
282 } else {
283 let escape = if let Some(item) = dialog.current_item_mut() {
287 if let SettingControl::TextList(state) = &mut item.control {
288 let was_on_addnew = state.focused_item.is_none();
289 let had_pending = !state.new_item_text.is_empty();
290 state.add_item();
291 if was_on_addnew && !had_pending {
292 true
293 } else {
294 state.focus_next();
295 false
296 }
297 } else {
298 false
299 }
300 } else {
301 false
302 };
303 if escape {
304 dialog.stop_editing();
305 dialog.focus_next();
306 }
307 }
308 }
309 KeyCode::Tab => {
310 if is_editing_json {
311 let is_valid = dialog
313 .current_item()
314 .map(|item| {
315 if let SettingControl::Json(state) = &item.control {
316 state.is_valid()
317 } else {
318 true
319 }
320 })
321 .unwrap_or(true);
322
323 if is_valid {
324 if let Some(item) = dialog.current_item_mut() {
326 if let SettingControl::Json(state) = &mut item.control {
327 state.commit();
328 }
329 }
330 dialog.stop_editing();
331 }
332 } else {
334 let escape_forward = if let Some(item) = dialog.current_item_mut() {
339 if let SettingControl::TextList(state) = &mut item.control {
340 state.add_item();
341 true
342 } else {
343 false
344 }
345 } else {
346 false
347 };
348 dialog.stop_editing();
349 if escape_forward {
350 dialog.focus_next();
351 }
352 }
353 }
354 _ => {}
355 }
356 InputResult::Consumed
357 }
358
359 fn handle_entry_dialog_dropdown(&mut self, event: &KeyEvent) -> InputResult {
361 let Some(dialog) = self.entry_dialog_mut() else {
362 return InputResult::Consumed;
363 };
364
365 match event.code {
366 KeyCode::Up => {
367 dialog.dropdown_prev();
368 }
369 KeyCode::Down => {
370 dialog.dropdown_next();
371 }
372 KeyCode::Enter => {
373 dialog.dropdown_confirm();
374 }
375 KeyCode::Esc => {
376 dialog.dropdown_confirm(); }
378 _ => {}
379 }
380 InputResult::Consumed
381 }
382
383 fn handle_entry_dialog_navigation(
385 &mut self,
386 event: &KeyEvent,
387 ctx: &mut InputContext,
388 ) -> InputResult {
389 match event.code {
390 KeyCode::Esc => {
391 let dirty = self.entry_dialog().map(|d| d.is_dirty()).unwrap_or(false);
396 if dirty {
397 self.showing_entry_discard_confirm = true;
398 self.entry_discard_confirm_selection = 0;
399 } else {
400 self.close_entry_dialog();
401 }
402 }
403 KeyCode::Up => {
404 if let Some(dialog) = self.entry_dialog_mut() {
405 dialog.focus_prev();
406 }
407 }
408 KeyCode::Down => {
409 if let Some(dialog) = self.entry_dialog_mut() {
410 dialog.focus_next();
411 }
412 }
413 KeyCode::Tab => {
414 if let Some(dialog) = self.entry_dialog_mut() {
416 dialog.focus_next();
417 }
418 }
419 KeyCode::BackTab => {
420 if let Some(dialog) = self.entry_dialog_mut() {
422 dialog.focus_prev();
423 }
424 }
425 KeyCode::Delete => {
426 let removed = self
431 .entry_dialog_mut()
432 .map(|dialog| {
433 if dialog.focus_on_buttons {
434 return false;
435 }
436 if let Some(item) = dialog.current_item_mut() {
437 if let SettingControl::TextList(state) = &mut item.control {
438 if let Some(idx) = state.focused_item {
439 state.remove_item(idx);
440 dialog.user_edited = true;
441 return true;
442 }
443 }
444 }
445 false
446 })
447 .unwrap_or(false);
448 if !removed {
449 }
452 }
453 KeyCode::Left => {
454 if let Some(dialog) = self.entry_dialog_mut() {
455 if dialog.focus_on_buttons && dialog.focused_button > 0 {
456 dialog.focused_button -= 1;
457 }
458 }
459 }
460 KeyCode::Right => {
461 if let Some(dialog) = self.entry_dialog_mut() {
462 if dialog.focus_on_buttons && dialog.focused_button + 1 < dialog.button_count()
463 {
464 dialog.focused_button += 1;
465 }
466 }
467 }
468 KeyCode::Enter => {
469 let button_action = self.entry_dialog().and_then(|dialog| {
473 if dialog.focus_on_buttons {
474 let has_delete = !dialog.is_new && !dialog.no_delete;
475 match dialog.focused_button {
476 0 => Some(ButtonAction::Save),
477 1 => Some(ButtonAction::Cancel),
478 2 if has_delete => Some(ButtonAction::Delete),
479 _ => None,
480 }
481 } else {
482 None
483 }
484 });
485
486 if let Some(action) = button_action {
487 match action {
488 ButtonAction::Save => self.save_entry_dialog(),
489 ButtonAction::Delete => self.request_entry_delete_confirm(),
490 ButtonAction::Cancel => self.close_entry_dialog(),
491 }
492 } else if event.modifiers.contains(KeyModifiers::CONTROL) {
493 self.save_entry_dialog();
495 } else {
496 let control_action = self
498 .entry_dialog()
499 .and_then(|dialog| {
500 dialog.current_item().map(|item| match &item.control {
501 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
502 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
503 SettingControl::Text(_)
504 | SettingControl::TextList(_)
505 | SettingControl::DualList(_)
506 | SettingControl::Number(_)
507 | SettingControl::Json(_) => Some(ControlAction::StartEditing),
508 SettingControl::Map(_) | SettingControl::ObjectArray(_) => {
509 Some(ControlAction::OpenNestedDialog)
510 }
511 _ => None,
512 })
513 })
514 .flatten();
515
516 if let Some(action) = control_action {
517 match action {
518 ControlAction::ToggleBool => {
519 if let Some(dialog) = self.entry_dialog_mut() {
520 dialog.toggle_bool();
521 }
522 }
523 ControlAction::ToggleDropdown => {
524 if let Some(dialog) = self.entry_dialog_mut() {
525 dialog.toggle_dropdown();
526 }
527 }
528 ControlAction::StartEditing => {
529 if let Some(dialog) = self.entry_dialog_mut() {
530 dialog.start_editing();
531 }
532 }
533 ControlAction::OpenNestedDialog => {
534 self.open_nested_entry_dialog();
535 }
536 }
537 }
538 }
539 }
540 KeyCode::Char(' ') => {
541 let control_action = self.entry_dialog().and_then(|dialog| {
543 if dialog.focus_on_buttons {
544 return None; }
546 dialog.current_item().and_then(|item| match &item.control {
547 SettingControl::Toggle(_) => Some(ControlAction::ToggleBool),
548 SettingControl::Dropdown(_) => Some(ControlAction::ToggleDropdown),
549 _ => None,
550 })
551 });
552
553 if let Some(action) = control_action {
554 match action {
555 ControlAction::ToggleBool => {
556 if let Some(dialog) = self.entry_dialog_mut() {
557 dialog.toggle_bool();
558 }
559 }
560 ControlAction::ToggleDropdown => {
561 if let Some(dialog) = self.entry_dialog_mut() {
562 dialog.toggle_dropdown();
563 }
564 }
565 _ => {}
566 }
567 }
568 }
569 KeyCode::Char(c) => {
570 let can_auto_edit = self
572 .entry_dialog()
573 .and_then(|dialog| {
574 if dialog.focus_on_buttons {
575 return None;
576 }
577 dialog.current_item().map(|item| match &item.control {
578 SettingControl::Text(_) | SettingControl::TextList(_) => true,
579 SettingControl::Number(_) => c.is_ascii_digit() || c == '-' || c == '.',
580 _ => false,
581 })
582 })
583 .unwrap_or(false);
584
585 if can_auto_edit {
586 if let Some(dialog) = self.entry_dialog_mut() {
587 dialog.start_editing();
588 }
589 return self.handle_entry_dialog_text_editing(
591 &KeyEvent::new(KeyCode::Char(c), event.modifiers),
592 ctx,
593 );
594 }
595 }
596 _ => {}
597 }
598 InputResult::Consumed
599 }
600
601 fn handle_confirm_dialog_input(
603 &mut self,
604 event: &KeyEvent,
605 ctx: &mut InputContext,
606 ) -> InputResult {
607 match event.code {
608 KeyCode::Left | KeyCode::BackTab => {
609 if self.confirm_dialog_selection > 0 {
610 self.confirm_dialog_selection -= 1;
611 }
612 InputResult::Consumed
613 }
614 KeyCode::Right | KeyCode::Tab => {
615 if self.confirm_dialog_selection < 2 {
616 self.confirm_dialog_selection += 1;
617 }
618 InputResult::Consumed
619 }
620 KeyCode::Enter => {
621 match self.confirm_dialog_selection {
622 0 => ctx.defer(DeferredAction::CloseSettings { save: true }), 1 => ctx.defer(DeferredAction::CloseSettings { save: false }), 2 => self.showing_confirm_dialog = false, _ => {}
626 }
627 InputResult::Consumed
628 }
629 KeyCode::Esc => {
630 self.showing_confirm_dialog = false;
631 InputResult::Consumed
632 }
633 KeyCode::Char('s') | KeyCode::Char('S') => {
634 ctx.defer(DeferredAction::CloseSettings { save: true });
635 InputResult::Consumed
636 }
637 KeyCode::Char('d') | KeyCode::Char('D') => {
638 ctx.defer(DeferredAction::CloseSettings { save: false });
639 InputResult::Consumed
640 }
641 _ => InputResult::Consumed, }
643 }
644
645 fn handle_reset_dialog_input(&mut self, event: &KeyEvent) -> InputResult {
647 match event.code {
648 KeyCode::Left | KeyCode::BackTab => {
649 if self.reset_dialog_selection > 0 {
650 self.reset_dialog_selection -= 1;
651 }
652 InputResult::Consumed
653 }
654 KeyCode::Right | KeyCode::Tab => {
655 if self.reset_dialog_selection < 1 {
656 self.reset_dialog_selection += 1;
657 }
658 InputResult::Consumed
659 }
660 KeyCode::Enter => {
661 match self.reset_dialog_selection {
662 0 => {
663 self.discard_changes();
665 self.showing_reset_dialog = false;
666 }
667 1 => {
668 self.showing_reset_dialog = false;
670 }
671 _ => {}
672 }
673 InputResult::Consumed
674 }
675 KeyCode::Esc => {
676 self.showing_reset_dialog = false;
677 InputResult::Consumed
678 }
679 KeyCode::Char('r') | KeyCode::Char('R') => {
680 self.discard_changes();
681 self.showing_reset_dialog = false;
682 InputResult::Consumed
683 }
684 _ => InputResult::Consumed, }
686 }
687
688 fn handle_entry_discard_confirm_input(&mut self, event: &KeyEvent) -> InputResult {
691 match event.code {
692 KeyCode::Left | KeyCode::BackTab => {
693 if self.entry_discard_confirm_selection > 0 {
694 self.entry_discard_confirm_selection -= 1;
695 }
696 }
697 KeyCode::Right | KeyCode::Tab => {
698 if self.entry_discard_confirm_selection < 1 {
699 self.entry_discard_confirm_selection += 1;
700 }
701 }
702 KeyCode::Enter => {
703 match self.entry_discard_confirm_selection {
704 0 => {
705 self.showing_entry_discard_confirm = false;
707 }
708 1 => {
709 self.showing_entry_discard_confirm = false;
711 self.close_entry_dialog();
712 }
713 _ => {}
714 }
715 }
716 KeyCode::Esc => {
717 self.showing_entry_discard_confirm = false;
719 }
720 KeyCode::Char('d') | KeyCode::Char('D') => {
721 self.showing_entry_discard_confirm = false;
722 self.close_entry_dialog();
723 }
724 _ => {}
725 }
726 InputResult::Consumed
727 }
728
729 fn handle_entry_delete_confirm_input(&mut self, event: &KeyEvent) -> InputResult {
732 match event.code {
733 KeyCode::Left | KeyCode::BackTab => {
734 if self.entry_delete_confirm_selection > 0 {
735 self.entry_delete_confirm_selection -= 1;
736 }
737 }
738 KeyCode::Right | KeyCode::Tab => {
739 if self.entry_delete_confirm_selection < 1 {
740 self.entry_delete_confirm_selection += 1;
741 }
742 }
743 KeyCode::Enter => match self.entry_delete_confirm_selection {
744 0 => {
745 self.showing_entry_delete_confirm = false;
746 }
747 1 => {
748 self.showing_entry_delete_confirm = false;
749 self.delete_entry_dialog();
750 }
751 _ => {}
752 },
753 KeyCode::Esc => {
754 self.showing_entry_delete_confirm = false;
755 }
756 _ => {}
757 }
758 InputResult::Consumed
759 }
760
761 fn handle_help_input(&mut self, _event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
763 self.showing_help = false;
765 InputResult::Consumed
766 }
767
768 fn handle_search_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
770 match event.code {
771 KeyCode::Esc => {
772 self.cancel_search();
773 InputResult::Consumed
774 }
775 KeyCode::Enter => {
776 self.jump_to_search_result();
777 InputResult::Consumed
778 }
779 KeyCode::Up => {
780 self.search_prev();
781 InputResult::Consumed
782 }
783 KeyCode::Down => {
784 self.search_next();
785 InputResult::Consumed
786 }
787 KeyCode::Char(c) => {
788 self.search_push_char(c);
789 InputResult::Consumed
790 }
791 KeyCode::Backspace => {
792 self.search_pop_char();
793 InputResult::Consumed
794 }
795 _ => InputResult::Consumed, }
797 }
798
799 fn handle_categories_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
801 match event.code {
802 KeyCode::Up => {
803 self.select_prev();
804 InputResult::Consumed
805 }
806 KeyCode::Down => {
807 self.select_next();
808 InputResult::Consumed
809 }
810 KeyCode::PageUp => {
811 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
813 self.tree_step(-viewport);
814 InputResult::Consumed
815 }
816 KeyCode::PageDown => {
817 let viewport = self.categories_scroll.scroll.viewport.max(1) as i32;
818 self.tree_step(viewport);
819 InputResult::Consumed
820 }
821 KeyCode::Home => {
822 let rows = self.visible_tree();
823 let cur = self.tree_cursor_index(&rows) as i32;
824 if cur > 0 {
825 self.tree_step(-cur);
826 }
827 InputResult::Consumed
828 }
829 KeyCode::End => {
830 let rows = self.visible_tree();
831 let cur = self.tree_cursor_index(&rows) as i32;
832 let last = rows.len() as i32 - 1;
833 if last > cur {
834 self.tree_step(last - cur);
835 }
836 InputResult::Consumed
837 }
838 KeyCode::Tab => {
839 self.toggle_focus();
840 InputResult::Consumed
841 }
842 KeyCode::BackTab => {
843 self.toggle_focus_backward();
844 InputResult::Consumed
845 }
846 KeyCode::Char('/') => {
847 self.start_search();
848 InputResult::Consumed
849 }
850 KeyCode::Char('?') => {
851 self.toggle_help();
852 InputResult::Consumed
853 }
854 KeyCode::Esc => {
855 self.request_close(ctx);
856 InputResult::Consumed
857 }
858 KeyCode::Right => {
859 let cat_idx = self.selected_category;
862 if self.is_category_expandable(cat_idx)
863 && !self.expanded_categories.contains(&cat_idx)
864 {
865 self.expanded_categories.insert(cat_idx);
866 }
867 InputResult::Consumed
868 }
869 KeyCode::Left => {
870 let cat_idx = self.selected_category;
872 if self.expanded_categories.contains(&cat_idx) {
873 self.expanded_categories.remove(&cat_idx);
874 self.tree_cursor_section = None;
879 }
880 InputResult::Consumed
881 }
882 _ => InputResult::Ignored, }
884 }
885
886 fn handle_settings_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
888 if self.editing_text {
890 return self.handle_text_editing_input(event, ctx);
891 }
892
893 if self.is_number_editing() {
895 return self.handle_number_editing_input(event, ctx);
896 }
897
898 if self.is_dropdown_open() {
900 return self.handle_dropdown_input(event, ctx);
901 }
902
903 match event.code {
904 KeyCode::Up => {
905 self.select_prev();
906 InputResult::Consumed
907 }
908 KeyCode::Down => {
909 self.select_next();
910 InputResult::Consumed
911 }
912 KeyCode::Tab => {
913 self.toggle_focus();
914 InputResult::Consumed
915 }
916 KeyCode::BackTab => {
917 self.toggle_focus_backward();
918 InputResult::Consumed
919 }
920 KeyCode::Left => {
921 self.update_control_focus(false);
924 self.focus.set(FocusPanel::Categories);
925 InputResult::Consumed
926 }
927 KeyCode::Enter | KeyCode::Char(' ') => {
928 self.handle_control_activate(ctx);
929 InputResult::Consumed
930 }
931 KeyCode::Char(c)
934 if self.is_number_control() && (c.is_ascii_digit() || c == '-' || c == '.') =>
935 {
936 self.start_number_editing();
937 self.number_insert(c);
938 self.on_value_changed();
939 InputResult::Consumed
940 }
941 KeyCode::PageDown => {
942 self.select_next_page();
943 InputResult::Consumed
944 }
945 KeyCode::PageUp => {
946 self.select_prev_page();
947 InputResult::Consumed
948 }
949 KeyCode::Char('/') => {
950 self.start_search();
951 InputResult::Consumed
952 }
953 KeyCode::Char('?') => {
954 self.toggle_help();
955 InputResult::Consumed
956 }
957 KeyCode::Delete => {
958 self.set_current_to_null();
960 InputResult::Consumed
961 }
962 KeyCode::Esc => {
963 self.request_close(ctx);
964 InputResult::Consumed
965 }
966 _ => InputResult::Ignored, }
968 }
969
970 fn handle_footer_input(&mut self, event: &KeyEvent, ctx: &mut InputContext) -> InputResult {
974 const FOOTER_BUTTON_COUNT: usize = 5;
975
976 match event.code {
977 KeyCode::Left | KeyCode::BackTab => {
978 if self.footer_button_index > 0 {
980 self.footer_button_index -= 1;
981 } else {
982 self.focus.set(FocusPanel::Settings);
983 }
984 InputResult::Consumed
985 }
986 KeyCode::Right => {
987 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
989 self.footer_button_index += 1;
990 }
991 InputResult::Consumed
992 }
993 KeyCode::Tab => {
994 if self.footer_button_index < FOOTER_BUTTON_COUNT - 1 {
996 self.footer_button_index += 1;
997 } else {
998 self.focus.set(FocusPanel::Categories);
999 }
1000 InputResult::Consumed
1001 }
1002 KeyCode::Enter => {
1003 match self.footer_button_index {
1004 0 => self.cycle_target_layer(), 1 => {
1006 let is_nullable_set = self
1009 .current_item()
1010 .map(|item| item.nullable && !item.is_null)
1011 .unwrap_or(false);
1012 if is_nullable_set {
1013 self.set_current_to_null();
1014 } else {
1015 self.request_reset();
1016 }
1017 }
1018 2 => ctx.defer(DeferredAction::CloseSettings { save: true }),
1019 3 => self.request_close(ctx),
1020 4 => ctx.defer(DeferredAction::OpenConfigFile {
1021 layer: self.target_layer,
1022 }), _ => {}
1024 }
1025 InputResult::Consumed
1026 }
1027 KeyCode::Esc => {
1028 self.request_close(ctx);
1029 InputResult::Consumed
1030 }
1031 KeyCode::Char('/') => {
1032 self.start_search();
1033 InputResult::Consumed
1034 }
1035 KeyCode::Char('?') => {
1036 self.toggle_help();
1037 InputResult::Consumed
1038 }
1039 _ => InputResult::Ignored, }
1041 }
1042
1043 fn handle_text_editing_input(
1045 &mut self,
1046 event: &KeyEvent,
1047 ctx: &mut InputContext,
1048 ) -> InputResult {
1049 let is_json = self.is_editing_json();
1050
1051 if is_json {
1052 return self.handle_json_editing_input(event, ctx);
1053 }
1054
1055 if self.is_editing_dual_list() {
1057 return self.handle_dual_list_editing_input(event);
1058 }
1059
1060 match event.code {
1061 KeyCode::Esc => {
1062 if !self.can_exit_text_editing() {
1064 return InputResult::Consumed;
1065 }
1066 self.stop_editing();
1067 InputResult::Consumed
1068 }
1069 KeyCode::Enter => {
1070 self.text_add_item();
1071 InputResult::Consumed
1072 }
1073 KeyCode::Char(c) => {
1074 self.text_insert(c);
1075 InputResult::Consumed
1076 }
1077 KeyCode::Backspace => {
1078 self.text_backspace();
1079 InputResult::Consumed
1080 }
1081 KeyCode::Delete => {
1082 self.text_remove_focused();
1083 InputResult::Consumed
1084 }
1085 KeyCode::Left => {
1086 self.text_move_left();
1087 InputResult::Consumed
1088 }
1089 KeyCode::Right => {
1090 self.text_move_right();
1091 InputResult::Consumed
1092 }
1093 KeyCode::Up => {
1094 self.text_focus_prev();
1095 InputResult::Consumed
1096 }
1097 KeyCode::Down => {
1098 self.text_focus_next();
1099 InputResult::Consumed
1100 }
1101 KeyCode::Tab => {
1102 self.stop_editing();
1104 self.toggle_focus();
1105 InputResult::Consumed
1106 }
1107 _ => InputResult::Consumed, }
1109 }
1110
1111 fn handle_dual_list_editing_input(&mut self, event: &KeyEvent) -> InputResult {
1113 use crate::view::controls::DualListColumn;
1114 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1115 match event.code {
1116 KeyCode::Esc => {
1117 self.stop_editing();
1118 }
1119 KeyCode::Tab | KeyCode::BackTab => {
1121 self.stop_editing();
1122 return InputResult::Ignored;
1124 }
1125 KeyCode::Up if shift => {
1126 self.with_current_dual_list_mut(|dl| dl.move_up());
1127 self.on_value_changed();
1128 }
1129 KeyCode::Down if shift => {
1130 self.with_current_dual_list_mut(|dl| dl.move_down());
1131 self.on_value_changed();
1132 }
1133 KeyCode::Up => {
1134 self.with_current_dual_list_mut(|dl| dl.cursor_up());
1135 }
1136 KeyCode::Down => {
1137 self.with_current_dual_list_mut(|dl| dl.cursor_down());
1138 }
1139 KeyCode::Right if shift => {
1140 let changed = self
1142 .with_current_dual_list_mut(|dl| {
1143 if dl.active_column == DualListColumn::Available {
1144 dl.add_selected();
1145 dl.active_column = DualListColumn::Included;
1147 dl.included_cursor = dl.included.len().saturating_sub(1);
1148 true
1149 } else {
1150 false
1151 }
1152 })
1153 .unwrap_or(false);
1154 if changed {
1155 self.on_value_changed();
1156 self.refresh_dual_list_sibling();
1157 }
1158 }
1159 KeyCode::Left if shift => {
1160 let changed = self
1162 .with_current_dual_list_mut(|dl| {
1163 if dl.active_column == DualListColumn::Included {
1164 let value = dl.included.get(dl.included_cursor).cloned();
1165 dl.remove_selected();
1166 dl.active_column = DualListColumn::Available;
1168 if let Some(val) = value {
1169 let avail = dl.available_items();
1170 if let Some(pos) = avail.iter().position(|(v, _)| *v == val) {
1171 dl.available_cursor = pos;
1172 }
1173 }
1174 true
1175 } else {
1176 false
1177 }
1178 })
1179 .unwrap_or(false);
1180 if changed {
1181 self.on_value_changed();
1182 self.refresh_dual_list_sibling();
1183 }
1184 }
1185 KeyCode::Right => {
1186 self.with_current_dual_list_mut(|dl| {
1188 dl.active_column = DualListColumn::Included;
1189 });
1190 }
1191 KeyCode::Left => {
1192 self.with_current_dual_list_mut(|dl| {
1194 dl.active_column = DualListColumn::Available;
1195 });
1196 }
1197 KeyCode::Enter => {
1198 let changed = self
1200 .with_current_dual_list_mut(|dl| match dl.active_column {
1201 DualListColumn::Available => dl.add_selected(),
1202 DualListColumn::Included => dl.remove_selected(),
1203 })
1204 .is_some();
1205 if changed {
1206 self.on_value_changed();
1207 self.refresh_dual_list_sibling();
1208 }
1209 }
1210 _ => {}
1211 }
1212 InputResult::Consumed
1213 }
1214
1215 fn handle_json_editing_input(
1217 &mut self,
1218 event: &KeyEvent,
1219 ctx: &mut InputContext,
1220 ) -> InputResult {
1221 match event.code {
1222 KeyCode::Esc | KeyCode::Tab => {
1223 self.json_exit_editing();
1225 }
1226 KeyCode::Enter => {
1227 self.json_insert_newline();
1228 }
1229 KeyCode::Char(c) => {
1230 if event.modifiers.contains(KeyModifiers::CONTROL) {
1231 match c {
1232 'a' | 'A' => self.json_select_all(),
1233 'c' | 'C' => {
1234 if let Some(text) = self.json_selected_text() {
1235 ctx.defer(DeferredAction::CopyToClipboard(text));
1236 }
1237 }
1238 'v' | 'V' => {
1239 ctx.defer(DeferredAction::PasteToSettings);
1240 }
1241 _ => {}
1242 }
1243 } else {
1244 self.text_insert(c);
1245 }
1246 }
1247 KeyCode::Backspace => {
1248 self.text_backspace();
1249 }
1250 KeyCode::Delete => {
1251 self.json_delete();
1252 }
1253 KeyCode::Left => {
1254 if event.modifiers.contains(KeyModifiers::SHIFT) {
1255 self.json_cursor_left_selecting();
1256 } else {
1257 self.text_move_left();
1258 }
1259 }
1260 KeyCode::Right => {
1261 if event.modifiers.contains(KeyModifiers::SHIFT) {
1262 self.json_cursor_right_selecting();
1263 } else {
1264 self.text_move_right();
1265 }
1266 }
1267 KeyCode::Up => {
1268 if event.modifiers.contains(KeyModifiers::SHIFT) {
1269 self.json_cursor_up_selecting();
1270 } else {
1271 self.json_cursor_up();
1272 }
1273 }
1274 KeyCode::Down => {
1275 if event.modifiers.contains(KeyModifiers::SHIFT) {
1276 self.json_cursor_down_selecting();
1277 } else {
1278 self.json_cursor_down();
1279 }
1280 }
1281 _ => {}
1282 }
1283 InputResult::Consumed
1284 }
1285
1286 fn handle_number_editing_input(
1288 &mut self,
1289 event: &KeyEvent,
1290 _ctx: &mut InputContext,
1291 ) -> InputResult {
1292 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
1293 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
1294
1295 match event.code {
1296 KeyCode::Esc => {
1297 self.number_cancel();
1298 }
1299 KeyCode::Enter => {
1300 self.number_confirm();
1301 }
1302 KeyCode::Tab | KeyCode::BackTab => {
1303 self.number_confirm();
1308 }
1309 KeyCode::Char('a') if ctrl => {
1310 self.number_select_all();
1311 }
1312 KeyCode::Char(c) => {
1313 self.number_insert(c);
1314 }
1315 KeyCode::Backspace if ctrl => {
1316 self.number_delete_word_backward();
1317 }
1318 KeyCode::Backspace => {
1319 self.number_backspace();
1320 }
1321 KeyCode::Delete if ctrl => {
1322 self.number_delete_word_forward();
1323 }
1324 KeyCode::Delete => {
1325 self.number_delete();
1326 }
1327 KeyCode::Left if ctrl && shift => {
1328 self.number_move_word_left_selecting();
1329 }
1330 KeyCode::Left if ctrl => {
1331 self.number_move_word_left();
1332 }
1333 KeyCode::Left if shift => {
1334 self.number_move_left_selecting();
1335 }
1336 KeyCode::Left => {
1337 self.number_move_left();
1338 }
1339 KeyCode::Right if ctrl && shift => {
1340 self.number_move_word_right_selecting();
1341 }
1342 KeyCode::Right if ctrl => {
1343 self.number_move_word_right();
1344 }
1345 KeyCode::Right if shift => {
1346 self.number_move_right_selecting();
1347 }
1348 KeyCode::Right => {
1349 self.number_move_right();
1350 }
1351 KeyCode::Home if shift => {
1352 self.number_move_home_selecting();
1353 }
1354 KeyCode::Home => {
1355 self.number_move_home();
1356 }
1357 KeyCode::End if shift => {
1358 self.number_move_end_selecting();
1359 }
1360 KeyCode::End => {
1361 self.number_move_end();
1362 }
1363 _ => {}
1364 }
1365 InputResult::Consumed }
1367
1368 fn handle_dropdown_input(&mut self, event: &KeyEvent, _ctx: &mut InputContext) -> InputResult {
1370 match event.code {
1371 KeyCode::Up => {
1372 self.dropdown_prev();
1373 InputResult::Consumed
1374 }
1375 KeyCode::Down => {
1376 self.dropdown_next();
1377 InputResult::Consumed
1378 }
1379 KeyCode::Home => {
1380 self.dropdown_home();
1381 InputResult::Consumed
1382 }
1383 KeyCode::End => {
1384 self.dropdown_end();
1385 InputResult::Consumed
1386 }
1387 KeyCode::Enter => {
1388 self.dropdown_confirm();
1389 InputResult::Consumed
1390 }
1391 KeyCode::Esc => {
1392 self.dropdown_cancel();
1393 InputResult::Consumed
1394 }
1395 _ => InputResult::Consumed, }
1397 }
1398
1399 fn request_reset(&mut self) {
1401 if self.has_changes() {
1402 self.showing_reset_dialog = true;
1403 self.reset_dialog_selection = 0;
1404 }
1405 }
1406
1407 fn request_close(&mut self, ctx: &mut InputContext) {
1409 if self.has_changes() {
1410 self.showing_confirm_dialog = true;
1411 self.confirm_dialog_selection = 0;
1412 } else {
1413 ctx.defer(DeferredAction::CloseSettings { save: false });
1414 }
1415 }
1416
1417 fn handle_control_activate(&mut self, _ctx: &mut InputContext) {
1419 if let Some(item) = self.current_item_mut() {
1420 match &mut item.control {
1421 SettingControl::Toggle(ref mut state) => {
1422 state.checked = !state.checked;
1423 self.on_value_changed();
1424 }
1425 SettingControl::Dropdown(_) => {
1426 self.dropdown_toggle();
1427 }
1428 SettingControl::Number(_) => {
1429 self.start_number_editing();
1430 }
1431 SettingControl::Text(_) => {
1432 self.start_editing();
1433 }
1434 SettingControl::TextList(_) | SettingControl::DualList(_) => {
1435 self.start_editing();
1436 }
1437 SettingControl::Map(ref mut state) => {
1438 if state.focused_entry.is_none() {
1439 if state.value_schema.is_some() {
1441 self.open_add_entry_dialog();
1442 }
1443 } else if state.value_schema.is_some() {
1444 self.open_entry_dialog();
1446 } else {
1447 if let Some(idx) = state.focused_entry {
1449 if state.expanded.contains(&idx) {
1450 state.expanded.retain(|&i| i != idx);
1451 } else {
1452 state.expanded.push(idx);
1453 }
1454 }
1455 }
1456 self.on_value_changed();
1457 }
1458 SettingControl::Json(_) => {
1459 self.start_editing();
1460 }
1461 SettingControl::ObjectArray(ref state) => {
1462 if state.focused_index.is_none() {
1463 if state.item_schema.is_some() {
1465 self.open_add_array_item_dialog();
1466 }
1467 } else if state.item_schema.is_some() {
1468 self.open_edit_array_item_dialog();
1470 }
1471 }
1472 SettingControl::Complex { .. } => {
1473 }
1475 }
1476 }
1477 }
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482 use super::*;
1483 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1484
1485 fn key(code: KeyCode) -> KeyEvent {
1486 KeyEvent::new(code, KeyModifiers::NONE)
1487 }
1488
1489 #[test]
1490 fn test_settings_is_modal() {
1491 let schema = include_str!("../../../plugins/config-schema.json");
1493 let config = crate::config::Config::default();
1494 let state = SettingsState::new(schema, &config).unwrap();
1495 assert!(state.is_modal());
1496 }
1497
1498 #[test]
1499 fn test_categories_panel_does_not_leak_to_settings() {
1500 let schema = include_str!("../../../plugins/config-schema.json");
1501 let config = crate::config::Config::default();
1502 let mut state = SettingsState::new(schema, &config).unwrap();
1503 state.visible = true;
1504 state.focus.set(FocusPanel::Categories);
1505
1506 let mut ctx = InputContext::new();
1507
1508 let result = state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1517 assert_eq!(result, InputResult::Ignored);
1518 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1519
1520 let result = state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1521 assert_eq!(result, InputResult::Consumed);
1522 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1523
1524 let result = state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1525 assert_eq!(result, InputResult::Consumed);
1526 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1527
1528 let result = state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1530 assert_eq!(result, InputResult::Consumed);
1531 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1532 }
1533
1534 #[test]
1535 fn test_tab_cycles_focus_panels() {
1536 let schema = include_str!("../../../plugins/config-schema.json");
1537 let config = crate::config::Config::default();
1538 let mut state = SettingsState::new(schema, &config).unwrap();
1539 state.visible = true;
1540
1541 let mut ctx = InputContext::new();
1542
1543 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1545
1546 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1548 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1549
1550 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1552 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1553 assert_eq!(state.footer_button_index, 0);
1554
1555 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1557 assert_eq!(state.footer_button_index, 1);
1558 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1559 assert_eq!(state.footer_button_index, 2);
1560 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1561 assert_eq!(state.footer_button_index, 3);
1562 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1563 assert_eq!(state.footer_button_index, 4); state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1565 assert_eq!(state.focus_panel(), FocusPanel::Categories);
1566
1567 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1570 assert_eq!(state.focus_panel(), FocusPanel::Settings);
1571
1572 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1574 assert_eq!(state.focus_panel(), FocusPanel::Footer);
1575 assert_eq!(
1576 state.footer_button_index, 0,
1577 "Footer should reset to Layer button (index 0) on second loop"
1578 );
1579 }
1580
1581 #[test]
1582 fn test_escape_shows_confirm_dialog_with_changes() {
1583 let schema = include_str!("../../../plugins/config-schema.json");
1584 let config = crate::config::Config::default();
1585 let mut state = SettingsState::new(schema, &config).unwrap();
1586 state.visible = true;
1587
1588 state
1590 .pending_changes
1591 .insert("/test".to_string(), serde_json::json!(true));
1592
1593 let mut ctx = InputContext::new();
1594
1595 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1597 assert!(state.showing_confirm_dialog);
1598 assert!(ctx.deferred_actions.is_empty()); }
1600
1601 #[test]
1602 fn test_escape_closes_directly_without_changes() {
1603 let schema = include_str!("../../../plugins/config-schema.json");
1604 let config = crate::config::Config::default();
1605 let mut state = SettingsState::new(schema, &config).unwrap();
1606 state.visible = true;
1607
1608 let mut ctx = InputContext::new();
1609
1610 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1612 assert!(!state.showing_confirm_dialog);
1613 assert_eq!(ctx.deferred_actions.len(), 1);
1614 assert!(matches!(
1615 ctx.deferred_actions[0],
1616 DeferredAction::CloseSettings { save: false }
1617 ));
1618 }
1619
1620 #[test]
1621 fn test_confirm_dialog_navigation() {
1622 let schema = include_str!("../../../plugins/config-schema.json");
1623 let config = crate::config::Config::default();
1624 let mut state = SettingsState::new(schema, &config).unwrap();
1625 state.visible = true;
1626 state.showing_confirm_dialog = true;
1627 state.confirm_dialog_selection = 0; let mut ctx = InputContext::new();
1630
1631 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1633 assert_eq!(state.confirm_dialog_selection, 1);
1634
1635 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1637 assert_eq!(state.confirm_dialog_selection, 2);
1638
1639 state.handle_key_event(&key(KeyCode::Right), &mut ctx);
1641 assert_eq!(state.confirm_dialog_selection, 2);
1642
1643 state.handle_key_event(&key(KeyCode::Left), &mut ctx);
1645 assert_eq!(state.confirm_dialog_selection, 1);
1646 }
1647
1648 #[test]
1649 fn test_search_mode_captures_typing() {
1650 let schema = include_str!("../../../plugins/config-schema.json");
1651 let config = crate::config::Config::default();
1652 let mut state = SettingsState::new(schema, &config).unwrap();
1653 state.visible = true;
1654
1655 let mut ctx = InputContext::new();
1656
1657 state.handle_key_event(&key(KeyCode::Char('/')), &mut ctx);
1659 assert!(state.search_active);
1660
1661 state.handle_key_event(&key(KeyCode::Char('t')), &mut ctx);
1663 state.handle_key_event(&key(KeyCode::Char('a')), &mut ctx);
1664 state.handle_key_event(&key(KeyCode::Char('b')), &mut ctx);
1665 assert_eq!(state.search_query, "tab");
1666
1667 state.handle_key_event(&key(KeyCode::Esc), &mut ctx);
1669 assert!(!state.search_active);
1670 assert!(state.search_query.is_empty());
1671 }
1672
1673 #[test]
1674 fn test_footer_button_activation() {
1675 let schema = include_str!("../../../plugins/config-schema.json");
1676 let config = crate::config::Config::default();
1677 let mut state = SettingsState::new(schema, &config).unwrap();
1678 state.visible = true;
1679 state.focus.set(FocusPanel::Footer);
1680 state.footer_button_index = 2; let mut ctx = InputContext::new();
1683
1684 state.handle_key_event(&key(KeyCode::Enter), &mut ctx);
1686 assert_eq!(ctx.deferred_actions.len(), 1);
1687 assert!(matches!(
1688 ctx.deferred_actions[0],
1689 DeferredAction::CloseSettings { save: true }
1690 ));
1691 }
1692
1693 #[test]
1698 fn test_tab_exits_number_editing() {
1699 use crate::view::settings::items::SettingControl;
1700
1701 let schema = include_str!("../../../plugins/config-schema.json");
1702 let config = crate::config::Config::default();
1703 let mut state = SettingsState::new(schema, &config).unwrap();
1704 state.visible = true;
1705 state.focus.set(FocusPanel::Settings);
1706
1707 let number_idx = state
1709 .pages
1710 .get(state.selected_category)
1711 .and_then(|page| {
1712 page.items
1713 .iter()
1714 .position(|item| matches!(item.control, SettingControl::Number(_)))
1715 })
1716 .expect("expected at least one Number control on the default page");
1717 state.selected_item = number_idx;
1718
1719 state.start_number_editing();
1721 assert!(
1722 state.is_number_editing(),
1723 "precondition: should be in number-editing mode"
1724 );
1725 state.number_insert('7');
1726
1727 let mut ctx = InputContext::new();
1728
1729 state.handle_key_event(&key(KeyCode::Tab), &mut ctx);
1731 assert!(
1732 !state.is_number_editing(),
1733 "Tab while editing a Number control must exit editing mode"
1734 );
1735 }
1736}