diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-08 03:54:30 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-08 03:55:02 +0300 |
| commit | 580db93eccc9f03cdc31841e294b8c18855013f6 (patch) | |
| tree | d5f46e05f16ccf425d8bd2bbe89a2ac04259df87 | |
| parent | a62ecd71e6fc69c01346d85d54b6264039ab2093 (diff) | |
fix: handle application keypad input
refs #493
| -rw-r--r-- | docs/next/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/input/parse.rs | 55 | ||||
| -rw-r--r-- | src/raw_input.rs | 24 |
3 files changed, 80 insertions, 0 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index ddf9cc6b..f01d8880 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Fixed +- Numeric keypad keys that send VT100 application-keypad escape sequences now enter their digits and operators instead of being dropped. (#493) - Codex panes now stay marked working when the live status header uses reasoning-summary text such as `Investigating code output` instead of the literal `Working` label. (#501) - Native pane URL clicks now use Cmd-click on macOS and Ctrl-click on other platforms. - Windows beta installs now default to the preview channel, persist `channel = "preview"` during install, reject switching to stable until stable Windows builds exist, and update through the Windows installer without probing unsupported live-handoff/session status paths. diff --git a/src/input/parse.rs b/src/input/parse.rs index 367c0f69..79f16817 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -134,6 +134,23 @@ fn parse_legacy_special_sequence(data: &str) -> Option<TerminalKey> { "\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" => Some(TerminalKey::new(KeyCode::F(1), KeyModifiers::empty())), "\x1bOQ" => Some(TerminalKey::new(KeyCode::F(2), KeyModifiers::empty())), "\x1bOR" => Some(TerminalKey::new(KeyCode::F(3), KeyModifiers::empty())), @@ -407,6 +424,44 @@ mod tests { } #[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"), diff --git a/src/raw_input.rs b/src/raw_input.rs index a03526d8..8925e4a4 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -953,6 +953,7 @@ mod tests { (b"\x1b[127;3u", KeyCode::Backspace, KeyModifiers::ALT), (b"\x1b[57420;1u", KeyCode::Down, KeyModifiers::empty()), (b"\x1b[57423;1u", KeyCode::Home, KeyModifiers::empty()), + (b"\x1bOq", KeyCode::Char('1'), KeyModifiers::empty()), (b"\x1b[49:33;2:1u", KeyCode::Char('1'), KeyModifiers::SHIFT), ]; @@ -964,6 +965,29 @@ mod tests { } #[test] + fn raw_framer_waits_for_application_keypad_sequence_final_byte() { + let mut framer = RawInputFramer::default(); + + assert!(framer.push(b"\x1bO").is_empty()); + let events = framer.push(b"q"); + + assert_eq!(events.len(), 1); + assert_raw_key( + events.into_iter().next().unwrap(), + KeyCode::Char('1'), + KeyModifiers::empty(), + ); + } + + #[test] + fn unsupported_ss3_sequence_stays_unsupported() { + let (event, consumed) = extract_one_event(b"\x1bOz").unwrap(); + + assert_eq!(consumed, 3); + assert!(matches!(event, RawInputEvent::Unsupported)); + } + + #[test] fn flushes_lone_escape_after_timeout() { let (tx, mut rx) = mpsc::channel(4); let mut buffer = vec![ESC]; |
