use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MediaKeyCode, ModifierKeyCode};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyId {
pub base: BaseKey,
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub super_: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BaseKey {
Char(char),
F(u8),
Enter,
Escape,
Backspace,
Delete,
Tab,
BackTab,
Up,
Down,
Left,
Right,
Home,
End,
PageUp,
PageDown,
Insert,
Media(MediaKeyCode),
Modifier(ModifierKeyCode),
Null,
}
impl fmt::Display for KeyId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.ctrl {
write!(f, "Ctrl+")?;
}
if self.alt {
write!(f, "Alt+")?;
}
if self.shift {
write!(f, "Shift+")?;
}
if self.super_ {
write!(f, "Super+")?;
}
write!(f, "{}", self.base)
}
}
impl fmt::Display for BaseKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BaseKey::Char(c) => write!(f, "{}", c),
BaseKey::F(n) => write!(f, "F{}", n),
BaseKey::Enter => write!(f, "Enter"),
BaseKey::Escape => write!(f, "Esc"),
BaseKey::Backspace => write!(f, "Backspace"),
BaseKey::Delete => write!(f, "Delete"),
BaseKey::Tab => write!(f, "Tab"),
BaseKey::BackTab => write!(f, "BackTab"),
BaseKey::Up => write!(f, "Up"),
BaseKey::Down => write!(f, "Down"),
BaseKey::Left => write!(f, "Left"),
BaseKey::Right => write!(f, "Right"),
BaseKey::Home => write!(f, "Home"),
BaseKey::End => write!(f, "End"),
BaseKey::PageUp => write!(f, "PageUp"),
BaseKey::PageDown => write!(f, "PageDown"),
BaseKey::Insert => write!(f, "Insert"),
BaseKey::Media(m) => write!(f, "Media({:?})", m),
BaseKey::Modifier(m) => write!(f, "Modifier({:?})", m),
BaseKey::Null => write!(f, "Null"),
}
}
}
impl From<KeyEvent> for KeyId {
fn from(event: KeyEvent) -> Self {
let mods = event.modifiers;
let base = match event.code {
KeyCode::Char(c) => BaseKey::Char(c.to_ascii_lowercase()),
KeyCode::F(n) => BaseKey::F(n),
KeyCode::Enter => BaseKey::Enter,
KeyCode::Esc => BaseKey::Escape,
KeyCode::Backspace => BaseKey::Backspace,
KeyCode::Delete => BaseKey::Delete,
KeyCode::Tab => BaseKey::Tab,
KeyCode::BackTab => BaseKey::BackTab,
KeyCode::Up => BaseKey::Up,
KeyCode::Down => BaseKey::Down,
KeyCode::Left => BaseKey::Left,
KeyCode::Right => BaseKey::Right,
KeyCode::Home => BaseKey::Home,
KeyCode::End => BaseKey::End,
KeyCode::PageUp => BaseKey::PageUp,
KeyCode::PageDown => BaseKey::PageDown,
KeyCode::Insert => BaseKey::Insert,
KeyCode::Media(m) => BaseKey::Media(m),
KeyCode::Modifier(m) => BaseKey::Modifier(m),
KeyCode::Null => BaseKey::Null,
KeyCode::CapsLock | KeyCode::ScrollLock | KeyCode::NumLock => BaseKey::Null,
KeyCode::Menu => BaseKey::Null,
KeyCode::PrintScreen | KeyCode::Pause | KeyCode::KeypadBegin => BaseKey::Null,
};
KeyId {
base,
ctrl: mods.contains(KeyModifiers::CONTROL),
shift: mods.contains(KeyModifiers::SHIFT),
alt: mods.contains(KeyModifiers::ALT),
super_: mods.contains(KeyModifiers::SUPER),
}
}
}
pub fn parse_key_id(s: &str) -> Option<KeyId> {
let s = s.trim();
if s.is_empty() {
return None;
}
let mut ctrl = false;
let mut shift = false;
let mut alt = false;
let mut super_ = false;
let parts: Vec<&str> = s.split('+').collect();
if parts.is_empty() {
return None;
}
let key_part = parts.last()?;
for &mod_part in &parts[..parts.len() - 1] {
match mod_part.trim().to_ascii_lowercase().as_str() {
"ctrl" | "control" => ctrl = true,
"shift" => shift = true,
"alt" | "option" => alt = true,
"super" | "cmd" | "command" => super_ = true,
_ => return None, }
}
let base = parse_base_key(key_part.trim())?;
Some(KeyId {
base,
ctrl,
shift,
alt,
super_,
})
}
fn parse_base_key(s: &str) -> Option<BaseKey> {
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"enter" | "return" => Some(BaseKey::Enter),
"esc" | "escape" => Some(BaseKey::Escape),
"backspace" | "bs" => Some(BaseKey::Backspace),
"delete" | "del" => Some(BaseKey::Delete),
"tab" => Some(BaseKey::Tab),
"backtab" => Some(BaseKey::BackTab),
"up" => Some(BaseKey::Up),
"down" => Some(BaseKey::Down),
"left" => Some(BaseKey::Left),
"right" => Some(BaseKey::Right),
"home" => Some(BaseKey::Home),
"end" => Some(BaseKey::End),
"pageup" | "page_up" | "pgup" => Some(BaseKey::PageUp),
"pagedown" | "page_down" | "pgdn" => Some(BaseKey::PageDown),
"insert" | "ins" => Some(BaseKey::Insert),
"space" => Some(BaseKey::Char(' ')),
_ => {
if let Some(n_str) = lower.strip_prefix('f') {
if let Ok(n) = n_str.parse::<u8>() {
if (1..=12).contains(&n) {
return Some(BaseKey::F(n));
}
}
}
let chars: Vec<char> = s.chars().collect();
if chars.len() == 1 {
return Some(BaseKey::Char(chars[0].to_ascii_lowercase()));
}
None
}
}
}
pub fn is_printable(key_id: &KeyId) -> bool {
if key_id.ctrl || key_id.alt || key_id.super_ {
return false;
}
matches!(key_id.base, BaseKey::Char(_))
}
pub fn as_char(key_id: &KeyId) -> Option<char> {
if key_id.ctrl || key_id.alt || key_id.super_ {
return None;
}
match key_id.base {
BaseKey::Char(c) => {
if key_id.shift {
Some(c.to_ascii_uppercase())
} else {
Some(c)
}
}
_ => None,
}
}
pub fn parse_kitty_sequence(data: &[u8]) -> Option<KeyId> {
if data.len() < 6 {
return None;
}
if data[0] != 0x1b || data[1] != b'[' {
return None;
}
let s = std::str::from_utf8(data).ok()?;
if !s.ends_with('u') {
return None;
}
let inner = &s[2..s.len() - 1];
let parts: Vec<&str> = inner.split(';').collect();
if parts.len() < 2 {
return None;
}
let codepoint: u32 = parts[0].parse().ok()?;
let modifier_field: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
let shift = (modifier_field & (1 << 0)) != 0;
let alt = (modifier_field & (1 << 1)) != 0;
let ctrl = (modifier_field & (1 << 2)) != 0;
let super_ = (modifier_field & (1 << 3)) != 0;
let base = if (57344..=65533).contains(&codepoint) {
kitty_special_key(codepoint)?
} else if (1..=26).contains(&codepoint) {
BaseKey::Char((b'a' + (codepoint - 1) as u8) as char)
} else {
match codepoint {
13 => BaseKey::Enter,
27 => BaseKey::Escape,
8 => BaseKey::Backspace,
9 => BaseKey::Tab,
127 => BaseKey::Delete,
cp => {
if let Some(c) = char::from_u32(cp) {
BaseKey::Char(c.to_ascii_lowercase())
} else {
return None;
}
}
}
};
Some(KeyId {
base,
ctrl,
shift,
alt,
super_,
})
}
fn kitty_special_key(cp: u32) -> Option<BaseKey> {
match cp {
57344 => Some(BaseKey::Up),
57345 => Some(BaseKey::Down),
57346 => Some(BaseKey::Left),
57347 => Some(BaseKey::Right),
57348 => Some(BaseKey::Insert),
57349 => Some(BaseKey::Delete),
57350 => Some(BaseKey::PageUp),
57351 => Some(BaseKey::PageDown),
57352 => Some(BaseKey::Home),
57353 => Some(BaseKey::End),
57358 => Some(BaseKey::F(1)),
57359 => Some(BaseKey::F(2)),
57360 => Some(BaseKey::F(3)),
57361 => Some(BaseKey::F(4)),
57362 => Some(BaseKey::F(5)),
57363 => Some(BaseKey::F(6)),
57364 => Some(BaseKey::F(7)),
57365 => Some(BaseKey::F(8)),
57366 => Some(BaseKey::F(9)),
57367 => Some(BaseKey::F(10)),
57368 => Some(BaseKey::F(11)),
57369 => Some(BaseKey::F(12)),
57370 => Some(BaseKey::BackTab),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_char() {
let k = parse_key_id("a").unwrap();
assert_eq!(k.base, BaseKey::Char('a'));
assert!(!k.ctrl);
assert!(!k.shift);
assert!(!k.alt);
}
#[test]
fn test_parse_ctrl_c() {
let k = parse_key_id("Ctrl+c").unwrap();
assert_eq!(k.base, BaseKey::Char('c'));
assert!(k.ctrl);
assert!(!k.shift);
}
#[test]
fn test_parse_shift_enter() {
let k = parse_key_id("Shift+Enter").unwrap();
assert_eq!(k.base, BaseKey::Enter);
assert!(k.shift);
}
#[test]
fn test_parse_alt_left() {
let k = parse_key_id("Alt+Left").unwrap();
assert_eq!(k.base, BaseKey::Left);
assert!(k.alt);
}
#[test]
fn test_parse_ctrl_shift_f() {
let k = parse_key_id("Ctrl+Shift+F").unwrap();
assert_eq!(k.base, BaseKey::Char('f'));
assert!(k.ctrl);
assert!(k.shift);
}
#[test]
fn test_parse_f5() {
let k = parse_key_id("F5").unwrap();
assert_eq!(k.base, BaseKey::F(5));
}
#[test]
fn test_parse_ctrl_f5() {
let k = parse_key_id("Ctrl+F5").unwrap();
assert_eq!(k.base, BaseKey::F(5));
assert!(k.ctrl);
}
#[test]
fn test_parse_space() {
let k = parse_key_id("Space").unwrap();
assert_eq!(k.base, BaseKey::Char(' '));
}
#[test]
fn test_parse_backtab() {
let k = parse_key_id("BackTab").unwrap();
assert_eq!(k.base, BaseKey::BackTab);
}
#[test]
fn test_parse_invalid() {
assert!(parse_key_id("").is_none());
assert!(parse_key_id("Foo+Bar").is_none());
}
#[test]
fn test_from_crossterm_key_event() {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
let event = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
let key_id = KeyId::from(event);
assert_eq!(key_id.base, BaseKey::Char('c'));
assert!(key_id.ctrl);
}
#[test]
fn test_display() {
let k = parse_key_id("Ctrl+Shift+Enter").unwrap();
assert_eq!(format!("{}", k), "Ctrl+Shift+Enter");
}
#[test]
fn test_is_printable() {
let k = parse_key_id("a").unwrap();
assert!(is_printable(&k));
let k = parse_key_id("Ctrl+a").unwrap();
assert!(!is_printable(&k));
}
#[test]
fn test_as_char() {
let k = parse_key_id("a").unwrap();
assert_eq!(as_char(&k), Some('a'));
let k = parse_key_id("Enter").unwrap();
assert_eq!(as_char(&k), None);
}
#[test]
fn test_parse_kitty_simple() {
let seq = b"\x1b[97;0u";
let key_id = parse_kitty_sequence(seq).unwrap();
assert_eq!(key_id.base, BaseKey::Char('a'));
assert!(!key_id.ctrl);
}
#[test]
fn test_parse_kitty_ctrl() {
let seq = b"\x1b[97;4u";
let key_id = parse_kitty_sequence(seq).unwrap();
assert_eq!(key_id.base, BaseKey::Char('a'));
assert!(key_id.ctrl);
}
#[test]
fn test_parse_kitty_shift_alt() {
let seq = b"\x1b[97;3u";
let key_id = parse_kitty_sequence(seq).unwrap();
assert!(key_id.shift);
assert!(key_id.alt);
assert!(!key_id.ctrl);
}
#[test]
fn test_parse_kitty_up() {
let seq = b"\x1b[57344;0u";
let key_id = parse_kitty_sequence(seq).unwrap();
assert_eq!(key_id.base, BaseKey::Up);
}
#[test]
fn test_parse_kitty_invalid() {
assert!(parse_kitty_sequence(b"").is_none());
assert!(parse_kitty_sequence(b"hello").is_none());
assert!(parse_kitty_sequence(b"\x1b[u").is_none());
}
}