aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/keymap.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/keymap.rs')
-rw-r--r--helix-term/src/keymap.rs688
1 files changed, 500 insertions, 188 deletions
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 1402d29d..a2fdbdd1 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,7 +1,14 @@
-use crate::commands::{self, Command};
+use crate::commands;
+pub use crate::commands::Command;
+use anyhow::{anyhow, Error, Result};
use helix_core::hashmap;
use helix_view::document::Mode;
-use std::collections::HashMap;
+use std::{
+ collections::HashMap,
+ fmt::Display,
+ ops::{Deref, DerefMut},
+ str::FromStr,
+};
// Kakoune-inspired:
// mode = {
@@ -95,8 +102,10 @@ use std::collections::HashMap;
// #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
-pub type Keymap = HashMap<KeyEvent, Command>;
-pub type Keymaps = HashMap<Mode, Keymap>;
+#[derive(Clone, Debug)]
+pub struct Keymap(pub HashMap<KeyEvent, Command>);
+#[derive(Clone, Debug)]
+pub struct Keymaps(pub HashMap<Mode, Keymap>);
#[macro_export]
macro_rules! key {
@@ -132,188 +141,491 @@ macro_rules! alt {
};
}
-pub fn default() -> Keymaps {
- let normal = hashmap!(
- key!('h') => commands::move_char_left as Command,
- key!('j') => commands::move_line_down,
- key!('k') => commands::move_line_up,
- key!('l') => commands::move_char_right,
-
- key!(Left) => commands::move_char_left,
- key!(Down) => commands::move_line_down,
- key!(Up) => commands::move_line_up,
- key!(Right) => commands::move_char_right,
-
- key!('t') => commands::find_till_char,
- key!('f') => commands::find_next_char,
- key!('T') => commands::till_prev_char,
- key!('F') => commands::find_prev_char,
- // and matching set for select mode (extend)
- //
- key!('r') => commands::replace,
- key!('R') => commands::replace_with_yanked,
-
- key!(Home) => commands::move_line_start,
- key!(End) => commands::move_line_end,
-
- key!('w') => commands::move_next_word_start,
- key!('b') => commands::move_prev_word_start,
- key!('e') => commands::move_next_word_end,
-
- key!('v') => commands::select_mode,
- key!('g') => commands::goto_mode,
- key!(':') => commands::command_mode,
-
- key!('i') => commands::insert_mode,
- key!('I') => commands::prepend_to_line,
- key!('a') => commands::append_mode,
- key!('A') => commands::append_to_line,
- key!('o') => commands::open_below,
- key!('O') => commands::open_above,
- // [<space> ]<space> equivalents too (add blank new line, no edit)
-
-
- key!('d') => commands::delete_selection,
- // TODO: also delete without yanking
- key!('c') => commands::change_selection,
- // TODO: also change delete without yanking
-
- // key!('r') => commands::replace_with_char,
-
- key!('s') => commands::select_regex,
- alt!('s') => commands::split_selection_on_newline,
- key!('S') => commands::split_selection,
- key!(';') => commands::collapse_selection,
- alt!(';') => commands::flip_selections,
- key!('%') => commands::select_all,
- key!('x') => commands::select_line,
- key!('X') => commands::extend_line,
- // or select mode X?
- // extend_to_whole_line, crop_to_whole_line
-
-
- key!('m') => commands::match_brackets,
- // TODO: refactor into
- // key!('m') => commands::select_to_matching,
- // key!('M') => commands::back_select_to_matching,
- // select mode extend equivalents
-
- // key!('.') => commands::repeat_insert,
- // repeat_select
-
- // TODO: figure out what key to use
- // key!('[') => commands::expand_selection, ??
- key!('[') => commands::left_bracket_mode,
- key!(']') => commands::right_bracket_mode,
-
- key!('/') => commands::search,
- // ? for search_reverse
- key!('n') => commands::search_next,
- key!('N') => commands::extend_search_next,
- // N for search_prev
- key!('*') => commands::search_selection,
-
- key!('u') => commands::undo,
- key!('U') => commands::redo,
-
- key!('y') => commands::yank,
- // yank_all
- key!('p') => commands::paste_after,
- // paste_all
- key!('P') => commands::paste_before,
-
- key!('>') => commands::indent,
- key!('<') => commands::unindent,
- key!('=') => commands::format_selections,
- key!('J') => commands::join_selections,
- // TODO: conflicts hover/doc
- key!('K') => commands::keep_selections,
- // TODO: and another method for inverse
-
- // TODO: clashes with space mode
- key!(' ') => commands::keep_primary_selection,
-
- // key!('q') => commands::record_macro,
- // key!('Q') => commands::replay_macro,
-
- // ~ / apostrophe => change case
- // & align selections
- // _ trim selections
-
- // C / altC = copy (repeat) selections on prev/next lines
-
- key!(Esc) => commands::normal_mode,
- key!(PageUp) => commands::page_up,
- key!(PageDown) => commands::page_down,
- ctrl!('b') => commands::page_up,
- ctrl!('f') => commands::page_down,
- ctrl!('u') => commands::half_page_up,
- ctrl!('d') => commands::half_page_down,
-
- ctrl!('w') => commands::window_mode,
-
- // move under <space>c
- ctrl!('c') => commands::toggle_comments,
- key!('K') => commands::hover,
-
- // z family for save/restore/combine from/to sels from register
-
- // supposedly ctrl!('i') but did not work
- key!(Tab) => commands::jump_forward,
- ctrl!('o') => commands::jump_backward,
- // ctrl!('s') => commands::save_selection,
-
- key!(' ') => commands::space_mode,
- key!('z') => commands::view_mode,
-
- key!('"') => commands::select_register,
- );
- // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
- // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
- // because some selection operations can now be done from normal mode, some from select mode.
- let mut select = normal.clone();
- select.extend(
- hashmap!(
- key!('h') => commands::extend_char_left as Command,
- key!('j') => commands::extend_line_down,
- key!('k') => commands::extend_line_up,
- key!('l') => commands::extend_char_right,
-
- key!(Left) => commands::extend_char_left,
- key!(Down) => commands::extend_line_down,
- key!(Up) => commands::extend_line_up,
- key!(Right) => commands::extend_char_right,
-
- key!('w') => commands::extend_next_word_start,
- key!('b') => commands::extend_prev_word_start,
- key!('e') => commands::extend_next_word_end,
-
- key!('t') => commands::extend_till_char,
- key!('f') => commands::extend_next_char,
-
- key!('T') => commands::extend_till_prev_char,
- key!('F') => commands::extend_prev_char,
- key!(Home) => commands::extend_line_start,
- key!(End) => commands::extend_line_end,
- key!(Esc) => commands::exit_select_mode,
- )
- .into_iter(),
- );
-
- hashmap!(
- // as long as you cast the first item, rust is able to infer the other cases
- // TODO: select could be normal mode with some bindings merged over
- Mode::Normal => normal,
- Mode::Select => select,
- Mode::Insert => hashmap!(
- key!(Esc) => commands::normal_mode as Command,
- key!(Backspace) => commands::insert::delete_char_backward,
- key!(Delete) => commands::insert::delete_char_forward,
- key!(Enter) => commands::insert::insert_newline,
- key!(Tab) => commands::insert::insert_tab,
-
- ctrl!('x') => commands::completion,
- ctrl!('w') => commands::insert::delete_word_backward,
- ),
- )
+impl Default for Keymaps {
+ fn default() -> Self {
+ let normal = Keymap(hashmap!(
+ key!('h') => Command::move_char_left,
+ key!('j') => Command::move_line_down,
+ key!('k') => Command::move_line_up,
+ key!('l') => Command::move_char_right,
+
+ key!(Left) => Command::move_char_left,
+ key!(Down) => Command::move_line_down,
+ key!(Up) => Command::move_line_up,
+ key!(Right) => Command::move_char_right,
+
+ key!('t') => Command::find_till_char,
+ key!('f') => Command::find_next_char,
+ key!('T') => Command::till_prev_char,
+ key!('F') => Command::find_prev_char,
+ // and matching set for select mode (extend)
+ //
+ key!('r') => Command::replace,
+ key!('R') => Command::replace_with_yanked,
+
+ key!(Home) => Command::move_line_start,
+ key!(End) => Command::move_line_end,
+
+ key!('w') => Command::move_next_word_start,
+ key!('b') => Command::move_prev_word_start,
+ key!('e') => Command::move_next_word_end,
+
+ key!('v') => Command::select_mode,
+ key!('g') => Command::goto_mode,
+ key!(':') => Command::command_mode,
+
+ key!('i') => Command::insert_mode,
+ key!('I') => Command::prepend_to_line,
+ key!('a') => Command::append_mode,
+ key!('A') => Command::append_to_line,
+ key!('o') => Command::open_below,
+ key!('O') => Command::open_above,
+ // [<space> ]<space> equivalents too (add blank new line, no edit)
+
+
+ key!('d') => Command::delete_selection,
+ // TODO: also delete without yanking
+ key!('c') => Command::change_selection,
+ // TODO: also change delete without yanking
+
+ // key!('r') => Command::replace_with_char,
+
+ key!('s') => Command::select_regex,
+ alt!('s') => Command::split_selection_on_newline,
+ key!('S') => Command::split_selection,
+ key!(';') => Command::collapse_selection,
+ alt!(';') => Command::flip_selections,
+ key!('%') => Command::select_all,
+ key!('x') => Command::select_line,
+ key!('X') => Command::extend_line,
+ // or select mode X?
+ // extend_to_whole_line, crop_to_whole_line
+
+
+ key!('m') => Command::match_brackets,
+ // TODO: refactor into
+ // key!('m') => commands::select_to_matching,
+ // key!('M') => commands::back_select_to_matching,
+ // select mode extend equivalents
+
+ // key!('.') => commands::repeat_insert,
+ // repeat_select
+
+ // TODO: figure out what key to use
+ // key!('[') => Command::expand_selection, ??
+ key!('[') => Command::left_bracket_mode,
+ key!(']') => Command::right_bracket_mode,
+
+ key!('/') => Command::search,
+ // ? for search_reverse
+ key!('n') => Command::search_next,
+ key!('N') => Command::extend_search_next,
+ // N for search_prev
+ key!('*') => Command::search_selection,
+
+ key!('u') => Command::undo,
+ key!('U') => Command::redo,
+
+ key!('y') => Command::yank,
+ // yank_all
+ key!('p') => Command::paste_after,
+ // paste_all
+ key!('P') => Command::paste_before,
+
+ key!('>') => Command::indent,
+ key!('<') => Command::unindent,
+ key!('=') => Command::format_selections,
+ key!('J') => Command::join_selections,
+ // TODO: conflicts hover/doc
+ key!('K') => Command::keep_selections,
+ // TODO: and another method for inverse
+
+ // TODO: clashes with space mode
+ key!(' ') => Command::keep_primary_selection,
+
+ // key!('q') => Command::record_macro,
+ // key!('Q') => Command::replay_macro,
+
+ // ~ / apostrophe => change case
+ // & align selections
+ // _ trim selections
+
+ // C / altC = copy (repeat) selections on prev/next lines
+
+ key!(Esc) => Command::normal_mode,
+ key!(PageUp) => Command::page_up,
+ key!(PageDown) => Command::page_down,
+ ctrl!('b') => Command::page_up,
+ ctrl!('f') => Command::page_down,
+ ctrl!('u') => Command::half_page_up,
+ ctrl!('d') => Command::half_page_down,
+
+ ctrl!('w') => Command::window_mode,
+
+ // move under <space>c
+ ctrl!('c') => Command::toggle_comments,
+ key!('K') => Command::hover,
+
+ // z family for save/restore/combine from/to sels from register
+
+ // supposedly ctrl!('i') but did not work
+ key!(Tab) => Command::jump_forward,
+ ctrl!('o') => Command::jump_backward,
+ // ctrl!('s') => Command::save_selection,
+
+ key!(' ') => Command::space_mode,
+ key!('z') => Command::view_mode,
+
+ key!('"') => Command::select_register,
+ ));
+ // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
+ // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
+ // because some selection operations can now be done from normal mode, some from select mode.
+ let mut select = normal.clone();
+ select.0.extend(
+ hashmap!(
+ key!('h') => Command::extend_char_left,
+ key!('j') => Command::extend_line_down,
+ key!('k') => Command::extend_line_up,
+ key!('l') => Command::extend_char_right,
+
+ key!(Left) => Command::extend_char_left,
+ key!(Down) => Command::extend_line_down,
+ key!(Up) => Command::extend_line_up,
+ key!(Right) => Command::extend_char_right,
+
+ key!('w') => Command::extend_next_word_start,
+ key!('b') => Command::extend_prev_word_start,
+ key!('e') => Command::extend_next_word_end,
+
+ key!('t') => Command::extend_till_char,
+ key!('f') => Command::extend_next_char,
+
+ key!('T') => Command::extend_till_prev_char,
+ key!('F') => Command::extend_prev_char,
+ key!(Home) => Command::extend_line_start,
+ key!(End) => Command::extend_line_end,
+ key!(Esc) => Command::exit_select_mode,
+ )
+ .into_iter(),
+ );
+
+ Keymaps(hashmap!(
+ // as long as you cast the first item, rust is able to infer the other cases
+ // TODO: select could be normal mode with some bindings merged over
+ Mode::Normal => normal,
+ Mode::Select => select,
+ Mode::Insert => Keymap(hashmap!(
+ key!(Esc) => Command::normal_mode as Command,
+ key!(Backspace) => Command::delete_char_backward,
+ key!(Delete) => Command::delete_char_forward,
+ key!(Enter) => Command::insert_newline,
+ key!(Tab) => Command::insert_tab,
+ ctrl!('x') => Command::completion,
+ ctrl!('w') => Command::delete_word_backward,
+ )),
+ ))
+ }
+}
+
+// Newtype wrapper over keys to allow toml serialization/parsing
+#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
+pub struct RepresentableKeyEvent(pub KeyEvent);
+impl Display for RepresentableKeyEvent {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self(key) = self;
+ f.write_fmt(format_args!(
+ "{}{}{}",
+ if key.modifiers.contains(KeyModifiers::SHIFT) {
+ "S-"
+ } else {
+ ""
+ },
+ if key.modifiers.contains(KeyModifiers::ALT) {
+ "A-"
+ } else {
+ ""
+ },
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
+ "C-"
+ } else {
+ ""
+ },
+ ))?;
+ match key.code {
+ KeyCode::Backspace => f.write_str("backspace")?,
+ KeyCode::Enter => f.write_str("ret")?,
+ KeyCode::Left => f.write_str("left")?,
+ KeyCode::Right => f.write_str("right")?,
+ KeyCode::Up => f.write_str("up")?,
+ KeyCode::Down => f.write_str("down")?,
+ KeyCode::Home => f.write_str("home")?,
+ KeyCode::End => f.write_str("end")?,
+ KeyCode::PageUp => f.write_str("pageup")?,
+ KeyCode::PageDown => f.write_str("pagedown")?,
+ KeyCode::Tab => f.write_str("tab")?,
+ KeyCode::BackTab => f.write_str("backtab")?,
+ KeyCode::Delete => f.write_str("del")?,
+ KeyCode::Insert => f.write_str("ins")?,
+ KeyCode::Null => f.write_str("null")?,
+ KeyCode::Esc => f.write_str("esc")?,
+ KeyCode::Char('<') => f.write_str("lt")?,
+ KeyCode::Char('>') => f.write_str("gt")?,
+ KeyCode::Char('+') => f.write_str("plus")?,
+ KeyCode::Char('-') => f.write_str("minus")?,
+ KeyCode::Char(';') => f.write_str("semicolon")?,
+ KeyCode::Char('%') => f.write_str("percent")?,
+ KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
+ KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
+ };
+ Ok(())
+ }
+}
+
+impl FromStr for RepresentableKeyEvent {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut tokens: Vec<_> = s.split('-').collect();
+ let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
+ "backspace" => KeyCode::Backspace,
+ "space" => KeyCode::Char(' '),
+ "ret" => KeyCode::Enter,
+ "lt" => KeyCode::Char('<'),
+ "gt" => KeyCode::Char('>'),
+ "plus" => KeyCode::Char('+'),
+ "minus" => KeyCode::Char('-'),
+ "semicolon" => KeyCode::Char(';'),
+ "percent" => KeyCode::Char('%'),
+ "left" => KeyCode::Left,
+ "right" => KeyCode::Right,
+ "up" => KeyCode::Down,
+ "home" => KeyCode::Home,
+ "end" => KeyCode::End,
+ "pageup" => KeyCode::PageUp,
+ "pagedown" => KeyCode::PageDown,
+ "tab" => KeyCode::Tab,
+ "backtab" => KeyCode::BackTab,
+ "del" => KeyCode::Delete,
+ "ins" => KeyCode::Insert,
+ "null" => KeyCode::Null,
+ "esc" => KeyCode::Esc,
+ single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
+ function if function.len() > 1 && function.starts_with('F') => {
+ let function: String = function.chars().skip(1).collect();
+ let function = str::parse::<u8>(&function)?;
+ (function > 0 && function < 13)
+ .then(|| KeyCode::F(function))
+ .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
+ }
+ invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
+ };
+
+ let mut modifiers = KeyModifiers::empty();
+ for token in tokens {
+ let flag = match token {
+ "S" => KeyModifiers::SHIFT,
+ "A" => KeyModifiers::ALT,
+ "C" => KeyModifiers::CONTROL,
+ _ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
+ };
+
+ if modifiers.contains(flag) {
+ return Err(anyhow!("Repeated key modifier '{}-'", token));
+ }
+ modifiers.insert(flag);
+ }
+
+ Ok(RepresentableKeyEvent(KeyEvent { code, modifiers }))
+ }
+}
+
+pub fn parse_keymaps(toml_keymaps: &HashMap<String, HashMap<String, String>>) -> Result<Keymaps> {
+ let mut keymaps = Keymaps::default();
+
+ for (mode, map) in toml_keymaps {
+ let mode = Mode::from_str(&mode)?;
+ for (key, command) in map {
+ let key = str::parse::<RepresentableKeyEvent>(&key)?;
+ let command = str::parse::<Command>(&command)?;
+ keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
+ }
+ }
+ Ok(keymaps)
+}
+
+impl Deref for Keymap {
+ type Target = HashMap<KeyEvent, Command>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Deref for Keymaps {
+ type Target = HashMap<Mode, Keymap>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for Keymap {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl DerefMut for Keymaps {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::config::Config;
+
+ use super::*;
+
+ impl PartialEq for Command {
+ fn eq(&self, other: &Self) -> bool {
+ self.name() == other.name()
+ }
+ }
+
+ #[test]
+ fn parsing_keymaps_config_file() {
+ let sample_keymaps = r#"
+ [keys.insert]
+ y = "move_line_down"
+ S-C-a = "delete_selection"
+
+ [keys.normal]
+ A-F12 = "move_next_word_end"
+ "#;
+
+ let config: Config = toml::from_str(sample_keymaps).unwrap();
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Insert)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::Char('y'),
+ modifiers: KeyModifiers::NONE
+ })
+ .unwrap(),
+ Command::move_line_down
+ );
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Insert)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
+ })
+ .unwrap(),
+ Command::delete_selection
+ );
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Normal)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::ALT
+ })
+ .unwrap(),
+ Command::move_next_word_end
+ );
+ }
+
+ #[test]
+ fn parsing_unmodified_keys() {
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("backspace").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("left").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Left,
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>(",").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char(','),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("w").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("F12").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+ }
+
+ fn parsing_modified_keys() {
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char('-'),
+ modifiers: KeyModifiers::SHIFT
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(2),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
+ })
+ );
+ }
+
+ #[test]
+ fn parsing_nonsensical_keys_fails() {
+ assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
+ }
}