use anyhow::{bail, Result};
use crossterm::event::{KeyCode, KeyModifiers};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyBinding {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyBinding {
pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub fn char(c: char) -> Self {
Self::new(KeyCode::Char(c), KeyModifiers::NONE)
}
pub fn ctrl(c: char) -> Self {
Self::new(KeyCode::Char(c), KeyModifiers::CONTROL)
}
pub fn parse_key(s: &str) -> Result<Self> {
let s = s.trim().to_lowercase();
let parts: Vec<&str> = s.splitn(2, '-').collect();
match parts.as_slice() {
["ctrl", rest] => {
let c = parse_single_char(rest)?;
Ok(Self::ctrl(c))
}
["shift", rest] => {
let c = parse_single_char(rest)?;
Ok(Self::new(
KeyCode::Char(c.to_ascii_uppercase()),
KeyModifiers::NONE,
))
}
[single] => {
match *single {
"enter" => Ok(Self::new(KeyCode::Enter, KeyModifiers::NONE)),
"esc" | "escape" => Ok(Self::new(KeyCode::Esc, KeyModifiers::NONE)),
"backspace" => Ok(Self::new(KeyCode::Backspace, KeyModifiers::NONE)),
"tab" => Ok(Self::new(KeyCode::Tab, KeyModifiers::NONE)),
"up" => Ok(Self::new(KeyCode::Up, KeyModifiers::NONE)),
"down" => Ok(Self::new(KeyCode::Down, KeyModifiers::NONE)),
"left" => Ok(Self::new(KeyCode::Left, KeyModifiers::NONE)),
"right" => Ok(Self::new(KeyCode::Right, KeyModifiers::NONE)),
"pageup" | "pgup" => Ok(Self::new(KeyCode::PageUp, KeyModifiers::NONE)),
"pagedown" | "pgdn" => Ok(Self::new(KeyCode::PageDown, KeyModifiers::NONE)),
"home" => Ok(Self::new(KeyCode::Home, KeyModifiers::NONE)),
"end" => Ok(Self::new(KeyCode::End, KeyModifiers::NONE)),
"f1" => Ok(Self::new(KeyCode::F(1), KeyModifiers::NONE)),
"f2" => Ok(Self::new(KeyCode::F(2), KeyModifiers::NONE)),
other => {
let original = s.as_str();
let c = parse_single_char(original)?;
let _ = other; Ok(Self::char(c))
}
}
}
_ => bail!("Cannot parse key binding: {s}"),
}
}
}
impl KeyBinding {
pub fn display(&self) -> String {
let key = match &self.code {
KeyCode::Char(c) => c.to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Left => "←".to_string(),
KeyCode::Right => "→".to_string(),
KeyCode::PageUp => "PgUp".to_string(),
KeyCode::PageDown => "PgDn".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::F(n) => format!("F{n}"),
_ => "?".to_string(),
};
if self.modifiers.contains(KeyModifiers::CONTROL) {
format!("ctrl-{key}")
} else {
key
}
}
pub(super) fn sort_priority(&self) -> u8 {
match &self.code {
KeyCode::Char(_) if self.modifiers == KeyModifiers::NONE => 0,
KeyCode::Up
| KeyCode::Down
| KeyCode::PageUp
| KeyCode::PageDown
| KeyCode::Home
| KeyCode::End
| KeyCode::F(_) => 1,
_ => 2,
}
}
}
fn parse_single_char(s: &str) -> Result<char> {
let mut chars = s.chars();
let c = chars
.next()
.ok_or_else(|| anyhow::anyhow!("Empty key string"))?;
if chars.next().is_some() {
bail!("Expected single character, got: {s}");
}
Ok(c)
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyCode;
#[test]
fn test_parse_ctrl() {
let b = KeyBinding::parse_key("ctrl-d").unwrap();
assert_eq!(b.code, KeyCode::Char('d'));
assert_eq!(b.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn test_parse_char() {
let b = KeyBinding::parse_key("q").unwrap();
assert_eq!(b.code, KeyCode::Char('q'));
assert_eq!(b.modifiers, KeyModifiers::NONE);
}
#[test]
fn test_parse_special_keys() {
assert_eq!(KeyBinding::parse_key("enter").unwrap().code, KeyCode::Enter);
assert_eq!(KeyBinding::parse_key("esc").unwrap().code, KeyCode::Esc);
assert_eq!(KeyBinding::parse_key("up").unwrap().code, KeyCode::Up);
}
}