#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct KeyModifiers(pub u8);
impl KeyModifiers {
pub const SHIFT: Self = Self(1);
pub const ALT: Self = Self(2);
pub const CONTROL: Self = Self(4);
pub const SUPER: Self = Self(8);
pub const HYPER: Self = Self(16);
pub const META: Self = Self(32);
pub const NONE: Self = Self(0);
#[must_use]
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
#[must_use]
pub const fn bits(self) -> u8 {
self.0
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
impl std::ops::BitOr for KeyModifiers {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
self.union(rhs)
}
}
impl std::ops::BitOrAssign for KeyModifiers {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl std::ops::BitAnd for KeyModifiers {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
impl std::fmt::Display for KeyModifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts: Vec<&'static str> = Vec::with_capacity(6);
if self.contains(Self::CONTROL) {
parts.push("Ctrl");
}
if self.contains(Self::ALT) {
parts.push("Alt");
}
if self.contains(Self::SUPER) {
parts.push("Super");
}
if self.contains(Self::HYPER) {
parts.push("Hyper");
}
if self.contains(Self::META) {
parts.push("Meta");
}
if self.contains(Self::SHIFT) {
parts.push("Shift");
}
f.write_str(&parts.join("+"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyEventKind {
Press,
Repeat,
Release,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyType {
Char(char),
Enter,
Tab,
Backspace,
Escape,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Home,
End,
PageUp,
PageDown,
Delete,
Insert,
F(u8),
Space,
}
impl std::fmt::Display for KeyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Char(c) => f.write_str(&c.to_string()),
Self::Enter => f.write_str("Enter"),
Self::Tab => f.write_str("Tab"),
Self::Backspace => f.write_str("Backspace"),
Self::Escape => f.write_str("Escape"),
Self::ArrowUp => f.write_str("Up"),
Self::ArrowDown => f.write_str("Down"),
Self::ArrowLeft => f.write_str("Left"),
Self::ArrowRight => f.write_str("Right"),
Self::Home => f.write_str("Home"),
Self::End => f.write_str("End"),
Self::PageUp => f.write_str("PageUp"),
Self::PageDown => f.write_str("PageDown"),
Self::Delete => f.write_str("Delete"),
Self::Insert => f.write_str("Insert"),
Self::F(n) => write!(f, "F{n}"),
Self::Space => f.write_str("Space"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ParsedKey {
pub key: KeyType,
pub modifiers: KeyModifiers,
pub kind: KeyEventKind,
}
const KP_UP: u32 = 57_344;
const KP_DOWN: u32 = 57_345;
const KP_LEFT: u32 = 57_346;
const KP_RIGHT: u32 = 57_347;
const KP_INSERT: u32 = 57_348;
const KP_DELETE: u32 = 57_349;
const KP_PAGE_UP: u32 = 57_350;
const KP_PAGE_DOWN: u32 = 57_351;
const KP_HOME: u32 = 57_352;
const KP_END: u32 = 57_353;
const KP_CAPS_LOCK: u32 = 57_354;
const KP_SCROLL_LOCK: u32 = 57_355;
const KP_NUM_LOCK: u32 = 57_356;
const KP_PRINT_SCREEN: u32 = 57_357;
const KP_PAUSE_F1: u32 = 57_358;
const KP_F2: u32 = 57_359;
const KP_F3: u32 = 57_360;
const KP_F4: u32 = 57_361;
const KP_F5: u32 = 57_362;
const KP_F6: u32 = 57_363;
const KP_F7: u32 = 57_364;
const KP_F8: u32 = 57_365;
const KP_F9: u32 = 57_366;
const KP_F10: u32 = 57_367;
const KP_F11: u32 = 57_368;
const KP_F12: u32 = 57_369;
const KP_BACKTAB: u32 = 57_370;
const KP_FIRST_SPECIAL: u32 = 57_344;
const KP_LAST_SPECIAL: u32 = 65_533;
fn map_special_key(cp: u32) -> Option<KeyType> {
if !(KP_FIRST_SPECIAL..=KP_LAST_SPECIAL).contains(&cp) {
return None;
}
match cp {
KP_UP => Some(KeyType::ArrowUp),
KP_DOWN => Some(KeyType::ArrowDown),
KP_LEFT => Some(KeyType::ArrowLeft),
KP_RIGHT => Some(KeyType::ArrowRight),
KP_INSERT => Some(KeyType::Insert),
KP_DELETE => Some(KeyType::Delete),
KP_PAGE_UP => Some(KeyType::PageUp),
KP_PAGE_DOWN => Some(KeyType::PageDown),
KP_HOME => Some(KeyType::Home),
KP_END => Some(KeyType::End),
KP_PAUSE_F1 => Some(KeyType::F(1)),
KP_F2 => Some(KeyType::F(2)),
KP_F3 => Some(KeyType::F(3)),
KP_F4 => Some(KeyType::F(4)),
KP_F5 => Some(KeyType::F(5)),
KP_F6 => Some(KeyType::F(6)),
KP_F7 => Some(KeyType::F(7)),
KP_F8 => Some(KeyType::F(8)),
KP_F9 => Some(KeyType::F(9)),
KP_F10 => Some(KeyType::F(10)),
KP_F11 => Some(KeyType::F(11)),
KP_F12 => Some(KeyType::F(12)),
KP_BACKTAB => Some(KeyType::Tab),
KP_CAPS_LOCK | KP_SCROLL_LOCK | KP_NUM_LOCK | KP_PRINT_SCREEN => None,
_ => None,
}
}
#[must_use]
pub fn parse_kitty_key(data: &[u8]) -> Option<ParsedKey> {
if data.len() < 3 {
return None;
}
if data[0] != 0x1b || data[1] != b'[' {
return None;
}
let last = *data.last()?;
let terminator_ok = matches!(last, b'u' | b'~' | b'A' | b'B' | b'C' | b'D' | b'H');
if !terminator_ok {
return None;
}
if data.len() == 3 {
let key = match last {
b'A' => KeyType::ArrowUp,
b'B' => KeyType::ArrowDown,
b'C' => KeyType::ArrowRight,
b'D' => KeyType::ArrowLeft,
b'H' => KeyType::Home,
_ => return None,
};
return Some(ParsedKey {
key,
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
});
}
let inner = &data[2..data.len() - 1];
let s = std::str::from_utf8(inner).ok()?;
let mut semi = s.split(';');
let key_part = semi.next()?;
let mod_part = match semi.next() {
Some(m) => m,
None if matches!(last, b'u' | b'~') => return None,
None => "1",
};
let mut key_fields = key_part.split(':');
let codepoint_str = key_fields.next()?;
let codepoint: u32 = codepoint_str.parse().ok()?;
let mut mod_fields = mod_part.split(':');
let raw_modifier: u32 = mod_fields.next().unwrap_or("1").parse().ok()?;
let modifier_bits = raw_modifier.saturating_sub(1);
let mask = modifier_bits & 0b11_111_111;
let modifiers = KeyModifiers::from_bits((mask & 0x3f) as u8);
let event_type = mod_fields.next().and_then(|s| s.parse::<u8>().ok()).map_or(
KeyEventKind::Press,
|n| match n {
2 => KeyEventKind::Repeat,
3 => KeyEventKind::Release,
_ => KeyEventKind::Press,
},
);
let key = if (KP_FIRST_SPECIAL..=KP_LAST_SPECIAL).contains(&codepoint) {
map_special_key(codepoint)?
} else if codepoint == 32 {
KeyType::Space
} else {
match char::from_u32(codepoint) {
Some(c) if !c.is_control() => KeyType::Char(c),
_ => return None,
}
};
Some(ParsedKey {
key,
modifiers,
kind: event_type,
})
}
#[must_use]
pub fn matches_legacy_key(data: &[u8], expected: &str) -> bool {
if expected.contains('+') {
return false;
}
let expected_key = match normalize_key_name(expected) {
Some(k) => k,
None => return false,
};
match expected_key {
LegacyKey::Enter => data == b"\r" || data == b"\n",
LegacyKey::Tab => data == b"\t",
LegacyKey::Backspace => data == b"\x7f" || data == b"\x08",
LegacyKey::Escape => data == b"\x1b",
LegacyKey::Up => data == b"\x1b[A",
LegacyKey::Down => data == b"\x1b[B",
LegacyKey::Right => data == b"\x1b[C",
LegacyKey::Left => data == b"\x1b[D",
LegacyKey::Home => data == b"\x1b[H" || data == b"\x1b[1~" || data == b"\x1bOH",
LegacyKey::End => data == b"\x1b[F" || data == b"\x1b[4~" || data == b"\x1bOF",
LegacyKey::Insert => data == b"\x1b[2~",
LegacyKey::Delete => data == b"\x1b[3~",
LegacyKey::PageUp => data == b"\x1b[5~",
LegacyKey::PageDown => data == b"\x1b[6~",
LegacyKey::F(n) => data == legacy_f_sequence(n).as_slice(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LegacyKey {
Enter,
Tab,
Backspace,
Escape,
Up,
Down,
Left,
Right,
Home,
End,
Insert,
Delete,
PageUp,
PageDown,
F(u8),
}
fn normalize_key_name(name: &str) -> Option<LegacyKey> {
match name.to_ascii_lowercase().as_str() {
"enter" | "return" | "cr" => Some(LegacyKey::Enter),
"tab" => Some(LegacyKey::Tab),
"backspace" | "bs" => Some(LegacyKey::Backspace),
"escape" | "esc" => Some(LegacyKey::Escape),
"up" => Some(LegacyKey::Up),
"down" => Some(LegacyKey::Down),
"right" => Some(LegacyKey::Right),
"left" => Some(LegacyKey::Left),
"home" => Some(LegacyKey::Home),
"end" => Some(LegacyKey::End),
"insert" => Some(LegacyKey::Insert),
"delete" | "del" => Some(LegacyKey::Delete),
"pageup" | "page_up" | "pgup" => Some(LegacyKey::PageUp),
"pagedown" | "page_down" | "pgdn" => Some(LegacyKey::PageDown),
"f1" => Some(LegacyKey::F(1)),
"f2" => Some(LegacyKey::F(2)),
"f3" => Some(LegacyKey::F(3)),
"f4" => Some(LegacyKey::F(4)),
"f5" => Some(LegacyKey::F(5)),
"f6" => Some(LegacyKey::F(6)),
"f7" => Some(LegacyKey::F(7)),
"f8" => Some(LegacyKey::F(8)),
"f9" => Some(LegacyKey::F(9)),
"f10" => Some(LegacyKey::F(10)),
"f11" => Some(LegacyKey::F(11)),
"f12" => Some(LegacyKey::F(12)),
_ => None,
}
}
fn legacy_f_sequence(n: u8) -> Vec<u8> {
match n {
1 => b"\x1bOP".to_vec(),
2 => b"\x1bOQ".to_vec(),
3 => b"\x1bOR".to_vec(),
4 => b"\x1bOS".to_vec(),
5 => b"\x1b[15~".to_vec(),
6 => b"\x1b[17~".to_vec(),
7 => b"\x1b[18~".to_vec(),
8 => b"\x1b[19~".to_vec(),
9 => b"\x1b[20~".to_vec(),
10 => b"\x1b[21~".to_vec(),
11 => b"\x1b[23~".to_vec(),
12 => b"\x1b[24~".to_vec(),
_ => Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_char() {
let k = parse_kitty_key(b"\x1b[97;1u").unwrap();
assert_eq!(k.key, KeyType::Char('a'));
assert_eq!(k.modifiers, KeyModifiers::NONE);
assert_eq!(k.kind, KeyEventKind::Press);
}
#[test]
fn parse_ctrl_modifier() {
let k = parse_kitty_key(b"\x1b[97;5u").unwrap();
assert_eq!(k.key, KeyType::Char('a'));
assert_eq!(k.modifiers, KeyModifiers::CONTROL);
}
#[test]
fn parse_shift_ctrl_modifier() {
let k = parse_kitty_key(b"\x1b[120;6u").unwrap();
assert_eq!(k.key, KeyType::Char('x'));
assert!(k.modifiers.contains(KeyModifiers::CONTROL));
assert!(k.modifiers.contains(KeyModifiers::SHIFT));
}
#[test]
fn parse_alt_super_modifier() {
let k = parse_kitty_key(b"\x1b[49;11u").unwrap();
assert_eq!(k.key, KeyType::Char('1'));
assert!(k.modifiers.contains(KeyModifiers::ALT));
assert!(k.modifiers.contains(KeyModifiers::SUPER));
}
#[test]
fn parse_arrow_up() {
let k = parse_kitty_key(b"\x1b[57344;1u").unwrap();
assert_eq!(k.key, KeyType::ArrowUp);
assert_eq!(k.modifiers, KeyModifiers::NONE);
}
#[test]
fn parse_function_key() {
let k = parse_kitty_key(b"\x1b[57362;1u").unwrap();
assert_eq!(k.key, KeyType::F(5));
}
#[test]
fn parse_release_event() {
let k = parse_kitty_key(b"\x1b[97;1:3u").unwrap();
assert_eq!(k.key, KeyType::Char('a'));
assert_eq!(k.kind, KeyEventKind::Release);
}
#[test]
fn parse_repeat_event() {
let k = parse_kitty_key(b"\x1b[57344;1:2u").unwrap();
assert_eq!(k.key, KeyType::ArrowUp);
assert_eq!(k.kind, KeyEventKind::Repeat);
}
#[test]
fn parse_strips_caps_lock_bit() {
let k = parse_kitty_key(b"\x1b[97;65u").unwrap();
assert_eq!(k.key, KeyType::Char('a'));
assert_eq!(k.modifiers, KeyModifiers::NONE);
}
#[test]
fn parse_too_short() {
assert!(parse_kitty_key(b"").is_none());
assert!(parse_kitty_key(b"\x1b[").is_none());
assert!(parse_kitty_key(b"\x1b[97u").is_none()); }
#[test]
fn parse_wrong_prefix() {
assert!(parse_kitty_key(b"hello").is_none());
assert!(parse_kitty_key(b"\x1b97;1u").is_none());
}
#[test]
fn parse_invalid_codepoint() {
assert!(parse_kitty_key(b"\x1b[0x110000;1u").is_none());
}
#[test]
fn parse_alternate_terminator() {
let k = parse_kitty_key(b"\x1b[97;1~").unwrap();
assert_eq!(k.key, KeyType::Char('a'));
}
#[test]
fn parse_arrow_shortcut_terminator() {
let k = parse_kitty_key(b"\x1b[A").unwrap();
assert_eq!(k.key, KeyType::ArrowUp);
}
#[test]
fn matches_legacy_arrow_up() {
assert!(matches_legacy_key(b"\x1b[A", "up"));
assert!(!matches_legacy_key(b"\x1b[A", "down"));
}
#[test]
fn matches_legacy_enter() {
assert!(matches_legacy_key(b"\r", "enter"));
assert!(matches_legacy_key(b"\n", "enter"));
}
#[test]
fn matches_legacy_function_key() {
assert!(matches_legacy_key(b"\x1b[15~", "f5"));
assert!(matches_legacy_key(b"\x1bOP", "f1"));
assert!(!matches_legacy_key(b"\x1b[15~", "f6"));
}
#[test]
fn matches_legacy_unknown_name() {
assert!(!matches_legacy_key(b"\x1b[A", "foo"));
}
#[test]
fn modifier_display() {
let m = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
assert_eq!(m.to_string(), "Ctrl+Shift");
let m = KeyModifiers::NONE;
assert_eq!(m.to_string(), "");
}
#[test]
fn keytype_display() {
assert_eq!(KeyType::ArrowUp.to_string(), "Up");
assert_eq!(KeyType::F(12).to_string(), "F12");
assert_eq!(KeyType::Char('q').to_string(), "q");
assert_eq!(KeyType::Space.to_string(), "Space");
}
}