use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEventKind}; use super::{KeyboardProtocol, MouseProtocolEncoding, TerminalKey}; const KITTY_FLAG_REPORT_EVENT_TYPES: u16 = 0b0000_0010; const KITTY_FLAG_REPORT_ALTERNATE_KEYS: u16 = 0b0000_0100; const KITTY_FLAG_REPORT_ALL_KEYS: u16 = 0b0000_1000; /// Encode a key event for a PTY child using the pane's negotiated keyboard protocol. #[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers pub fn encode_key(key: KeyEvent, protocol: KeyboardProtocol) -> Vec { encode_terminal_key(key.into(), protocol) } pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec { // A release event only produces bytes when the pane protocol reports event // types (Kitty REPORT_EVENT_TYPES). Otherwise the child expects a single // legacy byte per keystroke, so re-emitting it on release would double keys // like Enter/Backspace. The Ghostty wrapper can route release events through // this fallback, so guard the fallback encoder too. if key.kind == crossterm::event::KeyEventKind::Release && !protocol.reports_event_types() { return Vec::new(); } if let Some(bytes) = encode_text_input(&key) { return bytes; } if let KeyboardProtocol::Kitty { flags } = protocol { if let Some(bytes) = try_encode_csi_u(&key, flags) { return bytes; } } if key.kind == crossterm::event::KeyEventKind::Release && protocol.reports_event_types() { return Vec::new(); } encode_legacy(key) } #[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers pub fn encode_cursor_key(code: KeyCode, application_cursor: bool) -> Vec { match (code, application_cursor) { (KeyCode::Up, true) => b"\x1bOA".to_vec(), (KeyCode::Down, true) => b"\x1bOB".to_vec(), (KeyCode::Right, true) => b"\x1bOC".to_vec(), (KeyCode::Left, true) => b"\x1bOD".to_vec(), (KeyCode::Up, false) => b"\x1b[A".to_vec(), (KeyCode::Down, false) => b"\x1b[B".to_vec(), (KeyCode::Right, false) => b"\x1b[C".to_vec(), (KeyCode::Left, false) => b"\x1b[D".to_vec(), _ => encode_legacy(KeyEvent::new(code, KeyModifiers::empty()).into()), } } #[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers pub fn encode_mouse_scroll( kind: MouseEventKind, column: u16, row: u16, modifiers: KeyModifiers, encoding: MouseProtocolEncoding, ) -> Option> { let button = match kind { MouseEventKind::ScrollUp => 64u16, MouseEventKind::ScrollDown => 65u16, MouseEventKind::ScrollLeft => 66u16, MouseEventKind::ScrollRight => 67u16, _ => return None, }; encode_mouse_cb(button, false, column, row, modifiers, encoding) } #[allow(dead_code)] // exercised in input unit tests; pane runtime uses backend helpers pub fn encode_mouse_button( kind: MouseEventKind, column: u16, row: u16, modifiers: KeyModifiers, encoding: MouseProtocolEncoding, ) -> Option> { let (button, release) = match kind { MouseEventKind::Down(MouseButton::Left) => (0u16, false), MouseEventKind::Down(MouseButton::Middle) => (1u16, false), MouseEventKind::Down(MouseButton::Right) => (2u16, false), MouseEventKind::Up(MouseButton::Left) => (0u16, true), MouseEventKind::Up(MouseButton::Middle) => (1u16, true), MouseEventKind::Up(MouseButton::Right) => (2u16, true), MouseEventKind::Drag(MouseButton::Left) => (32u16, false), MouseEventKind::Drag(MouseButton::Middle) => (33u16, false), MouseEventKind::Drag(MouseButton::Right) => (34u16, false), _ => return None, }; encode_mouse_cb(button, release, column, row, modifiers, encoding) } #[allow(dead_code)] // only reached through mouse encoding helpers above fn encode_mouse_cb( base_button: u16, release: bool, column: u16, row: u16, modifiers: KeyModifiers, encoding: MouseProtocolEncoding, ) -> Option> { let mut cb = match (encoding, release) { (MouseProtocolEncoding::Sgr, true) => base_button, (_, true) => 3, (_, false) => base_button, }; if modifiers.contains(KeyModifiers::SHIFT) { cb += 4; } if modifiers.contains(KeyModifiers::ALT) { cb += 8; } if modifiers.contains(KeyModifiers::CONTROL) { cb += 16; } let column = column as u32 + 1; let row = row as u32 + 1; match encoding { MouseProtocolEncoding::Sgr => Some( format!( "\x1b[<{cb};{column};{row}{}", if release { 'm' } else { 'M' } ) .into_bytes(), ), MouseProtocolEncoding::Default => { let cb = u8::try_from(cb + 32).ok()?; let column = u8::try_from(column + 32).ok()?; let row = u8::try_from(row + 32).ok()?; Some(vec![0x1b, b'[', b'M', cb, column, row]) } MouseProtocolEncoding::Utf8 => { let mut bytes = Vec::with_capacity(16); bytes.extend_from_slice(b"\x1b[M"); push_mouse_codepoint(&mut bytes, cb as u32 + 32)?; push_mouse_codepoint(&mut bytes, column + 32)?; push_mouse_codepoint(&mut bytes, row + 32)?; Some(bytes) } } } #[allow(dead_code)] // only reached through mouse encoding helpers above fn push_mouse_codepoint(bytes: &mut Vec, value: u32) -> Option<()> { let ch = char::from_u32(value)?; let mut buf = [0u8; 4]; bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes()); Some(()) } /// CSI u encoding: \e[{codepoint};{modifiers}u /// Used when the child has pushed Kitty keyboard enhancement. /// Returns None if the key doesn't need CSI u (unmodified basic keys). fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option> { let mods = key.modifiers; let event_suffix = kitty_event_suffix(key, flags); let report_all_keys = flags & KITTY_FLAG_REPORT_ALL_KEYS != 0; if !report_all_keys && key.modifiers.is_empty() && matches!(key.code, KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace) { return None; } // Unmodified keys use legacy encoding (more compatible) if mods.is_empty() && event_suffix.is_none() && !report_all_keys { return None; } // Special keys (arrows, F-keys, etc.) have well-established legacy // xterm modified formats (\x1b[1;3A for Alt+Up, etc.) that are universally // understood. Even Ghostty sends these in legacy format with kitty mode on. // Only use CSI u for character keys and keys without legacy representations. match key.code { KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right | KeyCode::Home | KeyCode::End | KeyCode::PageUp | KeyCode::PageDown | KeyCode::Insert | KeyCode::Delete | KeyCode::F(_) if event_suffix.is_none() && !report_all_keys => { return None; // let legacy handle these } _ => {} } let (codepoint, alternate_shifted) = match key.code { KeyCode::Char(c) => { let base = canonical_kitty_char(c, mods); let shifted = alternate_shifted_codepoint(key, flags); (base as u32, shifted) } KeyCode::Enter => (13, None), KeyCode::Tab => (9, None), KeyCode::Backspace => (127, None), KeyCode::Esc => (27, None), KeyCode::Left => (57417, None), KeyCode::Right => (57418, None), KeyCode::Up => (57419, None), KeyCode::Down => (57420, None), KeyCode::PageUp => (57421, None), KeyCode::PageDown => (57422, None), KeyCode::Home => (57423, None), KeyCode::End => (57424, None), KeyCode::Insert => (57425, None), KeyCode::Delete => (57426, None), _ => return None, // fall back to legacy for unhandled keys }; let modifier = kitty_modifier(mods); let sequence = match (alternate_shifted, event_suffix) { (Some(shifted), Some(event)) => format!("\x1b[{codepoint}:{shifted};{modifier}:{event}u"), (Some(shifted), None) => format!("\x1b[{codepoint}:{shifted};{modifier}u"), (None, Some(event)) => format!("\x1b[{codepoint};{modifier}:{event}u"), (None, None) => format!("\x1b[{codepoint};{modifier}u"), }; Some(sequence.into_bytes()) } /// Legacy terminal encoding (standard escape sequences). fn encode_legacy(key: TerminalKey) -> Vec { let mods = key.modifiers; // Modified special keys (arrows, home, end, etc.) use xterm format: // \x1b[1;{modifier}A for arrows/home/end // \x1b[{n};{modifier}~ for insert/delete/pgup/pgdn // The ESC-prefix hack doesn't work for these since they're already escape sequences. if !mods.is_empty() { if let Some(bytes) = encode_modified_special(key.code, mods) { return bytes; } } // Alt modifier on character keys: prefix with ESC if mods.contains(KeyModifiers::ALT) { let inner = TerminalKey { modifiers: mods.difference(KeyModifiers::ALT), ..key }; let mut bytes = vec![0x1b]; bytes.extend(encode_legacy_inner(inner)); return bytes; } encode_legacy_inner(key) } /// xterm-style encoding for modified special keys. /// Modifier value: 1 + (shift?1:0) + (alt?2:0) + (ctrl?4:0) fn encode_modified_special(code: KeyCode, mods: KeyModifiers) -> Option> { let modifier = xterm_modifier(mods); if modifier <= 1 { return None; // no modifiers to encode } match code { // CSI 1;{mod}{letter} format KeyCode::Up => Some(format!("\x1b[1;{modifier}A").into_bytes()), KeyCode::Down => Some(format!("\x1b[1;{modifier}B").into_bytes()), KeyCode::Right => Some(format!("\x1b[1;{modifier}C").into_bytes()), KeyCode::Left => Some(format!("\x1b[1;{modifier}D").into_bytes()), KeyCode::Home => Some(format!("\x1b[1;{modifier}H").into_bytes()), KeyCode::End => Some(format!("\x1b[1;{modifier}F").into_bytes()), // CSI {n};{mod}~ format KeyCode::Insert => Some(format!("\x1b[2;{modifier}~").into_bytes()), KeyCode::Delete => Some(format!("\x1b[3;{modifier}~").into_bytes()), KeyCode::PageUp => Some(format!("\x1b[5;{modifier}~").into_bytes()), KeyCode::PageDown => Some(format!("\x1b[6;{modifier}~").into_bytes()), // F1-F4: CSI 1;{mod}{P-S} KeyCode::F(1) => Some(format!("\x1b[1;{modifier}P").into_bytes()), KeyCode::F(2) => Some(format!("\x1b[1;{modifier}Q").into_bytes()), KeyCode::F(3) => Some(format!("\x1b[1;{modifier}R").into_bytes()), KeyCode::F(4) => Some(format!("\x1b[1;{modifier}S").into_bytes()), // F5-F12: CSI {n};{mod}~ KeyCode::F(n @ 5..=12) => { let code = match n { 5 => 15, 6 => 17, 7 => 18, 8 => 19, 9 => 20, 10 => 21, 11 => 23, 12 => 24, _ => unreachable!(), }; Some(format!("\x1b[{code};{modifier}~").into_bytes()) } _ => None, } } /// xterm modifier encoding: 1 + shift(1) + alt(2) + ctrl(4) /// Used for legacy modified special keys (arrows, function keys, etc.) fn xterm_modifier(mods: KeyModifiers) -> u32 { let mut m = 1u32; if mods.contains(KeyModifiers::SHIFT) { m += 1; } if mods.contains(KeyModifiers::ALT) { m += 2; } if mods.contains(KeyModifiers::CONTROL) { m += 4; } m } /// Kitty protocol modifier encoding: 1 + shift(1) + alt(2) + ctrl(4) + super(8) + hyper(16) + meta(32) /// Superset of xterm — adds Super/Hyper/Meta bits. fn kitty_modifier(mods: KeyModifiers) -> u32 { let mut m = xterm_modifier(mods); if mods.contains(KeyModifiers::SUPER) { m += 8; } if mods.contains(KeyModifiers::HYPER) { m += 16; } if mods.contains(KeyModifiers::META) { m += 32; } m } fn encode_text_input(key: &TerminalKey) -> Option> { let ch = match key.code { KeyCode::Char(ch) => ch, _ => return None, }; if key.modifiers.is_empty() { match key.kind { crossterm::event::KeyEventKind::Press | crossterm::event::KeyEventKind::Repeat => { let mut buf = [0u8; 4]; return Some(ch.encode_utf8(&mut buf).as_bytes().to_vec()); } crossterm::event::KeyEventKind::Release => return Some(Vec::new()), } } if key.modifiers != KeyModifiers::SHIFT { return None; } let shifted_ch = shifted_text_char(key, ch)?; match key.kind { crossterm::event::KeyEventKind::Press | crossterm::event::KeyEventKind::Repeat => { let mut buf = [0u8; 4]; Some(shifted_ch.encode_utf8(&mut buf).as_bytes().to_vec()) } crossterm::event::KeyEventKind::Release => Some(Vec::new()), } } fn shifted_text_char(key: &TerminalKey, ch: char) -> Option { if let Some(shifted) = key.shifted_codepoint.and_then(char::from_u32) { return Some(shifted); } if ch.is_ascii_uppercase() { return Some(ch); } if ch.is_ascii_lowercase() { return Some(ch.to_ascii_uppercase()); } None } fn canonical_kitty_char(ch: char, mods: KeyModifiers) -> char { if mods.contains(KeyModifiers::SHIFT) && ch.is_ascii_uppercase() { ch.to_ascii_lowercase() } else { ch } } fn alternate_shifted_codepoint(key: &TerminalKey, flags: u16) -> Option { if flags & KITTY_FLAG_REPORT_ALTERNATE_KEYS == 0 { return None; } if let Some(shifted) = key.shifted_codepoint { return Some(shifted); } match key.code { KeyCode::Char(ch) if key.modifiers.contains(KeyModifiers::SHIFT) && ch.is_ascii_uppercase() => { Some(ch as u32) } _ => None, } } fn kitty_event_suffix(key: &TerminalKey, flags: u16) -> Option { if flags & KITTY_FLAG_REPORT_EVENT_TYPES == 0 { return None; } Some(match key.kind { crossterm::event::KeyEventKind::Press => 1, crossterm::event::KeyEventKind::Repeat => 2, crossterm::event::KeyEventKind::Release => 3, }) } fn encode_legacy_inner(key: TerminalKey) -> Vec { match key.code { KeyCode::Char(ch) => { if key.modifiers.contains(KeyModifiers::CONTROL) { let upper = ch.to_ascii_uppercase(); match upper { 'A'..='Z' => vec![upper as u8 - 64], ' ' | '@' | '2' => vec![0], '[' | '3' => vec![27], '\\' | '4' => vec![28], ']' | '5' => vec![29], '^' | '6' => vec![30], '_' | '/' | '7' | '-' => vec![31], _ => vec![ch as u8], } } else { let ch = if key.modifiers == KeyModifiers::SHIFT { shifted_text_char(&key, ch).unwrap_or(ch) } else { ch }; let mut buf = [0u8; 4]; ch.encode_utf8(&mut buf).as_bytes().to_vec() } } KeyCode::Enter => vec![b'\r'], KeyCode::Backspace => vec![127], KeyCode::Tab => vec![9], KeyCode::BackTab => vec![27, 91, 90], KeyCode::Esc => vec![27], KeyCode::Left => vec![27, 91, 68], KeyCode::Right => vec![27, 91, 67], KeyCode::Up => vec![27, 91, 65], KeyCode::Down => vec![27, 91, 66], KeyCode::Home => vec![27, 91, 72], KeyCode::End => vec![27, 91, 70], KeyCode::PageUp => vec![27, 91, 53, 126], KeyCode::PageDown => vec![27, 91, 54, 126], KeyCode::Delete => vec![27, 91, 51, 126], KeyCode::Insert => vec![27, 91, 50, 126], KeyCode::F(n) => encode_f_key(n), _ => vec![], } } fn encode_f_key(n: u8) -> Vec { match n { 1 => vec![27, 79, 80], 2 => vec![27, 79, 81], 3 => vec![27, 79, 82], 4 => vec![27, 79, 83], 5 => vec![27, 91, 49, 53, 126], 6 => vec![27, 91, 49, 55, 126], 7 => vec![27, 91, 49, 56, 126], 8 => vec![27, 91, 49, 57, 126], 9 => vec![27, 91, 50, 48, 126], 10 => vec![27, 91, 50, 49, 126], 11 => vec![27, 91, 50, 51, 126], 12 => vec![27, 91, 50, 52, 126], _ => vec![], } } #[cfg(test)] mod tests { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use super::*; use crate::input::parse_terminal_key_sequence; fn assert_terminal_key_eq( actual: TerminalKey, code: KeyCode, modifiers: KeyModifiers, kind: crossterm::event::KeyEventKind, shifted_codepoint: Option, ) { assert_eq!(actual.code, code); assert_eq!(actual.modifiers, modifiers); assert_eq!(actual.kind, kind); assert_eq!(actual.shifted_codepoint, shifted_codepoint); } #[test] fn legacy_enter() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']); } #[test] fn legacy_ctrl_c() { let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![3]); } #[test] fn legacy_ctrl_slash_aliases_ctrl_underscore() { let key = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![31]); } #[test] fn legacy_shift_enter_is_just_cr() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![b'\r']); } #[test] fn legacy_alt_up() { let key = KeyEvent::new(KeyCode::Up, KeyModifiers::ALT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;3A"); } #[test] fn legacy_shift_right() { let key = KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;2C"); } #[test] fn legacy_ctrl_left() { let key = KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;5D"); } #[test] fn legacy_ctrl_shift_end() { let key = KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[1;6F"); } #[test] fn legacy_alt_delete() { let key = KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[3;3~"); } #[test] fn legacy_shift_f5() { let key = KeyEvent::new(KeyCode::F(5), KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[15;2~"); } #[test] fn legacy_alt_char_still_esc_prefix() { let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1ba"); } #[test] fn legacy_alt_shift_punctuation_uses_shifted_text() { let key = parse_terminal_key_sequence("\x1b[44:60;4u").unwrap(); assert_eq!(encode_terminal_key(key, KeyboardProtocol::Legacy), b"\x1b<"); } #[test] fn legacy_alt_backspace_sends_escape_delete() { let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b\x7f"); } #[test] fn application_cursor_keys_use_ss3_sequences() { assert_eq!(encode_cursor_key(KeyCode::Up, true), b"\x1bOA"); assert_eq!(encode_cursor_key(KeyCode::Down, true), b"\x1bOB"); } #[test] fn normal_cursor_keys_use_csi_sequences() { assert_eq!(encode_cursor_key(KeyCode::Up, false), b"\x1b[A"); assert_eq!(encode_cursor_key(KeyCode::Down, false), b"\x1b[B"); } #[test] fn sgr_mouse_scroll_encodes_wheel_button_and_coordinates() { let encoded = encode_mouse_scroll( crossterm::event::MouseEventKind::ScrollDown, 4, 6, KeyModifiers::SHIFT, MouseProtocolEncoding::Sgr, ) .expect("mouse scroll should encode"); assert_eq!(encoded, b"\x1b[<69;5;7M"); } #[test] fn sgr_mouse_release_keeps_button_code() { let encoded = encode_mouse_button( crossterm::event::MouseEventKind::Up(crossterm::event::MouseButton::Left), 11, 9, KeyModifiers::empty(), MouseProtocolEncoding::Sgr, ) .expect("mouse release should encode"); assert_eq!(encoded, b"\x1b[<0;12;10m"); } #[test] fn kitty_shift_enter() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[13;2u" ); } #[test] fn kitty_ctrl_shift_a() { let key = KeyEvent::new( KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::SHIFT, ); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[97;6u" ); } #[test] fn kitty_shift_uppercase_letter_sends_text() { let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"L"); } #[test] fn kitty_shift_uppercase_letter_ignores_alternate_key_reporting_for_text() { let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 7 }), b"L"); } #[test] fn kitty_shift_lowercase_letter_sends_uppercase_text() { let key = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"L"); } #[test] fn kitty_alt_shift_uppercase_letter_uses_base_codepoint() { let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::ALT | KeyModifiers::SHIFT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[108;4u" ); } #[test] fn kitty_ctrl_shift_uppercase_letter_uses_base_codepoint() { let key = KeyEvent::new( KeyCode::Char('L'), KeyModifiers::CONTROL | KeyModifiers::SHIFT, ); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[108;6u" ); } #[test] fn legacy_shift_uppercase_letter_stays_uppercase() { let key = KeyEvent::new(KeyCode::Char('L'), KeyModifiers::SHIFT); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"L"); } #[test] fn kitty_alt_enter() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::ALT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[13;3u" ); } #[test] fn kitty_alt_backspace_uses_csi_u() { let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[127;3u" ); } #[test] fn kitty_plain_ctrl_c_uses_csi_u() { let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[99;5u" ); } #[test] fn kitty_plain_ctrl_c_includes_press_event_when_requested() { let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 3 }), b"\x1b[99;5:1u" ); } #[test] fn kitty_unmodified_uses_legacy() { let key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()); assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"a"); } #[test] fn kitty_report_event_types_keeps_basic_compatibility_keys_legacy() { let cases = [ (KeyCode::Enter, b"\r".as_slice()), (KeyCode::Tab, b"\t".as_slice()), (KeyCode::Backspace, b"\x7f".as_slice()), ]; for (code, expected) in cases { let press = KeyEvent::new_with_kind( code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, ); assert_eq!( encode_key(press, KeyboardProtocol::Kitty { flags: 3 }), expected, "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS" ); let repeat = KeyEvent::new_with_kind( code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Repeat, ); assert_eq!( encode_key(repeat, KeyboardProtocol::Kitty { flags: 3 }), expected, "{code:?} repeat should stay legacy-compatible without REPORT_ALL_KEYS" ); let release = KeyEvent::new_with_kind( code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Release, ); assert_eq!( encode_key(release, KeyboardProtocol::Kitty { flags: 3 }), b"", "{code:?} release should not fall back to legacy bytes" ); } } #[test] fn kitty_report_all_keys_encodes_basic_compatibility_keys_with_events() { let enter_press = KeyEvent::new_with_kind( KeyCode::Enter, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, ); assert_eq!( encode_key(enter_press, KeyboardProtocol::Kitty { flags: 9 }), b"\x1b[13;1u" ); let backspace_press = KeyEvent::new_with_kind( KeyCode::Backspace, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, ); assert_eq!( encode_key(backspace_press, KeyboardProtocol::Kitty { flags: 11 }), b"\x1b[127;1:1u" ); let backspace_release = KeyEvent::new_with_kind( KeyCode::Backspace, KeyModifiers::empty(), crossterm::event::KeyEventKind::Release, ); assert_eq!( encode_key(backspace_release, KeyboardProtocol::Kitty { flags: 11 }), b"\x1b[127;1:3u" ); } #[test] fn kitty_shift_tab() { let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[9;2u" ); } #[test] fn kitty_ctrl_shift_enter() { let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"\x1b[13;6u" ); } #[test] fn kitty_repeat_event_type_is_encoded_when_requested() { let key = KeyEvent::new_with_kind( KeyCode::Enter, KeyModifiers::SHIFT, crossterm::event::KeyEventKind::Repeat, ); assert_eq!( encode_key(key, KeyboardProtocol::Kitty { flags: 3 }), b"\x1b[13;2:2u" ); } #[test] fn kitty_shift_letter_release_does_not_emit_text() { let key = KeyEvent::new_with_kind( KeyCode::Char('L'), KeyModifiers::SHIFT, crossterm::event::KeyEventKind::Release, ); assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 7 }), b""); } #[test] fn release_bytes_gated_on_report_event_types() { for code in [KeyCode::Enter, KeyCode::Backspace] { let release = KeyEvent::new_with_kind( code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Release, ); // Legacy and Kitty disambiguate-only (no REPORT_EVENT_TYPES) must not // emit a byte on release, otherwise Enter/Backspace double (issue #769). assert_eq!(encode_key(release, KeyboardProtocol::Legacy), b""); assert_eq!( encode_key(release, KeyboardProtocol::Kitty { flags: 1 }), b"" ); } let modified_release = KeyEvent::new_with_kind( KeyCode::Enter, KeyModifiers::CONTROL, crossterm::event::KeyEventKind::Release, ); assert_eq!( encode_key(modified_release, KeyboardProtocol::Kitty { flags: 3 }), b"\x1b[13;5:3u" ); } #[test] fn kitty_shifted_symbol_sends_text() { let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT) .with_shifted_codepoint('!' as u32); assert_eq!( encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }), b"!" ); } #[test] fn legacy_modified_special_roundtrip_matrix() { let cases = [ KeyEvent::new(KeyCode::Up, KeyModifiers::ALT), KeyEvent::new(KeyCode::Down, KeyModifiers::ALT), KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT), KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Home, KeyModifiers::CONTROL), KeyEvent::new(KeyCode::End, KeyModifiers::CONTROL | KeyModifiers::SHIFT), KeyEvent::new(KeyCode::PageUp, KeyModifiers::ALT), KeyEvent::new(KeyCode::PageDown, KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Insert, KeyModifiers::SHIFT), KeyEvent::new(KeyCode::Delete, KeyModifiers::ALT), ]; for key in cases { let encoded = encode_key(key, KeyboardProtocol::Legacy); let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap(); assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None); } } #[test] fn kitty_shifted_symbol_prefers_text_over_roundtrip_key_identity() { let key = TerminalKey::new(KeyCode::Char('1'), KeyModifiers::SHIFT) .with_shifted_codepoint('!' as u32); let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }); assert_eq!(encoded, b"!"); } #[test] fn legacy_basic_special_roundtrip_matrix() { let cases = [ KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()), KeyEvent::new(KeyCode::Tab, KeyModifiers::empty()), KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty()), KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()), KeyEvent::new(KeyCode::Up, KeyModifiers::empty()), KeyEvent::new(KeyCode::Down, KeyModifiers::empty()), KeyEvent::new(KeyCode::Left, KeyModifiers::empty()), KeyEvent::new(KeyCode::Right, KeyModifiers::empty()), KeyEvent::new(KeyCode::Home, KeyModifiers::empty()), KeyEvent::new(KeyCode::End, KeyModifiers::empty()), KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty()), KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty()), KeyEvent::new(KeyCode::Insert, KeyModifiers::empty()), KeyEvent::new(KeyCode::Delete, KeyModifiers::empty()), ]; for key in cases { let encoded = encode_key(key, KeyboardProtocol::Legacy); let parsed = parse_terminal_key_sequence(std::str::from_utf8(&encoded).unwrap()).unwrap(); assert_terminal_key_eq(parsed, key.code, key.modifiers, key.kind, None); } } #[test] fn kitty_shifted_symbol_pair_matrix_is_encoded_as_text() { let cases = [('1', '!'), ('/', '?'), ('[', '{')]; for (base, shifted) in cases { let key = TerminalKey::new(KeyCode::Char(base), KeyModifiers::SHIFT) .with_shifted_codepoint(shifted as u32); let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }); assert_eq!(encoded, shifted.to_string().into_bytes(), "base={base}"); } } #[test] fn chinese_char_encodes_as_utf8() { let key = TerminalKey::new(KeyCode::Char('中'), KeyModifiers::empty()); let encoded = encode_terminal_key(key, KeyboardProtocol::Legacy); assert_eq!(encoded, "中".as_bytes()); } #[test] fn chinese_char_with_kitty_protocol_encodes_as_utf8() { let key = TerminalKey::new(KeyCode::Char('文'), KeyModifiers::empty()); let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }); assert_eq!(encoded, "文".as_bytes()); } #[test] fn chinese_char_with_modifiers_falls_back_to_kitty_encoding() { let key = TerminalKey::new(KeyCode::Char('测'), KeyModifiers::ALT); let encoded = encode_terminal_key(key, KeyboardProtocol::Kitty { flags: 7 }); assert!(!encoded.is_empty()); assert_ne!(encoded, "测".as_bytes()); } }