use crate::input::{
KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MediaKeyCode, ModifierKeyCode,
};
const FLAG_DISAMBIGUATE: u8 = 1;
const FLAG_REPORT_EVENTS: u8 = 2;
const FLAG_REPORT_ALTERNATE: u8 = 4;
const FLAG_REPORT_ALL: u8 = 8;
pub(crate) fn key_to_bytes_kitty(key: &KeyEvent, flags: u8) -> Option<Vec<u8>> {
let report_all = flags & FLAG_REPORT_ALL != 0;
let disambiguate = flags & FLAG_DISAMBIGUATE != 0;
let report_events = flags & FLAG_REPORT_EVENTS != 0;
let report_alternate = flags & FLAG_REPORT_ALTERNATE != 0;
let event_type = match key.kind {
KeyEventKind::Press => 1,
KeyEventKind::Repeat => 2,
KeyEventKind::Release => 3,
};
let modifiers = kitty_modifier(key);
let encoding = kitty_key_encoding(key)?;
let shifted_key = if report_alternate {
shifted_key_codepoint(key)
} else {
0
};
match encoding {
KittyEncoding::Char(codepoint) => {
let is_ambiguous = is_ambiguous_key(key.code);
let has_modifiers = modifiers > 1;
let is_non_press = event_type != 1;
if !report_all
&& !is_ambiguous
&& !has_modifiers
&& !is_non_press
&& let KeyCode::Char(c) = key.code
{
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
return Some(s.as_bytes().to_vec());
}
if report_all || (disambiguate && is_ambiguous) || has_modifiers || is_non_press {
return Some(encode_csi_u(
codepoint,
shifted_key,
modifiers,
event_type,
report_events,
));
}
if let KeyCode::Char(c) = key.code {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
return Some(s.as_bytes().to_vec());
}
Some(encode_csi_u(
codepoint,
shifted_key,
modifiers,
event_type,
report_events,
))
}
KittyEncoding::Functional { number, final_byte } => Some(encode_functional(
number,
final_byte,
modifiers,
event_type,
report_events,
)),
KittyEncoding::Ss3(final_byte) => {
if modifiers > 1 || (report_events && event_type != 1) {
Some(encode_functional(
1,
final_byte,
modifiers,
event_type,
report_events,
))
} else {
Some(vec![0x1b, b'O', final_byte])
}
}
}
}
fn kitty_modifier(key: &KeyEvent) -> u16 {
let effective = key.modifiers - key.consumed_modifiers;
let mut m: u16 = 1;
if effective.contains(KeyModifiers::SHIFT) {
m += 1;
}
if effective.contains(KeyModifiers::ALT) {
m += 2;
}
if effective.contains(KeyModifiers::CONTROL) {
m += 4;
}
if effective.contains(KeyModifiers::SUPER) {
m += 8;
}
if effective.contains(KeyModifiers::HYPER) {
m += 16;
}
if effective.contains(KeyModifiers::META) {
m += 32;
}
if key.state.contains(KeyEventState::CAPS_LOCK) {
m += 64;
}
if key.state.contains(KeyEventState::NUM_LOCK) {
m += 128;
}
m
}
fn shifted_key_codepoint(key: &KeyEvent) -> u32 {
let effective = key.modifiers - key.consumed_modifiers;
if !effective.contains(KeyModifiers::SHIFT) {
return 0;
}
if let (Some(unshifted), KeyCode::Char(c)) = (key.unshifted_codepoint, key.code)
&& c != unshifted
{
return u32::from(c);
}
match key.code {
KeyCode::Char(c) if c.is_ascii_alphabetic() => u32::from(c.to_ascii_uppercase()),
_ => 0,
}
}
fn is_ambiguous_key(code: KeyCode) -> bool {
matches!(
code,
KeyCode::Esc | KeyCode::Enter | KeyCode::Tab | KeyCode::BackTab | KeyCode::Backspace
)
}
enum KittyEncoding {
Char(u32),
Functional { number: u16, final_byte: u8 },
Ss3(u8),
}
fn kitty_key_encoding(key: &KeyEvent) -> Option<KittyEncoding> {
let is_keypad = key.state.contains(KeyEventState::KEYPAD);
Some(match key.code {
KeyCode::Modifier(m) => KittyEncoding::Char(modifier_codepoint(m)),
KeyCode::Esc => KittyEncoding::Char(27),
KeyCode::Enter => KittyEncoding::Char(13),
KeyCode::Tab => KittyEncoding::Char(9),
KeyCode::BackTab => KittyEncoding::Char(9),
KeyCode::Backspace => KittyEncoding::Char(127),
KeyCode::Delete => KittyEncoding::Functional {
number: 3,
final_byte: b'~',
},
KeyCode::Insert => KittyEncoding::Functional {
number: 2,
final_byte: b'~',
},
KeyCode::PageUp => KittyEncoding::Functional {
number: 5,
final_byte: b'~',
},
KeyCode::PageDown => KittyEncoding::Functional {
number: 6,
final_byte: b'~',
},
KeyCode::Up => KittyEncoding::Functional {
number: 1,
final_byte: b'A',
},
KeyCode::Down => KittyEncoding::Functional {
number: 1,
final_byte: b'B',
},
KeyCode::Right => KittyEncoding::Functional {
number: 1,
final_byte: b'C',
},
KeyCode::Left => KittyEncoding::Functional {
number: 1,
final_byte: b'D',
},
KeyCode::Home => KittyEncoding::Functional {
number: 1,
final_byte: b'H',
},
KeyCode::End => KittyEncoding::Functional {
number: 1,
final_byte: b'F',
},
KeyCode::F(1) => KittyEncoding::Ss3(b'P'),
KeyCode::F(2) => KittyEncoding::Ss3(b'Q'),
KeyCode::F(3) => KittyEncoding::Ss3(b'R'),
KeyCode::F(4) => KittyEncoding::Ss3(b'S'),
KeyCode::F(n @ 5..=12) => {
let number = match n {
5 => 15,
6 => 17,
7 => 18,
8 => 19,
9 => 20,
10 => 21,
11 => 23,
12 => 24,
_ => unreachable!(),
};
KittyEncoding::Functional {
number,
final_byte: b'~',
}
}
KeyCode::F(n @ 13..=35) => KittyEncoding::Char(57376 + u32::from(n) - 13),
KeyCode::Char(c) if is_keypad => {
let cp = numpad_codepoint(c);
KittyEncoding::Char(cp.unwrap_or(u32::from(c)))
}
KeyCode::Char(c) => {
let base = key
.unshifted_codepoint
.unwrap_or_else(|| c.to_ascii_lowercase());
KittyEncoding::Char(u32::from(base))
}
KeyCode::CapsLock => KittyEncoding::Char(57358),
KeyCode::ScrollLock => KittyEncoding::Char(57359),
KeyCode::NumLock => KittyEncoding::Char(57360),
KeyCode::PrintScreen => KittyEncoding::Char(57361),
KeyCode::Pause => KittyEncoding::Char(57362),
KeyCode::Menu => KittyEncoding::Char(57363),
KeyCode::KeypadBegin => KittyEncoding::Char(57427),
KeyCode::Media(m) => KittyEncoding::Char(media_codepoint(m)),
_ => return None,
})
}
fn modifier_codepoint(m: ModifierKeyCode) -> u32 {
match m {
ModifierKeyCode::LeftShift => 57441,
ModifierKeyCode::LeftControl => 57442,
ModifierKeyCode::LeftAlt => 57443,
ModifierKeyCode::LeftSuper => 57444,
ModifierKeyCode::LeftHyper => 57445,
ModifierKeyCode::LeftMeta => 57446,
ModifierKeyCode::RightShift => 57447,
ModifierKeyCode::RightControl => 57448,
ModifierKeyCode::RightAlt => 57449,
ModifierKeyCode::RightSuper => 57450,
ModifierKeyCode::RightHyper => 57451,
ModifierKeyCode::RightMeta => 57452,
ModifierKeyCode::IsoLevel3Shift => 57453,
ModifierKeyCode::IsoLevel5Shift => 57454,
}
}
fn numpad_codepoint(c: char) -> Option<u32> {
match c {
'0' => Some(57399),
'1' => Some(57400),
'2' => Some(57401),
'3' => Some(57402),
'4' => Some(57403),
'5' => Some(57404),
'6' => Some(57405),
'7' => Some(57406),
'8' => Some(57407),
'9' => Some(57408),
'.' => Some(57409),
'/' => Some(57410),
'*' => Some(57411),
'-' => Some(57412),
'+' => Some(57413),
'=' => Some(57415),
_ => None,
}
}
fn media_codepoint(m: MediaKeyCode) -> u32 {
match m {
MediaKeyCode::Play => 57428,
MediaKeyCode::Pause => 57429,
MediaKeyCode::PlayPause => 57430,
MediaKeyCode::Reverse => 57431,
MediaKeyCode::Stop => 57432,
MediaKeyCode::FastForward => 57433,
MediaKeyCode::Rewind => 57434,
MediaKeyCode::TrackNext => 57435,
MediaKeyCode::TrackPrevious => 57436,
MediaKeyCode::Record => 57437,
MediaKeyCode::LowerVolume => 57438,
MediaKeyCode::RaiseVolume => 57439,
MediaKeyCode::MuteVolume => 57440,
}
}
fn encode_csi_u(
codepoint: u32,
shifted_key: u32,
modifiers: u16,
event_type: u8,
report_events: bool,
) -> Vec<u8> {
let mut out = format!("\x1b[{codepoint}");
if shifted_key != 0 {
out.push(':');
out.push_str(&shifted_key.to_string());
}
let need_event = report_events && event_type != 1;
if modifiers > 1 || need_event {
out.push(';');
out.push_str(&modifiers.to_string());
if need_event {
out.push(':');
out.push_str(&event_type.to_string());
}
}
out.push('u');
out.into_bytes()
}
fn encode_functional(
number: u16,
final_byte: u8,
modifiers: u16,
event_type: u8,
report_events: bool,
) -> Vec<u8> {
let need_event = report_events && event_type != 1;
let need_modifier = modifiers > 1 || need_event;
let mut out = String::from("\x1b[");
if final_byte != b'~' && !need_modifier {
out.push(final_byte as char);
return out.into_bytes();
}
out.push_str(&number.to_string());
if need_modifier {
out.push(';');
out.push_str(&modifiers.to_string());
if need_event {
out.push(':');
out.push_str(&event_type.to_string());
}
}
out.push(final_byte as char);
out.into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
fn key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE)
}
fn ctrl_key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::CONTROL)
}
fn shift_key(code: KeyCode) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::SHIFT)
}
fn key_with_kind(code: KeyCode, kind: KeyEventKind) -> KeyEvent {
KeyEvent::new(code, KeyModifiers::NONE).with_kind(kind)
}
fn key_with_state(code: KeyCode, modifiers: KeyModifiers, state: KeyEventState) -> KeyEvent {
KeyEvent::new(code, modifiers).with_state(state)
}
#[test]
fn kitty_plain_letter_sends_literal() {
let result = key_to_bytes_kitty(&key(KeyCode::Char('a')), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"a");
}
#[test]
fn kitty_ctrl_c_uses_csi_u() {
let result = key_to_bytes_kitty(&ctrl_key(KeyCode::Char('c')), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[99;5u");
}
#[test]
fn kitty_ctrl_i_disambiguated_from_tab() {
let result = key_to_bytes_kitty(&ctrl_key(KeyCode::Char('i')), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[105;5u");
}
#[test]
fn kitty_escape_uses_csi_u() {
let result = key_to_bytes_kitty(&key(KeyCode::Esc), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[27u");
}
#[test]
fn kitty_enter_uses_csi_u() {
let result = key_to_bytes_kitty(&key(KeyCode::Enter), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[13u");
}
#[test]
fn kitty_tab_uses_csi_u() {
let result = key_to_bytes_kitty(&key(KeyCode::Tab), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[9u");
}
#[test]
fn kitty_backspace_uses_csi_u() {
let result = key_to_bytes_kitty(&key(KeyCode::Backspace), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[127u");
}
#[test]
fn kitty_report_all_letter() {
let result = key_to_bytes_kitty(&key(KeyCode::Char('a')), FLAG_REPORT_ALL).unwrap();
assert_eq!(result, b"\x1b[97u");
}
#[test]
fn kitty_release_event() {
let k = key_with_kind(KeyCode::Char('a'), KeyEventKind::Release);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_EVENTS).unwrap();
assert_eq!(result, b"\x1b[97;1:3u");
}
#[test]
fn kitty_repeat_event() {
let k = key_with_kind(KeyCode::Char('a'), KeyEventKind::Repeat);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_EVENTS).unwrap();
assert_eq!(result, b"\x1b[97;1:2u");
}
#[test]
fn kitty_ctrl_shift_a() {
let k = KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[97;6u");
}
#[test]
fn kitty_shift_a_with_report_alternate() {
let k = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_ALTERNATE).unwrap();
assert_eq!(result, b"\x1b[97:65;2u");
}
#[test]
fn kitty_shift_a_without_report_alternate() {
let k = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[97;2u");
}
#[test]
fn kitty_super_modifier() {
let k = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SUPER);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[97;9u");
}
#[test]
fn kitty_capslock_modifier() {
let k = key_with_state(
KeyCode::Char('a'),
KeyModifiers::NONE,
KeyEventState::CAPS_LOCK,
);
let result = key_to_bytes_kitty(&k, FLAG_REPORT_ALL).unwrap();
assert_eq!(result, b"\x1b[97;65u"); }
#[test]
fn kitty_arrow_up_ctrl() {
let result = key_to_bytes_kitty(&ctrl_key(KeyCode::Up), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[1;5A");
}
#[test]
fn kitty_f5_shift() {
let result = key_to_bytes_kitty(&shift_key(KeyCode::F(5)), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[15;2~");
}
#[test]
fn kitty_f13() {
let result = key_to_bytes_kitty(&key(KeyCode::F(13)), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[57376u");
}
#[test]
fn kitty_f20_ctrl() {
let result = key_to_bytes_kitty(&ctrl_key(KeyCode::F(20)), FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[57383;5u");
}
#[test]
fn kitty_numpad_5() {
let k = key_with_state(
KeyCode::Char('5'),
KeyModifiers::NONE,
KeyEventState::KEYPAD,
);
let result = key_to_bytes_kitty(&k, FLAG_REPORT_ALL).unwrap();
assert_eq!(result, b"\x1b[57404u");
}
#[test]
fn kitty_modifier_left_shift() {
let k = key(KeyCode::Modifier(ModifierKeyCode::LeftShift));
let result = key_to_bytes_kitty(&k, FLAG_REPORT_ALL).unwrap();
assert_eq!(result, b"\x1b[57441u");
}
#[test]
fn kitty_modifier_left_shift_release() {
let k = KeyEvent::new(
KeyCode::Modifier(ModifierKeyCode::LeftShift),
KeyModifiers::NONE,
)
.with_kind(KeyEventKind::Release);
let result = key_to_bytes_kitty(&k, FLAG_REPORT_ALL | FLAG_REPORT_EVENTS).unwrap();
assert_eq!(result, b"\x1b[57441;1:3u");
}
#[test]
fn kitty_arrow_release() {
let k = key_with_kind(KeyCode::Up, KeyEventKind::Release);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_EVENTS).unwrap();
assert_eq!(result, b"\x1b[1;1:3A");
}
#[test]
fn kitty_consumed_shift_no_alternate() {
let k = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT)
.with_unshifted_codepoint('a')
.with_consumed_modifiers(KeyModifiers::SHIFT);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_ALTERNATE).unwrap();
assert_eq!(result, b"A");
}
#[test]
fn kitty_unshifted_codepoint_symbol() {
let k =
KeyEvent::new(KeyCode::Char('!'), KeyModifiers::SHIFT).with_unshifted_codepoint('1');
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE | FLAG_REPORT_ALTERNATE).unwrap();
assert_eq!(result, b"\x1b[49:33;2u");
}
#[test]
fn kitty_consumed_modifiers_subtract() {
let k = KeyEvent::new(
KeyCode::Char('A'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
)
.with_unshifted_codepoint('a')
.with_consumed_modifiers(KeyModifiers::SHIFT);
let result = key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE).unwrap();
assert_eq!(result, b"\x1b[97;5u");
}
#[test]
fn kitty_unmapped_key_returns_none() {
let k = key(KeyCode::F(0));
assert_eq!(key_to_bytes_kitty(&k, FLAG_DISAMBIGUATE), None);
assert_eq!(key_to_bytes_kitty(&k, FLAG_REPORT_ALL), None);
}
}