use crate::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
const ESC: u8 = 0x1b;
const DEL: u8 = 0x7f;
const BS: u8 = 0x08;
mod modifier_bits {
pub const SHIFT: u8 = 1;
pub const ALT: u8 = 2;
pub const CTRL: u8 = 4;
pub const SUPER: u8 = 8;
pub const CAPS_LOCK: u8 = 64;
pub const NUM_LOCK: u8 = 128;
}
mod codepoints {
pub const ESCAPE: i64 = 27;
pub const TAB: i64 = 9;
pub const ENTER: i64 = 13;
pub const SPACE: i64 = 32;
pub const BACKSPACE: i64 = 127;
pub const KP_ENTER: i64 = 57414;
pub const UP: i64 = -1;
pub const DOWN: i64 = -2;
pub const RIGHT: i64 = -3;
pub const LEFT: i64 = -4;
pub const DELETE: i64 = -10;
pub const INSERT: i64 = -11;
pub const PAGE_UP: i64 = -12;
pub const PAGE_DOWN: i64 = -13;
pub const HOME: i64 = -14;
pub const END: i64 = -15;
}
static mut KITTY_PROTOCOL_ACTIVE: bool = false;
pub unsafe fn set_kitty_protocol_active(active: bool) {
KITTY_PROTOCOL_ACTIVE = active;
}
pub unsafe fn is_kitty_protocol_active() -> bool {
KITTY_PROTOCOL_ACTIVE
}
fn kitty_active() -> bool {
unsafe { KITTY_PROTOCOL_ACTIVE }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyEventType {
Press,
Repeat,
Release,
}
static mut LAST_EVENT_TYPE: KeyEventType = KeyEventType::Press;
pub unsafe fn last_key_event_type() -> KeyEventType {
LAST_EVENT_TYPE
}
fn set_last_event_type(ty: KeyEventType) {
unsafe {
LAST_EVENT_TYPE = ty;
}
}
fn parse_event_type_str(s: Option<&str>) -> KeyEventType {
match s {
Some("2") => KeyEventType::Repeat,
Some("3") => KeyEventType::Release,
_ => KeyEventType::Press,
}
}
fn normalize_kitty_functional_codepoint(cp: i64) -> i64 {
match cp {
57399 => 48, 57400 => 49, 57401 => 50, 57402 => 51, 57403 => 52, 57404 => 53, 57405 => 54, 57406 => 55, 57407 => 56, 57408 => 57, 57409 => 46, 57410 => 47, 57411 => 42, 57412 => 45, 57413 => 43, 57415 => 61, 57416 => 44, 57417 => codepoints::LEFT, 57418 => codepoints::RIGHT, 57419 => codepoints::UP, 57420 => codepoints::DOWN, 57421 => codepoints::PAGE_UP,
57422 => codepoints::PAGE_DOWN,
57423 => codepoints::HOME,
57424 => codepoints::END,
57425 => codepoints::INSERT,
57426 => codepoints::DELETE,
_ => cp,
}
}
fn normalize_shifted_letter(cp: i64, modifier: u8) -> i64 {
let effective = modifier & !(modifier_bits::CAPS_LOCK | modifier_bits::NUM_LOCK);
if (effective & modifier_bits::SHIFT) != 0 && cp >= 65 && cp <= 90 {
cp + 32
} else {
cp
}
}
fn effective_modifier(modifier: u8) -> u8 {
modifier & !(modifier_bits::CAPS_LOCK | modifier_bits::NUM_LOCK)
}
#[derive(Debug)]
struct ParsedKitty {
codepoint: i64,
shifted_key: Option<i64>,
base_layout_key: Option<i64>,
modifier: u8,
#[allow(dead_code)] event_type: KeyEventType,
}
fn parse_kitty_sequence(data: &[u8]) -> Option<ParsedKitty> {
let s = std::str::from_utf8(data).ok()?;
if !s.starts_with('\x1b') || s.len() < 4 {
return None;
}
if let Some(rest) = s.strip_prefix("\x1b[") {
if rest.ends_with('u') && !rest.starts_with('<') {
return parse_csi_u(rest.strip_suffix('u')?);
}
if let Some(arrow) = parse_arrow_kitty(rest) {
return Some(arrow);
}
if rest.ends_with('~') {
return parse_functional_kitty(rest.strip_suffix('~')?);
}
if let Some(he) = parse_home_end_kitty(rest) {
return Some(he);
}
}
None
}
fn parse_csi_u(inner: &str) -> Option<ParsedKitty> {
let (cp_part, mod_part) = if let Some(idx) = inner.find(';') {
(&inner[..idx], &inner[idx + 1..])
} else {
(inner, "")
};
let mut cp_iter = cp_part.splitn(3, ':');
let cp_str = cp_iter.next()?;
let shifted_str = cp_iter.next();
let base_str = cp_iter.next();
let codepoint: i64 = cp_str.parse().ok()?;
let shifted_key = shifted_str
.filter(|s| !s.is_empty())
.and_then(|s| s.parse().ok());
let base_layout_key = base_str.and_then(|s| s.parse().ok());
let (mod_val, event_type) = if mod_part.is_empty() {
(1u8, KeyEventType::Press)
} else {
let mut miter = mod_part.splitn(2, ':');
let m_str = miter.next()?;
let e_str = miter.next();
let m: u8 = m_str.parse().ok()?;
(m, parse_event_type_str(e_str))
};
set_last_event_type(event_type);
Some(ParsedKitty {
codepoint,
shifted_key,
base_layout_key,
modifier: mod_val.saturating_sub(1),
event_type,
})
}
fn parse_arrow_kitty(rest: &str) -> Option<ParsedKitty> {
if !rest.starts_with('1') {
return None;
}
let last = rest.chars().last()?;
let letter = match last {
'A' => codepoints::UP,
'B' => codepoints::DOWN,
'C' => codepoints::RIGHT,
'D' => codepoints::LEFT,
_ => return None,
};
let inner = &rest[1..rest.len() - 1]; if inner.is_empty() {
set_last_event_type(KeyEventType::Press);
return Some(ParsedKitty {
codepoint: letter,
shifted_key: None,
base_layout_key: None,
modifier: 0,
event_type: KeyEventType::Press,
});
}
let inner = inner.strip_prefix(';')?;
let mut iter = inner.splitn(2, ':');
let mod_str = iter.next()?;
let event_str = iter.next();
let mod_val: u8 = mod_str.parse().ok()?;
let event_type = parse_event_type_str(event_str);
set_last_event_type(event_type);
Some(ParsedKitty {
codepoint: letter,
shifted_key: None,
base_layout_key: None,
modifier: mod_val.saturating_sub(1),
event_type,
})
}
fn parse_functional_kitty(inner: &str) -> Option<ParsedKitty> {
let (num_part, rest) = if let Some(idx) = inner.find(';') {
(&inner[..idx], &inner[idx + 1..])
} else {
(inner, "")
};
let key_num: u32 = num_part.parse().ok()?;
let cp = match key_num {
2 => codepoints::INSERT,
3 => codepoints::DELETE,
5 => codepoints::PAGE_UP,
6 => codepoints::PAGE_DOWN,
7 => codepoints::HOME,
8 => codepoints::END,
_ => return None,
};
let (mod_val, event_type) = if rest.is_empty() {
(1u8, KeyEventType::Press)
} else {
let mut iter = rest.splitn(2, ':');
let m_str = iter.next()?;
let e_str = iter.next();
let m: u8 = m_str.parse().ok()?;
(m, parse_event_type_str(e_str))
};
set_last_event_type(event_type);
Some(ParsedKitty {
codepoint: cp,
shifted_key: None,
base_layout_key: None,
modifier: mod_val.saturating_sub(1),
event_type,
})
}
fn parse_home_end_kitty(rest: &str) -> Option<ParsedKitty> {
if !rest.starts_with('1') {
return None;
}
let last = rest.chars().last()?;
let cp = match last {
'H' => codepoints::HOME,
'F' => codepoints::END,
_ => return None,
};
let inner = &rest[1..rest.len() - 1];
if inner.is_empty() {
set_last_event_type(KeyEventType::Press);
return Some(ParsedKitty {
codepoint: cp,
shifted_key: None,
base_layout_key: None,
modifier: 0,
event_type: KeyEventType::Press,
});
}
let inner = inner.strip_prefix(';')?;
let mut iter = inner.splitn(2, ':');
let mod_str = iter.next()?;
let event_str = iter.next();
let mod_val: u8 = mod_str.parse().ok()?;
let event_type = parse_event_type_str(event_str);
set_last_event_type(event_type);
Some(ParsedKitty {
codepoint: cp,
shifted_key: None,
base_layout_key: None,
modifier: mod_val.saturating_sub(1),
event_type,
})
}
#[derive(Debug)]
struct ParsedModifyOtherKeys {
codepoint: i64,
modifier: u8,
}
fn parse_modify_other_keys(data: &[u8]) -> Option<ParsedModifyOtherKeys> {
let s = std::str::from_utf8(data).ok()?;
if !s.starts_with("\x1b[27;") || !s.ends_with('~') {
return None;
}
let inner = &s[5..s.len() - 1]; let mut parts = inner.splitn(2, ';');
let mod_str = parts.next()?;
let cp_str = parts.next()?;
let mod_val: u8 = mod_str.parse().ok()?;
let cp: i64 = cp_str.parse().ok()?;
Some(ParsedModifyOtherKeys {
codepoint: cp,
modifier: mod_val.saturating_sub(1),
})
}
pub fn decode_kitty_printable(data: &[u8]) -> Option<char> {
let kitty = parse_kitty_sequence(data)?;
let modifier = effective_modifier(kitty.modifier);
if (modifier & !(modifier_bits::SHIFT | modifier_bits::CAPS_LOCK | modifier_bits::NUM_LOCK)) != 0 {
return None;
}
if (modifier & (modifier_bits::ALT | modifier_bits::CTRL)) != 0 {
return None;
}
let mut cp = kitty.codepoint;
if (modifier & modifier_bits::SHIFT) != 0 {
if let Some(sk) = kitty.shifted_key {
cp = sk;
}
}
cp = normalize_kitty_functional_codepoint(cp);
if cp < 32 {
return None;
}
char::from_u32(cp as u32)
}
fn decode_modify_other_keys_printable(data: &[u8]) -> Option<char> {
let parsed = parse_modify_other_keys(data)?;
let modifier = effective_modifier(parsed.modifier);
if (modifier & !modifier_bits::SHIFT) != 0 {
return None;
}
if parsed.codepoint < 32 {
return None;
}
char::from_u32(parsed.codepoint as u32)
}
pub fn decode_printable_key(data: &[u8]) -> Option<char> {
decode_kitty_printable(data).or_else(|| decode_modify_other_keys_printable(data))
}
pub fn is_key_release(data: &[u8]) -> bool {
let s = match std::str::from_utf8(data) {
Ok(s) => s,
Err(_) => return false,
};
if s.contains("\x1b[200~") {
return false;
}
s.contains(":3u")
|| s.contains(":3~")
|| s.contains(":3A")
|| s.contains(":3B")
|| s.contains(":3C")
|| s.contains(":3D")
|| s.contains(":3H")
|| s.contains(":3F")
}
pub fn is_key_repeat(data: &[u8]) -> bool {
let s = match std::str::from_utf8(data) {
Ok(s) => s,
Err(_) => return false,
};
if s.contains("\x1b[200~") {
return false;
}
s.contains(":2u")
|| s.contains(":2~")
|| s.contains(":2A")
|| s.contains(":2B")
|| s.contains(":2C")
|| s.contains(":2D")
|| s.contains(":2H")
|| s.contains(":2F")
}
fn modifiers_from_bitfield(modifier: u8) -> KeyModifiers {
let eff = effective_modifier(modifier);
KeyModifiers {
shift: (eff & modifier_bits::SHIFT) != 0,
ctrl: (eff & modifier_bits::CTRL) != 0,
alt: (eff & modifier_bits::ALT) != 0,
meta: (eff & modifier_bits::SUPER) != 0,
}
}
fn codepoint_to_key_code(cp: i64) -> Option<KeyCode> {
match cp {
codepoints::ESCAPE => Some(KeyCode::Escape),
codepoints::TAB => Some(KeyCode::Tab),
codepoints::ENTER | codepoints::KP_ENTER => Some(KeyCode::Enter),
codepoints::SPACE => Some(KeyCode::Char(' ')),
codepoints::BACKSPACE => Some(KeyCode::Backspace),
codepoints::DELETE => Some(KeyCode::Delete),
codepoints::INSERT => Some(KeyCode::Insert),
codepoints::HOME => Some(KeyCode::Home),
codepoints::END => Some(KeyCode::End),
codepoints::PAGE_UP => Some(KeyCode::PageUp),
codepoints::PAGE_DOWN => Some(KeyCode::PageDown),
codepoints::UP => Some(KeyCode::Up),
codepoints::DOWN => Some(KeyCode::Down),
codepoints::LEFT => Some(KeyCode::Left),
codepoints::RIGHT => Some(KeyCode::Right),
cp if cp >= 48 && cp <= 57 => Some(KeyCode::Char(char::from_u32(cp as u32)?)),
cp if cp >= 97 && cp <= 122 => Some(KeyCode::Char(char::from_u32(cp as u32)?)),
cp if cp >= 65 && cp <= 90 => Some(KeyCode::Char(char::from_u32(cp as u32)?.to_ascii_lowercase())),
cp if cp >= 32 => char::from_u32(cp as u32).map(KeyCode::Char),
_ => None,
}
}
fn is_symbol_cp(cp: i64) -> bool {
matches!(
cp,
96 | 45 | 61 | 91 | 93 | 92 | 59 | 39 | 44 | 46 | 47 | 33 | 64 | 35 | 36 | 37 | 94 | 38 | 42 | 40 | 41 | 95 | 43 | 124 | 126 | 123 | 125 | 58 | 60 | 62 | 63 )
}
fn kitty_codepoint_to_key_code(parsed: &ParsedKitty) -> Option<KeyCode> {
let cp = normalize_kitty_functional_codepoint(parsed.codepoint);
let eff_cp = normalize_shifted_letter(cp, parsed.modifier);
let is_latin = eff_cp >= 97 && eff_cp <= 122;
let is_digit = eff_cp >= 48 && eff_cp <= 57;
let is_symbol = is_symbol_cp(eff_cp);
let use_base = !is_latin && !is_digit && !is_symbol;
let final_cp = if use_base {
parsed.base_layout_key.unwrap_or(eff_cp)
} else {
eff_cp
};
codepoint_to_key_code(final_cp)
}
fn parse_sgr_mouse(data: &[u8]) -> Option<Event> {
let s = std::str::from_utf8(data).ok()?;
if !s.starts_with("\x1b[<") {
return None;
}
let last = s.chars().last()?;
let release = last == 'm';
let inner = &s[3..s.len() - 1]; let mut parts = inner.split(';');
let button_raw: u16 = parts.next()?.parse().ok()?;
let col: u16 = parts.next()?.parse().ok()?;
let row: u16 = parts.next()?.parse().ok()?;
let (kind, button) = if button_raw >= 64 {
let scroll_kind = if button_raw == 64 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
(scroll_kind, MouseButton::None)
} else if button_raw >= 32 {
let btn = match button_raw - 32 {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::None,
};
(MouseEventKind::Drag, btn)
} else if release {
let btn = match button_raw {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::None,
};
(MouseEventKind::Release, btn)
} else {
let btn = match button_raw {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::None,
};
(MouseEventKind::Press, btn)
};
Some(Event::Mouse(MouseEvent {
kind,
button,
row: row.saturating_sub(1),
col: col.saturating_sub(1),
}))
}
fn parse_x10_mouse(data: &[u8]) -> Option<Event> {
if data.len() != 6 || data[0] != ESC || data[1] != b'[' || data[2] != b'M' {
return None;
}
let cb = data[3];
let cx = data[4];
let cy = data[5];
let button_raw = cb.wrapping_sub(32);
let (kind, button) = if button_raw >= 64 {
let scroll_kind = if button_raw == 64 {
MouseEventKind::ScrollUp
} else {
MouseEventKind::ScrollDown
};
(scroll_kind, MouseButton::None)
} else {
let btn = match button_raw & 0x03 {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
_ => MouseButton::None,
};
(MouseEventKind::Press, btn)
};
Some(Event::Mouse(MouseEvent {
kind,
button,
row: (cy.wrapping_sub(32) as u16).saturating_sub(1),
col: (cx.wrapping_sub(32) as u16).saturating_sub(1),
}))
}
fn decode_utf8_char(data: &[u8]) -> Option<char> {
let s = std::str::from_utf8(data).ok()?;
let ch = s.chars().next()?;
Some(ch)
}
pub fn parse_event(data: &[u8]) -> Option<Event> {
if data.is_empty() {
return None;
}
if data == b"\x1b[200~" || data == b"\x1b[201~" {
return None;
}
if data == b"\x1b[I" {
return Some(Event::FocusGained);
}
if data == b"\x1b[O" {
return Some(Event::FocusLost);
}
if data.starts_with(b"\x1b[<") {
return parse_sgr_mouse(data);
}
if data.starts_with(b"\x1b[M") {
return parse_x10_mouse(data);
}
if let Some(kitty) = parse_kitty_sequence(data) {
let code = kitty_codepoint_to_key_code(&kitty)?;
let modifiers = modifiers_from_bitfield(kitty.modifier);
return Some(Event::Key(KeyEvent::with_modifiers(code, modifiers)));
}
if let Some(mok) = parse_modify_other_keys(data) {
let code = codepoint_to_key_code(mok.codepoint)?;
let modifiers = modifiers_from_bitfield(mok.modifier);
return Some(Event::Key(KeyEvent::with_modifiers(code, modifiers)));
}
if data[0] == ESC {
return parse_legacy_escape(data);
}
parse_raw_char(data)
}
fn parse_legacy_escape(data: &[u8]) -> Option<Event> {
if data.len() == 1 && data[0] == ESC {
return Some(Event::key(KeyCode::Escape));
}
let s = std::str::from_utf8(data).ok()?;
if kitty_active() {
if data == b"\x1b\r" || data == b"\n" {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Enter,
KeyModifiers {
shift: true,
..KeyModifiers::default()
},
)));
}
}
if data.len() == 3 && data[0] == ESC && data[1] == b'O' {
match data[2] {
b'P' => return Some(Event::key(KeyCode::F(1))),
b'Q' => return Some(Event::key(KeyCode::F(2))),
b'R' => return Some(Event::key(KeyCode::F(3))),
b'S' => return Some(Event::key(KeyCode::F(4))),
b'A' => return Some(Event::key(KeyCode::Up)),
b'B' => return Some(Event::key(KeyCode::Down)),
b'C' => return Some(Event::key(KeyCode::Right)),
b'D' => return Some(Event::key(KeyCode::Left)),
b'H' => return Some(Event::key(KeyCode::Home)),
b'F' => return Some(Event::key(KeyCode::End)),
b'M' => return Some(Event::key(KeyCode::Enter)),
b'E' => return Some(Event::key(KeyCode::Char('5'))),
b'a' => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Up,
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)))
}
b'b' => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Down,
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)))
}
b'c' => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Right,
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)))
}
b'd' => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Left,
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)))
}
_ => {}
}
}
if data.len() >= 3 && data[0] == ESC && data[1] == b'[' {
return parse_csi_sequence(data, s);
}
if data.len() == 2 && data[0] == ESC {
return parse_esc_prefix(data[1]);
}
None
}
fn parse_csi_sequence(data: &[u8], s: &str) -> Option<Event> {
let inner = &s[2..];
if data == b"\x1b[A" {
return Some(Event::key(KeyCode::Up));
}
if data == b"\x1b[B" {
return Some(Event::key(KeyCode::Down));
}
if data == b"\x1b[C" {
return Some(Event::key(KeyCode::Right));
}
if data == b"\x1b[D" {
return Some(Event::key(KeyCode::Left));
}
if data == b"\x1b[H" {
return Some(Event::key(KeyCode::Home));
}
if data == b"\x1b[F" {
return Some(Event::key(KeyCode::End));
}
if data == b"\x1b[Z" {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Tab,
KeyModifiers {
shift: true,
..KeyModifiers::default()
},
)));
}
if data == b"\x1b[a" {
return Some(shift_key(KeyCode::Up));
}
if data == b"\x1b[b" {
return Some(shift_key(KeyCode::Down));
}
if data == b"\x1b[c" {
return Some(shift_key(KeyCode::Right));
}
if data == b"\x1b[d" {
return Some(shift_key(KeyCode::Left));
}
if inner.ends_with('~') {
return parse_csi_tilde(data, inner);
}
if data == b"\x1b[1;3A" {
return Some(alt_key(KeyCode::Up));
}
if data == b"\x1b[1;3B" {
return Some(alt_key(KeyCode::Down));
}
if data == b"\x1b[1;3C" {
return Some(alt_key(KeyCode::Right));
}
if data == b"\x1b[1;3D" {
return Some(alt_key(KeyCode::Left));
}
if data == b"\x1b[1;5A" {
return Some(ctrl_key(KeyCode::Up));
}
if data == b"\x1b[1;5B" {
return Some(ctrl_key(KeyCode::Down));
}
if data == b"\x1b[1;5C" {
return Some(ctrl_key(KeyCode::Right));
}
if data == b"\x1b[1;5D" {
return Some(ctrl_key(KeyCode::Left));
}
None
}
fn parse_csi_tilde(_data: &[u8], inner: &str) -> Option<Event> {
let payload = &inner[..inner.len() - 1];
let (num_str, modifier) = if let Some(idx) = payload.find(';') {
let n = &payload[..idx];
let m_str = &payload[idx + 1..];
let m: u8 = m_str.parse().ok()?;
(n, m.saturating_sub(1))
} else {
(payload, 0u8)
};
let num: u32 = num_str.parse().ok()?;
let code = match num {
1 => Some(KeyCode::Home), 2 => Some(KeyCode::Insert), 3 => Some(KeyCode::Delete), 4 => Some(KeyCode::End), 5 => Some(KeyCode::PageUp), 6 => Some(KeyCode::PageDown), 7 => Some(KeyCode::Home), 8 => Some(KeyCode::End), 11 => Some(KeyCode::F(1)), 12 => Some(KeyCode::F(2)), 13 => Some(KeyCode::F(3)), 14 => Some(KeyCode::F(4)), 15 => Some(KeyCode::F(5)), 17 => Some(KeyCode::F(6)), 18 => Some(KeyCode::F(7)), 19 => Some(KeyCode::F(8)), 20 => Some(KeyCode::F(9)), 21 => Some(KeyCode::F(10)), 23 => Some(KeyCode::F(11)), 24 => Some(KeyCode::F(12)), _ => None,
};
let code = code?;
if modifier == 0 {
return Some(Event::key(code));
}
let mods = modifiers_from_bitfield(modifier);
Some(Event::Key(KeyEvent::with_modifiers(code, mods)))
}
fn parse_esc_prefix(second: u8) -> Option<Event> {
if second >= 1 && second <= 26 {
let ch = char::from(second + 96); return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(ch),
KeyModifiers {
ctrl: true,
alt: true,
..KeyModifiers::default()
},
)));
}
if (second >= b'a' && second <= b'z') || (second >= b'0' && second <= b'9') {
let ch = second as char;
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(ch),
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
)));
}
if second == DEL {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Backspace,
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
)));
}
if second == BS {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Backspace,
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
)));
}
match second {
b'\r' if !kitty_active() => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Enter,
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
)));
}
b' ' => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(' '),
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
)));
}
0x1c => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char('\\'),
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)));
}
0x1d => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(']'),
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)));
}
0x1f => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char('-'),
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)));
}
ESC => {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char('['),
KeyModifiers {
ctrl: true,
alt: true,
..KeyModifiers::default()
},
)));
}
b'p' => {
return Some(alt_key(KeyCode::Up));
}
b'n' => {
return Some(alt_key(KeyCode::Down));
}
b'b' if !kitty_active() => {
return Some(alt_key(KeyCode::Left));
}
b'f' if !kitty_active() => {
return Some(alt_key(KeyCode::Right));
}
_ => {}
}
None
}
fn parse_raw_char(data: &[u8]) -> Option<Event> {
if data.len() == 1 {
let b = data[0];
if b == b'\r' {
return Some(Event::key(KeyCode::Enter));
}
if b == b'\n' && !kitty_active() {
return Some(Event::key(KeyCode::Enter));
}
if b == b'\t' {
return Some(Event::key(KeyCode::Tab));
}
if b == 0 {
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(' '),
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)));
}
if b == DEL {
return Some(Event::key(KeyCode::Backspace));
}
if b == BS {
return Some(Event::key(KeyCode::Backspace));
}
if b >= 1 && b <= 26 {
let ch = char::from(b + 96);
return Some(Event::Key(KeyEvent::with_modifiers(
KeyCode::Char(ch),
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
)));
}
if b >= 32 && b <= 126 {
return Some(Event::key(KeyCode::Char(b as char)));
}
}
if data.len() > 1 {
if let Some(ch) = decode_utf8_char(data) {
return Some(Event::key(KeyCode::Char(ch)));
}
}
None
}
fn shift_key(code: KeyCode) -> Event {
Event::Key(KeyEvent::with_modifiers(
code,
KeyModifiers {
shift: true,
..KeyModifiers::default()
},
))
}
fn ctrl_key(code: KeyCode) -> Event {
Event::Key(KeyEvent::with_modifiers(
code,
KeyModifiers {
ctrl: true,
..KeyModifiers::default()
},
))
}
fn alt_key(code: KeyCode) -> Event {
Event::Key(KeyEvent::with_modifiers(
code,
KeyModifiers {
alt: true,
..KeyModifiers::default()
},
))
}
fn parse_key_id(key_id: &str) -> Option<(String, bool, bool, bool, bool)> {
let lower = key_id.to_lowercase();
let parts: Vec<&str> = lower.split('+').collect();
let key = (*parts.last()?).to_string();
Some((
key,
parts.contains(&"ctrl"),
parts.contains(&"shift"),
parts.contains(&"alt"),
parts.contains(&"super"),
))
}
fn modifier_bits_from_parts(ctrl: bool, shift: bool, alt: bool, super_: bool) -> u8 {
let mut m = 0u8;
if shift {
m |= modifier_bits::SHIFT;
}
if alt {
m |= modifier_bits::ALT;
}
if ctrl {
m |= modifier_bits::CTRL;
}
if super_ {
m |= modifier_bits::SUPER;
}
m
}
fn key_name_to_codepoint(key: &str) -> Option<i64> {
match key {
"escape" | "esc" => Some(codepoints::ESCAPE),
"space" => Some(codepoints::SPACE),
"tab" => Some(codepoints::TAB),
"enter" | "return" => Some(codepoints::ENTER),
"backspace" => Some(codepoints::BACKSPACE),
"up" => Some(codepoints::UP),
"down" => Some(codepoints::DOWN),
"left" => Some(codepoints::LEFT),
"right" => Some(codepoints::RIGHT),
"insert" => Some(codepoints::INSERT),
"delete" => Some(codepoints::DELETE),
"home" => Some(codepoints::HOME),
"end" => Some(codepoints::END),
"pageup" => Some(codepoints::PAGE_UP),
"pagedown" => Some(codepoints::PAGE_DOWN),
s if s.starts_with('f') => {
None
}
_ => None,
}
}
pub fn matches_key(data: &[u8], key_id: &str) -> bool {
let Some((key, ctrl, shift, alt, super_)) = parse_key_id(key_id) else {
return false;
};
let modifier = modifier_bits_from_parts(ctrl, shift, alt, super_);
let try_kitty = |expected_cp: i64, expected_mod: u8| -> bool {
let Some(kitty) = parse_kitty_sequence(data) else {
return false;
};
let actual_mod = effective_modifier(kitty.modifier);
let expected_mod = effective_modifier(expected_mod);
if actual_mod != expected_mod {
return false;
}
let actual_cp = normalize_shifted_letter(
normalize_kitty_functional_codepoint(kitty.codepoint),
kitty.modifier,
);
let expected_cp = normalize_shifted_letter(
normalize_kitty_functional_codepoint(expected_cp),
expected_mod,
);
if actual_cp == expected_cp {
return true;
}
if let Some(base) = kitty.base_layout_key {
if base == expected_cp {
let cp = actual_cp;
let is_latin = cp >= 97 && cp <= 122;
let is_digit = cp >= 48 && cp <= 57;
let is_symbol = is_symbol_cp(cp);
if !is_latin && !is_digit && !is_symbol {
return true;
}
}
}
false
};
let try_mok = |expected_cp: i64, expected_mod: u8| -> bool {
let Some(mok) = parse_modify_other_keys(data) else {
return false;
};
let actual_mod = effective_modifier(mok.modifier);
let expected_mod = effective_modifier(expected_mod);
if actual_mod != expected_mod {
return false;
}
let actual = normalize_shifted_letter(mok.codepoint, mok.modifier);
let expected = normalize_shifted_letter(expected_cp, expected_mod);
actual == expected
};
match key.as_str() {
"escape" | "esc" => {
if modifier != 0 {
return false;
}
if data == b"\x1b" {
return true;
}
if try_kitty(codepoints::ESCAPE, 0) {
return true;
}
if try_mok(codepoints::ESCAPE, 0) {
return true;
}
false
}
"space" => {
if !kitty_active() {
if modifier == modifier_bits::CTRL && data == b"\x00" {
return true;
}
if modifier == modifier_bits::ALT && data == b"\x1b " {
return true;
}
}
if modifier == 0 && (data == b" " || try_kitty(codepoints::SPACE, 0) || try_mok(codepoints::SPACE, 0)) {
return true;
}
try_kitty(codepoints::SPACE, modifier) || try_mok(codepoints::SPACE, modifier)
}
"tab" => {
if modifier == modifier_bits::SHIFT {
return data == b"\x1b[Z"
|| try_kitty(codepoints::TAB, modifier_bits::SHIFT)
|| try_mok(codepoints::TAB, modifier_bits::SHIFT);
}
if modifier == 0 {
return data == b"\t" || try_kitty(codepoints::TAB, 0);
}
try_kitty(codepoints::TAB, modifier) || try_mok(codepoints::TAB, modifier)
}
"enter" | "return" => {
if modifier == modifier_bits::SHIFT {
if try_kitty(codepoints::ENTER, modifier_bits::SHIFT)
|| try_kitty(codepoints::KP_ENTER, modifier_bits::SHIFT)
|| try_mok(codepoints::ENTER, modifier_bits::SHIFT)
{
return true;
}
if kitty_active() {
return data == b"\x1b\r" || data == b"\n";
}
return false;
}
if modifier == modifier_bits::ALT {
if try_kitty(codepoints::ENTER, modifier_bits::ALT)
|| try_kitty(codepoints::KP_ENTER, modifier_bits::ALT)
|| try_mok(codepoints::ENTER, modifier_bits::ALT)
{
return true;
}
if !kitty_active() {
return data == b"\x1b\r";
}
return false;
}
if modifier == 0 {
if data == b"\r"
|| (!kitty_active() && data == b"\n")
|| data == b"\x1bOM"
|| try_kitty(codepoints::ENTER, 0)
|| try_kitty(codepoints::KP_ENTER, 0)
{
return true;
}
}
try_kitty(codepoints::ENTER, modifier)
|| try_kitty(codepoints::KP_ENTER, modifier)
|| try_mok(codepoints::ENTER, modifier)
}
"backspace" => {
if modifier == modifier_bits::ALT {
if data == b"\x1b\x7f" || data == b"\x1b\x08" {
return true;
}
return try_kitty(codepoints::BACKSPACE, modifier_bits::ALT)
|| try_mok(codepoints::BACKSPACE, modifier_bits::ALT);
}
if modifier == modifier_bits::CTRL {
if data == b"\x08" || data == b"\x7f" {
return true;
}
return try_kitty(codepoints::BACKSPACE, modifier_bits::CTRL)
|| try_mok(codepoints::BACKSPACE, modifier_bits::CTRL);
}
if modifier == 0 {
if data == b"\x7f" || data == b"\x08" {
return true;
}
return try_kitty(codepoints::BACKSPACE, 0) || try_mok(codepoints::BACKSPACE, 0);
}
try_kitty(codepoints::BACKSPACE, modifier) || try_mok(codepoints::BACKSPACE, modifier)
}
_ => {
if key.len() == 1 {
let Some(ch) = key.chars().next() else {
return false;
};
let cp = ch as i64;
let is_letter = ch.is_ascii_lowercase();
let is_digit = ch.is_ascii_digit();
let is_symbol = is_symbol_cp(cp);
if is_letter || is_digit || is_symbol {
if modifier == modifier_bits::CTRL | modifier_bits::ALT
&& !kitty_active()
{
if let Some(ctrl_ch) = raw_ctrl_char(ch) {
if data.len() == 2 && data[0] == ESC && data[1] == ctrl_ch as u8 {
return true;
}
}
}
if modifier == modifier_bits::ALT
&& !kitty_active()
&& (is_letter || is_digit)
{
if data.len() == 2 && data[0] == ESC && data[1] == ch as u8 {
return true;
}
}
if modifier == modifier_bits::CTRL {
if let Some(ctrl_ch) = raw_ctrl_char(ch) {
if data.len() == 1 && data[0] == ctrl_ch as u8 {
return true;
}
}
return try_kitty(cp, modifier_bits::CTRL)
|| try_mok(cp, modifier_bits::CTRL);
}
if modifier == modifier_bits::SHIFT | modifier_bits::CTRL {
return try_kitty(cp, modifier_bits::SHIFT | modifier_bits::CTRL)
|| try_mok(cp, modifier_bits::SHIFT | modifier_bits::CTRL);
}
if modifier == modifier_bits::SHIFT {
if is_letter && data.len() == 1 && data[0] == ch.to_ascii_uppercase() as u8 {
return true;
}
return try_kitty(cp, modifier_bits::SHIFT)
|| try_mok(cp, modifier_bits::SHIFT);
}
if modifier != 0 {
return try_kitty(cp, modifier) || try_mok(cp, modifier);
}
if data.len() == 1 && data[0] == ch as u8 {
return true;
}
if let Ok(s) = std::str::from_utf8(data) {
if s == key {
return true;
}
}
return try_kitty(cp, 0);
}
}
if key.starts_with('f') && key.len() <= 3 {
if let Ok(num) = key[1..].parse::<u8>() {
if num >= 1 && num <= 12 && modifier == 0 {
return matches_legacy_fn(data, num);
}
}
}
if let Some(cp) = key_name_to_codepoint(&key) {
if modifier == 0 {
return matches_legacy_cp(data, &key) || try_kitty(cp, 0);
}
if matches_legacy_modifier_cp(data, &key, modifier) {
return true;
}
return try_kitty(cp, modifier);
}
false
}
}
}
fn raw_ctrl_char(ch: char) -> Option<char> {
let lower = ch.to_ascii_lowercase();
let code = lower as u32;
if (code >= 97 && code <= 122)
|| lower == '['
|| lower == '\\'
|| lower == ']'
|| lower == '_'
{
Some(char::from((code & 0x1f) as u8))
} else if lower == '-' {
Some(char::from(31u8)) } else {
None
}
}
fn matches_legacy_fn(data: &[u8], num: u8) -> bool {
let candidates: &[&[u8]] = match num {
1 => &[b"\x1bOP", b"\x1b[11~", b"\x1b[[A"],
2 => &[b"\x1bOQ", b"\x1b[12~", b"\x1b[[B"],
3 => &[b"\x1bOR", b"\x1b[13~", b"\x1b[[C"],
4 => &[b"\x1bOS", b"\x1b[14~", b"\x1b[[D"],
5 => &[b"\x1b[15~", b"\x1b[[E"],
6 => &[b"\x1b[17~"],
7 => &[b"\x1b[18~"],
8 => &[b"\x1b[19~"],
9 => &[b"\x1b[20~"],
10 => &[b"\x1b[21~"],
11 => &[b"\x1b[23~"],
12 => &[b"\x1b[24~"],
_ => &[],
};
candidates.iter().any(|c| data == *c)
}
fn matches_legacy_cp(data: &[u8], key: &str) -> bool {
let seqs: &[&[u8]] = match key {
"up" => &[b"\x1b[A", b"\x1bOA"],
"down" => &[b"\x1b[B", b"\x1bOB"],
"right" => &[b"\x1b[C", b"\x1bOC"],
"left" => &[b"\x1b[D", b"\x1bOD"],
"home" => &[b"\x1b[H", b"\x1bOH", b"\x1b[1~", b"\x1b[7~"],
"end" => &[b"\x1b[F", b"\x1bOF", b"\x1b[4~", b"\x1b[8~"],
"insert" => &[b"\x1b[2~"],
"delete" => &[b"\x1b[3~"],
"pageup" => &[b"\x1b[5~", b"\x1b[[5~"],
"pagedown" => &[b"\x1b[6~", b"\x1b[[6~"],
_ => &[],
};
seqs.iter().any(|s| data == *s)
}
fn matches_legacy_modifier_cp(data: &[u8], key: &str, modifier: u8) -> bool {
if modifier == modifier_bits::SHIFT {
let seqs: &[&[u8]] = match key {
"up" => &[b"\x1b[a"],
"down" => &[b"\x1b[b"],
"right" => &[b"\x1b[c"],
"left" => &[b"\x1b[d"],
"insert" => &[b"\x1b[2$"],
"delete" => &[b"\x1b[3$"],
"pageup" => &[b"\x1b[5$"],
"pagedown" => &[b"\x1b[6$"],
"home" => &[b"\x1b[7$"],
"end" => &[b"\x1b[8$"],
_ => &[],
};
return seqs.iter().any(|s| data == *s);
}
if modifier == modifier_bits::CTRL {
let seqs: &[&[u8]] = match key {
"up" => &[b"\x1bOa"],
"down" => &[b"\x1bOb"],
"right" => &[b"\x1bOc"],
"left" => &[b"\x1bOd"],
"insert" => &[b"\x1b[2^"],
"delete" => &[b"\x1b[3^"],
"pageup" => &[b"\x1b[5^"],
"pagedown" => &[b"\x1b[6^"],
"home" => &[b"\x1b[7^"],
"end" => &[b"\x1b[8^"],
_ => &[],
};
return seqs.iter().any(|s| data == *s);
}
false
}
pub fn parse_key(data: &[u8]) -> Option<String> {
if let Some(kitty) = parse_kitty_sequence(data) {
return format_kitty_key(&kitty);
}
if let Some(mok) = parse_modify_other_keys(data) {
return format_codepoint_key(mok.codepoint, mok.modifier);
}
let s = std::str::from_utf8(data).ok()?;
if kitty_active() {
if data == b"\x1b\r" || data == b"\n" {
return Some("shift+enter".to_string());
}
}
if let Some(id) = legacy_sequence_id(s) {
return Some(id.to_string());
}
if data == b"\x1b" {
return Some("escape".to_string());
}
if data == b"\x1c" {
return Some("ctrl+\\".to_string());
}
if data == b"\x1d" {
return Some("ctrl+]".to_string());
}
if data == b"\x1f" {
return Some("ctrl+-".to_string());
}
if data == b"\t" {
return Some("tab".to_string());
}
if data == b"\r" || (!kitty_active() && data == b"\n") || data == b"\x1bOM" {
return Some("enter".to_string());
}
if data == b"\x00" {
return Some("ctrl+space".to_string());
}
if data == b" " {
return Some("space".to_string());
}
if data == b"\x7f" || data == b"\x08" {
return Some("backspace".to_string());
}
if data == b"\x1b[Z" {
return Some("shift+tab".to_string());
}
if !kitty_active() && data == b"\x1b\r" {
return Some("alt+enter".to_string());
}
if !kitty_active() && data == b"\x1b " {
return Some("alt+space".to_string());
}
if data == b"\x1b\x7f" || data == b"\x1b\x08" {
return Some("alt+backspace".to_string());
}
if !kitty_active() && data == b"\x1bB" {
return Some("alt+left".to_string());
}
if !kitty_active() && data == b"\x1bF" {
return Some("alt+right".to_string());
}
if data.len() == 2 && data[0] == ESC {
let code = data[1];
if code >= 1 && code <= 26 {
return Some(format!("ctrl+alt+{}", char::from(code + 96)));
}
if (code >= b'a' && code <= b'z') || (code >= b'0' && code <= b'9') {
return Some(format!("alt+{}", code as char));
}
}
if data == b"\x1b\x1b" {
return Some("ctrl+alt+[".to_string());
}
if data == b"\x1b\x1c" {
return Some("ctrl+alt+\\".to_string());
}
if data == b"\x1b\x1d" {
return Some("ctrl+alt+]".to_string());
}
if data == b"\x1b\x1f" {
return Some("ctrl+alt+-".to_string());
}
if data == b"\x1b[A" {
return Some("up".to_string());
}
if data == b"\x1b[B" {
return Some("down".to_string());
}
if data == b"\x1b[C" {
return Some("right".to_string());
}
if data == b"\x1b[D" {
return Some("left".to_string());
}
if data == b"\x1b[H" || data == b"\x1bOH" {
return Some("home".to_string());
}
if data == b"\x1b[F" || data == b"\x1bOF" {
return Some("end".to_string());
}
if data == b"\x1b[3~" {
return Some("delete".to_string());
}
if data == b"\x1b[5~" {
return Some("pageup".to_string());
}
if data == b"\x1b[6~" {
return Some("pagedown".to_string());
}
if data.len() == 1 {
let code = data[0];
if code >= 1 && code <= 26 {
return Some(format!("ctrl+{}", char::from(code + 96)));
}
if code >= 32 && code <= 126 {
return Some((code as char).to_string());
}
}
if data.len() > 1 {
if let Ok(s) = std::str::from_utf8(data) {
if s.chars().all(|c| c >= ' ' as char) {
return Some(s.to_string());
}
}
}
None
}
fn legacy_sequence_id(s: &str) -> Option<&'static str> {
Some(match s {
"\x1bOA" => "up",
"\x1bOB" => "down",
"\x1bOC" => "right",
"\x1bOD" => "left",
"\x1bOH" => "home",
"\x1bOF" => "end",
"\x1b[E" => "clear",
"\x1bOE" => "clear",
"\x1bOe" => "ctrl+clear",
"\x1b[e" => "shift+clear",
"\x1b[2~" => "insert",
"\x1b[2$" => "shift+insert",
"\x1b[2^" => "ctrl+insert",
"\x1b[3$" => "shift+delete",
"\x1b[3^" => "ctrl+delete",
"\x1b[[5~" => "pageup",
"\x1b[[6~" => "pagedown",
"\x1b[a" => "shift+up",
"\x1b[b" => "shift+down",
"\x1b[c" => "shift+right",
"\x1b[d" => "shift+left",
"\x1bOa" => "ctrl+up",
"\x1bOb" => "ctrl+down",
"\x1bOc" => "ctrl+right",
"\x1bOd" => "ctrl+left",
"\x1b[5$" => "shift+pageup",
"\x1b[6$" => "shift+pagedown",
"\x1b[7$" => "shift+home",
"\x1b[8$" => "shift+end",
"\x1b[5^" => "ctrl+pageup",
"\x1b[6^" => "ctrl+pagedown",
"\x1b[7^" => "ctrl+home",
"\x1b[8^" => "ctrl+end",
"\x1bOP" => "f1",
"\x1bOQ" => "f2",
"\x1bOR" => "f3",
"\x1bOS" => "f4",
"\x1b[11~" => "f1",
"\x1b[12~" => "f2",
"\x1b[13~" => "f3",
"\x1b[14~" => "f4",
"\x1b[[A" => "f1",
"\x1b[[B" => "f2",
"\x1b[[C" => "f3",
"\x1b[[D" => "f4",
"\x1b[[E" => "f5",
"\x1b[15~" => "f5",
"\x1b[17~" => "f6",
"\x1b[18~" => "f7",
"\x1b[19~" => "f8",
"\x1b[20~" => "f9",
"\x1b[21~" => "f10",
"\x1b[23~" => "f11",
"\x1b[24~" => "f12",
"\x1bb" => "alt+left",
"\x1bf" => "alt+right",
"\x1bp" => "alt+up",
"\x1bn" => "alt+down",
_ => return None,
})
}
fn format_kitty_key(kitty: &ParsedKitty) -> Option<String> {
format_codepoint_key_impl(
normalize_kitty_functional_codepoint(kitty.codepoint),
kitty.modifier,
kitty.base_layout_key,
)
}
fn format_codepoint_key(cp: i64, modifier: u8) -> Option<String> {
format_codepoint_key_impl(cp, modifier, None)
}
fn format_codepoint_key_impl(cp: i64, modifier: u8, base_layout_key: Option<i64>) -> Option<String> {
let normalized = normalize_kitty_functional_codepoint(cp);
let identity = normalize_shifted_letter(normalized, modifier);
let is_latin = identity >= 97 && identity <= 122;
let is_digit = identity >= 48 && identity <= 57;
let is_symbol = is_symbol_cp(identity);
let effective = if is_latin || is_digit || is_symbol {
identity
} else {
base_layout_key.unwrap_or(identity)
};
let key_name = codepoint_to_key_name(effective)?;
let eff_mod = effective_modifier(modifier);
let supported = modifier_bits::SHIFT
| modifier_bits::CTRL
| modifier_bits::ALT
| modifier_bits::SUPER;
if (eff_mod & !supported) != 0 {
return None;
}
let mut parts: Vec<&str> = Vec::new();
if (eff_mod & modifier_bits::SHIFT) != 0 {
parts.push("shift");
}
if (eff_mod & modifier_bits::CTRL) != 0 {
parts.push("ctrl");
}
if (eff_mod & modifier_bits::ALT) != 0 {
parts.push("alt");
}
if (eff_mod & modifier_bits::SUPER) != 0 {
parts.push("super");
}
if parts.is_empty() {
Some(key_name)
} else {
let mut result = parts.join("+");
result.push('+');
result.push_str(&key_name);
Some(result)
}
}
fn codepoint_to_key_name(cp: i64) -> Option<String> {
match cp {
codepoints::ESCAPE => Some("escape".to_string()),
codepoints::TAB => Some("tab".to_string()),
codepoints::ENTER | codepoints::KP_ENTER => Some("enter".to_string()),
codepoints::SPACE => Some("space".to_string()),
codepoints::BACKSPACE => Some("backspace".to_string()),
codepoints::DELETE => Some("delete".to_string()),
codepoints::INSERT => Some("insert".to_string()),
codepoints::HOME => Some("home".to_string()),
codepoints::END => Some("end".to_string()),
codepoints::PAGE_UP => Some("pageup".to_string()),
codepoints::PAGE_DOWN => Some("pagedown".to_string()),
codepoints::UP => Some("up".to_string()),
codepoints::DOWN => Some("down".to_string()),
codepoints::LEFT => Some("left".to_string()),
codepoints::RIGHT => Some("right".to_string()),
cp if cp >= 32 && cp <= 126 => Some((cp as u8 as char).to_string()),
cp => char::from_u32(cp as u32).map(|c| c.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_plain_char() {
let event = parse_event(b"a").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers,
..
}) => {
assert!(!modifiers.ctrl);
assert!(!modifiers.alt);
assert!(!modifiers.shift);
}
_ => panic!("expected Char('a'), got {:?}", event),
}
}
#[test]
fn test_parse_escape() {
let event = parse_event(b"\x1b").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Escape,
..
}) => {}
_ => panic!("expected Escape"),
}
}
#[test]
fn test_parse_enter() {
let event = parse_event(b"\r").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Enter,
..
}) => {}
_ => panic!("expected Enter"),
}
}
#[test]
fn test_parse_tab() {
let event = parse_event(b"\t").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Tab,
..
}) => {}
_ => panic!("expected Tab"),
}
}
#[test]
fn test_parse_backspace() {
let event = parse_event(b"\x7f").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Backspace,
..
}) => {}
_ => panic!("expected Backspace"),
}
}
#[test]
fn test_parse_ctrl_c() {
let event = parse_event(b"\x03").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers,
}) => {
assert!(modifiers.ctrl);
}
_ => panic!("expected Ctrl+C"),
}
}
#[test]
fn test_parse_ctrl_space() {
let event = parse_event(b"\x00").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char(' '),
modifiers,
}) => {
assert!(modifiers.ctrl);
}
_ => panic!("expected Ctrl+Space"),
}
}
#[test]
fn test_parse_arrow_up_csi() {
let event = parse_event(b"\x1b[A").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Up,
modifiers,
}) => {
assert!(!modifiers.shift && !modifiers.ctrl && !modifiers.alt);
}
_ => panic!("expected Up"),
}
}
#[test]
fn test_parse_arrow_up_ss3() {
let event = parse_event(b"\x1bOA").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {}
_ => panic!("expected Up"),
}
}
#[test]
fn test_parse_f1_ss3() {
let event = parse_event(b"\x1bOP").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::F(1), ..
}) => {}
_ => panic!("expected F(1)"),
}
}
#[test]
fn test_parse_f5_csi() {
let event = parse_event(b"\x1b[15~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::F(5), ..
}) => {}
_ => panic!("expected F(5)"),
}
}
#[test]
fn test_parse_home() {
let event = parse_event(b"\x1b[H").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Home, ..
}) => {}
_ => panic!("expected Home"),
}
}
#[test]
fn test_parse_end() {
let event = parse_event(b"\x1b[F").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::End, ..
}) => {}
_ => panic!("expected End"),
}
}
#[test]
fn test_parse_insert() {
let event = parse_event(b"\x1b[2~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Insert, ..
}) => {}
_ => panic!("expected Insert"),
}
}
#[test]
fn test_parse_delete() {
let event = parse_event(b"\x1b[3~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Delete, ..
}) => {}
_ => panic!("expected Delete"),
}
}
#[test]
fn test_parse_pageup() {
let event = parse_event(b"\x1b[5~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::PageUp, ..
}) => {}
_ => panic!("expected PageUp"),
}
}
#[test]
fn test_parse_pagedown() {
let event = parse_event(b"\x1b[6~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::PageDown, ..
}) => {}
_ => panic!("expected PageDown"),
}
}
#[test]
fn test_parse_shift_tab() {
let event = parse_event(b"\x1b[Z").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Tab,
modifiers,
}) => {
assert!(modifiers.shift);
}
_ => panic!("expected Shift+Tab"),
}
}
#[test]
fn test_parse_alt_letter() {
let event = parse_event(b"\x1ba").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('a'),
modifiers,
}) => {
assert!(modifiers.alt);
assert!(!modifiers.ctrl);
}
_ => panic!("expected Alt+a"),
}
}
#[test]
fn test_parse_alt_backspace() {
let event = parse_event(b"\x1b\x7f").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Backspace,
modifiers,
}) => {
assert!(modifiers.alt);
}
_ => panic!("expected Alt+Backspace"),
}
}
#[test]
fn test_parse_focus_gained() {
let event = parse_event(b"\x1b[I").unwrap();
assert_eq!(event, Event::FocusGained);
}
#[test]
fn test_parse_focus_lost() {
let event = parse_event(b"\x1b[O").unwrap();
assert_eq!(event, Event::FocusLost);
}
#[test]
fn test_parse_sgr_mouse_press() {
let event = parse_event(b"\x1b[<0;10;5M").unwrap();
match event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Press,
button: MouseButton::Left,
row: 4,
col: 9,
}) => {}
_ => panic!("expected left mouse press at (9,4), got {:?}", event),
}
}
#[test]
fn test_parse_sgr_mouse_release() {
let event = parse_event(b"\x1b[<0;10;5m").unwrap();
match event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Release,
button: MouseButton::Left,
row: 4,
col: 9,
}) => {}
_ => panic!("expected left mouse release"),
}
}
#[test]
fn test_parse_sgr_mouse_scroll() {
let event = parse_event(b"\x1b[<64;10;5M").unwrap();
match event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => {}
_ => panic!("expected scroll up"),
}
}
#[test]
fn test_parse_x10_mouse() {
let event = parse_event(b"\x1b[M \x21\x21").unwrap();
match event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Press,
button: MouseButton::Left,
..
}) => {}
_ => panic!("expected X10 mouse press"),
}
}
#[test]
fn test_parse_kitty_arrow_up() {
let event = parse_event(b"\x1b[1;1A").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Up, ..
}) => {}
_ => panic!("expected Up from Kitty"),
}
}
#[test]
fn test_parse_kitty_ctrl_c() {
let event = parse_event(b"\x1b[99;5u").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers,
}) => {
assert!(modifiers.ctrl);
}
_ => panic!("expected Ctrl+c from Kitty"),
}
}
#[test]
fn test_parse_modify_other_keys_ctrl_c() {
let event = parse_event(b"\x1b[27;5;99~").unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers,
}) => {
assert!(modifiers.ctrl);
}
_ => panic!("expected Ctrl+c from modifyOtherKeys"),
}
}
#[test]
fn test_decode_printable_kitty() {
let ch = decode_kitty_printable(b"\x1b[97u").unwrap();
assert_eq!(ch, 'a');
}
#[test]
fn test_decode_printable_rejects_ctrl() {
assert!(decode_kitty_printable(b"\x1b[97;5u").is_none());
}
#[test]
fn test_matches_key_escape() {
assert!(matches_key(b"\x1b", "escape"));
assert!(matches_key(b"\x1b", "esc"));
assert!(!matches_key(b"\x1b", "ctrl+escape"));
}
#[test]
fn test_matches_key_ctrl_c() {
assert!(matches_key(b"\x03", "ctrl+c"));
assert!(!matches_key(b"\x03", "ctrl+d"));
}
#[test]
fn test_matches_key_enter() {
assert!(matches_key(b"\r", "enter"));
assert!(matches_key(b"\r", "return"));
}
#[test]
fn test_matches_key_shift_tab() {
assert!(matches_key(b"\x1b[Z", "shift+tab"));
}
#[test]
fn test_matches_key_alt_a() {
assert!(matches_key(b"\x1ba", "alt+a"));
}
#[test]
fn test_matches_key_function() {
assert!(matches_key(b"\x1bOP", "f1"));
assert!(matches_key(b"\x1b[15~", "f5"));
}
#[test]
fn test_parse_key_string() {
assert_eq!(parse_key(b"\x1b"), Some("escape".to_string()));
assert_eq!(parse_key(b"\r"), Some("enter".to_string()));
assert_eq!(parse_key(b"\t"), Some("tab".to_string()));
assert_eq!(parse_key(b"a"), Some("a".to_string()));
assert_eq!(parse_key(b"\x03"), Some("ctrl+c".to_string()));
assert_eq!(parse_key(b"\x1b[Z"), Some("shift+tab".to_string()));
assert_eq!(parse_key(b"\x1b[A"), Some("up".to_string()));
assert_eq!(parse_key(b"\x1b[B"), Some("down".to_string()));
assert_eq!(parse_key(b"\x1b[C"), Some("right".to_string()));
assert_eq!(parse_key(b"\x1b[D"), Some("left".to_string()));
}
#[test]
fn test_is_key_release() {
assert!(is_key_release(b"\x1b[97;1:3u"));
assert!(!is_key_release(b"\x1b[97;1:1u"));
assert!(!is_key_release(b"a"));
}
#[test]
fn test_is_key_repeat() {
assert!(is_key_repeat(b"\x1b[97;1:2u"));
assert!(!is_key_repeat(b"\x1b[97;1:1u"));
}
#[test]
fn test_utf8_multibyte() {
let event = parse_event("é".as_bytes()).unwrap();
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('é'),
..
}) => {}
_ => panic!("expected Char('é')"),
}
}
}