summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Oram <dev@mitmaro.ca>2023-07-16 16:45:56 -0230
committerTim Oram <dev@mitmaro.ca>2023-07-17 15:49:21 -0230
commit75ea729cea6bbe748424e0573a302257218f90aa (patch)
tree4e17a63d95022f90b5ddf116cdf1f8ebae335098
parent731ec26d1e3cabf75bfd13d2f4c874f65b98d929 (diff)
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.
-rw-r--r--src/config/src/utils/get_input.rs105
-rw-r--r--src/core/src/components/confirm/mod.rs1
-rw-r--r--src/core/src/components/shared/editable_line.rs7
-rw-r--r--src/input/src/event.rs7
-rw-r--r--src/input/src/key_bindings.rs12
-rw-r--r--src/input/src/key_event.rs72
6 files changed, 131 insertions, 73 deletions
diff --git a/src/config/src/utils/get_input.rs b/src/config/src/utils/get_input.rs
index 1c77765..1205b26 100644
--- a/src/config/src/utils/get_input.rs
+++ b/src/config/src/utils/get_input.rs
@@ -9,8 +9,8 @@ pub(crate) fn get_input(config: Option<&Config>, name: &str, default: &str) -> R
for mut value in input.split_whitespace().map(String::from) {
let mut modifiers = vec![];
- if let Some(index) = value.to_lowercase().find("shift+") {
- modifiers.push("Shift");
+ let shift_index = value.to_lowercase().find("shift+");
+ if let Some(index) = shift_index {
value.replace_range(index..index + 6, "");
}
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
value.replace_range(index..index + 4, "");
}
- values.push(format!(
- "{}{}",
- modifiers.join(""),
- match value.to_lowercase().as_ref() {
- "backspace" => String::from("Backspace"),
- "backtab" => String::from("BackTab"),
- "delete" => String::from("Delete"),
- "down" => String::from("Down"),
- "end" => String::from("End"),
- "enter" => String::from("Enter"),
- "esc" => String::from("Esc"),
- "home" => String::from("Home"),
- "insert" => String::from("Insert"),
- "left" => String::from("Left"),
- "pagedown" => String::from("PageDown"),
- "pageup" => String::from("PageUp"),
- "right" => String::from("Right"),
- "tab" => String::from("Tab"),
- "up" => String::from("Up"),
- v => {
- if v.len() > 1 {
- // allow F{number} values
- if v.starts_with('f') && v[1..].parse::<u8>().is_ok() {
- v.to_uppercase()
- }
- else {
- return Err(ConfigError::new(
- name,
- input.as_str(),
- ConfigErrorCause::InvalidKeyBinding,
- ));
- }
- }
- else {
- value
- }
- },
+ let mut key = match value.to_lowercase().as_ref() {
+ "backspace" => String::from("Backspace"),
+ "backtab" => String::from("BackTab"),
+ "delete" => String::from("Delete"),
+ "down" => String::from("Down"),
+ "end" => String::from("End"),
+ "enter" => String::from("Enter"),
+ "esc" => String::from("Esc"),
+ "home" => String::from("Home"),
+ "insert" => String::from("Insert"),
+ "left" => String::from("Left"),
+ "pagedown" => String::from("PageDown"),
+ "pageup" => String::from("PageUp"),
+ "right" => String::from("Right"),
+ "tab" => String::from("Tab"),
+ "up" => String::from("Up"),
+ v => {
+ let v_len = v.chars().count();
+ // allow F{number} values
+ if v_len > 1 && v.starts_with('f') && v[1..].parse::<u8>().is_ok() {
+ v.to_uppercase()
+ }
+ else if v_len == 1 {
+ value
+ }
+ else {
+ return Err(ConfigError::new(
+ name,
+ input.as_str(),
+ ConfigErrorCause::InvalidKeyBinding,
+ ));
+ }
+ },
+ };
+
+ // Shift support was partially removed, due to Shift not being universally reported, but still maintain
+ // some backwards compatibility with printable characters
+ if shift_index.is_some() {
+ if key.len() == 1 {
+ key = key.to_uppercase();
+ }
+ else {
+ modifiers.push("Shift");
}
- ));
+ }
+
+ values.push(format!("{}{}", modifiers.join(""), key));
}
Ok(values)
}
@@ -75,7 +83,9 @@ mod tests {
use crate::testutils::{invalid_utf, with_git_config};
#[rstest]
- #[case::single("a", "a")]
+ #[case::single_lower("a", "a")]
+ #[case::single_upper("A", "A")]
+ #[case::single_non_ascii("ẞ", "ẞ")]
#[case::backspace("backspace", "Backspace")]
#[case::backtab("backtab", "BackTab")]
#[case::delete("delete", "Delete")]
@@ -97,16 +107,19 @@ mod tests {
#[case::modifier_character_uppercase("Control+A", "ControlA")]
#[case::modifier_character_number("Control+1", "Control1")]
#[case::modifier_character_special("Control++", "Control+")]
+ #[case::modifier_character_non_ascii("Control+ẞ", "Controlẞ")]
#[case::modifier_character("Control+a", "Controla")]
#[case::modifier_special("Control+End", "ControlEnd")]
#[case::modifier_function("Control+F32", "ControlF32")]
- #[case::modifier_control_alt_shift_lowercase("alt+shift+control+end", "ShiftControlAltEnd")]
- #[case::modifier_control_alt_shift_mixedcase("aLt+shIft+conTrol+eNd", "ShiftControlAltEnd")]
- #[case::modifier_control_alt_shift_out_of_order_1("Alt+Shift+Control+End", "ShiftControlAltEnd")]
- #[case::modifier_control_alt_shift_out_of_order_2("Shift+Control+Alt+End", "ShiftControlAltEnd")]
+ #[case::modifier_control_alt_shift_lowercase("alt+shift+control+end", "ControlAltShiftEnd")]
+ #[case::modifier_control_alt_shift_mixedcase("aLt+shIft+conTrol+eNd", "ControlAltShiftEnd")]
+ #[case::modifier_control_alt_shift_out_of_order_1("Alt+Shift+Control+End", "ControlAltShiftEnd")]
+ #[case::modifier_control_alt_shift_out_of_order_2("Shift+Control+Alt+End", "ControlAltShiftEnd")]
#[case::modifier_only_shift("Shift+End", "ShiftEnd")]
#[case::modifier_only_control("Control+End", "ControlEnd")]
- #[case::multiple("a b c d", "a,b,c,d")]
+ #[case::shift_with_printable_lower("Shift+a", "A")]
+ #[case::shift_with_printable_upper("Shift+A", "A")]
+ #[case::multiple("a b ẞ c d", "a,b,ẞ,c,d")]
#[case::multiple_with_modifiers("Control+End Control+A", "ControlEnd,ControlA")]
fn read_value(#[case] binding: &str, #[case] expected: &str) {
with_git_config(&["[test]", format!("value = {binding}").as_str()], |git_config| {
diff --git a/src/core/src/components/confirm/mod.rs b/src/core/src/components/confirm/mod.rs
index dd76c0a..6c0d009 100644
--- a/src/core/src/components/confirm/mod.rs
+++ b/src/core/src/components/confirm/mod.rs
@@ -41,7 +41,6 @@ impl Confirm {
if let Event::Key(key) = event {
if let KeyCode::Char(c) = key.code {
let mut event_lower_modifiers = key.modifiers;
- event_lower_modifiers.remove(KeyModifiers::SHIFT);
let event_lower = Event::Key(KeyEvent::new(
KeyCode::Char(c.to_ascii_lowercase()),
event_lower_modifiers,
diff --git a/src/core/src/components/shared/editable_line.rs b/src/core/src/components/shared/editable_line.rs
index 42e46e3..cb3167d 100644
--- a/src/core/src/components/shared/editable_line.rs
+++ b/src/core/src/components/shared/editable_line.rs
@@ -186,7 +186,7 @@ impl EditableLine {
},
Event::Key(KeyEvent {
code: KeyCode::Char(c),
- modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
+ modifiers: KeyModifiers::NONE,
}) => {
let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
.take(self.cursor_position)
@@ -485,10 +485,7 @@ mod tests {
fn add_character_uppercase() {
let mut editable_line = EditableLine::new();
editable_line.set_content("abcd");
- _ = editable_line.handle_event(Event::Key(KeyEvent {
- code: KeyCode::Char('X'),
- modifiers: KeyModifiers::SHIFT,
- }));
+ _ = editable_line.handle_event(Event::from(KeyCode::Char('X')));
assert_rendered_output!(
Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
view_data_from_editable_line!(&editable_line),
diff --git a/src/input/src/event.rs b/src/input/src/event.rs
index 9b57e10..7ad9d3d 100644
--- a/src/input/src/event.rs
+++ b/src/input/src/event.rs
@@ -93,13 +93,10 @@ mod tests {
fn from_crossterm_key_event_char_with_modifier() {
let event = Event::from(ct_event::Event::Key(ct_event::KeyEvent::new(
KeyCode::Char('?'),
- KeyModifiers::SHIFT,
+ KeyModifiers::ALT,
)));
- assert_eq!(
- event,
- Event::Key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::SHIFT))
- );
+ assert_eq!(event, Event::Key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::ALT)));
}
#[test]
diff --git a/src/input/src/key_bindings.rs b/src/input/src/key_bindings.rs
index ab22276..0c5184a 100644
--- a/src/input/src/key_bindings.rs
+++ b/src/input/src/key_bindings.rs
@@ -84,10 +84,7 @@ pub fn map_keybindings<CustomEvent: crate::CustomEvent>(bindings: &[String]) ->
let key_number = k[1..].parse::<u8>().unwrap_or(1);
KeyCode::F(key_number)
},
- k => {
- // printable characters cannot use shift
- KeyCode::Char(k.chars().next().expect("Expected only one character from Char KeyCode"))
- },
+ k => KeyCode::Char(k.chars().next().expect("Expected only one character from Char KeyCode")),
};
Event::Key(KeyEvent::new(code, modifiers))
})
@@ -168,11 +165,4 @@ mod tests {
Event::from(key_code)
]);
}
-
- #[test]
- fn map_keybindings_key_code_char_remove_shift() {
- assert_eq!(map_keybindings::<TestEvent>(&[String::from("ShiftA")]), vec![
- Event::from(KeyCode::Char('A'))
- ]);
- }
}
diff --git a/src/input/src/key_event.rs b/src/input/src/key_event.rs
index 40d9a2a..2eaf13e 100644
--- a/src/input/src/key_event.rs
+++ b/src/input/src/key_event.rs
@@ -15,13 +15,11 @@ impl KeyEvent {
#[must_use]
#[inline]
pub fn new(mut code: KeyCode, mut modifiers: KeyModifiers) -> Self {
- // ensure that uppercase characters always have SHIFT
+ // normalize keys with the SHIFT modifier
if let KeyCode::Char(c) = code {
- if c.is_ascii_uppercase() {
- modifiers.insert(KeyModifiers::SHIFT);
- }
- else if modifiers.contains(KeyModifiers::SHIFT) {
+ if modifiers.contains(KeyModifiers::SHIFT) {
code = KeyCode::Char(c.to_ascii_uppercase());
+ modifiers.remove(KeyModifiers::SHIFT);
}
}
Self { code, modifiers }
@@ -41,3 +39,67 @@ impl From<KeyCode> for KeyEvent {
Self::new(code, KeyModifiers::empty())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn new_non_character() {
+ assert_eq!(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT), KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::ALT
+ });
+ }
+
+ #[test]
+ fn new_lowercase_character_without_shift() {
+ assert_eq!(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE), KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::NONE
+ });
+ }
+
+ #[test]
+ fn new_uppercase_character_without_shift() {
+ assert_eq!(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::NONE), KeyEvent {
+ code: KeyCode::Char('A'),
+ modifiers: KeyModifiers::NONE
+ });
+ }
+
+ #[test]
+ fn new_lowercase_character_with_shift() {
+ assert_eq!(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT), KeyEvent {
+ code: KeyCode::Char('A'),
+ modifiers: KeyModifiers::NONE
+ });
+ }
+
+ #[test]
+ fn new_uppercase_character_with_shift() {
+ assert_eq!(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT), KeyEvent {
+ code: KeyCode::Char('A'),
+ modifiers: KeyModifiers::NONE
+ });
+ }
+
+ #[test]
+ fn from_crossterm_key_event() {
+ assert_eq!(
+ KeyEvent::from(crossterm::event::KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)),
+ KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::ALT
+ }
+ );
+ }
+
+ #[test]
+ fn from_keycode() {
+ assert_eq!(KeyEvent::from(KeyCode::Char('a')), KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::NONE
+ });
+ }
+}