diff options
Diffstat (limited to 'helix-view/src/input.rs')
-rw-r--r-- | helix-view/src/input.rs | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs new file mode 100644 index 00000000..ab417819 --- /dev/null +++ b/helix-view/src/input.rs @@ -0,0 +1,226 @@ +//! Input event handling, currently backed by crossterm. +use anyhow::{anyhow, Error}; +use crossterm::event; +use serde::de::{self, Deserialize, Deserializer}; +use std::fmt; + +pub use crossterm::event::{KeyCode, KeyModifiers}; + +/// Represents a key event. +// We use a newtype here because we want to customize Deserialize and Display. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)] +pub struct KeyEvent { + pub code: KeyCode, + pub modifiers: KeyModifiers, +} + +impl fmt::Display for KeyEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{}{}", + if self.modifiers.contains(KeyModifiers::SHIFT) { + "S-" + } else { + "" + }, + if self.modifiers.contains(KeyModifiers::ALT) { + "A-" + } else { + "" + }, + if self.modifiers.contains(KeyModifiers::CONTROL) { + "C-" + } else { + "" + }, + ))?; + match self.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 std::str::FromStr for KeyEvent { + 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(KeyEvent { code, modifiers }) + } +} + +impl<'de> Deserialize<'de> for KeyEvent { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + +impl From<event::KeyEvent> for KeyEvent { + fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent { + KeyEvent { code, modifiers } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parsing_unmodified_keys() { + assert_eq!( + str::parse::<KeyEvent>("backspace").unwrap(), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::<KeyEvent>("left").unwrap(), + KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::<KeyEvent>(",").unwrap(), + KeyEvent { + code: KeyCode::Char(','), + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::<KeyEvent>("w").unwrap(), + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::<KeyEvent>("F12").unwrap(), + KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::NONE + } + ); + } + + #[test] + fn parsing_modified_keys() { + assert_eq!( + str::parse::<KeyEvent>("S-minus").unwrap(), + KeyEvent { + code: KeyCode::Char('-'), + modifiers: KeyModifiers::SHIFT + } + ); + + assert_eq!( + str::parse::<KeyEvent>("C-A-S-F12").unwrap(), + KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT + } + ); + + assert_eq!( + str::parse::<KeyEvent>("S-C-2").unwrap(), + KeyEvent { + code: KeyCode::Char('2'), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL + } + ); + } + + #[test] + fn parsing_nonsensical_keys_fails() { + assert!(str::parse::<KeyEvent>("F13").is_err()); + assert!(str::parse::<KeyEvent>("F0").is_err()); + assert!(str::parse::<KeyEvent>("aaa").is_err()); + assert!(str::parse::<KeyEvent>("S-S-a").is_err()); + assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err()); + assert!(str::parse::<KeyEvent>("FU").is_err()); + assert!(str::parse::<KeyEvent>("123").is_err()); + assert!(str::parse::<KeyEvent>("S--").is_err()); + } +} |