use galaxyui_core::keymap::Keystroke; use galaxyui_core::platform::OperatingSystem; use super::*; use crate::model::indexing::Point; use crate::model::mouse::{MouseAction, MouseButton, MouseState}; use crate::model::TermMode; fn validate_keystroke_test_cases( test_cases: &[(Keystroke, Vec)], mock_terminal_model: &T, ) { for (key, expected_result) in test_cases.iter() { let result = KeystrokeWithDetails { keystroke: key, key_without_modifiers: None, chars: None, } .to_escape_sequence(mock_terminal_model); log::debug!("Key: {}", key.key); assert_eq!(Some(expected_result), result.as_ref()); } } fn validate_mouse_test_cases( test_cases: &[(MouseState, Vec)], mock_terminal_model: &T, ) { for (mouse_state, expected_result) in test_cases.iter() { let result = mouse_state.to_escape_sequence(mock_terminal_model); log::debug!( "Mouse action: {:#?}, Mouse button: {:#?}, Point: {:#?}", mouse_state.action(), mouse_state.button(), mouse_state.maybe_point() ); assert_eq!(Some(expected_result), result.as_ref()); } } #[test] fn test_keystroke_to_c0_control_code() { // Expected mapping taken from the VT220 spec // [here](https://vt100.net/docs/vt220-rm/chapter3.html#S3.2.5), table 3.2.5. let test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse("ctrl- ").unwrap(), vec![C0::NUL]), (Keystroke::parse("ctrl-2").unwrap(), vec![C0::NUL]), (Keystroke::parse("ctrl-3").unwrap(), vec![C0::ESC]), (Keystroke::parse("ctrl-4").unwrap(), vec![C0::FS]), (Keystroke::parse("ctrl-5").unwrap(), vec![C0::GS]), (Keystroke::parse("ctrl-6").unwrap(), vec![C0::RS]), (Keystroke::parse("ctrl-7").unwrap(), vec![C0::US]), (Keystroke::parse("ctrl-8").unwrap(), vec![C0::DEL]), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_shift_backspace_emits_del_sequence() { // Regression test: Shift+Backspace must emit DEL (0x7f), not BS (0x08). // 0x08 is Ctrl+H, which readline-style TUIs interpret as backward-kill-word. let test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse("backspace").unwrap(), vec![C0::DEL]), (Keystroke::parse("shift-backspace").unwrap(), vec![C0::DEL]), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_mouse_actions_to_escape_sequence() { // Validating we produce the correct escape sequences. let test_cases: &[(MouseState, Vec)] = &[ ( MouseState::new(MouseButton::Right, MouseAction::Pressed, Default::default()) .set_point(Point::new(10, 10)), // [<2;11;11M vec![ C0::ESC, b'[', b'<', b'2', b';', b'1', b'1', b';', b'1', b'1', b'M', ], ), ( MouseState::new(MouseButton::Left, MouseAction::Released, Default::default()) .set_point(Point::new(5, 5)), // [<0;6;6m vec![C0::ESC, b'[', b'<', b'0', b';', b'6', b';', b'6', b'm'], ), ( MouseState::new( MouseButton::LeftDrag, MouseAction::Pressed, Default::default(), ) .set_point(Point::new(5, 5)), // [<32;6;6M vec![ C0::ESC, b'[', b'<', b'3', b'2', b';', b'6', b';', b'6', b'M', ], ), ( MouseState::new(MouseButton::Move, MouseAction::Pressed, Default::default()) .set_point(Point::new(5, 5)), // [<35;6;6M vec![ C0::ESC, b'[', b'<', b'3', b'5', b';', b'6', b';', b'6', b'M', ], ), ( MouseState::new( MouseButton::Wheel, MouseAction::Scrolled { delta: 2 }, Default::default(), ) .set_point(Point::new(5, 5)), // [<64;6;6M[<64;6;6M // Repeated twice since the scroll action was for 2 lines vec![ C0::ESC, b'[', b'<', b'6', b'4', b';', b'6', b';', b'6', b'M', C0::ESC, b'[', b'<', b'6', b'4', b';', b'6', b';', b'6', b'M', ], ), ]; let terminal_model_mock = TerminalModelMock::new(); validate_mouse_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_cursor_movement_keystroke_without_modifier_to_escape_sequence() { // Expected mapping taken from the xterm spec // [here](https://www.xfree86.org/current/ctlseqs.html). let test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse("up").unwrap(), vec![C0::ESC, b'[', b'A']), (Keystroke::parse("down").unwrap(), vec![C0::ESC, b'[', b'B']), ( Keystroke::parse("right").unwrap(), vec![C0::ESC, b'[', b'C'], ), (Keystroke::parse("left").unwrap(), vec![C0::ESC, b'[', b'D']), (Keystroke::parse("home").unwrap(), vec![C0::ESC, b'[', b'H']), (Keystroke::parse("end").unwrap(), vec![C0::ESC, b'[', b'F']), ]; let mut terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); let app_cursor_term_mode_test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse("up").unwrap(), vec![C0::ESC, b'O', b'A']), (Keystroke::parse("down").unwrap(), vec![C0::ESC, b'O', b'B']), ( Keystroke::parse("right").unwrap(), vec![C0::ESC, b'O', b'C'], ), (Keystroke::parse("left").unwrap(), vec![C0::ESC, b'O', b'D']), (Keystroke::parse("home").unwrap(), vec![C0::ESC, b'O', b'H']), (Keystroke::parse("end").unwrap(), vec![C0::ESC, b'O', b'F']), ]; // with cursor keys mode terminal_model_mock.set_mode(TermMode::APP_CURSOR); for (key, expected_result) in app_cursor_term_mode_test_cases.iter() { let result = KeystrokeWithDetails { keystroke: key, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock); log::debug!("Key: {}", key.key); assert_eq!(Some(expected_result), result.as_ref()); } validate_keystroke_test_cases(app_cursor_term_mode_test_cases, &terminal_model_mock); } #[test] fn test_cursor_movement_keystroke_with_modifier_to_escape_sequence() { // Expected mapping taken from the xterm spec // [here](https://www.xfree86.org/current/ctlseqs.html). let test_cases: &[(Keystroke, Vec)] = &[ ( Keystroke::parse("shift-up").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'2', b'A'], ), ( Keystroke::parse("alt-down").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'3', b'B'], ), ( Keystroke::parse("shift-alt-right").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'4', b'C'], ), ( Keystroke::parse("ctrl-left").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'5', b'D'], ), ( Keystroke::parse("shift-ctrl-home").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'6', b'H'], ), ( Keystroke::parse("ctrl-alt-end").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'7', b'F'], ), ]; let mut terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); let app_cursor_term_mode_test_cases: &[(Keystroke, Vec)] = &[ ( Keystroke::parse("ctrl-shift-alt-up").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'8', b'A'], ), ( Keystroke::parse("meta-down").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'3', b'B'], ), ( Keystroke::parse("shift-right").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'2', b'C'], ), ( Keystroke::parse("alt-left").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'3', b'D'], ), ( Keystroke::parse("shift-alt-home").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'4', b'H'], ), ( Keystroke::parse("ctrl-end").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'5', b'F'], ), ]; // with cursor keys mode terminal_model_mock.set_mode(TermMode::APP_CURSOR); validate_keystroke_test_cases(app_cursor_term_mode_test_cases, &terminal_model_mock); } #[test] fn test_fn_keystroke_without_modifier_to_escape_sequence() { // Expected mapping taken from the xterm spec // [here](https://www.xfree86.org/current/ctlseqs.html#PC-Style%20Function%20Keys), under // the section titled 'PC-Style Function Keys'. let test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse("f1").unwrap(), vec![C0::ESC, b'O', b'P']), (Keystroke::parse("f2").unwrap(), vec![C0::ESC, b'O', b'Q']), (Keystroke::parse("f3").unwrap(), vec![C0::ESC, b'O', b'R']), (Keystroke::parse("f4").unwrap(), vec![C0::ESC, b'O', b'S']), ( Keystroke::parse("f5").unwrap(), vec![C0::ESC, b'[', b'1', b'5', b'~'], ), ( Keystroke::parse("f6").unwrap(), vec![C0::ESC, b'[', b'1', b'7', b'~'], ), ( Keystroke::parse("f7").unwrap(), vec![C0::ESC, b'[', b'1', b'8', b'~'], ), ( Keystroke::parse("f8").unwrap(), vec![C0::ESC, b'[', b'1', b'9', b'~'], ), ( Keystroke::parse("f9").unwrap(), vec![C0::ESC, b'[', b'2', b'0', b'~'], ), ( Keystroke::parse("f10").unwrap(), vec![C0::ESC, b'[', b'2', b'1', b'~'], ), ( Keystroke::parse("f11").unwrap(), vec![C0::ESC, b'[', b'2', b'3', b'~'], ), ( Keystroke::parse("f12").unwrap(), vec![C0::ESC, b'[', b'2', b'4', b'~'], ), ( Keystroke::parse("f13").unwrap(), vec![C0::ESC, b'[', b'2', b'5', b'~'], ), ( Keystroke::parse("f14").unwrap(), vec![C0::ESC, b'[', b'2', b'6', b'~'], ), ( Keystroke::parse("f15").unwrap(), vec![C0::ESC, b'[', b'2', b'8', b'~'], ), ( Keystroke::parse("f16").unwrap(), vec![C0::ESC, b'[', b'2', b'9', b'~'], ), ( Keystroke::parse("f17").unwrap(), vec![C0::ESC, b'[', b'3', b'1', b'~'], ), ( Keystroke::parse("f18").unwrap(), vec![C0::ESC, b'[', b'3', b'2', b'~'], ), ( Keystroke::parse("f19").unwrap(), vec![C0::ESC, b'[', b'3', b'3', b'~'], ), ( Keystroke::parse("f20").unwrap(), vec![C0::ESC, b'[', b'3', b'4', b'~'], ), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_fn_keystroke_with_modifier_to_escape_sequence() { let test_cases: &[(Keystroke, Vec)] = &[ ( Keystroke::parse("shift-f1").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'2', b'P'], ), ( Keystroke::parse("alt-f2").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'3', b'Q'], ), ( Keystroke::parse("shift-alt-f3").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'4', b'R'], ), ( Keystroke::parse("ctrl-f4").unwrap(), vec![C0::ESC, b'[', b'1', b';', b'5', b'S'], ), ( Keystroke::parse("ctrl-shift-f5").unwrap(), vec![C0::ESC, b'[', b'1', b'5', b';', b'6', b'~'], ), ( Keystroke::parse("ctrl-alt-f6").unwrap(), vec![C0::ESC, b'[', b'1', b'7', b';', b'7', b'~'], ), ( Keystroke::parse("ctrl-shift-alt-f7").unwrap(), vec![C0::ESC, b'[', b'1', b'8', b';', b'8', b'~'], ), ( Keystroke::parse("meta-f8").unwrap(), vec![C0::ESC, b'[', b'1', b'9', b';', b'3', b'~'], ), ( Keystroke::parse("shift-f9").unwrap(), vec![C0::ESC, b'[', b'2', b'0', b';', b'2', b'~'], ), ( Keystroke::parse("alt-f10").unwrap(), vec![C0::ESC, b'[', b'2', b'1', b';', b'3', b'~'], ), ( Keystroke::parse("shift-alt-f11").unwrap(), vec![C0::ESC, b'[', b'2', b'3', b';', b'4', b'~'], ), ( Keystroke::parse("ctrl-f12").unwrap(), vec![C0::ESC, b'[', b'2', b'4', b';', b'5', b'~'], ), ( Keystroke::parse("ctrl-shift-f13").unwrap(), vec![C0::ESC, b'[', b'2', b'5', b';', b'6', b'~'], ), ( Keystroke::parse("ctrl-alt-f14").unwrap(), vec![C0::ESC, b'[', b'2', b'6', b';', b'7', b'~'], ), ( Keystroke::parse("ctrl-shift-alt-f15").unwrap(), vec![C0::ESC, b'[', b'2', b'8', b';', b'8', b'~'], ), ( Keystroke::parse("shift-f16").unwrap(), vec![C0::ESC, b'[', b'2', b'9', b';', b'2', b'~'], ), ( Keystroke::parse("alt-f17").unwrap(), vec![C0::ESC, b'[', b'3', b'1', b';', b'3', b'~'], ), ( Keystroke::parse("shift-alt-f18").unwrap(), vec![C0::ESC, b'[', b'3', b'2', b';', b'4', b'~'], ), ( Keystroke::parse("ctrl-f19").unwrap(), vec![C0::ESC, b'[', b'3', b'3', b';', b'5', b'~'], ), ( Keystroke::parse("ctrl-shift-f20").unwrap(), vec![C0::ESC, b'[', b'3', b'4', b';', b'6', b'~'], ), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_meta_keystroke_to_escape_sequence() { fn metaify(keystroke: &str) -> String { if OperatingSystem::get().is_mac() { format!("meta-{keystroke}") } else { format!("alt-{keystroke}") } } let test_cases: &[(Keystroke, Vec)] = &[ (Keystroke::parse(metaify("a")).unwrap(), vec![C0::ESC, b'a']), (Keystroke::parse(metaify("1")).unwrap(), vec![C0::ESC, b'1']), ( Keystroke::parse(metaify("'")).unwrap(), vec![C0::ESC, b'\''], ), ]; let terminal_model_mock = TerminalModelMock::new(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_unmatched_keystroke_does_not_yield_escape_sequence() { let test_cases: &[Keystroke] = &[ Keystroke::parse("a").unwrap(), Keystroke::parse("1").unwrap(), Keystroke::parse("'").unwrap(), ]; let terminal_model_mock = TerminalModelMock::new(); for key in test_cases.iter() { let result = KeystrokeWithDetails { keystroke: key, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock); assert_eq!(result, None); } } struct TerminalModelMock { term_mode: TermMode, } impl TerminalModelMock { fn new() -> Self { Self { term_mode: TermMode::default(), } } fn set_mode(&mut self, mode: TermMode) { self.term_mode |= mode; } } impl ModeProvider for TerminalModelMock { fn is_term_mode_set(&self, mode: TermMode) -> bool { self.term_mode.intersects(mode) } } /// Creates a mock with KEYBOARD_REPORT_ALL_AS_ESCAPE so all keys use CSI u format. fn mock_with_all_keys_as_escape() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE); mock } /// Creates a mock with only KEYBOARD_DISAMBIGUATE_ESCAPE — CSI u only for ambiguous/modified keys. fn mock_with_disambiguate_only() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_DISAMBIGUATE_ESCAPE); mock } /// Creates a mock with REPORT_ALL_KEYS_AS_ESCAPE + REPORT_ALTERNATE_KEYS. fn mock_with_alternate_keys() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE); mock.set_mode(TermMode::KEYBOARD_REPORT_ALTERNATE_KEYS); mock } /// Creates a mock with REPORT_ALL_KEYS_AS_ESCAPE + REPORT_ASSOCIATED_TEXT. fn mock_with_associated_text() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE); mock.set_mode(TermMode::KEYBOARD_REPORT_ASSOCIATED_TEXT); mock } /// Creates a mock with REPORT_ALL_KEYS_AS_ESCAPE + REPORT_EVENT_TYPES. fn mock_with_event_types() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE); mock.set_mode(TermMode::KEYBOARD_REPORT_EVENT_TYPES); mock } /// Creates a mock with all enhancement flags enabled. fn mock_with_all_flags() -> TerminalModelMock { let mut mock = TerminalModelMock::new(); mock.set_mode(TermMode::KEYBOARD_REPORT_ALL_AS_ESCAPE); mock.set_mode(TermMode::KEYBOARD_REPORT_EVENT_TYPES); mock.set_mode(TermMode::KEYBOARD_REPORT_ALTERNATE_KEYS); mock.set_mode(TermMode::KEYBOARD_REPORT_ASSOCIATED_TEXT); mock } #[test] fn test_keyboard_enhancement_basic_keys() { // Test basic keys with REPORT_ALL_KEYS_AS_ESCAPE flag (flag 8) // This flag makes ALL keys use CSI u format // Note: Per Kitty protocol, ;1 is omitted when there are no modifiers let test_cases: &[(Keystroke, Vec)] = &[ // Enter (key code 13) (Keystroke::parse("enter").unwrap(), b"\x1b[13u".to_vec()), ( Keystroke::parse("shift-enter").unwrap(), b"\x1b[13;2u".to_vec(), ), ( Keystroke::parse("ctrl-enter").unwrap(), b"\x1b[13;5u".to_vec(), ), ( Keystroke::parse("alt-enter").unwrap(), b"\x1b[13;3u".to_vec(), ), // Tab (key code 9) (Keystroke::parse("tab").unwrap(), b"\x1b[9u".to_vec()), ( Keystroke::parse("shift-tab").unwrap(), b"\x1b[9;2u".to_vec(), ), // Escape (key code 27) (Keystroke::parse("escape").unwrap(), b"\x1b[27u".to_vec()), ( Keystroke::parse("shift-escape").unwrap(), b"\x1b[27;2u".to_vec(), ), // Backspace (key code 127) ( Keystroke::parse("backspace").unwrap(), b"\x1b[127u".to_vec(), ), ( Keystroke::parse("ctrl-backspace").unwrap(), b"\x1b[127;5u".to_vec(), ), // Space (key code 32) (Keystroke::parse(" ").unwrap(), b"\x1b[32u".to_vec()), (Keystroke::parse("ctrl- ").unwrap(), b"\x1b[32;5u".to_vec()), ]; let terminal_model_mock = mock_with_all_keys_as_escape(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_keyboard_enhancement_function_keys() { // Test F13-F20 with keyboard enhancement protocol // Note: Only F13-F20 are currently parseable by Keystroke::parse() // F21-F35 support exists in keystroke_to_csi_u() but requires runtime key events // Per Kitty protocol, ;1 is omitted when there are no modifiers let test_cases: &[(Keystroke, Vec)] = &[ // F13-F20 (codes 57376-57383) (Keystroke::parse("f13").unwrap(), b"\x1b[57376u".to_vec()), ( Keystroke::parse("shift-f14").unwrap(), b"\x1b[57377;2u".to_vec(), ), (Keystroke::parse("f15").unwrap(), b"\x1b[57378u".to_vec()), ( Keystroke::parse("ctrl-f16").unwrap(), b"\x1b[57379;5u".to_vec(), ), (Keystroke::parse("f20").unwrap(), b"\x1b[57383u".to_vec()), ]; let terminal_model_mock = mock_with_all_keys_as_escape(); validate_keystroke_test_cases(test_cases, &terminal_model_mock); } #[test] fn test_keyboard_enhancement_disambiguate_only() { // Test with only DISAMBIGUATE_ESC_CODES flag (flag 1) // Only ambiguous keys (like Escape) and modified keys should use CSI u // Unmodified non-ambiguous keys should fall back to legacy encoding let terminal_model_mock = mock_with_disambiguate_only(); // Escape is ambiguous - should always use CSI u with flag 1 let escape = Keystroke::parse("escape").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &escape, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[27u".to_vec()) ); // Shift+Enter is ambiguous: legacy encoding sends the same bytes for Enter // and Shift+Enter, so CSI u is needed to preserve the Shift modifier. let shift_enter = Keystroke::parse("shift-enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_enter, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[13;2u".to_vec()) ); // Shift+Tab is also ambiguous for the same reason. let shift_tab = Keystroke::parse("shift-tab").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_tab, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[9;2u".to_vec()) ); // But Shift+a is NOT ambiguous — Shift changes the character to 'A'. let shift_a = Keystroke::parse("shift-A").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), None ); // Ctrl+a has modifiers - should use CSI u. let ctrl_a = Keystroke::parse("ctrl-a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &ctrl_a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[97;5u".to_vec()) ); // Plain Enter without modifiers - NOT ambiguous, should not use CSI u. let enter = Keystroke::parse("enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &enter, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), None ); } #[test] fn test_keyboard_enhancement_unshifted_keycode_for_shifted_printables() { let terminal_model_mock = mock_with_all_keys_as_escape(); // Shifted symbols should use the unshifted keycode in CSI u. // The key_without_modifiers is provided by the platform (e.g., winit or macOS UCKeyTranslate). assert_eq!( KeystrokeWithDetails { keystroke: &Keystroke::parse("shift-@").unwrap(), key_without_modifiers: Some("2"), chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[50;2u".to_vec()) ); assert_eq!( KeystrokeWithDetails { keystroke: &Keystroke::parse("shift-%").unwrap(), key_without_modifiers: Some("5"), chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[53;2u".to_vec()) ); // Uppercase letters are lowercased universally, so platform info is not required. assert_eq!( KeystrokeWithDetails { keystroke: &Keystroke::parse("shift-A").unwrap(), key_without_modifiers: Some("a"), chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[97;2u".to_vec()) ); // Verify the letter case also works without platform info. assert_eq!( KeystrokeWithDetails { keystroke: &Keystroke::parse("shift-A").unwrap(), key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), Some(b"\x1b[97;2u".to_vec()) ); } #[test] fn test_keyboard_enhancement_mac_option_without_meta_mapping_is_not_disambiguated() { if !OperatingSystem::get().is_mac() { return; } let terminal_model_mock = mock_with_disambiguate_only(); // On macOS with Option-as-Meta disabled, Alt should not force CSI u in disambiguate-only mode. let alt_a = Keystroke::parse("alt-a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &alt_a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&terminal_model_mock), None ); } #[test] fn test_keyboard_enhancement_alternate_keys() { // With REPORT_ALTERNATE_KEYS (flag 4): shifted keys include the shifted code after a colon. let mock = mock_with_alternate_keys(); // shift+a: base=97 (a), alternate=65 (A) → CSI 97:65;2u let shift_a = Keystroke::parse("shift-A").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_a, key_without_modifiers: Some("a"), chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[97:65;2u".to_vec()) ); // shift+@ (shift+2): base=50 (2), alternate=64 (@) → CSI 50:64;2u let shift_at = Keystroke::parse("shift-@").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_at, key_without_modifiers: Some("2"), chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[50:64;2u".to_vec()) ); // shift+% (shift+5): base=53 (5), alternate=37 (%) → CSI 53:37;2u let shift_pct = Keystroke::parse("shift-%").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_pct, key_without_modifiers: Some("5"), chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[53:37;2u".to_vec()) ); // Without shift, no alternate key is reported. let a = Keystroke::parse("a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[97u".to_vec()) ); // ctrl+a: no shift → no alternate key even with flag 4. let ctrl_a = Keystroke::parse("ctrl-a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &ctrl_a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[97;5u".to_vec()) ); // Special keys (enter) don't have alternate keys. let shift_enter = Keystroke::parse("shift-enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_enter, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[13;2u".to_vec()) ); } #[test] fn test_keyboard_enhancement_associated_text() { // With REPORT_ASSOCIATED_TEXT (flag 16): text codepoints appended as third parameter. let mock = mock_with_associated_text(); // Plain 'a': OS text is "a" (97) → CSI 97;1;97u (modifiers=1 must be present for text field) let a = Keystroke::parse("a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &a, key_without_modifiers: None, chars: Some("a"), } .to_escape_sequence(&mock), Some(b"\x1b[97;1;97u".to_vec()) ); // shift+A: OS text is "A" (65) → CSI 97;2;65u let shift_a = Keystroke::parse("shift-A").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_a, key_without_modifiers: Some("a"), chars: Some("A"), } .to_escape_sequence(&mock), Some(b"\x1b[97;2;65u".to_vec()) ); // ctrl+a: OS text is control character "\x01" → filtered, no associated text → CSI 97;5u let ctrl_a = Keystroke::parse("ctrl-a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &ctrl_a, key_without_modifiers: None, chars: Some("\x01"), } .to_escape_sequence(&mock), Some(b"\x1b[97;5u".to_vec()) ); // Enter: OS text is "\r" (control character) → no associated text → CSI 13u let enter = Keystroke::parse("enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &enter, key_without_modifiers: None, chars: Some("\r"), } .to_escape_sequence(&mock), Some(b"\x1b[13u".to_vec()) ); // Escape: OS text is "\x1b" (control character) → no associated text → CSI 27u let escape = Keystroke::parse("escape").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &escape, key_without_modifiers: None, chars: Some("\x1b"), } .to_escape_sequence(&mock), Some(b"\x1b[27u".to_vec()) ); } #[test] fn test_keyboard_enhancement_alternate_keys_with_associated_text() { // Both flags 4 and 16 active simultaneously. let mock = mock_with_all_flags(); // shift+A: alternate=65, text="A"=65 → CSI 97:65;2;65u (press event type omitted) let shift_a = Keystroke::parse("shift-A").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_a, key_without_modifiers: Some("a"), chars: Some("A"), } .to_escape_sequence(&mock), Some(b"\x1b[97:65;2;65u".to_vec()) ); // shift+@ (shift+2): alternate=64, text="@"=64 → CSI 50:64;2;64u let shift_at = Keystroke::parse("shift-@").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_at, key_without_modifiers: Some("2"), chars: Some("@"), } .to_escape_sequence(&mock), Some(b"\x1b[50:64;2;64u".to_vec()) ); // Plain 'z': no alternate (no shift), text="z"=122 → CSI 122;1;122u let z = Keystroke::parse("z").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &z, key_without_modifiers: None, chars: Some("z"), } .to_escape_sequence(&mock), Some(b"\x1b[122;1;122u".to_vec()) ); } #[test] fn test_keyboard_enhancement_event_types() { // With REPORT_EVENT_TYPES (flag 2): press is the default event type and is omitted. // This flag only changes behavior for repeat/release events (not yet handled for // regular keystrokes). Verify that enabling it doesn't change press encoding. let mock = mock_with_event_types(); // Plain 'a': press is default, omitted → CSI 97u (same as without flag 2) let a = Keystroke::parse("a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[97u".to_vec()) ); // ctrl+a: modifiers=5 → CSI 97;5u let ctrl_a = Keystroke::parse("ctrl-a").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &ctrl_a, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[97;5u".to_vec()) ); // Enter: no modifiers → CSI 13u let enter = Keystroke::parse("enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &enter, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[13u".to_vec()) ); // shift+enter: modifiers=2 → CSI 13;2u let shift_enter = Keystroke::parse("shift-enter").unwrap(); assert_eq!( KeystrokeWithDetails { keystroke: &shift_enter, key_without_modifiers: None, chars: None, } .to_escape_sequence(&mock), Some(b"\x1b[13;2u".to_vec()) ); }