#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Key {
Char(char),
Enter,
Escape,
Backspace,
Delete,
Tab,
BackTab,
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
F(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyEvent {
pub key: Key,
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
}
impl KeyEvent {
pub const fn plain(key: Key) -> Self {
KeyEvent {
key,
ctrl: false,
alt: false,
shift: false,
}
}
pub const fn ctrl(key: Key) -> Self {
KeyEvent {
key,
ctrl: true,
alt: false,
shift: false,
}
}
pub const fn ch(c: char) -> Self {
KeyEvent::plain(Key::Char(c))
}
pub fn parse(spec: &str) -> Option<Self> {
let mut ctrl = false;
let mut alt = false;
let mut shift = false;
let mut last = spec.trim();
loop {
let lower = last.to_ascii_lowercase();
if let Some(rest) = lower.strip_prefix("ctrl+") {
ctrl = true;
last = &last[last.len() - rest.len()..];
} else if let Some(rest) = lower.strip_prefix("alt+") {
alt = true;
last = &last[last.len() - rest.len()..];
} else if let Some(rest) = lower.strip_prefix("shift+") {
shift = true;
last = &last[last.len() - rest.len()..];
} else {
break;
}
}
let key = if last.chars().count() == 1 {
Key::Char(last.chars().next()?)
} else {
match last.to_ascii_lowercase().as_str() {
"enter" | "return" => Key::Enter,
"esc" | "escape" => Key::Escape,
"backspace" => Key::Backspace,
"delete" | "del" => Key::Delete,
"tab" => Key::Tab,
"backtab" => Key::BackTab,
"up" => Key::Up,
"down" => Key::Down,
"left" => Key::Left,
"right" => Key::Right,
"home" => Key::Home,
"end" => Key::End,
"pageup" | "pgup" => Key::PageUp,
"pagedown" | "pgdn" => Key::PageDown,
other if other.starts_with('f') && other.len() <= 3 => {
let n: u8 = other[1..].parse().ok()?;
if (1..=12).contains(&n) {
Key::F(n)
} else {
return None;
}
}
_ => return None,
}
};
Some(KeyEvent {
key,
ctrl,
alt,
shift,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_plain_char() {
assert_eq!(KeyEvent::parse("r"), Some(KeyEvent::ch('r')));
assert_eq!(KeyEvent::parse("R"), Some(KeyEvent::ch('R')));
}
#[test]
fn parse_ctrl_chord_case_insensitive() {
assert_eq!(
KeyEvent::parse("Ctrl+R"),
Some(KeyEvent::ctrl(Key::Char('R')))
);
assert_eq!(
KeyEvent::parse("ctrl+r"),
Some(KeyEvent::ctrl(Key::Char('r')))
);
}
#[test]
fn parse_named_keys() {
assert_eq!(KeyEvent::parse("enter"), Some(KeyEvent::plain(Key::Enter)));
assert_eq!(KeyEvent::parse("Esc"), Some(KeyEvent::plain(Key::Escape)));
assert_eq!(
KeyEvent::parse("ctrl+pgup"),
Some(KeyEvent::ctrl(Key::PageUp))
);
}
#[test]
fn parse_function_keys() {
assert_eq!(KeyEvent::parse("f1"), Some(KeyEvent::plain(Key::F(1))));
assert_eq!(KeyEvent::parse("F12"), Some(KeyEvent::plain(Key::F(12))));
assert_eq!(KeyEvent::parse("f13"), None);
assert_eq!(KeyEvent::parse("f0"), None);
}
#[test]
fn parse_multiple_modifiers() {
assert_eq!(
KeyEvent::parse("ctrl+alt+x"),
Some(KeyEvent {
key: Key::Char('x'),
ctrl: true,
alt: true,
shift: false
})
);
}
#[test]
fn parse_unknown_returns_none() {
assert_eq!(KeyEvent::parse(""), None);
assert_eq!(KeyEvent::parse("bogus-key"), None);
assert_eq!(KeyEvent::parse("ctrl+"), None);
}
}