initial commit
This commit is contained in:
+559
@@ -0,0 +1,559 @@
|
||||
//! 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,
|
||||
}
|
||||
|
||||
impl<'a> KeyEncoder<'a> {
|
||||
pub fn new(state: &'a KeyboardState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
|
||||
/// 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: CSI 1;mod X or SS3 X (no modifiers).
|
||||
fn encode_arrow(&self, letter: u8, mod_param: Option<u8>) -> Vec<u8> {
|
||||
if let Some(m) = mod_param {
|
||||
vec![0x1b, b'[', b'1', b';', b'0' + (m / 10), b'0' + (m % 10), letter]
|
||||
.into_iter()
|
||||
.filter(|&b| b != b'0' || m >= 10)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.take(6 + if m >= 10 { 1 } else { 0 })
|
||||
.collect()
|
||||
} else {
|
||||
// SS3 letter (cursor key mode) or CSI 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
|
||||
}
|
||||
Reference in New Issue
Block a user