use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KittyFlags: u32 {
const DISAMBIGUATE = 0b00001;
const EVENT_TYPES = 0b00010;
const ALTERNATE_KEYS = 0b00100;
const ALL_AS_ESCAPES = 0b01000;
const ASSOCIATED_TEXT = 0b10000;
}
}
impl Default for KittyFlags {
fn default() -> Self {
KittyFlags::DISAMBIGUATE
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Modifiers: u8 {
const SHIFT = 0b00000001;
const ALT = 0b00000010;
const CTRL = 0b00000100;
const SUPER = 0b00001000;
const HYPER = 0b00010000;
const META = 0b00100000;
const CAPS_LOCK = 0b01000000;
const NUM_LOCK = 0b10000000;
}
}
impl Modifiers {
pub(crate) fn from_wire(value: u32) -> Self {
let bits = value.saturating_sub(1) as u8;
Modifiers::from_bits_truncate(bits)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyEventType {
Press,
Repeat,
Release,
}
impl Default for KeyEventType {
fn default() -> Self {
KeyEventType::Press
}
}
impl KeyEventType {
fn from_wire(value: u32) -> Self {
match value {
0 | 1 => KeyEventType::Press,
2 => KeyEventType::Repeat,
3 => KeyEventType::Release,
_ => KeyEventType::Press,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct KeyEvent {
pub code: u32,
pub modifiers: Modifiers,
pub event_type: KeyEventType,
pub shifted_key: Option<u32>,
pub base_key: Option<u32>,
pub text: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionalKey {
Escape,
Enter,
Tab,
Backspace,
Insert,
Delete,
Left,
Right,
Up,
Down,
PageUp,
PageDown,
Home,
End,
CapsLock,
ScrollLock,
NumLock,
PrintScreen,
Pause,
Menu,
F(u8),
Kp0,
Kp1,
Kp2,
Kp3,
Kp4,
Kp5,
Kp6,
Kp7,
Kp8,
Kp9,
KpDecimal,
KpDivide,
KpMultiply,
KpSubtract,
KpAdd,
KpEnter,
KpEqual,
KpSeparator,
KpLeft,
KpRight,
KpUp,
KpDown,
KpPageUp,
KpPageDown,
KpHome,
KpEnd,
KpInsert,
KpDelete,
KpBegin,
MediaPlay,
MediaPause,
MediaPlayPause,
MediaReverse,
MediaStop,
MediaFastForward,
MediaRewind,
MediaTrackNext,
MediaTrackPrev,
MediaRecord,
LowerVolume,
RaiseVolume,
MuteVolume,
LeftShift,
LeftCtrl,
LeftAlt,
LeftSuper,
LeftHyper,
LeftMeta,
RightShift,
RightCtrl,
RightAlt,
RightSuper,
RightHyper,
RightMeta,
IsoLevel3Shift,
IsoLevel5Shift,
}
impl KeyEvent {
pub fn new(code: u32) -> Self {
Self {
code,
..Default::default()
}
}
pub fn with_modifiers(code: u32, modifiers: Modifiers) -> Self {
Self {
code,
modifiers,
..Default::default()
}
}
pub fn is_shift(&self) -> bool {
self.modifiers.contains(Modifiers::SHIFT)
}
pub fn is_ctrl(&self) -> bool {
self.modifiers.contains(Modifiers::CTRL)
}
pub fn is_alt(&self) -> bool {
self.modifiers.contains(Modifiers::ALT)
}
pub fn is_super(&self) -> bool {
self.modifiers.contains(Modifiers::SUPER)
}
pub fn is_press(&self) -> bool {
matches!(self.event_type, KeyEventType::Press)
}
pub fn is_repeat(&self) -> bool {
matches!(self.event_type, KeyEventType::Repeat)
}
pub fn is_release(&self) -> bool {
matches!(self.event_type, KeyEventType::Release)
}
pub fn functional(&self) -> Option<FunctionalKey> {
functional_from_code(self.code)
}
pub(crate) fn from_sequence(seq: &[u8]) -> Option<Self> {
if seq.len() < 3 || seq[0] != 0x1b || seq[1] != b'[' {
return None;
}
let terminator = *seq.last()?;
let params = std::str::from_utf8(&seq[2..seq.len() - 1]).ok()?;
let primary: Vec<Vec<Option<u32>>> = params
.split(';')
.map(|field| {
field
.split(':')
.map(|sub| {
if sub.is_empty() {
None
} else {
sub.parse::<u32>().ok().map(Some).unwrap_or(None)
}
})
.collect()
})
.collect();
let sub = |i: usize, j: usize, default: u32| -> u32 {
primary
.get(i)
.and_then(|v| v.get(j).copied().flatten())
.unwrap_or(default)
};
let sub_opt = |i: usize, j: usize| -> Option<u32> {
primary.get(i).and_then(|v| v.get(j).copied().flatten())
};
let code = match terminator {
b'u' | b'~' => sub(0, 0, 1),
_ => 1,
};
let shifted_key = sub_opt(0, 1);
let base_key = sub_opt(0, 2);
let mods_wire = sub(1, 0, 1);
let event_wire = sub(1, 1, 1);
let modifiers = Modifiers::from_wire(mods_wire);
let event_type = KeyEventType::from_wire(event_wire);
let text = primary.get(2).and_then(|field| {
let mut s = String::new();
for cp in field.iter().flatten() {
if let Some(c) = char::from_u32(*cp) {
s.push(c);
}
}
if s.is_empty() { None } else { Some(s) }
});
let resolved_code = match terminator {
b'u' => code,
b'~' => functional_tilde_code(code).unwrap_or(code),
b'A' => PUA_UP,
b'B' => PUA_DOWN,
b'C' => PUA_RIGHT,
b'D' => PUA_LEFT,
b'E' => PUA_KP_BEGIN,
b'F' => PUA_END,
b'H' => PUA_HOME,
b'P' => PUA_F1,
b'Q' => PUA_F2,
b'R' => PUA_F3,
b'S' => PUA_F4,
b'Z' => {
return Some(KeyEvent {
code: PUA_TAB,
modifiers: Modifiers::SHIFT,
event_type: KeyEventType::Press,
shifted_key: None,
base_key: None,
text: None,
});
}
_ => return None,
};
Some(KeyEvent {
code: resolved_code,
modifiers,
event_type,
shifted_key,
base_key,
text,
})
}
}
const PUA_ESCAPE: u32 = 57344;
const PUA_ENTER: u32 = 57345;
const PUA_TAB: u32 = 57346;
const PUA_BACKSPACE: u32 = 57347;
const PUA_INSERT: u32 = 57348;
const PUA_DELETE: u32 = 57349;
const PUA_LEFT: u32 = 57350;
const PUA_RIGHT: u32 = 57351;
const PUA_UP: u32 = 57352;
const PUA_DOWN: u32 = 57353;
const PUA_PAGE_UP: u32 = 57354;
const PUA_PAGE_DOWN: u32 = 57355;
const PUA_HOME: u32 = 57356;
const PUA_END: u32 = 57357;
const PUA_CAPS_LOCK: u32 = 57358;
const PUA_SCROLL_LOCK: u32 = 57359;
const PUA_NUM_LOCK: u32 = 57360;
const PUA_PRINT_SCREEN: u32 = 57361;
const PUA_PAUSE: u32 = 57362;
const PUA_MENU: u32 = 57363;
const PUA_F1: u32 = 57364;
const PUA_F2: u32 = 57365;
const PUA_F3: u32 = 57366;
const PUA_F4: u32 = 57367;
const PUA_KP_BEGIN: u32 = 57427;
fn functional_from_code(code: u32) -> Option<FunctionalKey> {
use FunctionalKey::*;
if (57364..=57398).contains(&code) {
let n = (code - 57364 + 1) as u8;
return Some(F(n));
}
if (57399..=57408).contains(&code) {
let k = match code - 57399 {
0 => Kp0,
1 => Kp1,
2 => Kp2,
3 => Kp3,
4 => Kp4,
5 => Kp5,
6 => Kp6,
7 => Kp7,
8 => Kp8,
_ => Kp9,
};
return Some(k);
}
match code {
PUA_ESCAPE => Some(Escape),
PUA_ENTER => Some(Enter),
PUA_TAB => Some(Tab),
PUA_BACKSPACE => Some(Backspace),
PUA_INSERT => Some(Insert),
PUA_DELETE => Some(Delete),
PUA_LEFT => Some(Left),
PUA_RIGHT => Some(Right),
PUA_UP => Some(Up),
PUA_DOWN => Some(Down),
PUA_PAGE_UP => Some(PageUp),
PUA_PAGE_DOWN => Some(PageDown),
PUA_HOME => Some(Home),
PUA_END => Some(End),
PUA_CAPS_LOCK => Some(CapsLock),
PUA_SCROLL_LOCK => Some(ScrollLock),
PUA_NUM_LOCK => Some(NumLock),
PUA_PRINT_SCREEN => Some(PrintScreen),
PUA_PAUSE => Some(Pause),
PUA_MENU => Some(Menu),
57409 => Some(KpDecimal),
57410 => Some(KpDivide),
57411 => Some(KpMultiply),
57412 => Some(KpSubtract),
57413 => Some(KpAdd),
57414 => Some(KpEnter),
57415 => Some(KpEqual),
57416 => Some(KpSeparator),
57417 => Some(KpLeft),
57418 => Some(KpRight),
57419 => Some(KpUp),
57420 => Some(KpDown),
57421 => Some(KpPageUp),
57422 => Some(KpPageDown),
57423 => Some(KpHome),
57424 => Some(KpEnd),
57425 => Some(KpInsert),
57426 => Some(KpDelete),
PUA_KP_BEGIN => Some(KpBegin),
57428 => Some(MediaPlay),
57429 => Some(MediaPause),
57430 => Some(MediaPlayPause),
57431 => Some(MediaReverse),
57432 => Some(MediaStop),
57433 => Some(MediaFastForward),
57434 => Some(MediaRewind),
57435 => Some(MediaTrackNext),
57436 => Some(MediaTrackPrev),
57437 => Some(MediaRecord),
57438 => Some(LowerVolume),
57439 => Some(RaiseVolume),
57440 => Some(MuteVolume),
57441 => Some(LeftShift),
57442 => Some(LeftCtrl),
57443 => Some(LeftAlt),
57444 => Some(LeftSuper),
57445 => Some(LeftHyper),
57446 => Some(LeftMeta),
57447 => Some(RightShift),
57448 => Some(RightCtrl),
57449 => Some(RightAlt),
57450 => Some(RightSuper),
57451 => Some(RightHyper),
57452 => Some(RightMeta),
57453 => Some(IsoLevel3Shift),
57454 => Some(IsoLevel5Shift),
0x08 => Some(Backspace),
0x09 => Some(Tab),
0x0d => Some(Enter),
0x1b => Some(Escape),
0x7f => Some(Backspace),
_ => None,
}
}
fn functional_tilde_code(n: u32) -> Option<u32> {
match n {
1 => Some(PUA_HOME),
2 => Some(PUA_INSERT),
3 => Some(PUA_DELETE),
4 => Some(PUA_END),
5 => Some(PUA_PAGE_UP),
6 => Some(PUA_PAGE_DOWN),
7 => Some(PUA_HOME),
8 => Some(PUA_END),
11 => Some(PUA_F1),
12 => Some(PUA_F2),
13 => Some(PUA_F3),
14 => Some(PUA_F4),
15 => Some(57368), 17 => Some(57369), 18 => Some(57370), 19 => Some(57371), 20 => Some(57372), 21 => Some(57373), 23 => Some(57374), 24 => Some(57375), _ => None,
}
}
pub(crate) fn push_sequence(flags: KittyFlags) -> String {
format!("\x1b[>{}u", flags.bits())
}
pub(crate) fn pop_n_sequence(n: u32) -> String {
format!("\x1b[<{}u", n)
}
pub(crate) fn pop_sequence() -> String {
"\x1b[<u".to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetMode {
Replace = 1,
Union = 2,
Difference = 3,
}
pub(crate) fn set_sequence(flags: KittyFlags, mode: SetMode) -> String {
format!("\x1b[={};{}u", flags.bits(), mode as u8)
}
pub(crate) fn query_sequence() -> String {
"\x1b[?u".to_string()
}
#[cfg(test)]
fn enable_sequence(flags: KittyFlags) -> String {
push_sequence(flags)
}
#[cfg(test)]
fn disable_sequence() -> String {
pop_sequence()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flag_bits_match_spec() {
assert_eq!(KittyFlags::DISAMBIGUATE.bits(), 1);
assert_eq!(KittyFlags::EVENT_TYPES.bits(), 2);
assert_eq!(KittyFlags::ALTERNATE_KEYS.bits(), 4);
assert_eq!(KittyFlags::ALL_AS_ESCAPES.bits(), 8);
assert_eq!(KittyFlags::ASSOCIATED_TEXT.bits(), 16);
}
#[test]
fn default_is_disambiguate() {
assert_eq!(KittyFlags::default(), KittyFlags::DISAMBIGUATE);
}
#[test]
fn modifier_wire_subtracts_one() {
assert_eq!(Modifiers::from_wire(1), Modifiers::empty());
assert_eq!(Modifiers::from_wire(0), Modifiers::empty());
assert_eq!(Modifiers::from_wire(2), Modifiers::SHIFT);
assert_eq!(Modifiers::from_wire(5), Modifiers::CTRL);
assert_eq!(Modifiers::from_wire(6), Modifiers::CTRL | Modifiers::SHIFT);
assert_eq!(
Modifiers::from_wire(8),
Modifiers::SHIFT | Modifiers::ALT | Modifiers::CTRL
);
}
#[test]
fn parse_simple_u() {
let e = KeyEvent::from_sequence(b"\x1b[65u").unwrap();
assert_eq!(e.code, 65);
assert_eq!(e.modifiers, Modifiers::empty());
assert_eq!(e.event_type, KeyEventType::Press);
}
#[test]
fn parse_defaults_keycode_to_one() {
let e = KeyEvent::from_sequence(b"\x1b[u").unwrap();
assert_eq!(e.code, 1);
}
#[test]
fn parse_ctrl_alone_is_wire_five() {
let e = KeyEvent::from_sequence(b"\x1b[65;5u").unwrap();
assert!(e.is_ctrl());
assert!(!e.is_shift());
}
#[test]
fn parse_ctrl_shift_is_wire_six() {
let e = KeyEvent::from_sequence(b"\x1b[65;6u").unwrap();
assert!(e.is_ctrl());
assert!(e.is_shift());
}
#[test]
fn parse_release_is_colon_subfield() {
let e = KeyEvent::from_sequence(b"\x1b[97;1:3u").unwrap();
assert_eq!(e.code, 97);
assert_eq!(e.event_type, KeyEventType::Release);
assert!(e.is_release());
}
#[test]
fn parse_repeat_with_ctrl() {
let e = KeyEvent::from_sequence(b"\x1b[97;5:2u").unwrap();
assert!(e.is_ctrl());
assert_eq!(e.event_type, KeyEventType::Repeat);
assert!(e.is_repeat());
}
#[test]
fn parse_alternate_keys_are_subfields_of_keycode() {
let e = KeyEvent::from_sequence(b"\x1b[97:65;2u").unwrap();
assert_eq!(e.code, 97);
assert_eq!(e.shifted_key, Some(65));
assert!(e.is_shift());
}
#[test]
fn parse_base_key_is_second_subfield() {
let e = KeyEvent::from_sequence(b"\x1b[97:65:97;2u").unwrap();
assert_eq!(e.code, 97);
assert_eq!(e.shifted_key, Some(65));
assert_eq!(e.base_key, Some(97));
}
#[test]
fn parse_associated_text() {
let e = KeyEvent::from_sequence(b"\x1b[97;1;97u").unwrap();
assert_eq!(e.text.as_deref(), Some("a"));
}
#[test]
fn parse_associated_text_multi_codepoint() {
let e = KeyEvent::from_sequence(b"\x1b[101;1;233u").unwrap();
assert_eq!(e.text.as_deref(), Some("é"));
}
#[test]
fn parse_rejects_malformed() {
assert!(KeyEvent::from_sequence(b"").is_none());
assert!(KeyEvent::from_sequence(b"\x1b[").is_none());
assert!(KeyEvent::from_sequence(b"hi").is_none());
assert!(KeyEvent::from_sequence(b"\x1b[65x").is_none());
}
#[test]
fn parse_legacy_arrow_uses_pua_code() {
let e = KeyEvent::from_sequence(b"\x1b[1;5A").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::Up));
assert!(e.is_ctrl());
}
#[test]
fn parse_legacy_tilde_with_modifiers() {
let e = KeyEvent::from_sequence(b"\x1b[3;2~").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::Delete));
assert!(e.is_shift());
}
#[test]
fn parse_kitty_arrow_with_event_type() {
let e = KeyEvent::from_sequence(b"\x1b[1;2:3A").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::Up));
assert!(e.is_shift());
assert_eq!(e.event_type, KeyEventType::Release);
}
#[test]
fn parse_function_key_legacy() {
let e = KeyEvent::from_sequence(b"\x1b[15~").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::F(5)));
}
#[test]
fn parse_function_key_kitty_pua() {
let e = KeyEvent::from_sequence(b"\x1b[57368u").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::F(5)));
}
#[test]
fn parse_f1_legacy_letter() {
let e = KeyEvent::from_sequence(b"\x1b[1;2P").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::F(1)));
assert!(e.is_shift());
}
#[test]
fn parse_functional_resolves_pua_enter() {
let e = KeyEvent::from_sequence(b"\x1b[57345u").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::Enter));
}
#[test]
fn parse_functional_resolves_keypad_digit() {
let e = KeyEvent::from_sequence(b"\x1b[57404u").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::Kp5));
}
#[test]
fn parse_functional_resolves_lone_modifier() {
let e = KeyEvent::from_sequence(b"\x1b[57441u").unwrap();
assert_eq!(e.functional(), Some(FunctionalKey::LeftShift));
}
#[test]
fn push_sequence_encodes_flag_bits() {
assert_eq!(push_sequence(KittyFlags::DISAMBIGUATE), "\x1b[>1u");
assert_eq!(
push_sequence(KittyFlags::DISAMBIGUATE | KittyFlags::EVENT_TYPES),
"\x1b[>3u"
);
let all = KittyFlags::DISAMBIGUATE
| KittyFlags::EVENT_TYPES
| KittyFlags::ALTERNATE_KEYS
| KittyFlags::ALL_AS_ESCAPES
| KittyFlags::ASSOCIATED_TEXT;
assert_eq!(push_sequence(all), "\x1b[>31u");
}
#[test]
fn pop_sequences() {
assert_eq!(pop_sequence(), "\x1b[<u");
assert_eq!(pop_n_sequence(3), "\x1b[<3u");
}
#[test]
fn set_sequence_encodes_mode() {
let flags = KittyFlags::DISAMBIGUATE | KittyFlags::EVENT_TYPES;
assert_eq!(set_sequence(flags, SetMode::Replace), "\x1b[=3;1u");
assert_eq!(set_sequence(flags, SetMode::Union), "\x1b[=3;2u");
assert_eq!(set_sequence(flags, SetMode::Difference), "\x1b[=3;3u");
}
#[test]
fn query_returns_csi_question_u() {
assert_eq!(query_sequence(), "\x1b[?u");
}
#[test]
fn legacy_aliases_delegate() {
assert_eq!(
enable_sequence(KittyFlags::DISAMBIGUATE),
push_sequence(KittyFlags::DISAMBIGUATE),
);
assert_eq!(disable_sequence(), pop_sequence());
}
}