use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
const MAX_FUNCTION_KEY: u8 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyChord {
code: KeyCode,
ctrl: bool,
alt: bool,
shift: bool,
}
impl KeyChord {
#[must_use]
pub fn parse(text: &str) -> Option<KeyChord> {
let parts: Vec<&str> = text
.split('+')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect();
let (code_token, modifiers) = parts.split_last()?;
let mut chord = KeyChord {
code: code_from_token(code_token)?,
ctrl: false,
alt: false,
shift: false,
};
for modifier in modifiers {
match modifier.to_ascii_lowercase().as_str() {
"ctrl" | "control" => chord.ctrl = true,
"alt" | "option" => chord.alt = true,
"shift" => chord.shift = true,
_ => return None,
}
}
Some(chord)
}
#[must_use]
pub fn from_key(key: KeyEvent) -> Self {
KeyChord {
code: key.code,
ctrl: key.modifiers.contains(KeyModifiers::CONTROL),
alt: key.modifiers.contains(KeyModifiers::ALT),
shift: !matches!(key.code, KeyCode::Char(_))
&& key.modifiers.contains(KeyModifiers::SHIFT),
}
}
#[must_use]
pub fn matches(&self, key: &KeyEvent) -> bool {
if self.code != key.code
|| self.ctrl != key.modifiers.contains(KeyModifiers::CONTROL)
|| self.alt != key.modifiers.contains(KeyModifiers::ALT)
{
return false;
}
if matches!(self.code, KeyCode::Char(_)) {
return true;
}
self.shift == key.modifiers.contains(KeyModifiers::SHIFT)
}
#[must_use]
pub fn display(&self) -> String {
let mut text = String::new();
if self.ctrl {
text.push_str("ctrl+");
}
if self.alt {
text.push_str("alt+");
}
if self.shift && !matches!(self.code, KeyCode::Char(_)) {
text.push_str("shift+");
}
text.push_str(&token_for_code(self.code));
text
}
#[must_use]
pub fn to_key(&self) -> KeyEvent {
let mut modifiers = KeyModifiers::NONE;
if self.ctrl {
modifiers |= KeyModifiers::CONTROL;
}
if self.alt {
modifiers |= KeyModifiers::ALT;
}
if self.shift {
modifiers |= KeyModifiers::SHIFT;
}
KeyEvent::new(self.code, modifiers)
}
}
fn code_from_token(token: &str) -> Option<KeyCode> {
let lower = token.to_ascii_lowercase();
let code = match lower.as_str() {
"enter" | "return" => KeyCode::Enter,
"esc" | "escape" => KeyCode::Esc,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"space" => KeyCode::Char(' '),
"backspace" => KeyCode::Backspace,
"insert" | "ins" => KeyCode::Insert,
"up" => KeyCode::Up,
"down" => KeyCode::Down,
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"pgup" | "pageup" => KeyCode::PageUp,
"pgdn" | "pgdown" | "pagedown" => KeyCode::PageDown,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"del" | "delete" => KeyCode::Delete,
_ => return function_or_char(token, &lower),
};
Some(code)
}
fn function_or_char(token: &str, lower: &str) -> Option<KeyCode> {
if let Some(digits) = lower.strip_prefix('f')
&& let Ok(number) = digits.parse::<u8>()
&& (1..=MAX_FUNCTION_KEY).contains(&number)
{
return Some(KeyCode::F(number));
}
let mut chars = token.chars();
let first = chars.next()?;
if chars.next().is_some() {
return None;
}
Some(KeyCode::Char(first))
}
fn token_for_code(code: KeyCode) -> String {
match code {
KeyCode::Char(' ') => "space".to_string(),
KeyCode::Char(ch) => ch.to_string(),
KeyCode::Enter => "enter".to_string(),
KeyCode::Esc => "esc".to_string(),
KeyCode::Tab => "tab".to_string(),
KeyCode::BackTab => "backtab".to_string(),
KeyCode::Backspace => "backspace".to_string(),
KeyCode::Insert => "insert".to_string(),
KeyCode::Up => "up".to_string(),
KeyCode::Down => "down".to_string(),
KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(),
KeyCode::PageUp => "pgup".to_string(),
KeyCode::PageDown => "pgdn".to_string(),
KeyCode::Home => "home".to_string(),
KeyCode::End => "end".to_string(),
KeyCode::Delete => "del".to_string(),
KeyCode::F(number) => format!("f{number}"),
_ => "?".to_string(),
}
}