Skip to content

Commit 75ea729

Browse files
committed
Normalize Shift modifier for character keycodes
Depending on the platform, reporting of the SHIFT modifier is not always supported. This results in cases where keybindings that usually use the SHIFT modifier (such as the ? character) not working as expected. This change updates the keybindings, to remove and ignore any SHIFT modifiers for any character keybinding, as well as convert the character to the uppercase variant. To reflect this change, the event handler has been changed to remove the SHIFT modifier for all characters, instead of the old behavior of removing the SHIFT modifier only for uppercase ASCII characters.
1 parent 731ec26 commit 75ea729

File tree

6 files changed

+131
-73
lines changed

6 files changed

+131
-73
lines changed

src/config/src/utils/get_input.rs

Lines changed: 59 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ pub(crate) fn get_input(config: Option<&Config>, name: &str, default: &str) -> R
99
for mut value in input.split_whitespace().map(String::from) {
1010
let mut modifiers = vec![];
1111

12-
if let Some(index) = value.to_lowercase().find("shift+") {
13-
modifiers.push("Shift");
12+
let shift_index = value.to_lowercase().find("shift+");
13+
if let Some(index) = shift_index {
1414
value.replace_range(index..index + 6, "");
1515
}
1616
if let Some(index) = value.to_lowercase().find("control+") {
@@ -22,45 +22,53 @@ pub(crate) fn get_input(config: Option<&Config>, name: &str, default: &str) -> R
2222
value.replace_range(index..index + 4, "");
2323
}
2424

25-
values.push(format!(
26-
"{}{}",
27-
modifiers.join(""),
28-
match value.to_lowercase().as_ref() {
29-
"backspace" => String::from("Backspace"),
30-
"backtab" => String::from("BackTab"),
31-
"delete" => String::from("Delete"),
32-
"down" => String::from("Down"),
33-
"end" => String::from("End"),
34-
"enter" => String::from("Enter"),
35-
"esc" => String::from("Esc"),
36-
"home" => String::from("Home"),
37-
"insert" => String::from("Insert"),
38-
"left" => String::from("Left"),
39-
"pagedown" => String::from("PageDown"),
40-
"pageup" => String::from("PageUp"),
41-
"right" => String::from("Right"),
42-
"tab" => String::from("Tab"),
43-
"up" => String::from("Up"),
44-
v => {
45-
if v.len() > 1 {
46-
// allow F{number} values
47-
if v.starts_with('f') && v[1..].parse::<u8>().is_ok() {
48-
v.to_uppercase()
49-
}
50-
else {
51-
return Err(ConfigError::new(
52-
name,
53-
input.as_str(),
54-
ConfigErrorCause::InvalidKeyBinding,
55-
));
56-
}
57-
}
58-
else {
59-
value
60-
}
61-
},
25+
let mut key = match value.to_lowercase().as_ref() {
26+
"backspace" => String::from("Backspace"),
27+
"backtab" => String::from("BackTab"),
28+
"delete" => String::from("Delete"),
29+
"down" => String::from("Down"),
30+
"end" => String::from("End"),
31+
"enter" => String::from("Enter"),
32+
"esc" => String::from("Esc"),
33+
"home" => String::from("Home"),
34+
"insert" => String::from("Insert"),
35+
"left" => String::from("Left"),
36+
"pagedown" => String::from("PageDown"),
37+
"pageup" => String::from("PageUp"),
38+
"right" => String::from("Right"),
39+
"tab" => String::from("Tab"),
40+
"up" => String::from("Up"),
41+
v => {
42+
let v_len = v.chars().count();
43+
// allow F{number} values
44+
if v_len > 1 && v.starts_with('f') && v[1..].parse::<u8>().is_ok() {
45+
v.to_uppercase()
46+
}
47+
else if v_len == 1 {
48+
value
49+
}
50+
else {
51+
return Err(ConfigError::new(
52+
name,
53+
input.as_str(),
54+
ConfigErrorCause::InvalidKeyBinding,
55+
));
56+
}
57+
},
58+
};
59+
60+
// Shift support was partially removed, due to Shift not being universally reported, but still maintain
61+
// some backwards compatibility with printable characters
62+
if shift_index.is_some() {
63+
if key.len() == 1 {
64+
key = key.to_uppercase();
65+
}
66+
else {
67+
modifiers.push("Shift");
6268
}
63-
));
69+
}
70+
71+
values.push(format!("{}{}", modifiers.join(""), key));
6472
}
6573
Ok(values)
6674
}
@@ -75,7 +83,9 @@ mod tests {
7583
use crate::testutils::{invalid_utf, with_git_config};
7684

7785
#[rstest]
78-
#[case::single("a", "a")]
86+
#[case::single_lower("a", "a")]
87+
#[case::single_upper("A", "A")]
88+
#[case::single_non_ascii("ẞ", "ẞ")]
7989
#[case::backspace("backspace", "Backspace")]
8090
#[case::backtab("backtab", "BackTab")]
8191
#[case::delete("delete", "Delete")]
@@ -97,16 +107,19 @@ mod tests {
97107
#[case::modifier_character_uppercase("Control+A", "ControlA")]
98108
#[case::modifier_character_number("Control+1", "Control1")]
99109
#[case::modifier_character_special("Control++", "Control+")]
110+
#[case::modifier_character_non_ascii("Control+ẞ", "Controlẞ")]
100111
#[case::modifier_character("Control+a", "Controla")]
101112
#[case::modifier_special("Control+End", "ControlEnd")]
102113
#[case::modifier_function("Control+F32", "ControlF32")]
103-
#[case::modifier_control_alt_shift_lowercase("alt+shift+control+end", "ShiftControlAltEnd")]
104-
#[case::modifier_control_alt_shift_mixedcase("aLt+shIft+conTrol+eNd", "ShiftControlAltEnd")]
105-
#[case::modifier_control_alt_shift_out_of_order_1("Alt+Shift+Control+End", "ShiftControlAltEnd")]
106-
#[case::modifier_control_alt_shift_out_of_order_2("Shift+Control+Alt+End", "ShiftControlAltEnd")]
114+
#[case::modifier_control_alt_shift_lowercase("alt+shift+control+end", "ControlAltShiftEnd")]
115+
#[case::modifier_control_alt_shift_mixedcase("aLt+shIft+conTrol+eNd", "ControlAltShiftEnd")]
116+
#[case::modifier_control_alt_shift_out_of_order_1("Alt+Shift+Control+End", "ControlAltShiftEnd")]
117+
#[case::modifier_control_alt_shift_out_of_order_2("Shift+Control+Alt+End", "ControlAltShiftEnd")]
107118
#[case::modifier_only_shift("Shift+End", "ShiftEnd")]
108119
#[case::modifier_only_control("Control+End", "ControlEnd")]
109-
#[case::multiple("a b c d", "a,b,c,d")]
120+
#[case::shift_with_printable_lower("Shift+a", "A")]
121+
#[case::shift_with_printable_upper("Shift+A", "A")]
122+
#[case::multiple("a b ẞ c d", "a,b,ẞ,c,d")]
110123
#[case::multiple_with_modifiers("Control+End Control+A", "ControlEnd,ControlA")]
111124
fn read_value(#[case] binding: &str, #[case] expected: &str) {
112125
with_git_config(&["[test]", format!("value = {binding}").as_str()], |git_config| {

src/core/src/components/confirm/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ impl Confirm {
4141
if let Event::Key(key) = event {
4242
if let KeyCode::Char(c) = key.code {
4343
let mut event_lower_modifiers = key.modifiers;
44-
event_lower_modifiers.remove(KeyModifiers::SHIFT);
4544
let event_lower = Event::Key(KeyEvent::new(
4645
KeyCode::Char(c.to_ascii_lowercase()),
4746
event_lower_modifiers,

src/core/src/components/shared/editable_line.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ impl EditableLine {
186186
},
187187
Event::Key(KeyEvent {
188188
code: KeyCode::Char(c),
189-
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
189+
modifiers: KeyModifiers::NONE,
190190
}) => {
191191
let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
192192
.take(self.cursor_position)
@@ -485,10 +485,7 @@ mod tests {
485485
fn add_character_uppercase() {
486486
let mut editable_line = EditableLine::new();
487487
editable_line.set_content("abcd");
488-
_ = editable_line.handle_event(Event::Key(KeyEvent {
489-
code: KeyCode::Char('X'),
490-
modifiers: KeyModifiers::SHIFT,
491-
}));
488+
_ = editable_line.handle_event(Event::from(KeyCode::Char('X')));
492489
assert_rendered_output!(
493490
Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
494491
view_data_from_editable_line!(&editable_line),

src/input/src/event.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ mod tests {
9393
fn from_crossterm_key_event_char_with_modifier() {
9494
let event = Event::from(ct_event::Event::Key(ct_event::KeyEvent::new(
9595
KeyCode::Char('?'),
96-
KeyModifiers::SHIFT,
96+
KeyModifiers::ALT,
9797
)));
9898

99-
assert_eq!(
100-
event,
101-
Event::Key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::SHIFT))
102-
);
99+
assert_eq!(event, Event::Key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::ALT)));
103100
}
104101

105102
#[test]

src/input/src/key_bindings.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ pub fn map_keybindings<CustomEvent: crate::CustomEvent>(bindings: &[String]) ->
8484
let key_number = k[1..].parse::<u8>().unwrap_or(1);
8585
KeyCode::F(key_number)
8686
},
87-
k => {
88-
// printable characters cannot use shift
89-
KeyCode::Char(k.chars().next().expect("Expected only one character from Char KeyCode"))
90-
},
87+
k => KeyCode::Char(k.chars().next().expect("Expected only one character from Char KeyCode")),
9188
};
9289
Event::Key(KeyEvent::new(code, modifiers))
9390
})
@@ -168,11 +165,4 @@ mod tests {
168165
Event::from(key_code)
169166
]);
170167
}
171-
172-
#[test]
173-
fn map_keybindings_key_code_char_remove_shift() {
174-
assert_eq!(map_keybindings::<TestEvent>(&[String::from("ShiftA")]), vec![
175-
Event::from(KeyCode::Char('A'))
176-
]);
177-
}
178168
}

src/input/src/key_event.rs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ impl KeyEvent {
1515
#[must_use]
1616
#[inline]
1717
pub fn new(mut code: KeyCode, mut modifiers: KeyModifiers) -> Self {
18-
// ensure that uppercase characters always have SHIFT
18+
// normalize keys with the SHIFT modifier
1919
if let KeyCode::Char(c) = code {
20-
if c.is_ascii_uppercase() {
21-
modifiers.insert(KeyModifiers::SHIFT);
22-
}
23-
else if modifiers.contains(KeyModifiers::SHIFT) {
20+
if modifiers.contains(KeyModifiers::SHIFT) {
2421
code = KeyCode::Char(c.to_ascii_uppercase());
22+
modifiers.remove(KeyModifiers::SHIFT);
2523
}
2624
}
2725
Self { code, modifiers }
@@ -41,3 +39,67 @@ impl From<KeyCode> for KeyEvent {
4139
Self::new(code, KeyModifiers::empty())
4240
}
4341
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
use super::*;
46+
47+
#[test]
48+
fn new_non_character() {
49+
assert_eq!(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT), KeyEvent {
50+
code: KeyCode::Backspace,
51+
modifiers: KeyModifiers::ALT
52+
});
53+
}
54+
55+
#[test]
56+
fn new_lowercase_character_without_shift() {
57+
assert_eq!(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE), KeyEvent {
58+
code: KeyCode::Char('a'),
59+
modifiers: KeyModifiers::NONE
60+
});
61+
}
62+
63+
#[test]
64+
fn new_uppercase_character_without_shift() {
65+
assert_eq!(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::NONE), KeyEvent {
66+
code: KeyCode::Char('A'),
67+
modifiers: KeyModifiers::NONE
68+
});
69+
}
70+
71+
#[test]
72+
fn new_lowercase_character_with_shift() {
73+
assert_eq!(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT), KeyEvent {
74+
code: KeyCode::Char('A'),
75+
modifiers: KeyModifiers::NONE
76+
});
77+
}
78+
79+
#[test]
80+
fn new_uppercase_character_with_shift() {
81+
assert_eq!(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT), KeyEvent {
82+
code: KeyCode::Char('A'),
83+
modifiers: KeyModifiers::NONE
84+
});
85+
}
86+
87+
#[test]
88+
fn from_crossterm_key_event() {
89+
assert_eq!(
90+
KeyEvent::from(crossterm::event::KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)),
91+
KeyEvent {
92+
code: KeyCode::Char('a'),
93+
modifiers: KeyModifiers::ALT
94+
}
95+
);
96+
}
97+
98+
#[test]
99+
fn from_keycode() {
100+
assert_eq!(KeyEvent::from(KeyCode::Char('a')), KeyEvent {
101+
code: KeyCode::Char('a'),
102+
modifiers: KeyModifiers::NONE
103+
});
104+
}
105+
}

0 commit comments

Comments
 (0)