use crate::kitty::{FunctionalKey, KeyEvent, KeyEventType, Modifiers};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Key {
Char(char),
F(u8),
Up,
Down,
Left,
Right,
Enter,
Backspace,
Delete,
Insert,
Home,
End,
PageUp,
PageDown,
Tab,
BackTab,
Escape,
Ctrl(char),
Alt(char),
Enhanced(KeyEvent),
Paste(String),
Unknown,
}
impl Key {
pub(crate) fn from_escape_sequence(seq: &[u8]) -> Option<Self> {
if seq.is_empty() {
return None;
}
if seq.len() == 1 && seq[0] == 0x1b {
return Some(Key::Escape);
}
if seq.len() >= 2 && seq[0] == 0x1b && seq[1] == 0x1b {
let inner = Self::from_escape_sequence(&seq[1..])?;
return Some(apply_alt(inner));
}
if seq.len() == 2 && seq[0] == 0x1b {
let ch = seq[1];
if (0x20..=0x7e).contains(&ch) {
return Some(Key::Alt(ch as char));
}
}
if seq.len() >= 3 && seq[0] == 0x1b && seq[1] == b'O' {
return match seq[2] {
b'P' => Some(Key::F(1)),
b'Q' => Some(Key::F(2)),
b'R' => Some(Key::F(3)),
b'S' => Some(Key::F(4)),
_ => None,
};
}
if seq.len() < 3 || seq[0] != 0x1b || seq[1] != b'[' {
return None;
}
let event = KeyEvent::from_sequence(seq)?;
Some(key_from_event(event))
}
}
fn key_from_event(event: KeyEvent) -> Key {
let only_press = matches!(event.event_type, KeyEventType::Press);
let no_alts = event.shifted_key.is_none() && event.base_key.is_none();
let no_text = event.text.is_none();
let baked_in = Modifiers::CAPS_LOCK | Modifiers::NUM_LOCK;
let chars_lossless = (event.modifiers & !(baked_in | Modifiers::SHIFT)).is_empty();
let fns_lossless = (event.modifiers & !baked_in).is_empty();
if let Some(fk) = event.functional() {
if only_press
&& no_alts
&& no_text
&& matches!(fk, FunctionalKey::Tab)
&& event.modifiers == Modifiers::SHIFT
{
return Key::BackTab;
}
if only_press && no_alts && no_text && fns_lossless {
if let Some(basic) = functional_to_basic_key(fk) {
return basic;
}
}
return Key::Enhanced(event);
}
if only_press && no_alts && no_text {
if let Some(ch) = char::from_u32(event.code) {
let mods = event.modifiers;
let pure_ctrl = mods == Modifiers::CTRL;
let pure_alt = mods == Modifiers::ALT;
if pure_ctrl && ch.is_ascii() {
return Key::Ctrl(ch.to_ascii_lowercase());
}
if pure_alt && ch.is_ascii() {
return Key::Alt(ch);
}
if chars_lossless {
return Key::Char(ch);
}
}
}
Key::Enhanced(event)
}
fn apply_alt(key: Key) -> Key {
match key {
Key::Char(ch) if ch.is_ascii() => Key::Alt(ch),
Key::Enhanced(mut ev) => {
ev.modifiers |= Modifiers::ALT;
Key::Enhanced(ev)
}
k => {
if let Some(code) = basic_key_pua_code(&k) {
Key::Enhanced(KeyEvent {
code,
modifiers: Modifiers::ALT,
..Default::default()
})
} else {
if matches!(k, Key::Escape) {
Key::Alt('\x1b')
} else {
k
}
}
}
}
}
fn basic_key_pua_code(k: &Key) -> Option<u32> {
Some(match k {
Key::Escape => 57344,
Key::Enter => 57345,
Key::Tab => 57346,
Key::Backspace => 57347,
Key::Insert => 57348,
Key::Delete => 57349,
Key::Left => 57350,
Key::Right => 57351,
Key::Up => 57352,
Key::Down => 57353,
Key::PageUp => 57354,
Key::PageDown => 57355,
Key::Home => 57356,
Key::End => 57357,
Key::F(n) if (1..=35).contains(n) => 57364 + (*n as u32) - 1,
_ => return None,
})
}
fn functional_to_basic_key(fk: FunctionalKey) -> Option<Key> {
Some(match fk {
FunctionalKey::Escape => Key::Escape,
FunctionalKey::Enter | FunctionalKey::KpEnter => Key::Enter,
FunctionalKey::Tab => Key::Tab,
FunctionalKey::Backspace => Key::Backspace,
FunctionalKey::Insert | FunctionalKey::KpInsert => Key::Insert,
FunctionalKey::Delete | FunctionalKey::KpDelete => Key::Delete,
FunctionalKey::Left | FunctionalKey::KpLeft => Key::Left,
FunctionalKey::Right | FunctionalKey::KpRight => Key::Right,
FunctionalKey::Up | FunctionalKey::KpUp => Key::Up,
FunctionalKey::Down | FunctionalKey::KpDown => Key::Down,
FunctionalKey::PageUp | FunctionalKey::KpPageUp => Key::PageUp,
FunctionalKey::PageDown | FunctionalKey::KpPageDown => Key::PageDown,
FunctionalKey::Home | FunctionalKey::KpHome => Key::Home,
FunctionalKey::End | FunctionalKey::KpEnd => Key::End,
FunctionalKey::F(n) => Key::F(n),
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kitty::KeyEventType;
#[test]
fn bare_escape() {
assert_eq!(Key::from_escape_sequence(&[0x1b]), Some(Key::Escape));
}
#[test]
fn legacy_arrows_no_mods() {
assert_eq!(Key::from_escape_sequence(b"\x1b[A"), Some(Key::Up));
assert_eq!(Key::from_escape_sequence(b"\x1b[B"), Some(Key::Down));
assert_eq!(Key::from_escape_sequence(b"\x1b[C"), Some(Key::Right));
assert_eq!(Key::from_escape_sequence(b"\x1b[D"), Some(Key::Left));
}
#[test]
fn legacy_home_end() {
assert_eq!(Key::from_escape_sequence(b"\x1b[H"), Some(Key::Home));
assert_eq!(Key::from_escape_sequence(b"\x1b[F"), Some(Key::End));
assert_eq!(Key::from_escape_sequence(b"\x1b[1~"), Some(Key::Home));
assert_eq!(Key::from_escape_sequence(b"\x1b[4~"), Some(Key::End));
}
#[test]
fn backtab_via_z_terminator() {
assert_eq!(Key::from_escape_sequence(b"\x1b[Z"), Some(Key::BackTab));
}
#[test]
fn backtab_via_kitty_tab_with_shift() {
assert_eq!(Key::from_escape_sequence(b"\x1b[9;2u"), Some(Key::BackTab));
}
#[test]
fn legacy_tilde_special_keys() {
assert_eq!(Key::from_escape_sequence(b"\x1b[2~"), Some(Key::Insert));
assert_eq!(Key::from_escape_sequence(b"\x1b[3~"), Some(Key::Delete));
assert_eq!(Key::from_escape_sequence(b"\x1b[5~"), Some(Key::PageUp));
assert_eq!(Key::from_escape_sequence(b"\x1b[6~"), Some(Key::PageDown));
}
#[test]
fn legacy_function_keys_via_ss3() {
assert_eq!(Key::from_escape_sequence(b"\x1bOP"), Some(Key::F(1)));
assert_eq!(Key::from_escape_sequence(b"\x1bOQ"), Some(Key::F(2)));
assert_eq!(Key::from_escape_sequence(b"\x1bOR"), Some(Key::F(3)));
assert_eq!(Key::from_escape_sequence(b"\x1bOS"), Some(Key::F(4)));
}
#[test]
fn legacy_function_keys_via_tilde() {
assert_eq!(Key::from_escape_sequence(b"\x1b[15~"), Some(Key::F(5)));
assert_eq!(Key::from_escape_sequence(b"\x1b[17~"), Some(Key::F(6)));
assert_eq!(Key::from_escape_sequence(b"\x1b[24~"), Some(Key::F(12)));
}
#[test]
fn kitty_arrow_with_ctrl_becomes_enhanced() {
let k = Key::from_escape_sequence(b"\x1b[1;5A").unwrap();
match k {
Key::Enhanced(e) => {
assert!(e.is_ctrl());
assert!(!e.is_shift());
assert_eq!(e.functional(), Some(FunctionalKey::Up));
}
_ => panic!("expected Key::Enhanced for Ctrl+Up, got {:?}", k),
}
}
#[test]
fn kitty_arrow_with_shift_becomes_enhanced() {
let k = Key::from_escape_sequence(b"\x1b[1;2A").unwrap();
match k {
Key::Enhanced(e) => {
assert!(e.is_shift());
assert_eq!(e.functional(), Some(FunctionalKey::Up));
}
_ => panic!("expected Enhanced for Shift+Up, got {:?}", k),
}
}
#[test]
fn kitty_release_event_becomes_enhanced() {
let k = Key::from_escape_sequence(b"\x1b[97;1:3u").unwrap();
match k {
Key::Enhanced(e) => assert_eq!(e.event_type, KeyEventType::Release),
_ => panic!("expected Key::Enhanced for release, got {:?}", k),
}
}
#[test]
fn kitty_plain_press_collapses_to_char() {
let k = Key::from_escape_sequence(b"\x1b[97u").unwrap();
assert_eq!(k, Key::Char('a'));
}
#[test]
fn kitty_ctrl_char_collapses_to_ctrl_variant() {
let k = Key::from_escape_sequence(b"\x1b[97;5u").unwrap();
assert_eq!(k, Key::Ctrl('a'));
}
#[test]
fn kitty_alt_char_collapses_to_alt_variant() {
let k = Key::from_escape_sequence(b"\x1b[120;3u").unwrap();
assert_eq!(k, Key::Alt('x'));
}
#[test]
fn kitty_ctrl_alt_stays_enhanced() {
let k = Key::from_escape_sequence(b"\x1b[120;7u").unwrap();
match k {
Key::Enhanced(e) => {
assert!(e.is_ctrl());
assert!(e.is_alt());
}
_ => panic!("expected Enhanced for Ctrl+Alt, got {:?}", k),
}
}
#[test]
fn kitty_pua_enter_collapses_to_enter() {
let k = Key::from_escape_sequence(b"\x1b[57345u").unwrap();
assert_eq!(k, Key::Enter);
}
#[test]
fn kitty_f5_via_pua() {
let k = Key::from_escape_sequence(b"\x1b[57368u").unwrap();
assert_eq!(k, Key::F(5));
}
#[test]
fn kitty_alternate_keys_stay_enhanced() {
let k = Key::from_escape_sequence(b"\x1b[97:65;2u").unwrap();
assert!(matches!(k, Key::Enhanced(_)));
}
#[test]
fn meta_prefix_left_is_alt_left() {
let k = Key::from_escape_sequence(b"\x1b\x1b[D").unwrap();
match k {
Key::Enhanced(e) => {
assert_eq!(e.functional(), Some(FunctionalKey::Left));
assert!(e.is_alt());
}
_ => panic!("expected Enhanced Alt+Left, got {:?}", k),
}
}
#[test]
fn meta_prefix_with_modifier_combines_alt_and_ctrl() {
let k = Key::from_escape_sequence(b"\x1b\x1b[1;5D").unwrap();
match k {
Key::Enhanced(e) => {
assert_eq!(e.functional(), Some(FunctionalKey::Left));
assert!(e.is_alt());
assert!(e.is_ctrl());
}
_ => panic!("expected Enhanced Alt+Ctrl+Left, got {:?}", k),
}
}
#[test]
fn emacs_meta_b_is_alt_b() {
assert_eq!(Key::from_escape_sequence(b"\x1bb"), Some(Key::Alt('b')));
assert_eq!(Key::from_escape_sequence(b"\x1bf"), Some(Key::Alt('f')));
}
#[test]
fn rejects_garbage() {
assert!(Key::from_escape_sequence(b"").is_none());
assert!(Key::from_escape_sequence(b"\x1b[65x").is_none());
}
}