use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyChord {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyChord {
pub fn to_display_string(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.modifiers.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl".into());
}
if self.modifiers.contains(KeyModifiers::ALT) {
parts.push("Alt".into());
}
if self.modifiers.contains(KeyModifiers::SHIFT) {
parts.push("Shift".into());
}
if self.modifiers.contains(KeyModifiers::SUPER) {
parts.push("Super".into());
}
let key = match self.code {
KeyCode::Char(' ') => "Space".to_string(),
KeyCode::Char(c) => c.to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "Tab".to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Delete => "Delete".to_string(),
KeyCode::Insert => "Insert".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PageUp".to_string(),
KeyCode::PageDown => "PageDown".to_string(),
KeyCode::Up => "Up".to_string(),
KeyCode::Down => "Down".to_string(),
KeyCode::Left => "Left".to_string(),
KeyCode::Right => "Right".to_string(),
KeyCode::F(n) => format!("F{n}"),
KeyCode::Null => "Null".to_string(),
other => format!("{other:?}"),
};
parts.push(key);
parts.join("+")
}
pub fn parse(s: &str) -> Result<Self, String> {
let mut mods = KeyModifiers::empty();
let mut code: Option<KeyCode> = None;
let mut shift_present = false;
for raw in s.split('+') {
let token = raw.trim();
if token.is_empty() {
continue;
}
match token.to_ascii_lowercase().as_str() {
"ctrl" | "control" => mods.insert(KeyModifiers::CONTROL),
"shift" => {
shift_present = true;
}
"alt" | "meta" | "option" => mods.insert(KeyModifiers::ALT),
"super" | "cmd" | "command" => mods.insert(KeyModifiers::SUPER),
_ => {
if code.is_some() {
return Err(format!("more than one key in `{s}`"));
}
code = Some(parse_code(token)?);
}
}
}
let mut code = code.ok_or_else(|| format!("no key code in `{s}`"))?;
if let KeyCode::Char(c) = code {
if shift_present {
mods.insert(KeyModifiers::SHIFT);
code = KeyCode::Char(c.to_ascii_uppercase());
} else if c.is_ascii_alphabetic() {
code = KeyCode::Char(c.to_ascii_lowercase());
}
} else if shift_present {
mods.insert(KeyModifiers::SHIFT);
}
Ok(Self { code, modifiers: mods })
}
pub fn matches(&self, ev: &KeyEvent) -> bool {
let mut ev_mods = ev.modifiers;
let ev_code = match ev.code {
KeyCode::BackTab => {
ev_mods.insert(KeyModifiers::SHIFT);
KeyCode::Tab
}
KeyCode::Char(c) if c.is_ascii_alphabetic() => {
if ev_mods.contains(KeyModifiers::SHIFT) {
KeyCode::Char(c.to_ascii_uppercase())
} else {
KeyCode::Char(c.to_ascii_lowercase())
}
}
other => other,
};
let mask = KeyModifiers::CONTROL
| KeyModifiers::SHIFT
| KeyModifiers::ALT
| KeyModifiers::SUPER;
ev_code == self.code && (ev_mods & mask) == (self.modifiers & mask)
}
}
fn parse_code(name: &str) -> Result<KeyCode, String> {
let lower = name.to_ascii_lowercase();
Ok(match lower.as_str() {
"tab" => KeyCode::Tab,
"enter" | "return" => KeyCode::Enter,
"esc" | "escape" => KeyCode::Esc,
"space" => KeyCode::Char(' '),
"backspace" => KeyCode::Backspace,
"delete" | "del" => KeyCode::Delete,
"insert" | "ins" => KeyCode::Insert,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" | "pgup" => KeyCode::PageUp,
"pagedown" | "pgdown" | "pgdn" => KeyCode::PageDown,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
s if s.starts_with('f') && s.len() >= 2 && s.len() <= 3 => {
let n: u8 = s[1..]
.parse()
.map_err(|_| format!("bad function key `{name}`"))?;
if !(1..=24).contains(&n) {
return Err(format!("function key {n} out of range"));
}
KeyCode::F(n)
}
s if s.chars().count() == 1 => {
let c = s.chars().next().unwrap();
KeyCode::Char(c)
}
_ => return Err(format!("unknown key `{name}`")),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, mods)
}
#[test]
fn parse_ctrl_s() {
let k = KeyChord::parse("Ctrl+s").unwrap();
assert!(k.matches(&ev(KeyCode::Char('s'), KeyModifiers::CONTROL)));
assert!(!k.matches(&ev(KeyCode::Char('s'), KeyModifiers::NONE)));
}
#[test]
fn parse_ctrl_slash() {
let k = KeyChord::parse("Ctrl+/").unwrap();
assert!(k.matches(&ev(KeyCode::Char('/'), KeyModifiers::CONTROL)));
}
#[test]
fn parse_shift_tab() {
let k = KeyChord::parse("Shift+Tab").unwrap();
assert!(k.matches(&ev(KeyCode::Tab, KeyModifiers::SHIFT)));
assert!(k.matches(&ev(KeyCode::BackTab, KeyModifiers::NONE)));
}
#[test]
fn parse_pageup() {
let k = KeyChord::parse("PageUp").unwrap();
assert!(k.matches(&ev(KeyCode::PageUp, KeyModifiers::NONE)));
}
#[test]
fn parse_ctrl_shift_letter() {
let k = KeyChord::parse("Ctrl+Shift+c").unwrap();
let mods = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
assert!(k.matches(&ev(KeyCode::Char('C'), mods)));
assert!(k.matches(&ev(KeyCode::Char('c'), mods)));
}
}