diff --git a/src/event.rs b/src/event.rs index 7d5e9ebe3..e22873669 100644 --- a/src/event.rs +++ b/src/event.rs @@ -436,6 +436,42 @@ impl Command for DisableBracketedPaste { } } +/// A command that enables DECKPAM (application keypad) mode. +/// +/// It should be paired with [`DisableApplicationKeypad`] at the end of execution. +/// +/// When enabled, numeric keypad keys send distinct escape sequences ([`KeyCode::Keypad0`] +/// through [`KeyCode::Keypad9`], etc.) instead of regular characters. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableApplicationKeypad; + +impl Command for EnableApplicationKeypad { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str("\x1B=") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + Ok(()) + } +} + +/// A command that disables DECKPAM (application keypad) mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableApplicationKeypad; + +impl Command for DisableApplicationKeypad { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str("\x1B>") + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + // DECKPAM is handled by the terminal emulator via ANSI sequences + Ok(()) + } +} + /// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys. /// /// It should be paired with [`PopKeyboardEnhancementFlags`] at the end of execution. @@ -1292,6 +1328,42 @@ pub enum KeyCode { /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with /// [`PushKeyboardEnhancementFlags`]. KeypadBegin, + /// Keypad 0 key (DECKPAM application mode). + Keypad0, + /// Keypad 1 key (DECKPAM application mode). + Keypad1, + /// Keypad 2 key (DECKPAM application mode). + Keypad2, + /// Keypad 3 key (DECKPAM application mode). + Keypad3, + /// Keypad 4 key (DECKPAM application mode). + Keypad4, + /// Keypad 5 key (DECKPAM application mode). + Keypad5, + /// Keypad 6 key (DECKPAM application mode). + Keypad6, + /// Keypad 7 key (DECKPAM application mode). + Keypad7, + /// Keypad 8 key (DECKPAM application mode). + Keypad8, + /// Keypad 9 key (DECKPAM application mode). + Keypad9, + /// Keypad multiply (*) key (DECKPAM application mode). + KeypadMultiply, + /// Keypad plus (+) key (DECKPAM application mode). + KeypadPlus, + /// Keypad comma (,) key (DECKPAM application mode). + KeypadComma, + /// Keypad minus (-) key (DECKPAM application mode). + KeypadMinus, + /// Keypad period (.) key (DECKPAM application mode). + KeypadPeriod, + /// Keypad divide (/) key (DECKPAM application mode). + KeypadDivide, + /// Keypad equal (=) key (DECKPAM application mode). + KeypadEqual, + /// Keypad enter key (DECKPAM application mode). + KeypadEnter, /// A media key. /// /// **Note:** these keys can only be read if @@ -1445,6 +1517,24 @@ impl Display for KeyCode { KeyCode::Pause => write!(f, "Pause"), KeyCode::Menu => write!(f, "Menu"), KeyCode::KeypadBegin => write!(f, "Begin"), + KeyCode::Keypad0 => write!(f, "Keypad 0"), + KeyCode::Keypad1 => write!(f, "Keypad 1"), + KeyCode::Keypad2 => write!(f, "Keypad 2"), + KeyCode::Keypad3 => write!(f, "Keypad 3"), + KeyCode::Keypad4 => write!(f, "Keypad 4"), + KeyCode::Keypad5 => write!(f, "Keypad 5"), + KeyCode::Keypad6 => write!(f, "Keypad 6"), + KeyCode::Keypad7 => write!(f, "Keypad 7"), + KeyCode::Keypad8 => write!(f, "Keypad 8"), + KeyCode::Keypad9 => write!(f, "Keypad 9"), + KeyCode::KeypadMultiply => write!(f, "Keypad *"), + KeyCode::KeypadPlus => write!(f, "Keypad +"), + KeyCode::KeypadComma => write!(f, "Keypad ,"), + KeyCode::KeypadMinus => write!(f, "Keypad -"), + KeyCode::KeypadPeriod => write!(f, "Keypad ."), + KeyCode::KeypadDivide => write!(f, "Keypad /"), + KeyCode::KeypadEqual => write!(f, "Keypad ="), + KeyCode::KeypadEnter => write!(f, "Keypad Enter"), KeyCode::Media(media) => write!(f, "{media}"), KeyCode::Modifier(modifier) => write!(f, "{modifier}"), } diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 8897096c0..c14f8c945 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -46,31 +46,61 @@ pub(crate) fn parse_event( if buffer.len() == 2 { Ok(None) } else { - match buffer[2] { - b'D' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) - } - b'C' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Right.into(), - )))), - b'A' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into())))) - } - b'B' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into())))) - } - b'H' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into())))) - } - b'F' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into())))) + // Check for DECKPAM sequences with modifiers: ESC O + // Modifier codes: 2=Shift, 3=Alt, 4=Shift+Alt, 5=Ctrl, 6=Shift+Ctrl, + // 7=Alt+Ctrl, 8=Shift+Alt+Ctrl + let (modifier, key_index) = if buffer[2].is_ascii_digit() { + // We have a modifier byte, need to wait for the key byte + if buffer.len() == 3 { + return Ok(None); // Wait for the key byte } + (Some(buffer[2]), 3) + } else { + (None, 2) + }; + + // Parse modifiers using the same bitwise extraction used elsewhere in the codebase + // Convert ASCII digit ('2'-'8') to numeric value (2-8) first + let modifiers = if let Some(modifier_byte) = modifier { + parse_modifiers(modifier_byte - b'0') + } else { + KeyModifiers::empty() + }; + + let keycode = match buffer[key_index] { + b'D' => KeyCode::Left, + b'C' => KeyCode::Right, + b'A' => KeyCode::Up, + b'B' => KeyCode::Down, + b'H' => KeyCode::Home, + b'F' => KeyCode::End, // F1-F4 - val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::F(1 + val - b'P').into(), - )))), - _ => Err(could_not_parse_event_error()), - } + val @ b'P'..=b'S' => KeyCode::F(1 + val - b'P'), + // DECKPAM keypad keys (application mode) + b'p' => KeyCode::Keypad0, + b'q' => KeyCode::Keypad1, + b'r' => KeyCode::Keypad2, + b's' => KeyCode::Keypad3, + b't' => KeyCode::Keypad4, + b'u' => KeyCode::Keypad5, + b'v' => KeyCode::Keypad6, + b'w' => KeyCode::Keypad7, + b'x' => KeyCode::Keypad8, + b'y' => KeyCode::Keypad9, + b'j' => KeyCode::KeypadMultiply, + b'k' => KeyCode::KeypadPlus, + b'l' => KeyCode::KeypadComma, + b'm' => KeyCode::KeypadMinus, + b'n' => KeyCode::KeypadPeriod, + b'o' => KeyCode::KeypadDivide, + b'X' => KeyCode::KeypadEqual, + b'M' => KeyCode::KeypadEnter, + _ => return Err(could_not_parse_event_error()), + }; + + Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( + keycode, modifiers, + ))))) } } b'[' => parse_csi(buffer), @@ -1503,4 +1533,387 @@ mod tests { )))), ); } + + #[test] + fn test_parse_deckpam_keypad_numeric_keys() { + // Test keypad 0-9 without modifiers + assert_eq!( + parse_event(b"\x1BOp", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad0, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOq", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad1, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOr", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad2, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOs", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad3, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOt", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad4, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOu", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad5, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOv", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad6, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOw", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad7, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOx", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad8, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOy", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad9, + KeyModifiers::empty() + )))), + ); + } + + #[test] + fn test_parse_deckpam_keypad_operators() { + // Test keypad operator keys without modifiers + assert_eq!( + parse_event(b"\x1BOj", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadMultiply, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOk", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadPlus, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOl", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadComma, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOm", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadMinus, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOn", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadPeriod, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOo", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadDivide, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOX", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadEqual, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOM", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadEnter, + KeyModifiers::empty() + )))), + ); + } + + #[test] + fn test_parse_deckpam_keypad_with_shift_modifier() { + // Test keypad keys with Shift modifier (modifier code 2) + assert_eq!( + parse_event(b"\x1BO2p", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad0, + KeyModifiers::SHIFT + )))), + ); + assert_eq!( + parse_event(b"\x1BO2q", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad1, + KeyModifiers::SHIFT + )))), + ); + assert_eq!( + parse_event(b"\x1BO2j", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadMultiply, + KeyModifiers::SHIFT + )))), + ); + assert_eq!( + parse_event(b"\x1BO2M", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadEnter, + KeyModifiers::SHIFT + )))), + ); + } + + #[test] + fn test_parse_deckpam_keypad_with_alt_modifier() { + // Test keypad keys with Alt modifier (modifier code 3) + assert_eq!( + parse_event(b"\x1BO3p", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad0, + KeyModifiers::ALT + )))), + ); + assert_eq!( + parse_event(b"\x1BO3y", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad9, + KeyModifiers::ALT + )))), + ); + assert_eq!( + parse_event(b"\x1BO3k", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadPlus, + KeyModifiers::ALT + )))), + ); + } + + #[test] + fn test_parse_deckpam_keypad_with_ctrl_modifier() { + // Test keypad keys with Control modifier (modifier code 5) + assert_eq!( + parse_event(b"\x1BO5p", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad0, + KeyModifiers::CONTROL + )))), + ); + assert_eq!( + parse_event(b"\x1BO5x", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad8, + KeyModifiers::CONTROL + )))), + ); + assert_eq!( + parse_event(b"\x1BO5o", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::KeypadDivide, + KeyModifiers::CONTROL + )))), + ); + } + + #[test] + fn test_parse_deckpam_keypad_with_combined_modifiers() { + // Test Shift+Alt (modifier code 4) + assert_eq!( + parse_event(b"\x1BO4p", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad0, + KeyModifiers::SHIFT | KeyModifiers::ALT + )))), + ); + + // Test Shift+Ctrl (modifier code 6) + assert_eq!( + parse_event(b"\x1BO6q", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad1, + KeyModifiers::SHIFT | KeyModifiers::CONTROL + )))), + ); + + // Test Alt+Ctrl (modifier code 7) + assert_eq!( + parse_event(b"\x1BO7r", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad2, + KeyModifiers::ALT | KeyModifiers::CONTROL + )))), + ); + + // Test Shift+Alt+Ctrl (modifier code 8) + assert_eq!( + parse_event(b"\x1BO8s", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Keypad3, + KeyModifiers::SHIFT | KeyModifiers::ALT | KeyModifiers::CONTROL + )))), + ); + } + + #[test] + fn test_parse_deckpam_incomplete_sequences() { + // Test that incomplete sequences return Ok(None) to wait for more bytes + assert_eq!( + parse_event(b"\x1BO", true).unwrap(), + None, + "Incomplete ESC O sequence should wait for more bytes" + ); + + assert_eq!( + parse_event(b"\x1BO2", true).unwrap(), + None, + "Incomplete ESC O with modifier should wait for key byte" + ); + } + + #[test] + fn test_parse_deckpam_existing_keys_still_work() { + // Ensure existing ESC O sequences (arrows, Home, End, F1-F4) still work + assert_eq!( + parse_event(b"\x1BOD", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Left, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOC", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Right, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOA", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Up, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOB", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Down, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOH", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Home, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOF", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::End, + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOP", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::F(1), + KeyModifiers::empty() + )))), + ); + assert_eq!( + parse_event(b"\x1BOQ", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::F(2), + KeyModifiers::empty() + )))), + ); + } + + #[test] + fn test_parse_deckpam_existing_keys_with_modifiers() { + // Test that existing keys work with the new modifier support + assert_eq!( + parse_event(b"\x1BO2D", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Left, + KeyModifiers::SHIFT + )))), + ); + assert_eq!( + parse_event(b"\x1BO5C", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Right, + KeyModifiers::CONTROL + )))), + ); + assert_eq!( + parse_event(b"\x1BO3H", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::Home, + KeyModifiers::ALT + )))), + ); + assert_eq!( + parse_event(b"\x1BO7P", false).unwrap(), + Some(InternalEvent::Event(Event::Key(KeyEvent::new( + KeyCode::F(1), + KeyModifiers::ALT | KeyModifiers::CONTROL + )))), + ); + } + + #[test] + fn test_parse_deckpam_invalid_sequences() { + // Test that invalid key codes after ESC O result in error + assert!( + parse_event(b"\x1BOz", false).is_err(), + "Invalid DECKPAM key code should return error" + ); + assert!( + parse_event(b"\x1BO2z", false).is_err(), + "Invalid DECKPAM key code with modifier should return error" + ); + } } diff --git a/src/event/sys/windows/parse.rs b/src/event/sys/windows/parse.rs index 97677ecf6..c03f5fa35 100644 --- a/src/event/sys/windows/parse.rs +++ b/src/event/sys/windows/parse.rs @@ -5,10 +5,12 @@ use winapi::um::{ SHIFT_PRESSED, }, winuser::{ - GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK, - VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT, - VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, - VK_TAB, VK_UP, + GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_ADD, + VK_BACK, VK_CONTROL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, + VK_F24, VK_HOME, VK_INSERT, VK_LEFT, VK_MENU, VK_MULTIPLY, VK_NEXT, VK_NUMPAD0, VK_NUMPAD1, + VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7, VK_NUMPAD8, + VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SEPARATOR, VK_SHIFT, VK_SUBTRACT, VK_TAB, + VK_UP, }, }; @@ -257,6 +259,22 @@ fn parse_key_event_record(key_event: &KeyEventRecord) -> Option VK_INSERT => Some(KeyCode::Insert), VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab), VK_TAB => Some(KeyCode::Tab), + VK_NUMPAD0 => Some(KeyCode::Keypad0), + VK_NUMPAD1 => Some(KeyCode::Keypad1), + VK_NUMPAD2 => Some(KeyCode::Keypad2), + VK_NUMPAD3 => Some(KeyCode::Keypad3), + VK_NUMPAD4 => Some(KeyCode::Keypad4), + VK_NUMPAD5 => Some(KeyCode::Keypad5), + VK_NUMPAD6 => Some(KeyCode::Keypad6), + VK_NUMPAD7 => Some(KeyCode::Keypad7), + VK_NUMPAD8 => Some(KeyCode::Keypad8), + VK_NUMPAD9 => Some(KeyCode::Keypad9), + VK_MULTIPLY => Some(KeyCode::KeypadMultiply), + VK_ADD => Some(KeyCode::KeypadPlus), + VK_SUBTRACT => Some(KeyCode::KeypadMinus), + VK_DIVIDE => Some(KeyCode::KeypadDivide), + VK_DECIMAL => Some(KeyCode::KeypadPeriod), + VK_SEPARATOR => Some(KeyCode::KeypadComma), _ => { let utf16 = key_event.u_char; match utf16 {