diff --git a/src/event.rs b/src/event.rs index 55dc0a9..9298619 100644 --- a/src/event.rs +++ b/src/event.rs @@ -118,6 +118,36 @@ 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. + /// + /// `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, +} + +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 +173,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 +197,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 +210,7 @@ impl KeyEvent { modifiers, kind: KeyEventKind::Press, state: KeyEventState::NONE, + enhancements: KeyEventEnhancements::new(), } } } @@ -188,6 +222,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..d80acea 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), @@ -389,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> { @@ -421,26 +432,29 @@ 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 (modifier_mask, kind_code) = modifier_and_kind_parsed(modifier_part)?; ( 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 +462,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 +479,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 +523,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 +557,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 { @@ -573,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), - 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 u8, - ), - 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 { @@ -611,7 +627,8 @@ fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { code, modifiers, kind, - state: KeyEventState::NONE, + state, + enhancements: KeyEventEnhancements::new(), }); Ok(Some(event)) @@ -627,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), - parse_key_event_kind(kind_code), - parse_modifiers_to_state(modifier_mask), - ) - } 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, @@ -658,6 +678,7 @@ fn parse_csi_special_key_code(buffer: &[u8]) -> Result> { modifiers, kind, state, + enhancements: KeyEventEnhancements::new(), }); Ok(Some(event)) @@ -985,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 { @@ -1000,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, @@ -1378,4 +1400,223 @@ 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, + } + }) + ); + } + + #[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() + ))) + ); + } } diff --git a/src/parse/windows.rs b/src/parse/windows.rs index 292e38e..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::{ @@ -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)); }