589 lines
18 KiB
Rust
589 lines
18 KiB
Rust
//! Kitty keyboard protocol implementation.
|
|
//!
|
|
//! This module implements the progressive keyboard enhancement protocol
|
|
//! as specified at: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
|
|
use bitflags::bitflags;
|
|
|
|
bitflags! {
|
|
/// Keyboard enhancement flags for the Kitty keyboard protocol.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct KeyboardFlags: u8 {
|
|
/// Disambiguate escape codes (report Esc, alt+key, ctrl+key using CSI u).
|
|
const DISAMBIGUATE = 0b00001;
|
|
/// Report key repeat and release events.
|
|
const REPORT_EVENTS = 0b00010;
|
|
/// Report alternate keys (shifted key, base layout key).
|
|
const REPORT_ALTERNATES = 0b00100;
|
|
/// Report all keys as escape codes (including text-generating keys).
|
|
const REPORT_ALL_KEYS = 0b01000;
|
|
/// Report associated text with key events.
|
|
const REPORT_TEXT = 0b10000;
|
|
}
|
|
}
|
|
|
|
/// Key event types for the keyboard protocol.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum KeyEventType {
|
|
Press = 1,
|
|
Repeat = 2,
|
|
Release = 3,
|
|
}
|
|
|
|
/// Modifier flags for key events.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct Modifiers {
|
|
pub shift: bool,
|
|
pub alt: bool,
|
|
pub ctrl: bool,
|
|
pub super_key: bool,
|
|
pub hyper: bool,
|
|
pub meta: bool,
|
|
pub caps_lock: bool,
|
|
pub num_lock: bool,
|
|
}
|
|
|
|
impl Modifiers {
|
|
/// Encodes modifiers as a decimal number (1 + bitfield).
|
|
/// Returns None if no modifiers are active.
|
|
pub fn encode(&self) -> Option<u8> {
|
|
let mut bits: u8 = 0;
|
|
if self.shift {
|
|
bits |= 1;
|
|
}
|
|
if self.alt {
|
|
bits |= 2;
|
|
}
|
|
if self.ctrl {
|
|
bits |= 4;
|
|
}
|
|
if self.super_key {
|
|
bits |= 8;
|
|
}
|
|
if self.hyper {
|
|
bits |= 16;
|
|
}
|
|
if self.meta {
|
|
bits |= 32;
|
|
}
|
|
if self.caps_lock {
|
|
bits |= 64;
|
|
}
|
|
if self.num_lock {
|
|
bits |= 128;
|
|
}
|
|
|
|
if bits == 0 { None } else { Some(1 + bits) }
|
|
}
|
|
|
|
/// Returns true if any modifier is active.
|
|
pub fn any(&self) -> bool {
|
|
self.shift
|
|
|| self.alt
|
|
|| self.ctrl
|
|
|| self.super_key
|
|
|| self.hyper
|
|
|| self.meta
|
|
|| self.caps_lock
|
|
|| self.num_lock
|
|
}
|
|
}
|
|
|
|
/// Functional key codes from the Kitty keyboard protocol.
|
|
/// These are Unicode Private Use Area codepoints (57344 - 63743).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(u32)]
|
|
pub enum FunctionalKey {
|
|
Escape = 27,
|
|
Enter = 13,
|
|
Tab = 9,
|
|
Backspace = 127,
|
|
Insert = 57348,
|
|
Delete = 57349,
|
|
Left = 57350,
|
|
Right = 57351,
|
|
Up = 57352,
|
|
Down = 57353,
|
|
PageUp = 57354,
|
|
PageDown = 57355,
|
|
Home = 57356,
|
|
End = 57357,
|
|
CapsLock = 57358,
|
|
ScrollLock = 57359,
|
|
NumLock = 57360,
|
|
PrintScreen = 57361,
|
|
Pause = 57362,
|
|
Menu = 57363,
|
|
F1 = 57364,
|
|
F2 = 57365,
|
|
F3 = 57366,
|
|
F4 = 57367,
|
|
F5 = 57368,
|
|
F6 = 57369,
|
|
F7 = 57370,
|
|
F8 = 57371,
|
|
F9 = 57372,
|
|
F10 = 57373,
|
|
F11 = 57374,
|
|
F12 = 57375,
|
|
F13 = 57376,
|
|
F14 = 57377,
|
|
F15 = 57378,
|
|
F16 = 57379,
|
|
F17 = 57380,
|
|
F18 = 57381,
|
|
F19 = 57382,
|
|
F20 = 57383,
|
|
F21 = 57384,
|
|
F22 = 57385,
|
|
F23 = 57386,
|
|
F24 = 57387,
|
|
F25 = 57388,
|
|
// Keypad keys
|
|
KpDecimal = 57409,
|
|
KpDivide = 57410,
|
|
KpMultiply = 57411,
|
|
KpSubtract = 57412,
|
|
KpAdd = 57413,
|
|
KpEnter = 57414,
|
|
KpEqual = 57415,
|
|
KpSeparator = 57416,
|
|
KpLeft = 57417,
|
|
KpRight = 57418,
|
|
KpUp = 57419,
|
|
KpDown = 57420,
|
|
KpPageUp = 57421,
|
|
KpPageDown = 57422,
|
|
KpHome = 57423,
|
|
KpEnd = 57424,
|
|
KpInsert = 57425,
|
|
KpDelete = 57426,
|
|
KpBegin = 57427,
|
|
// Media keys
|
|
MediaPlay = 57428,
|
|
MediaPause = 57429,
|
|
MediaPlayPause = 57430,
|
|
MediaReverse = 57431,
|
|
MediaStop = 57432,
|
|
MediaFastForward = 57433,
|
|
MediaRewind = 57434,
|
|
MediaTrackNext = 57435,
|
|
MediaTrackPrevious = 57436,
|
|
MediaRecord = 57437,
|
|
LowerVolume = 57438,
|
|
RaiseVolume = 57439,
|
|
MuteVolume = 57440,
|
|
// Modifier keys
|
|
LeftShift = 57441,
|
|
LeftControl = 57442,
|
|
LeftAlt = 57443,
|
|
LeftSuper = 57444,
|
|
LeftHyper = 57445,
|
|
LeftMeta = 57446,
|
|
RightShift = 57447,
|
|
RightControl = 57448,
|
|
RightAlt = 57449,
|
|
RightSuper = 57450,
|
|
RightHyper = 57451,
|
|
RightMeta = 57452,
|
|
IsoLevel3Shift = 57453,
|
|
IsoLevel5Shift = 57454,
|
|
}
|
|
|
|
/// Keyboard protocol state.
|
|
#[derive(Debug, Clone)]
|
|
pub struct KeyboardState {
|
|
/// Current enhancement flags.
|
|
flags: KeyboardFlags,
|
|
/// Stack of pushed flag states (for push/pop).
|
|
stack: Vec<KeyboardFlags>,
|
|
}
|
|
|
|
impl Default for KeyboardState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl KeyboardState {
|
|
/// Maximum stack size to prevent DoS.
|
|
const MAX_STACK_SIZE: usize = 16;
|
|
|
|
pub fn new() -> Self {
|
|
Self {
|
|
flags: KeyboardFlags::empty(),
|
|
stack: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Gets the current keyboard enhancement flags.
|
|
pub fn flags(&self) -> KeyboardFlags {
|
|
self.flags
|
|
}
|
|
|
|
/// Sets keyboard flags using the specified mode.
|
|
/// mode 1: set all flags to the given value
|
|
/// mode 2: set bits that are set in flags, leave others unchanged
|
|
/// mode 3: reset bits that are set in flags, leave others unchanged
|
|
pub fn set_flags(&mut self, flags: u8, mode: u8) {
|
|
let new_flags = KeyboardFlags::from_bits_truncate(flags);
|
|
match mode {
|
|
1 => self.flags = new_flags,
|
|
2 => self.flags |= new_flags,
|
|
3 => self.flags &= !new_flags,
|
|
_ => self.flags = new_flags, // Default to mode 1
|
|
}
|
|
}
|
|
|
|
/// Pushes current flags onto the stack and optionally sets new flags.
|
|
pub fn push(&mut self, flags: Option<u8>) {
|
|
// Evict oldest entry if stack is full
|
|
if self.stack.len() >= Self::MAX_STACK_SIZE {
|
|
self.stack.remove(0);
|
|
}
|
|
self.stack.push(self.flags);
|
|
if let Some(f) = flags {
|
|
self.flags = KeyboardFlags::from_bits_truncate(f);
|
|
}
|
|
}
|
|
|
|
/// Pops entries from the stack.
|
|
pub fn pop(&mut self, count: usize) {
|
|
let count = count.max(1);
|
|
for _ in 0..count {
|
|
if let Some(flags) = self.stack.pop() {
|
|
self.flags = flags;
|
|
} else {
|
|
// Stack is empty, reset all flags
|
|
self.flags = KeyboardFlags::empty();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns whether the DISAMBIGUATE flag is set.
|
|
pub fn disambiguate(&self) -> bool {
|
|
self.flags.contains(KeyboardFlags::DISAMBIGUATE)
|
|
}
|
|
|
|
/// Returns whether the REPORT_EVENTS flag is set.
|
|
pub fn report_events(&self) -> bool {
|
|
self.flags.contains(KeyboardFlags::REPORT_EVENTS)
|
|
}
|
|
|
|
/// Returns whether the REPORT_ALTERNATES flag is set.
|
|
pub fn report_alternates(&self) -> bool {
|
|
self.flags.contains(KeyboardFlags::REPORT_ALTERNATES)
|
|
}
|
|
|
|
/// Returns whether the REPORT_ALL_KEYS flag is set.
|
|
pub fn report_all_keys(&self) -> bool {
|
|
self.flags.contains(KeyboardFlags::REPORT_ALL_KEYS)
|
|
}
|
|
|
|
/// Returns whether the REPORT_TEXT flag is set.
|
|
pub fn report_text(&self) -> bool {
|
|
self.flags.contains(KeyboardFlags::REPORT_TEXT)
|
|
}
|
|
}
|
|
|
|
/// Encodes a key event according to the Kitty keyboard protocol.
|
|
pub struct KeyEncoder<'a> {
|
|
state: &'a KeyboardState,
|
|
/// Whether application cursor keys mode (DECCKM) is enabled.
|
|
/// When true, arrow keys send SS3 format (ESC O letter).
|
|
/// When false, arrow keys send CSI format (ESC [ letter).
|
|
application_cursor_keys: bool,
|
|
}
|
|
|
|
impl<'a> KeyEncoder<'a> {
|
|
pub fn new(state: &'a KeyboardState) -> Self {
|
|
Self {
|
|
state,
|
|
application_cursor_keys: false,
|
|
}
|
|
}
|
|
|
|
/// Creates a new KeyEncoder with application cursor keys mode setting.
|
|
pub fn with_cursor_mode(
|
|
state: &'a KeyboardState,
|
|
application_cursor_keys: bool,
|
|
) -> Self {
|
|
Self {
|
|
state,
|
|
application_cursor_keys,
|
|
}
|
|
}
|
|
|
|
/// Encodes a functional key press to bytes.
|
|
pub fn encode_functional(
|
|
&self,
|
|
key: FunctionalKey,
|
|
modifiers: Modifiers,
|
|
event_type: KeyEventType,
|
|
) -> Vec<u8> {
|
|
let key_code = key as u32;
|
|
|
|
// Special handling for legacy keys in legacy mode
|
|
if self.state.flags().is_empty() {
|
|
return self.encode_legacy_functional(key, modifiers);
|
|
}
|
|
|
|
self.encode_csi_u(key_code, modifiers, event_type, None)
|
|
}
|
|
|
|
/// Encodes a Unicode character key press.
|
|
pub fn encode_char(
|
|
&self,
|
|
c: char,
|
|
modifiers: Modifiers,
|
|
event_type: KeyEventType,
|
|
) -> Vec<u8> {
|
|
let key_code = c as u32;
|
|
|
|
// In legacy mode without REPORT_ALL_KEYS, just send the character
|
|
// (with legacy ctrl/alt handling)
|
|
if !self.state.report_all_keys() {
|
|
return self.encode_legacy_text(c, modifiers);
|
|
}
|
|
|
|
// With REPORT_ALL_KEYS, encode as CSI u
|
|
let text = if self.state.report_text() {
|
|
Some(c)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
self.encode_csi_u(key_code, modifiers, event_type, text)
|
|
}
|
|
|
|
/// Encodes a key event as CSI u format.
|
|
fn encode_csi_u(
|
|
&self,
|
|
key_code: u32,
|
|
modifiers: Modifiers,
|
|
event_type: KeyEventType,
|
|
text: Option<char>,
|
|
) -> Vec<u8> {
|
|
let mut result = Vec::with_capacity(16);
|
|
result.extend_from_slice(b"\x1b[");
|
|
result.extend_from_slice(key_code.to_string().as_bytes());
|
|
|
|
let mod_value = modifiers.encode();
|
|
let has_event_type =
|
|
self.state.report_events() && event_type != KeyEventType::Press;
|
|
|
|
if mod_value.is_some() || has_event_type || text.is_some() {
|
|
result.push(b';');
|
|
if let Some(m) = mod_value {
|
|
result.extend_from_slice(m.to_string().as_bytes());
|
|
} else if has_event_type {
|
|
result.push(b'1'); // Default modifier value
|
|
}
|
|
|
|
if has_event_type {
|
|
result.push(b':');
|
|
result.extend_from_slice(
|
|
(event_type as u8).to_string().as_bytes(),
|
|
);
|
|
}
|
|
}
|
|
|
|
if let Some(text_char) = text {
|
|
result.push(b';');
|
|
result.extend_from_slice((text_char as u32).to_string().as_bytes());
|
|
}
|
|
|
|
result.push(b'u');
|
|
result
|
|
}
|
|
|
|
/// Encodes functional keys in legacy mode.
|
|
fn encode_legacy_functional(
|
|
&self,
|
|
key: FunctionalKey,
|
|
modifiers: Modifiers,
|
|
) -> Vec<u8> {
|
|
let mod_param = modifiers.encode();
|
|
|
|
match key {
|
|
FunctionalKey::Escape => {
|
|
if modifiers.alt {
|
|
vec![0x1b, 0x1b]
|
|
} else {
|
|
vec![0x1b]
|
|
}
|
|
}
|
|
FunctionalKey::Enter => {
|
|
if modifiers.alt {
|
|
vec![0x1b, 0x0d]
|
|
} else {
|
|
vec![0x0d]
|
|
}
|
|
}
|
|
FunctionalKey::Tab => {
|
|
if modifiers.shift && !modifiers.alt && !modifiers.ctrl {
|
|
// Shift+Tab -> CSI Z
|
|
vec![0x1b, b'[', b'Z']
|
|
} else if modifiers.alt {
|
|
vec![0x1b, 0x09]
|
|
} else {
|
|
vec![0x09]
|
|
}
|
|
}
|
|
FunctionalKey::Backspace => {
|
|
if modifiers.ctrl {
|
|
if modifiers.alt {
|
|
vec![0x1b, 0x08]
|
|
} else {
|
|
vec![0x08]
|
|
}
|
|
} else if modifiers.alt {
|
|
vec![0x1b, 0x7f]
|
|
} else {
|
|
vec![0x7f]
|
|
}
|
|
}
|
|
// Arrow keys
|
|
FunctionalKey::Up => self.encode_arrow(b'A', mod_param),
|
|
FunctionalKey::Down => self.encode_arrow(b'B', mod_param),
|
|
FunctionalKey::Right => self.encode_arrow(b'C', mod_param),
|
|
FunctionalKey::Left => self.encode_arrow(b'D', mod_param),
|
|
FunctionalKey::Home => self.encode_arrow(b'H', mod_param),
|
|
FunctionalKey::End => self.encode_arrow(b'F', mod_param),
|
|
// Function keys F1-F4 (SS3 in legacy mode without modifiers)
|
|
FunctionalKey::F1 => self.encode_f1_f4(b'P', mod_param),
|
|
FunctionalKey::F2 => self.encode_f1_f4(b'Q', mod_param),
|
|
FunctionalKey::F3 => self.encode_f1_f4(b'R', mod_param),
|
|
FunctionalKey::F4 => self.encode_f1_f4(b'S', mod_param),
|
|
// Function keys F5-F12 (CSI number ~)
|
|
FunctionalKey::F5 => self.encode_tilde(15, mod_param),
|
|
FunctionalKey::F6 => self.encode_tilde(17, mod_param),
|
|
FunctionalKey::F7 => self.encode_tilde(18, mod_param),
|
|
FunctionalKey::F8 => self.encode_tilde(19, mod_param),
|
|
FunctionalKey::F9 => self.encode_tilde(20, mod_param),
|
|
FunctionalKey::F10 => self.encode_tilde(21, mod_param),
|
|
FunctionalKey::F11 => self.encode_tilde(23, mod_param),
|
|
FunctionalKey::F12 => self.encode_tilde(24, mod_param),
|
|
// Navigation keys
|
|
FunctionalKey::Insert => self.encode_tilde(2, mod_param),
|
|
FunctionalKey::Delete => self.encode_tilde(3, mod_param),
|
|
FunctionalKey::PageUp => self.encode_tilde(5, mod_param),
|
|
FunctionalKey::PageDown => self.encode_tilde(6, mod_param),
|
|
// Other functional keys - encode as CSI u
|
|
_ => {
|
|
let key_code = key as u32;
|
|
self.encode_csi_u(
|
|
key_code,
|
|
modifiers,
|
|
KeyEventType::Press,
|
|
None,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Encodes arrow/home/end keys based on DECCKM mode:
|
|
/// - Normal mode (application_cursor_keys=false): CSI letter (ESC [ letter)
|
|
/// - Application mode (application_cursor_keys=true): SS3 letter (ESC O letter)
|
|
/// With modifiers, always use CSI 1;mod letter format.
|
|
fn encode_arrow(&self, letter: u8, mod_param: Option<u8>) -> Vec<u8> {
|
|
if let Some(m) = mod_param {
|
|
// With modifiers: CSI 1;mod letter
|
|
let mut result = vec![0x1b, b'[', b'1', b';'];
|
|
result.extend_from_slice(m.to_string().as_bytes());
|
|
result.push(letter);
|
|
result
|
|
} else if self.application_cursor_keys {
|
|
// Application cursor mode: SS3 letter (ESC O letter)
|
|
vec![0x1b, b'O', letter]
|
|
} else {
|
|
// Normal cursor mode: CSI letter (ESC [ letter)
|
|
vec![0x1b, b'[', letter]
|
|
}
|
|
}
|
|
|
|
/// Encodes F1-F4: SS3 letter (no mods) or CSI 1;mod letter (with mods).
|
|
fn encode_f1_f4(&self, letter: u8, mod_param: Option<u8>) -> Vec<u8> {
|
|
if let Some(m) = mod_param {
|
|
let mut result = vec![0x1b, b'[', b'1', b';'];
|
|
result.extend_from_slice(m.to_string().as_bytes());
|
|
result.push(letter);
|
|
result
|
|
} else {
|
|
vec![0x1b, b'O', letter]
|
|
}
|
|
}
|
|
|
|
/// Encodes CSI number ; modifier ~ format.
|
|
fn encode_tilde(&self, number: u8, mod_param: Option<u8>) -> Vec<u8> {
|
|
let mut result = vec![0x1b, b'['];
|
|
result.extend_from_slice(number.to_string().as_bytes());
|
|
if let Some(m) = mod_param {
|
|
result.push(b';');
|
|
result.extend_from_slice(m.to_string().as_bytes());
|
|
}
|
|
result.push(b'~');
|
|
result
|
|
}
|
|
|
|
/// Encodes text keys in legacy mode.
|
|
fn encode_legacy_text(&self, c: char, modifiers: Modifiers) -> Vec<u8> {
|
|
// For plain text without modifiers, just send UTF-8
|
|
if !modifiers.any() {
|
|
let mut buf = [0u8; 4];
|
|
let s = c.encode_utf8(&mut buf);
|
|
return s.as_bytes().to_vec();
|
|
}
|
|
|
|
// Handle ctrl modifier for ASCII keys
|
|
if modifiers.ctrl && !modifiers.shift && c.is_ascii_lowercase() {
|
|
let ctrl_code = (c as u8) - b'a' + 1;
|
|
if modifiers.alt {
|
|
return vec![0x1b, ctrl_code];
|
|
} else {
|
|
return vec![ctrl_code];
|
|
}
|
|
}
|
|
|
|
// Handle ctrl+space
|
|
if modifiers.ctrl && c == ' ' {
|
|
if modifiers.alt {
|
|
return vec![0x1b, 0x00];
|
|
} else {
|
|
return vec![0x00];
|
|
}
|
|
}
|
|
|
|
// Handle alt modifier alone
|
|
if modifiers.alt && !modifiers.ctrl {
|
|
let mut buf = [0u8; 4];
|
|
let s = c.encode_utf8(&mut buf);
|
|
let mut result = vec![0x1b];
|
|
result.extend_from_slice(s.as_bytes());
|
|
return result;
|
|
}
|
|
|
|
// Handle shift (just send the shifted character)
|
|
if modifiers.shift && !modifiers.ctrl && !modifiers.alt {
|
|
let shifted = c.to_uppercase().next().unwrap_or(c);
|
|
let mut buf = [0u8; 4];
|
|
let s = shifted.encode_utf8(&mut buf);
|
|
return s.as_bytes().to_vec();
|
|
}
|
|
|
|
// For complex modifier combinations, use CSI u encoding even in "legacy" mode
|
|
// This provides better compatibility than dropping the key
|
|
let key_code = c as u32;
|
|
self.encode_csi_u(key_code, modifiers, KeyEventType::Press, None)
|
|
}
|
|
}
|
|
|
|
/// Generates the response for a keyboard mode query (CSI ? u).
|
|
pub fn query_response(flags: KeyboardFlags) -> Vec<u8> {
|
|
let mut result = vec![0x1b, b'[', b'?'];
|
|
result.extend_from_slice(flags.bits().to_string().as_bytes());
|
|
result.push(b'u');
|
|
result
|
|
}
|