use crossterm::event::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode}; use super::TerminalKey; #[allow(dead_code)] // Next step: raw stdin parser will feed TerminalKey directly through this path. pub fn parse_terminal_key_sequence(data: &str) -> Option { parse_kitty_key_sequence(data) .or_else(|| parse_modify_other_keys_sequence(data)) .or_else(|| parse_legacy_key_sequence(data)) } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn parse_kitty_key_sequence(data: &str) -> Option { let body = data.strip_prefix("\x1b[")?.strip_suffix('u')?; let (main, event_type) = match body.rsplit_once(':') { Some((head, tail)) if tail.chars().all(|ch| ch.is_ascii_digit()) && head.contains(';') => { (head, Some(tail)) } _ => (body, None), }; let (key_part, modifier_part) = main.rsplit_once(';').unwrap_or((main, "1")); let modifier = modifier_part.parse::().ok()?.checked_sub(1)?; let mut key_fields = key_part.split(':'); let codepoint = key_fields.next()?.parse::().ok()?; let shifted_codepoint = key_fields .next() .filter(|field| !field.is_empty()) .and_then(|field| field.parse::().ok()); let code = kitty_codepoint_to_keycode(codepoint)?; let kind = parse_kitty_event_type(event_type)?; Some(TerminalKey { code, modifiers: key_modifiers_from_u8(modifier), kind, shifted_codepoint, }) } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn parse_modify_other_keys_sequence(data: &str) -> Option { let body = data.strip_prefix("\x1b[27;")?.strip_suffix('~')?; let (modifier_part, codepoint_part) = body.split_once(';')?; let modifier = modifier_part.parse::().ok()?.checked_sub(1)?; let codepoint = codepoint_part.parse::().ok()?; Some(TerminalKey::new( kitty_codepoint_to_keycode(codepoint)?, key_modifiers_from_u8(modifier), )) } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn parse_legacy_key_sequence(data: &str) -> Option { if let Some(key) = parse_legacy_special_sequence(data) { return Some(key); } match data { // In raw mode, Enter is carriage return. A bare line feed is ambiguous in // terminal input and is commonly used for Ctrl+J or Shift+Enter workarounds. // Preserve LF by letting it fall through to legacy control-byte parsing. "\r" => Some(TerminalKey::new(KeyCode::Enter, KeyModifiers::empty())), "\t" => Some(TerminalKey::new(KeyCode::Tab, KeyModifiers::empty())), "\x1b" => Some(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())), "\x1b\x7f" => Some(TerminalKey::new(KeyCode::Backspace, KeyModifiers::ALT)), "\x7f" => Some(TerminalKey::new(KeyCode::Backspace, KeyModifiers::empty())), _ if data.starts_with('\x1b') => { let rest = data.strip_prefix('\x1b')?; if rest.chars().count() == 1 { let ch = rest.chars().next()?; Some(TerminalKey::new(KeyCode::Char(ch), KeyModifiers::ALT)) } else { None } } _ if data.chars().count() == 1 => { let ch = data.chars().next()?; if let Some(ctrl_key) = parse_legacy_ctrl_char(ch) { return Some(ctrl_key); } let mut modifiers = KeyModifiers::empty(); let code = if ch.is_ascii_uppercase() { modifiers |= KeyModifiers::SHIFT; KeyCode::Char(ch) } else { KeyCode::Char(ch) }; Some(TerminalKey::new(code, modifiers)) } _ => None, } } fn parse_legacy_ctrl_char(ch: char) -> Option { match ch as u32 { 0 => Some(TerminalKey::new(KeyCode::Char(' '), KeyModifiers::CONTROL)), 1..=26 => Some(TerminalKey::new( KeyCode::Char(char::from_u32((ch as u32) + 96)?), KeyModifiers::CONTROL, )), 27 => Some(TerminalKey::new(KeyCode::Char('['), KeyModifiers::CONTROL)), 28 => Some(TerminalKey::new(KeyCode::Char('\\'), KeyModifiers::CONTROL)), 29 => Some(TerminalKey::new(KeyCode::Char(']'), KeyModifiers::CONTROL)), 30 => Some(TerminalKey::new(KeyCode::Char('^'), KeyModifiers::CONTROL)), 31 => Some(TerminalKey::new(KeyCode::Char('-'), KeyModifiers::CONTROL)), _ => None, } } fn parse_legacy_special_sequence(data: &str) -> Option { match data { "\x1b\x1b[A" => Some(TerminalKey::new(KeyCode::Up, KeyModifiers::ALT)), "\x1b\x1b[B" => Some(TerminalKey::new(KeyCode::Down, KeyModifiers::ALT)), "\x1b\x1b[C" => Some(TerminalKey::new(KeyCode::Right, KeyModifiers::ALT)), "\x1b\x1b[D" => Some(TerminalKey::new(KeyCode::Left, KeyModifiers::ALT)), "\x1b[A" | "\x1bOA" => Some(TerminalKey::new(KeyCode::Up, KeyModifiers::empty())), "\x1b[B" | "\x1bOB" => Some(TerminalKey::new(KeyCode::Down, KeyModifiers::empty())), "\x1b[C" | "\x1bOC" => Some(TerminalKey::new(KeyCode::Right, KeyModifiers::empty())), "\x1b[D" | "\x1bOD" => Some(TerminalKey::new(KeyCode::Left, KeyModifiers::empty())), "\x1b[H" | "\x1bOH" | "\x1b[1~" | "\x1b[7~" => { Some(TerminalKey::new(KeyCode::Home, KeyModifiers::empty())) } "\x1b[F" | "\x1bOF" | "\x1b[4~" | "\x1b[8~" => { Some(TerminalKey::new(KeyCode::End, KeyModifiers::empty())) } "\x1b[5~" => Some(TerminalKey::new(KeyCode::PageUp, KeyModifiers::empty())), "\x1b[6~" => Some(TerminalKey::new(KeyCode::PageDown, KeyModifiers::empty())), "\x1b[2~" => Some(TerminalKey::new(KeyCode::Insert, KeyModifiers::empty())), "\x1b[3~" => Some(TerminalKey::new(KeyCode::Delete, KeyModifiers::empty())), "\x1bOp" => Some(TerminalKey::new(KeyCode::Char('0'), KeyModifiers::empty())), "\x1bOq" => Some(TerminalKey::new(KeyCode::Char('1'), KeyModifiers::empty())), "\x1bOr" => Some(TerminalKey::new(KeyCode::Char('2'), KeyModifiers::empty())), "\x1bOs" => Some(TerminalKey::new(KeyCode::Char('3'), KeyModifiers::empty())), "\x1bOt" => Some(TerminalKey::new(KeyCode::Char('4'), KeyModifiers::empty())), "\x1bOu" => Some(TerminalKey::new(KeyCode::Char('5'), KeyModifiers::empty())), "\x1bOv" => Some(TerminalKey::new(KeyCode::Char('6'), KeyModifiers::empty())), "\x1bOw" => Some(TerminalKey::new(KeyCode::Char('7'), KeyModifiers::empty())), "\x1bOx" => Some(TerminalKey::new(KeyCode::Char('8'), KeyModifiers::empty())), "\x1bOy" => Some(TerminalKey::new(KeyCode::Char('9'), KeyModifiers::empty())), "\x1bOn" => Some(TerminalKey::new(KeyCode::Char('.'), KeyModifiers::empty())), "\x1bOl" => Some(TerminalKey::new(KeyCode::Char(','), KeyModifiers::empty())), "\x1bOm" => Some(TerminalKey::new(KeyCode::Char('-'), KeyModifiers::empty())), "\x1bOk" => Some(TerminalKey::new(KeyCode::Char('+'), KeyModifiers::empty())), "\x1bOj" => Some(TerminalKey::new(KeyCode::Char('*'), KeyModifiers::empty())), "\x1bOo" => Some(TerminalKey::new(KeyCode::Char('/'), KeyModifiers::empty())), "\x1bOM" => Some(TerminalKey::new(KeyCode::Enter, KeyModifiers::empty())), "\x1bOP" | "\x1b[11~" => Some(TerminalKey::new(KeyCode::F(1), KeyModifiers::empty())), "\x1bOQ" | "\x1b[12~" => Some(TerminalKey::new(KeyCode::F(2), KeyModifiers::empty())), "\x1bOR" | "\x1b[13~" => Some(TerminalKey::new(KeyCode::F(3), KeyModifiers::empty())), "\x1bOS" | "\x1b[14~" => Some(TerminalKey::new(KeyCode::F(4), KeyModifiers::empty())), "\x1b[15~" => Some(TerminalKey::new(KeyCode::F(5), KeyModifiers::empty())), "\x1b[17~" => Some(TerminalKey::new(KeyCode::F(6), KeyModifiers::empty())), "\x1b[18~" => Some(TerminalKey::new(KeyCode::F(7), KeyModifiers::empty())), "\x1b[19~" => Some(TerminalKey::new(KeyCode::F(8), KeyModifiers::empty())), "\x1b[20~" => Some(TerminalKey::new(KeyCode::F(9), KeyModifiers::empty())), "\x1b[21~" => Some(TerminalKey::new(KeyCode::F(10), KeyModifiers::empty())), "\x1b[23~" => Some(TerminalKey::new(KeyCode::F(11), KeyModifiers::empty())), "\x1b[24~" => Some(TerminalKey::new(KeyCode::F(12), KeyModifiers::empty())), "\x1b[Z" => Some(TerminalKey::new(KeyCode::BackTab, KeyModifiers::SHIFT)), _ => parse_xterm_modified_special_sequence(data), } } fn parse_xterm_modified_special_sequence(data: &str) -> Option { let body = data.strip_prefix("\x1b[")?; if let Some(body) = body.strip_prefix("1;") { let suffix_char = body.chars().last()?; if suffix_char.is_ascii_alphabetic() { let modifier_and_event = body.strip_suffix(suffix_char)?; let (modifier_text, event_type) = split_modifier_and_event(modifier_and_event); let mod_value = modifier_text.parse::().ok()?.checked_sub(1)?; let code = match suffix_char { 'A' => KeyCode::Up, 'B' => KeyCode::Down, 'C' => KeyCode::Right, 'D' => KeyCode::Left, 'H' => KeyCode::Home, 'F' => KeyCode::End, 'P' => KeyCode::F(1), 'Q' => KeyCode::F(2), 'R' => KeyCode::F(3), 'S' => KeyCode::F(4), _ => return None, }; return Some( TerminalKey::new(code, key_modifiers_from_u8(mod_value)) .with_kind(parse_kitty_event_type(event_type)?), ); } } let tilde_body = body.strip_suffix('~')?; let (code_part, modifier_part) = tilde_body.split_once(';')?; let (modifier_text, event_type) = split_modifier_and_event(modifier_part); let mod_value = modifier_text.parse::().ok()?.checked_sub(1)?; let code = match code_part { "2" => KeyCode::Insert, "3" => KeyCode::Delete, "5" => KeyCode::PageUp, "6" => KeyCode::PageDown, "15" => KeyCode::F(5), "17" => KeyCode::F(6), "18" => KeyCode::F(7), "19" => KeyCode::F(8), "20" => KeyCode::F(9), "21" => KeyCode::F(10), "23" => KeyCode::F(11), "24" => KeyCode::F(12), _ => return None, }; Some( TerminalKey::new(code, key_modifiers_from_u8(mod_value)) .with_kind(parse_kitty_event_type(event_type)?), ) } fn split_modifier_and_event(input: &str) -> (&str, Option<&str>) { match input.split_once(':') { Some((modifier, event)) if !modifier.is_empty() => (modifier, Some(event)), _ => (input, None), } } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn parse_kitty_event_type(value: Option<&str>) -> Option { match value.unwrap_or("1") { "1" => Some(crossterm::event::KeyEventKind::Press), "2" => Some(crossterm::event::KeyEventKind::Repeat), "3" => Some(crossterm::event::KeyEventKind::Release), _ => None, } } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn kitty_codepoint_to_keycode(codepoint: u32) -> Option { match codepoint { 8 | 127 => Some(KeyCode::Backspace), 9 => Some(KeyCode::Tab), 13 | 57414 => Some(KeyCode::Enter), 27 => Some(KeyCode::Esc), 57358 => Some(KeyCode::CapsLock), 57359 => Some(KeyCode::ScrollLock), 57360 => Some(KeyCode::NumLock), 57361 => Some(KeyCode::PrintScreen), 57362 => Some(KeyCode::Pause), 57363 => Some(KeyCode::Menu), 57376..=57398 => Some(KeyCode::F((codepoint - 57376 + 13) as u8)), 57417 => Some(KeyCode::Left), 57418 => Some(KeyCode::Right), 57419 => Some(KeyCode::Up), 57420 => Some(KeyCode::Down), 57421 => Some(KeyCode::PageUp), 57422 => Some(KeyCode::PageDown), 57423 => Some(KeyCode::Home), 57424 => Some(KeyCode::End), 57425 => Some(KeyCode::Insert), 57426 => Some(KeyCode::Delete), 57427 => Some(KeyCode::KeypadBegin), 57428 => Some(KeyCode::Media(MediaKeyCode::Play)), 57429 => Some(KeyCode::Media(MediaKeyCode::Pause)), 57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)), 57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)), 57432 => Some(KeyCode::Media(MediaKeyCode::Stop)), 57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)), 57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)), 57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)), 57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)), 57437 => Some(KeyCode::Media(MediaKeyCode::Record)), 57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)), 57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)), 57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)), 57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)), 57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)), 57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)), 57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)), 57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)), 57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)), 57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)), 57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)), 57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)), 57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)), 57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)), 57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)), 57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)), 57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)), value if is_kitty_functional_codepoint(value) => None, value => char::from_u32(value).map(KeyCode::Char), } } fn is_kitty_functional_codepoint(codepoint: u32) -> bool { (57358..=57454).contains(&codepoint) } #[allow(dead_code)] // Reserved for the upcoming raw stdin parser. fn key_modifiers_from_u8(modifier: u8) -> KeyModifiers { let mut mods = KeyModifiers::empty(); if modifier & 0b0000_0001 != 0 { mods |= KeyModifiers::SHIFT; } if modifier & 0b0000_0010 != 0 { mods |= KeyModifiers::ALT; } if modifier & 0b0000_0100 != 0 { mods |= KeyModifiers::CONTROL; } if modifier & 0b0000_1000 != 0 { mods |= KeyModifiers::SUPER; } if modifier & 0b0001_0000 != 0 { mods |= KeyModifiers::HYPER; } if modifier & 0b0010_0000 != 0 { mods |= KeyModifiers::META; } mods } #[cfg(test)] mod tests { use crossterm::event::{KeyCode, KeyModifiers, ModifierKeyCode}; use super::*; use crate::input::{encode_terminal_key, KeyboardProtocol}; 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); } fn decode_hex(hex: &str) -> Vec { let hex = hex.trim(); assert_eq!(hex.len() % 2, 0, "hex string must have even length"); (0..hex.len()) .step_by(2) .map(|idx| u8::from_str_radix(&hex[idx..idx + 2], 16).unwrap()) .collect() } fn parse_fixture_key_code(value: &str) -> KeyCode { match value { "enter" => KeyCode::Enter, "tab" => KeyCode::Tab, "backspace" => KeyCode::Backspace, "esc" => KeyCode::Esc, "up" => KeyCode::Up, "down" => KeyCode::Down, "left" => KeyCode::Left, "right" => KeyCode::Right, "home" => KeyCode::Home, "end" => KeyCode::End, "pageup" => KeyCode::PageUp, "pagedown" => KeyCode::PageDown, "insert" => KeyCode::Insert, "delete" => KeyCode::Delete, value if value.starts_with("char:") => { KeyCode::Char(value.trim_start_matches("char:").chars().next().unwrap()) } other => panic!("unsupported fixture key code: {other}"), } } fn parse_fixture_modifiers(value: &str) -> KeyModifiers { if value == "-" || value.is_empty() { return KeyModifiers::empty(); } let mut modifiers = KeyModifiers::empty(); for part in value.split('+') { match part { "shift" => modifiers |= KeyModifiers::SHIFT, "alt" => modifiers |= KeyModifiers::ALT, "control" => modifiers |= KeyModifiers::CONTROL, "super" => modifiers |= KeyModifiers::SUPER, "hyper" => modifiers |= KeyModifiers::HYPER, "meta" => modifiers |= KeyModifiers::META, other => panic!("unsupported fixture modifier: {other}"), } } modifiers } fn parse_fixture_kind(value: &str) -> crossterm::event::KeyEventKind { match value { "press" => crossterm::event::KeyEventKind::Press, "repeat" => crossterm::event::KeyEventKind::Repeat, "release" => crossterm::event::KeyEventKind::Release, other => panic!("unsupported fixture kind: {other}"), } } #[test] fn parse_legacy_f_keys() { let cases = [ ("\x1bOP", KeyCode::F(1)), ("\x1b[11~", KeyCode::F(1)), ("\x1bOQ", KeyCode::F(2)), ("\x1b[12~", KeyCode::F(2)), ("\x1bOR", KeyCode::F(3)), ("\x1b[13~", KeyCode::F(3)), ("\x1bOS", KeyCode::F(4)), ("\x1b[14~", KeyCode::F(4)), ]; for (sequence, code) in cases { assert_terminal_key_eq( parse_terminal_key_sequence(sequence).expect("f key should parse"), code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); } assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[15~").expect("f5 should parse"), KeyCode::F(5), KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); assert_eq!(parse_terminal_key_sequence("\x1b[10~"), None); assert_eq!(parse_terminal_key_sequence("\x1b[16~"), None); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[1~").expect("home should parse"), KeyCode::Home, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[4~").expect("end should parse"), KeyCode::End, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[5~").expect("pageup should parse"), KeyCode::PageUp, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[6~").expect("pagedown should parse"), KeyCode::PageDown, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); } #[test] fn parse_legacy_application_keypad_sequences() { let cases = [ ("\x1bOp", KeyCode::Char('0')), ("\x1bOq", KeyCode::Char('1')), ("\x1bOr", KeyCode::Char('2')), ("\x1bOs", KeyCode::Char('3')), ("\x1bOt", KeyCode::Char('4')), ("\x1bOu", KeyCode::Char('5')), ("\x1bOv", KeyCode::Char('6')), ("\x1bOw", KeyCode::Char('7')), ("\x1bOx", KeyCode::Char('8')), ("\x1bOy", KeyCode::Char('9')), ("\x1bOn", KeyCode::Char('.')), ("\x1bOl", KeyCode::Char(',')), ("\x1bOm", KeyCode::Char('-')), ("\x1bOk", KeyCode::Char('+')), ("\x1bOj", KeyCode::Char('*')), ("\x1bOo", KeyCode::Char('/')), ("\x1bOM", KeyCode::Enter), ]; for (sequence, code) in cases { assert_terminal_key_eq( parse_terminal_key_sequence(sequence).expect("keypad sequence should parse"), code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); } } #[test] fn unknown_legacy_ss3_sequence_remains_unsupported() { assert!(parse_terminal_key_sequence("\x1bOz").is_none()); } #[test] fn parse_modified_f_keys() { assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[1;2P").expect("shift+f1 should parse"), KeyCode::F(1), KeyModifiers::SHIFT, crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[1;3S").expect("alt+f4 should parse"), KeyCode::F(4), KeyModifiers::ALT, crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[1;4S").expect("shift+alt+f4 should parse"), KeyCode::F(4), KeyModifiers::SHIFT | KeyModifiers::ALT, crossterm::event::KeyEventKind::Press, None, ); assert_terminal_key_eq( parse_terminal_key_sequence("\x1b[15;2~").expect("shift+f5 should parse"), KeyCode::F(5), KeyModifiers::SHIFT, crossterm::event::KeyEventKind::Press, None, ); assert_eq!(parse_terminal_key_sequence("\x1b[11;2~"), None); assert_eq!(parse_terminal_key_sequence("\x1b[14;1~"), None); assert_eq!(parse_terminal_key_sequence("\x1b[14;3~"), None); } #[test] fn parse_kitty_sequence_preserves_shifted_symbol_pair() { let key = parse_terminal_key_sequence("\x1b[49:33;2:1u").unwrap(); assert_eq!(key.code, KeyCode::Char('1')); assert_eq!(key.modifiers, KeyModifiers::SHIFT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); assert_eq!(key.shifted_codepoint, Some('!' as u32)); } #[test] fn parse_kitty_sequence_preserves_shifted_letter_pair_and_release() { let key = parse_terminal_key_sequence("\x1b[108:76;2:3u").unwrap(); assert_eq!(key.code, KeyCode::Char('l')); assert_eq!(key.modifiers, KeyModifiers::SHIFT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Release); assert_eq!(key.shifted_codepoint, Some('L' as u32)); } #[test] fn parse_kitty_alt_backspace_sequence() { let key = parse_terminal_key_sequence("\x1b[127;3u").unwrap(); assert_eq!(key.code, KeyCode::Backspace); assert_eq!(key.modifiers, KeyModifiers::ALT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); assert_eq!(key.shifted_codepoint, None); } #[test] fn parse_modify_other_keys_sequence() { let key = parse_terminal_key_sequence("\x1b[27;6;108~").unwrap(); assert_eq!(key.code, KeyCode::Char('l')); assert_eq!(key.modifiers, KeyModifiers::CONTROL | KeyModifiers::SHIFT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); assert_eq!(key.shifted_codepoint, None); } #[test] fn parse_legacy_uppercase_letter_as_shifted_char() { let key = parse_terminal_key_sequence("L").unwrap(); assert_eq!(key.code, KeyCode::Char('L')); assert_eq!(key.modifiers, KeyModifiers::SHIFT); } #[test] fn parse_legacy_up_arrow_sequence() { let key = parse_terminal_key_sequence("\x1b[A").unwrap(); assert_eq!(key.code, KeyCode::Up); assert_eq!(key.modifiers, KeyModifiers::empty()); } #[test] fn parse_legacy_alt_backspace_sequence() { let key = parse_terminal_key_sequence("\x1b\x7f").unwrap(); assert_eq!(key.code, KeyCode::Backspace); assert_eq!(key.modifiers, KeyModifiers::ALT); } #[test] fn parse_kitty_modifier_sequence() { let key = parse_terminal_key_sequence("\x1b[57441;2:1u").unwrap(); assert_eq!(key.code, KeyCode::Modifier(ModifierKeyCode::LeftShift)); assert_eq!(key.modifiers, KeyModifiers::SHIFT); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); } #[test] fn parse_ghostty_enhanced_up_arrow_press_sequence() { let key = parse_terminal_key_sequence("\x1b[1;1:1A").unwrap(); assert_eq!(key.code, KeyCode::Up); assert_eq!(key.modifiers, KeyModifiers::empty()); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); } #[test] fn parse_ghostty_enhanced_up_arrow_release_sequence() { let key = parse_terminal_key_sequence("\x1b[1;1:3A").unwrap(); assert_eq!(key.code, KeyCode::Up); assert_eq!(key.modifiers, KeyModifiers::empty()); assert_eq!(key.kind, crossterm::event::KeyEventKind::Release); } #[test] fn parse_ghostty_enhanced_pageup_press_sequence() { let key = parse_terminal_key_sequence("\x1b[5;1:1~").unwrap(); assert_eq!(key.code, KeyCode::PageUp); assert_eq!(key.modifiers, KeyModifiers::empty()); assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); } #[test] fn parse_ghostty_enhanced_pagedown_release_sequence() { let key = parse_terminal_key_sequence("\x1b[6;1:3~").unwrap(); assert_eq!(key.code, KeyCode::PageDown); assert_eq!(key.modifiers, KeyModifiers::empty()); assert_eq!(key.kind, crossterm::event::KeyEventKind::Release); } #[test] fn parse_ghostty_enhanced_delete_repeat_sequence() { let key = parse_terminal_key_sequence("\x1b[3;1:2~").unwrap(); assert_eq!(key.code, KeyCode::Delete); assert_eq!(key.modifiers, KeyModifiers::empty()); assert_eq!(key.kind, crossterm::event::KeyEventKind::Repeat); } #[test] fn parse_xterm_alt_up_arrow_sequence() { let key = parse_terminal_key_sequence("\x1b[1;3A").unwrap(); assert_eq!(key.code, KeyCode::Up); assert_eq!(key.modifiers, KeyModifiers::ALT); } #[test] fn parse_xterm_alt_down_arrow_sequence() { let key = parse_terminal_key_sequence("\x1b[1;3B").unwrap(); assert_eq!(key.code, KeyCode::Down); assert_eq!(key.modifiers, KeyModifiers::ALT); } #[test] fn parse_kitty_functional_up_arrow_sequence() { let key = parse_terminal_key_sequence("\x1b[57419;1u").unwrap(); assert_eq!(key.code, KeyCode::Up); assert_eq!(key.modifiers, KeyModifiers::empty()); } #[test] fn parse_legacy_ctrl_b_sequence() { let key = parse_terminal_key_sequence("\x02").unwrap(); assert_eq!(key.code, KeyCode::Char('b')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); } #[test] fn parse_legacy_ctrl_c_sequence() { let key = parse_terminal_key_sequence("\x03").unwrap(); assert_eq!(key.code, KeyCode::Char('c')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); } #[test] fn parse_legacy_lf_sequence_as_ctrl_j() { let key = parse_terminal_key_sequence("\n").unwrap(); assert_eq!(key.code, KeyCode::Char('j')); assert_eq!(key.modifiers, KeyModifiers::CONTROL); } #[test] fn legacy_lf_roundtrips_as_lf() { let key = parse_terminal_key_sequence("\n").unwrap(); assert_eq!(encode_terminal_key(key, KeyboardProtocol::Legacy), b"\n"); } #[test] fn legacy_ctrl_byte_matrix_is_covered() { for (byte, expected) in [ (b'\x01', 'a'), (b'\x02', 'b'), (b'\x03', 'c'), (b'\x1a', 'z'), ] { let key = parse_terminal_key_sequence(std::str::from_utf8(&[byte]).unwrap()).unwrap(); assert_terminal_key_eq( key, KeyCode::Char(expected), KeyModifiers::CONTROL, crossterm::event::KeyEventKind::Press, None, ); } for (byte, expected) in [ (b'\x1c', '\\'), (b'\x1d', ']'), (b'\x1e', '^'), (b'\x1f', '-'), ] { let key = parse_terminal_key_sequence(std::str::from_utf8(&[byte]).unwrap()).unwrap(); assert_terminal_key_eq( key, KeyCode::Char(expected), KeyModifiers::CONTROL, crossterm::event::KeyEventKind::Press, None, ); } } #[test] fn kitty_functional_key_matrix_is_covered() { let cases = [ ("\x1b[57417;1u", KeyCode::Left), ("\x1b[57418;1u", KeyCode::Right), ("\x1b[57419;1u", KeyCode::Up), ("\x1b[57420;1u", KeyCode::Down), ("\x1b[57421;1u", KeyCode::PageUp), ("\x1b[57422;1u", KeyCode::PageDown), ("\x1b[57423;1u", KeyCode::Home), ("\x1b[57424;1u", KeyCode::End), ("\x1b[57425;1u", KeyCode::Insert), ("\x1b[57426;1u", KeyCode::Delete), ]; for (sequence, code) in cases { let parsed = parse_terminal_key_sequence(sequence).unwrap(); assert_terminal_key_eq( parsed, code, KeyModifiers::empty(), crossterm::event::KeyEventKind::Press, None, ); } } fn assert_fixture_corpus_parses(corpus: &str) { for line in corpus.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let mut columns: Vec<_> = line.split('\t').collect(); if columns.len() == 5 { columns.push(""); } let (family, bytes_hex, code, modifiers, kind, shifted) = match columns.len() { 6 => { if columns[1].chars().all(|ch| ch.is_ascii_hexdigit()) { ( columns[0], columns[1], columns[2], columns[3], columns[4], columns[5], ) } else { ( columns[0], columns[2], columns[3], columns[4], columns[5], "", ) } } 7 => ( columns[0], columns[2], columns[3], columns[4], columns[5], columns[6], ), _ => panic!("fixture row must have 6 or 7 columns: {line}"), }; assert!( bytes_hex.chars().all(|ch| ch.is_ascii_hexdigit()), "non-hex fixture bytes for {family}: {bytes_hex}" ); let bytes = decode_hex(bytes_hex); let text = std::str::from_utf8(&bytes).unwrap(); let parsed = parse_terminal_key_sequence(text) .unwrap_or_else(|| panic!("fixture failed to parse: {family}")); assert_terminal_key_eq( parsed, parse_fixture_key_code(code), parse_fixture_modifiers(modifiers), parse_fixture_kind(kind), if shifted.is_empty() { None } else { Some(shifted.parse::().unwrap()) }, ); } } #[test] fn keyboard_protocol_corpus_fixture_parses() { let corpus = include_str!("../../tests/fixtures/keyboard_protocol_corpus.tsv"); assert_fixture_corpus_parses(corpus); } #[test] fn macos_terminal_variants_fixture_parses() { let corpus = include_str!("../../tests/fixtures/macos_terminal_variants.tsv"); for line in corpus.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } let mut columns: Vec<_> = line.split('\t').collect(); if columns.len() == 6 { columns.push(""); } assert_eq!( columns.len(), 7, "macOS fixture row must have 7 columns: {line}" ); let source = format!("{}:{}", columns[0], columns[1]); let transformed = [ source.as_str(), columns[2], columns[3], columns[4], columns[5], columns[6], ] .join("\t"); assert_fixture_corpus_parses(&transformed); } } #[test] fn linux_terminal_variants_fixture_parses() { let corpus = include_str!("../../tests/fixtures/linux_terminal_variants.tsv"); assert_fixture_corpus_parses(corpus); } }