@@ -561,69 +561,21 @@ impl PromptDialog { |
| 561 | 561 | } |
| 562 | 562 | |
| 563 | 563 | fn handle_key(&mut self, key_event: &KeyEvent) { |
| 564 | | - if self.request.feedback_only { |
| 565 | | - // Feedback dialogs are transient; ignore keypresses so the submit key |
| 566 | | - // from the previous prompt cannot dismiss success/error feedback early. |
| 567 | | - return; |
| 568 | | - } |
| 569 | | - |
| 570 | | - match key_event.key { |
| 571 | | - Key::Escape => { |
| 572 | | - self.exit = Some(PromptExit::Canceled); |
| 573 | | - } |
| 574 | | - Key::Return => { |
| 575 | | - let submitted = std::mem::take(&mut self.input); |
| 576 | | - self.cursor = 0; |
| 577 | | - self.exit = Some(PromptExit::Submitted(submitted)); |
| 578 | | - } |
| 579 | | - Key::Left => { |
| 580 | | - if self.cursor > 0 { |
| 581 | | - self.cursor -= 1; |
| 582 | | - } |
| 583 | | - } |
| 584 | | - Key::Right => { |
| 585 | | - if self.cursor < self.input.chars().count() { |
| 586 | | - self.cursor += 1; |
| 587 | | - } |
| 588 | | - } |
| 589 | | - Key::Home => { |
| 590 | | - self.cursor = 0; |
| 591 | | - } |
| 592 | | - Key::End => { |
| 593 | | - self.cursor = self.input.chars().count(); |
| 594 | | - } |
| 595 | | - Key::Backspace => { |
| 596 | | - remove_char_before(&mut self.input, &mut self.cursor); |
| 597 | | - } |
| 598 | | - Key::Delete => { |
| 599 | | - remove_char_at(&mut self.input, self.cursor); |
| 600 | | - } |
| 601 | | - Key::Space => { |
| 602 | | - if key_event.modifiers.is_empty() || key_event.modifiers.shift { |
| 603 | | - insert_char_at(&mut self.input, self.cursor, ' '); |
| 604 | | - self.cursor += 1; |
| 605 | | - } |
| 606 | | - } |
| 607 | | - Key::Char(ch) => { |
| 608 | | - if key_event.modifiers.ctrl |
| 609 | | - || key_event.modifiers.alt |
| 610 | | - || key_event.modifiers.super_key |
| 611 | | - { |
| 612 | | - return; |
| 613 | | - } |
| 614 | | - let resolved = self |
| 615 | | - .keymap |
| 616 | | - .as_ref() |
| 617 | | - .and_then(|keymap| keymap.char_for_event(key_event)) |
| 618 | | - .unwrap_or(ch); |
| 619 | | - if resolved.is_control() { |
| 620 | | - return; |
| 621 | | - } |
| 622 | | - insert_char_at(&mut self.input, self.cursor, resolved); |
| 623 | | - self.cursor += 1; |
| 624 | | - } |
| 625 | | - _ => {} |
| 626 | | - } |
| 564 | + let resolved_char = if matches!(key_event.key, Key::Char(_)) { |
| 565 | + self.keymap |
| 566 | + .as_ref() |
| 567 | + .and_then(|keymap| keymap.char_for_event(key_event)) |
| 568 | + } else { |
| 569 | + None |
| 570 | + }; |
| 571 | + apply_key_event( |
| 572 | + &mut self.input, |
| 573 | + &mut self.cursor, |
| 574 | + &mut self.exit, |
| 575 | + self.request.feedback_only, |
| 576 | + key_event, |
| 577 | + resolved_char, |
| 578 | + ); |
| 627 | 579 | } |
| 628 | 580 | |
| 629 | 581 | fn render(&mut self) -> Result<()> { |
@@ -816,6 +768,73 @@ fn display_value(input: &str, mode: PromptMode) -> String { |
| 816 | 768 | } |
| 817 | 769 | } |
| 818 | 770 | |
| 771 | +fn apply_key_event( |
| 772 | + input: &mut String, |
| 773 | + cursor: &mut usize, |
| 774 | + exit: &mut Option<PromptExit>, |
| 775 | + feedback_only: bool, |
| 776 | + key_event: &KeyEvent, |
| 777 | + resolved_char: Option<char>, |
| 778 | +) { |
| 779 | + if feedback_only { |
| 780 | + // Feedback dialogs are transient; ignore keypresses so submit/escape |
| 781 | + // from the previous prompt cannot dismiss success/error feedback early. |
| 782 | + return; |
| 783 | + } |
| 784 | + |
| 785 | + match key_event.key { |
| 786 | + Key::Escape => { |
| 787 | + *exit = Some(PromptExit::Canceled); |
| 788 | + } |
| 789 | + Key::Return => { |
| 790 | + let submitted = std::mem::take(input); |
| 791 | + *cursor = 0; |
| 792 | + *exit = Some(PromptExit::Submitted(submitted)); |
| 793 | + } |
| 794 | + Key::Left => { |
| 795 | + if *cursor > 0 { |
| 796 | + *cursor -= 1; |
| 797 | + } |
| 798 | + } |
| 799 | + Key::Right => { |
| 800 | + if *cursor < input.chars().count() { |
| 801 | + *cursor += 1; |
| 802 | + } |
| 803 | + } |
| 804 | + Key::Home => { |
| 805 | + *cursor = 0; |
| 806 | + } |
| 807 | + Key::End => { |
| 808 | + *cursor = input.chars().count(); |
| 809 | + } |
| 810 | + Key::Backspace => { |
| 811 | + remove_char_before(input, cursor); |
| 812 | + } |
| 813 | + Key::Delete => { |
| 814 | + remove_char_at(input, *cursor); |
| 815 | + } |
| 816 | + Key::Space => { |
| 817 | + if key_event.modifiers.is_empty() || key_event.modifiers.shift { |
| 818 | + insert_char_at(input, *cursor, ' '); |
| 819 | + *cursor += 1; |
| 820 | + } |
| 821 | + } |
| 822 | + Key::Char(ch) => { |
| 823 | + if key_event.modifiers.ctrl || key_event.modifiers.alt || key_event.modifiers.super_key |
| 824 | + { |
| 825 | + return; |
| 826 | + } |
| 827 | + let resolved = resolved_char.unwrap_or(ch); |
| 828 | + if resolved.is_control() { |
| 829 | + return; |
| 830 | + } |
| 831 | + insert_char_at(input, *cursor, resolved); |
| 832 | + *cursor += 1; |
| 833 | + } |
| 834 | + _ => {} |
| 835 | + } |
| 836 | +} |
| 837 | + |
| 819 | 838 | fn display_prefix(input: &str, cursor: usize, mode: PromptMode) -> String { |
| 820 | 839 | let prefix = prefix_chars(input, cursor); |
| 821 | 840 | match mode { |
@@ -873,6 +892,16 @@ fn scrub_string(value: &mut String) { |
| 873 | 892 | #[cfg(test)] |
| 874 | 893 | mod tests { |
| 875 | 894 | use super::*; |
| 895 | + use gartk_core::Modifiers; |
| 896 | + |
| 897 | + fn key_event(key: Key) -> KeyEvent { |
| 898 | + KeyEvent { |
| 899 | + key, |
| 900 | + keycode: 0, |
| 901 | + modifiers: Modifiers::NONE, |
| 902 | + pressed: true, |
| 903 | + } |
| 904 | + } |
| 876 | 905 | |
| 877 | 906 | #[test] |
| 878 | 907 | fn display_value_masks_secret_text() { |
@@ -949,4 +978,126 @@ mod tests { |
| 949 | 978 | assert_eq!(spanish.label_password, "Contrasena"); |
| 950 | 979 | assert_eq!(spanish.footer_wait, "Espere"); |
| 951 | 980 | } |
| 981 | + |
| 982 | + #[test] |
| 983 | + fn apply_key_event_submits_and_clears_input_on_return() { |
| 984 | + let mut input = "secret".to_string(); |
| 985 | + let mut cursor = input.chars().count(); |
| 986 | + let mut exit = None; |
| 987 | + |
| 988 | + apply_key_event( |
| 989 | + &mut input, |
| 990 | + &mut cursor, |
| 991 | + &mut exit, |
| 992 | + false, |
| 993 | + &key_event(Key::Return), |
| 994 | + None, |
| 995 | + ); |
| 996 | + |
| 997 | + assert_eq!(cursor, 0); |
| 998 | + assert!(input.is_empty()); |
| 999 | + assert_eq!(exit, Some(PromptExit::Submitted("secret".to_string()))); |
| 1000 | + } |
| 1001 | + |
| 1002 | + #[test] |
| 1003 | + fn apply_key_event_ignores_keys_for_feedback_only_dialogs() { |
| 1004 | + let mut input = "ok".to_string(); |
| 1005 | + let mut cursor = 1; |
| 1006 | + let mut exit = None; |
| 1007 | + |
| 1008 | + apply_key_event( |
| 1009 | + &mut input, |
| 1010 | + &mut cursor, |
| 1011 | + &mut exit, |
| 1012 | + true, |
| 1013 | + &key_event(Key::Escape), |
| 1014 | + None, |
| 1015 | + ); |
| 1016 | + |
| 1017 | + assert_eq!(input, "ok"); |
| 1018 | + assert_eq!(cursor, 1); |
| 1019 | + assert!(exit.is_none()); |
| 1020 | + } |
| 1021 | + |
| 1022 | + #[test] |
| 1023 | + fn apply_key_event_respects_navigation_and_edit_shortcuts() { |
| 1024 | + let mut input = "abcd".to_string(); |
| 1025 | + let mut cursor = 2; |
| 1026 | + let mut exit = None; |
| 1027 | + |
| 1028 | + apply_key_event( |
| 1029 | + &mut input, |
| 1030 | + &mut cursor, |
| 1031 | + &mut exit, |
| 1032 | + false, |
| 1033 | + &key_event(Key::Left), |
| 1034 | + None, |
| 1035 | + ); |
| 1036 | + assert_eq!(cursor, 1); |
| 1037 | + |
| 1038 | + apply_key_event( |
| 1039 | + &mut input, |
| 1040 | + &mut cursor, |
| 1041 | + &mut exit, |
| 1042 | + false, |
| 1043 | + &key_event(Key::Backspace), |
| 1044 | + None, |
| 1045 | + ); |
| 1046 | + assert_eq!(input, "bcd"); |
| 1047 | + assert_eq!(cursor, 0); |
| 1048 | + |
| 1049 | + apply_key_event( |
| 1050 | + &mut input, |
| 1051 | + &mut cursor, |
| 1052 | + &mut exit, |
| 1053 | + false, |
| 1054 | + &key_event(Key::End), |
| 1055 | + None, |
| 1056 | + ); |
| 1057 | + assert_eq!(cursor, 3); |
| 1058 | + |
| 1059 | + apply_key_event( |
| 1060 | + &mut input, |
| 1061 | + &mut cursor, |
| 1062 | + &mut exit, |
| 1063 | + false, |
| 1064 | + &key_event(Key::Delete), |
| 1065 | + None, |
| 1066 | + ); |
| 1067 | + assert_eq!(input, "bcd"); |
| 1068 | + |
| 1069 | + apply_key_event( |
| 1070 | + &mut input, |
| 1071 | + &mut cursor, |
| 1072 | + &mut exit, |
| 1073 | + false, |
| 1074 | + &key_event(Key::Home), |
| 1075 | + None, |
| 1076 | + ); |
| 1077 | + assert_eq!(cursor, 0); |
| 1078 | + } |
| 1079 | + |
| 1080 | + #[test] |
| 1081 | + fn apply_key_event_ignores_ctrl_shortcuts_and_control_chars() { |
| 1082 | + let mut input = String::new(); |
| 1083 | + let mut cursor = 0; |
| 1084 | + let mut exit = None; |
| 1085 | + |
| 1086 | + let mut ctrl_event = key_event(Key::Char('x')); |
| 1087 | + ctrl_event.modifiers.ctrl = true; |
| 1088 | + apply_key_event(&mut input, &mut cursor, &mut exit, false, &ctrl_event, None); |
| 1089 | + assert!(input.is_empty()); |
| 1090 | + assert_eq!(cursor, 0); |
| 1091 | + |
| 1092 | + apply_key_event( |
| 1093 | + &mut input, |
| 1094 | + &mut cursor, |
| 1095 | + &mut exit, |
| 1096 | + false, |
| 1097 | + &key_event(Key::Char('a')), |
| 1098 | + Some('\n'), |
| 1099 | + ); |
| 1100 | + assert!(input.is_empty()); |
| 1101 | + assert_eq!(cursor, 0); |
| 1102 | + } |
| 952 | 1103 | } |