From b3b5c9dd93a6b2bdd5a26b5af34312052a17a645 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 13 Jul 2026 09:57:21 +0200 Subject: [PATCH 1/4] Preserve Kitty key event metadata --- src/event.rs | 34 ++++- src/parse.rs | 308 ++++++++++++++++++++++++++++++++++--------- src/parse/windows.rs | 2 + 3 files changed, 284 insertions(+), 60 deletions(-) diff --git a/src/event.rs b/src/event.rs index 55dc0a9..5808e5e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -118,6 +118,33 @@ impl Event { } } +/// Optional metadata carried by an enhanced keyboard protocol key event. +/// +/// The Kitty keyboard protocol can report the shifted key, the key in the base keyboard layout, +/// and the text generated by a key event in addition to its canonical key code. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct KeyEventEnhancements { + /// The character that would be produced with Shift applied. + pub shifted_key: Option, + + /// The character in the base keyboard layout (without modifiers/layout translations applied). + pub base_layout_key: Option, + + /// The Unicode text associated with the key event. + pub associated_text: Option, +} + +impl KeyEventEnhancements { + /// Creates a new, empty set of key event enhancements. + pub const fn new() -> Self { + Self { + shifted_key: None, + base_layout_key: None, + associated_text: None, + } + } +} + /// A key event plus modifiers and protocol state. /// /// `KeyEvent` appears inside [`Event::Key`], which is normally returned by [`EventReader::read`] @@ -143,7 +170,7 @@ impl Event { /// /// [crossterm key events]: https://docs.rs/crossterm/latest/crossterm/event/struct.KeyEvent.html /// [missing key combinations]: https://github.com/crossterm-rs/crossterm/issues/685 -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct KeyEvent { /// The key identity. pub code: KeyCode, @@ -167,6 +194,9 @@ pub struct KeyEvent { /// This is empty unless the input source reports state outside the ordinary modifier mask, /// such as keypad-originated input, Caps Lock, or Num Lock. pub state: KeyEventState, + + /// Optional metadata reported by an enhanced keyboard protocol. + pub enhancements: KeyEventEnhancements, } impl KeyEvent { @@ -177,6 +207,7 @@ impl KeyEvent { modifiers, kind: KeyEventKind::Press, state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), } } } @@ -188,6 +219,7 @@ impl From for KeyEvent { kind: KeyEventKind::Press, modifiers: Modifiers::NONE, state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), } } } diff --git a/src/parse.rs b/src/parse.rs index 4c0e3b3..73da5b4 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -39,8 +39,8 @@ use crate::{ dcs, osc, }, event::{ - KeyCode, KeyEvent, KeyEventKind, KeyEventState, MediaKeyCode, ModifierKeyCode, Modifiers, - MouseButton, MouseEvent, MouseEventKind, + KeyCode, KeyEvent, KeyEventEnhancements, KeyEventKind, KeyEventState, MediaKeyCode, + ModifierKeyCode, Modifiers, MouseButton, MouseEvent, MouseEventKind, }, style, Event, }; @@ -295,6 +295,7 @@ fn parse_csi(buffer: &[u8]) -> Result> { modifiers: Modifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), })), b'M' => return parse_csi_normal_mouse(buffer), b'<' => return parse_csi_sgr_mouse(buffer), @@ -421,26 +422,48 @@ fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { // enabled progressively. The full sequence is parsed as: // // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u - let mut codepoints = split.next().ok_or(MalformedSequenceError)?.split(':'); + let first_part = split.next().ok_or(MalformedSequenceError)?; + let parts: Vec<&str> = first_part.split(':').collect(); + if parts.len() > 3 { + bail!(); + } - let codepoint = codepoints - .next() - .ok_or(MalformedSequenceError)? + let codepoint = parts[0] .parse::() .map_err(|_| MalformedSequenceError)?; - let (mut modifiers, kind, state_from_modifiers) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { + let (modifiers, kind, state_from_modifiers) = match split.next() { + None | Some("") => (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE), + Some(modifier_part) => { + let mut sub_split = modifier_part.split(':'); + let modifier_mask_str = sub_split.next().ok_or(MalformedSequenceError)?; + let modifier_mask = modifier_mask_str + .parse::() + .map_err(|_| MalformedSequenceError)?; + if modifier_mask == 0 || modifier_mask > 256 { + bail!(); + } + let kind_code = if let Some(kind_str) = sub_split.next() { + let k = kind_str.parse::().map_err(|_| MalformedSequenceError)?; + if !(1..=3).contains(&k) { + bail!(); + } + k + } else { + 1 + }; + if sub_split.next().is_some() { + bail!(); + } ( parse_modifiers(modifier_mask), parse_key_event_kind(kind_code), parse_modifiers_to_state(modifier_mask), ) - } else { - (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) - }; + } + }; - let (mut code, state_from_keycode) = { + let (code, state_from_keycode) = { if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) { (special_key_code, state) } else if let Some(c) = char::from_u32(codepoint) { @@ -448,13 +471,6 @@ fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { match c { '\x1B' => KeyCode::Escape, '\r' => KeyCode::Enter, - /* - // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get - // newlines as input is because the terminal converts \r into \n for us. When we - // enter raw mode, we disable that, so \n no longer has any meaning - it's better to - // use Ctrl+J. Waiting to handle it here means it gets picked up later - '\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter, - */ '\t' => { if modifiers.contains(Modifiers::SHIFT) { KeyCode::BackTab @@ -472,43 +488,43 @@ fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { } }; - if let KeyCode::Modifier(modifier_keycode) = code { - match modifier_keycode { - ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => { - modifiers.set(Modifiers::ALT, true) - } - ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => { - modifiers.set(Modifiers::CONTROL, true) - } - ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => { - modifiers.set(Modifiers::SHIFT, true) - } - ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => { - modifiers.set(Modifiers::SUPER, true) - } - ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => { - modifiers.set(Modifiers::HYPER, true) - } - ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => { - modifiers.set(Modifiers::META, true) + let shifted_key = if parts.len() > 1 && !parts[1].is_empty() { + let cp = parts[1] + .parse::() + .map_err(|_| MalformedSequenceError)?; + Some(char::from_u32(cp).ok_or(MalformedSequenceError)?) + } else { + None + }; + + let base_layout_key = if parts.len() > 2 && !parts[2].is_empty() { + let cp = parts[2] + .parse::() + .map_err(|_| MalformedSequenceError)?; + Some(char::from_u32(cp).ok_or(MalformedSequenceError)?) + } else { + None + }; + + let associated_text = match split.next() { + None => None, + Some(text_str) => { + if text_str.is_empty() { + Some(String::new()) + } else { + let mut text = String::new(); + for part in text_str.split(':') { + let cp = part.parse::().map_err(|_| MalformedSequenceError)?; + let c = char::from_u32(cp).ok_or(MalformedSequenceError)?; + text.push(c); + } + Some(text) } - _ => {} } - } + }; - // When the "report alternate keys" flag is enabled in the Kitty Keyboard Protocol - // and the terminal sends a keyboard event containing shift, the sequence will - // contain an additional codepoint separated by a ':' character which contains - // the shifted character according to the keyboard layout. - if modifiers.contains(Modifiers::SHIFT) { - if let Some(shifted_c) = codepoints - .next() - .and_then(|codepoint| codepoint.parse::().ok()) - .and_then(char::from_u32) - { - code = KeyCode::Char(shifted_c); - modifiers.set(Modifiers::SHIFT, false); - } + if split.next().is_some() { + bail!(); } let event = Event::Key(KeyEvent { @@ -516,12 +532,17 @@ fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { modifiers, kind, state: state_from_keycode | state_from_modifiers, + enhancements: KeyEventEnhancements { + shifted_key, + base_layout_key, + associated_text, + }, }); Ok(Some(event)) } -fn parse_modifiers(mask: u8) -> Modifiers { +fn parse_modifiers(mask: u16) -> Modifiers { let modifier_mask = mask.saturating_sub(1); let mut modifiers = Modifiers::empty(); if modifier_mask & 1 != 0 { @@ -545,7 +566,7 @@ fn parse_modifiers(mask: u8) -> Modifiers { modifiers } -fn parse_modifiers_to_state(mask: u8) -> KeyEventState { +fn parse_modifiers_to_state(mask: u16) -> KeyEventState { let modifier_mask = mask.saturating_sub(1); let mut state = KeyEventState::empty(); if modifier_mask & 64 != 0 { @@ -576,7 +597,7 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { let (modifiers, kind) = if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { ( - parse_modifiers(modifier_mask), + parse_modifiers(modifier_mask as u16), parse_key_event_kind(kind_code), ) } else if buffer.len() > 3 { @@ -584,7 +605,7 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { parse_modifiers( (buffer[buffer.len() - 2] as char) .to_digit(10) - .ok_or(MalformedSequenceError)? as u8, + .ok_or(MalformedSequenceError)? as u16, ), KeyEventKind::Press, ) @@ -612,6 +633,7 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { modifiers, kind, state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), }); Ok(Some(event)) @@ -630,9 +652,9 @@ fn parse_csi_special_key_code(buffer: &[u8]) -> Result> { let (modifiers, kind, state) = if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { ( - parse_modifiers(modifier_mask), + parse_modifiers(modifier_mask as u16), parse_key_event_kind(kind_code), - parse_modifiers_to_state(modifier_mask), + parse_modifiers_to_state(modifier_mask as u16), ) } else { (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) @@ -658,6 +680,7 @@ fn parse_csi_special_key_code(buffer: &[u8]) -> Result> { modifiers, kind, state, + enhancements: KeyEventEnhancements::new(), }); Ok(Some(event)) @@ -1378,4 +1401,171 @@ mod test { let event = parse_event(b"\x1b[200~\x1b[201~", false).unwrap(); assert_eq!(event, Some(Event::Paste("".to_string()))); } + + #[test] + fn parse_kitty_csi_u_metadata() { + // Shift+1: canonical '1' (49), shifted '!' (33), Shift modifier (2) + let event = parse_event(b"\x1b[49:33;2u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('1'), + modifiers: Modifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + enhancements: KeyEventEnhancements { + shifted_key: Some('!'), + base_layout_key: None, + associated_text: None, + } + }) + ); + + // Shift+1 with base layout: canonical '1' (49), shifted '!' (33), base layout '2' (50), Shift modifier (2) + let event = parse_event(b"\x1b[49:33:50;2u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('1'), + modifiers: Modifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + enhancements: KeyEventEnhancements { + shifted_key: Some('!'), + base_layout_key: Some('2'), + associated_text: None, + } + }) + ); + + // Omitted shifted key: canonical '1' (49), empty shifted key, base layout '2' (50), Shift modifier (2) + let event = parse_event(b"\x1b[49::50;2u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('1'), + modifiers: Modifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + enhancements: KeyEventEnhancements { + shifted_key: None, + base_layout_key: Some('2'), + associated_text: None, + } + }) + ); + + // Associated text "abc" (codepoints 97, 98, 99) with Shift+1 + let event = parse_event(b"\x1b[49;2;97:98:99u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('1'), + modifiers: Modifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + enhancements: KeyEventEnhancements { + shifted_key: None, + base_layout_key: None, + associated_text: Some("abc".to_string()), + } + }) + ); + + // Malformed Unicode scalar values in associated text must fail + assert!(parse_event(b"\x1b[49;2;1114112u", false).is_err()); + assert!(parse_event(b"\x1b[49;2;55296u", false).is_err()); + + // Test encoded mask 256 (all modifiers set) + let event = parse_event(b"\x1b[97;256u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: Modifiers::SHIFT + | Modifiers::ALT + | Modifiers::CONTROL + | Modifiers::SUPER + | Modifiers::HYPER + | Modifiers::META, + kind: KeyEventKind::Press, + state: KeyEventState::CAPS_LOCK | KeyEventState::NUM_LOCK, + enhancements: KeyEventEnhancements::new(), + }) + ); + + // Test malformed modifier/kind + assert!(parse_event(b"\x1b[97;0u", false).is_err()); // modifier mask 0 is invalid + assert!(parse_event(b"\x1b[97;257u", false).is_err()); // modifier mask > 256 is invalid + assert!(parse_event(b"\x1b[97;2:4u", false).is_err()); // event kind 4 is invalid + assert!(parse_event(b"\x1b[97;au", false).is_err()); // malformed modifier + assert!(parse_event(b"\x1b[97;2:au", false).is_err()); // malformed kind + assert!(parse_event(b"\x1b[97;2:1:1u", false).is_err()); // excess subfields in modifier parameter + + // Test excess fields + assert!(parse_event(b"\x1b[97:33:50:99;2u", false).is_err()); // >3 alternate-key subfields + assert!(parse_event(b"\x1b[97;2;97;98u", false).is_err()); // trailing semicolon fields after associated text + assert!(parse_event(b"\x1b[97;2;97;;u", false).is_err()); // trailing semicolon fields after associated text + assert!(parse_event(b"\x1b[97;2;;;u", false).is_err()); // trailing semicolon field after empty associated text + + // Test empty/missing optional fields + let event = parse_event(b"\x1b[97u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: Modifiers::NONE, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), + }) + ); + + let event = parse_event(b"\x1b[97;u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: Modifiers::NONE, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), + }) + ); + + let event = parse_event(b"\x1b[97;;u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: Modifiers::NONE, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + enhancements: KeyEventEnhancements { + shifted_key: None, + base_layout_key: None, + associated_text: Some("".to_string()), + }, + }) + ); + + assert!(parse_event(b"\x1b[97;;;u", false).is_err()); + + // Test functional PUA alternate retained as char (e.g. 57399 is U+E037) + let event = parse_event(b"\x1b[97:57399;2u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: Modifiers::SHIFT, + kind: KeyEventKind::Press, + state: KeyEventState::NONE, + enhancements: KeyEventEnhancements { + shifted_key: Some('\u{E037}'), + base_layout_key: None, + associated_text: None, + } + }) + ); + } } diff --git a/src/parse/windows.rs b/src/parse/windows.rs index 292e38e..5ffddb5 100644 --- a/src/parse/windows.rs +++ b/src/parse/windows.rs @@ -319,6 +319,7 @@ pub(crate) mod legacy { modifiers, kind, state: KeyEventState::empty(), + enhancements: KeyEventEnhancements::new(), }; return Some(WindowsKeyEvent::KeyEvent(key_event)); } @@ -387,6 +388,7 @@ pub(crate) mod legacy { modifiers, kind, state: KeyEventState::empty(), + enhancements: KeyEventEnhancements::new(), }; return Some(WindowsKeyEvent::KeyEvent(key_event)); } From e8d7226b600b4c86481b1ffb68c472b92db57ed9 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 13 Jul 2026 10:12:07 +0200 Subject: [PATCH 2/4] Fix Windows legacy key metadata import --- src/parse/windows.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parse/windows.rs b/src/parse/windows.rs index 5ffddb5..ba14be8 100644 --- a/src/parse/windows.rs +++ b/src/parse/windows.rs @@ -125,8 +125,8 @@ pub(crate) mod legacy { use std::{io, ptr}; use crate::event::{ - KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseButton, MouseEvent, - MouseEventKind, + KeyCode, KeyEvent, KeyEventEnhancements, KeyEventKind, KeyEventState, Modifiers, MouseButton, + MouseEvent, MouseEventKind, }; use crate::{Event, OneBased}; use windows_sys::Win32::Foundation::{ From 9207a77bd5f30a8fe6f2a5b44d51787992337359 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 13 Jul 2026 10:13:38 +0200 Subject: [PATCH 3/4] Document empty associated key text --- src/event.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/event.rs b/src/event.rs index 5808e5e..9298619 100644 --- a/src/event.rs +++ b/src/event.rs @@ -131,6 +131,9 @@ pub struct KeyEventEnhancements { pub base_layout_key: Option, /// The Unicode text associated with the key event. + /// + /// `None` means the protocol omitted the associated-text field. An empty string means the + /// field was present but reported no generated text. pub associated_text: Option, } From a6f8068c0860aa81bfce18704f87c087e19b4ccb Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 13 Jul 2026 10:28:27 +0200 Subject: [PATCH 4/4] Validate enhanced keyboard parameters --- src/parse.rs | 177 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 114 insertions(+), 63 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 73da5b4..d80acea 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -390,16 +390,26 @@ where .map_err(|_| MalformedSequenceError) } -fn modifier_and_kind_parsed(iter: &mut dyn Iterator) -> Result<(u8, u8)> { - let mut sub_split = iter.next().ok_or(MalformedSequenceError)?.split(':'); - - let modifier_mask = next_parsed::(&mut sub_split)?; - - if let Ok(kind_code) = next_parsed::(&mut sub_split) { - Ok((modifier_mask, kind_code)) - } else { - Ok((modifier_mask, 1)) +fn modifier_and_kind_parsed(parameter: &str) -> Result<(u16, u8)> { + let mut fields = parameter.split(':'); + let modifier_mask = next_parsed::(&mut fields)?; + if !(1..=256).contains(&modifier_mask) { + bail!(); + } + let kind = match fields.next() { + Some(field) => { + let kind = field.parse::().map_err(|_| MalformedSequenceError)?; + if !(1..=3).contains(&kind) { + bail!(); + } + kind + } + None => 1, + }; + if fields.next().is_some() { + bail!(); } + Ok((modifier_mask, kind)) } fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { @@ -435,26 +445,7 @@ fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { let (modifiers, kind, state_from_modifiers) = match split.next() { None | Some("") => (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE), Some(modifier_part) => { - let mut sub_split = modifier_part.split(':'); - let modifier_mask_str = sub_split.next().ok_or(MalformedSequenceError)?; - let modifier_mask = modifier_mask_str - .parse::() - .map_err(|_| MalformedSequenceError)?; - if modifier_mask == 0 || modifier_mask > 256 { - bail!(); - } - let kind_code = if let Some(kind_str) = sub_split.next() { - let k = kind_str.parse::().map_err(|_| MalformedSequenceError)?; - if !(1..=3).contains(&k) { - bail!(); - } - k - } else { - 1 - }; - if sub_split.next().is_some() { - bail!(); - } + let (modifier_mask, kind_code) = modifier_and_kind_parsed(modifier_part)?; ( parse_modifiers(modifier_mask), parse_key_event_kind(kind_code), @@ -594,24 +585,28 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { split.next(); - let (modifiers, kind) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { - ( - parse_modifiers(modifier_mask as u16), - parse_key_event_kind(kind_code), - ) - } else if buffer.len() > 3 { - ( - parse_modifiers( - (buffer[buffer.len() - 2] as char) - .to_digit(10) - .ok_or(MalformedSequenceError)? as u16, - ), - KeyEventKind::Press, - ) - } else { - (Modifiers::NONE, KeyEventKind::Press) - }; + let (modifiers, kind, state) = if let Some(parameter) = split.next() { + let (modifier_mask, kind_code) = modifier_and_kind_parsed(parameter)?; + ( + parse_modifiers(modifier_mask), + parse_key_event_kind(kind_code), + parse_modifiers_to_state(modifier_mask), + ) + } else if buffer.len() > 3 { + let modifier_mask = (buffer[buffer.len() - 2] as char) + .to_digit(10) + .ok_or(MalformedSequenceError)? as u16; + ( + parse_modifiers(modifier_mask), + KeyEventKind::Press, + parse_modifiers_to_state(modifier_mask), + ) + } else { + (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) + }; + if split.next().is_some() { + bail!(); + } let key = buffer[buffer.len() - 1]; let code = match key { @@ -632,7 +627,7 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { code, modifiers, kind, - state: KeyEventState::NONE, + state, enhancements: KeyEventEnhancements::new(), }); @@ -649,16 +644,19 @@ fn parse_csi_special_key_code(buffer: &[u8]) -> Result> { // This CSI sequence can be a list of semicolon-separated numbers. let first = next_parsed::(&mut split)?; - let (modifiers, kind, state) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { - ( - parse_modifiers(modifier_mask as u16), - parse_key_event_kind(kind_code), - parse_modifiers_to_state(modifier_mask as u16), - ) - } else { - (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) - }; + let (modifiers, kind, state) = if let Some(parameter) = split.next() { + let (modifier_mask, kind_code) = modifier_and_kind_parsed(parameter)?; + ( + parse_modifiers(modifier_mask), + parse_key_event_kind(kind_code), + parse_modifiers_to_state(modifier_mask), + ) + } else { + (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) + }; + if split.next().is_some() { + bail!(); + } let code = match first { 1 | 7 => KeyCode::Home, @@ -1008,7 +1006,9 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result> return Ok(None); } - let bits = buffer[3]; + let bits = str::from_utf8(&buffer[3..buffer.len() - 1])? + .parse::() + .map_err(|_| MalformedSequenceError)?; let mut flags = KittyKeyboardFlags::empty(); if bits & 1 != 0 { @@ -1023,10 +1023,9 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result> if bits & 8 != 0 { flags |= KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; } - // TODO: support this - // if bits & 16 != 0 { - // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; - // } + if bits & 16 != 0 { + flags |= KittyKeyboardFlags::REPORT_ASSOCIATED_TEXT; + } Ok(Some(Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags( flags, @@ -1568,4 +1567,56 @@ mod test { }) ); } + + #[test] + fn parse_modifier_key_state_matches_protocol_event_state() { + let Event::Key(press) = parse_event(b"\x1b[57443;3:1u", false).unwrap().unwrap() else { + panic!("expected key event"); + }; + assert_eq!(press.code, KeyCode::Modifier(ModifierKeyCode::LeftAlt)); + assert_eq!(press.kind, KeyEventKind::Press); + assert_eq!(press.modifiers, Modifiers::ALT); + + let Event::Key(release) = parse_event(b"\x1b[57443;1:3u", false).unwrap().unwrap() else { + panic!("expected key event"); + }; + assert_eq!(release.code, KeyCode::Modifier(ModifierKeyCode::LeftAlt)); + assert_eq!(release.kind, KeyEventKind::Release); + assert_eq!(release.modifiers, Modifiers::NONE); + } + + #[test] + fn parse_enhanced_legacy_key_modifiers_strictly() { + let Event::Key(key) = parse_event(b"\x1b[1;256A", false).unwrap().unwrap() else { + panic!("expected key event"); + }; + assert_eq!(key.code, KeyCode::Up); + assert_eq!( + key.state, + KeyEventState::CAPS_LOCK | KeyEventState::NUM_LOCK + ); + + for malformed in [ + b"\x1b[1;0A".as_slice(), + b"\x1b[1;257A", + b"\x1b[1;2:0A", + b"\x1b[1;2:4A", + b"\x1b[1;2:1:1A", + b"\x1b[1;2;3A", + b"\x1b[3;2:4~", + ] { + assert!(parse_event(malformed, false).is_err(), "{malformed:?}"); + } + } + + #[test] + fn parse_all_keyboard_enhancement_flags() { + let event = parse_event(b"\x1b[?31u", false).unwrap().unwrap(); + assert_eq!( + event, + Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags( + KittyKeyboardFlags::all() + ))) + ); + } }