From a7a1410abb76d8f926ccdf0b36c42e8ce057c555 Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Sun, 13 Sep 2026 22:14:24 +0400 Subject: fix(input): accept parameterized CSI-tilde F1-F4 keys (#4067) Terminals such as foot send F1-F4 as parameterized CSI-tilde sequences (for example F3 as `\x1b[13;1:1~`). `parse_xterm_modified_special_sequence` mapped codes 15-24 but not 11-14, so these presses were dropped before keybindings saw them. Map 11-14 to F1-F4, matching the unmodified `\x1b[11~`..`\x1b[14~` aliases already accepted. refs #1809 --- src/input/parse.rs | 32 +++++++++++++++++++++++++++++--- src/raw_input.rs | 7 +++++-- 2 files changed, 34 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/input/parse.rs b/src/input/parse.rs index 79d8811f..0213bf3f 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -252,6 +252,10 @@ fn parse_xterm_modified_special_sequence(data: &str) -> Option { "3" => KeyCode::Delete, "5" => KeyCode::PageUp, "6" => KeyCode::PageDown, + "11" => KeyCode::F(1), + "12" => KeyCode::F(2), + "13" => KeyCode::F(3), + "14" => KeyCode::F(4), "15" => KeyCode::F(5), "17" => KeyCode::F(6), "18" => KeyCode::F(7), @@ -637,9 +641,31 @@ mod tests { 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_parameterized_csi_tilde_f1_through_f4() { + // foot and some rxvt-style hosts emit F1-F4 as `CSI ;~` + // (for example F3 as `\x1b[13;1:1~`), reusing the same code table as the + // unmodified `\x1b[11~`..`\x1b[14~` forms. + let cases = [ + ("\x1b[11;2~", KeyCode::F(1), KeyModifiers::SHIFT), + ("\x1b[12;1~", KeyCode::F(2), KeyModifiers::empty()), + ("\x1b[13;1:1~", KeyCode::F(3), KeyModifiers::empty()), + ("\x1b[13;2~", KeyCode::F(3), KeyModifiers::SHIFT), + ("\x1b[14;3~", KeyCode::F(4), KeyModifiers::ALT), + ]; + + for (sequence, code, modifiers) in cases { + let parsed = parse_terminal_key_sequence(sequence).unwrap(); + assert_terminal_key_eq( + parsed, + code, + modifiers, + crossterm::event::KeyEventKind::Press, + None, + ); + } } #[test] diff --git a/src/raw_input.rs b/src/raw_input.rs index 8d0e3a8e..fc6ff52e 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -1535,6 +1535,9 @@ mod tests { (b"\x1b[57423;1u", KeyCode::Home, KeyModifiers::empty()), (b"\x1bOq", KeyCode::Char('1'), KeyModifiers::empty()), (b"\x1b[14~", KeyCode::F(4), KeyModifiers::empty()), + (b"\x1b[11;2~", KeyCode::F(1), KeyModifiers::SHIFT), + (b"\x1b[13;1:1~", KeyCode::F(3), KeyModifiers::empty()), + (b"\x1b[14;3~", KeyCode::F(4), KeyModifiers::ALT), (b"\x1b[57364;1u", KeyCode::F(1), KeyModifiers::empty()), (b"\x1b[57366;1u", KeyCode::F(3), KeyModifiers::empty()), (b"\x1b[57366;2u", KeyCode::F(3), KeyModifiers::SHIFT), @@ -1574,11 +1577,11 @@ mod tests { } #[test] - fn modified_rxvt_f_key_alias_stays_unsupported() { + fn parses_modified_rxvt_f_key_alias() { let (event, consumed) = extract_one_event(b"\x1b[14;3~").unwrap(); assert_eq!(consumed, 7); - assert!(matches!(event, RawInputEvent::Unsupported)); + assert_raw_key(event, KeyCode::F(4), KeyModifiers::ALT); } #[test] -- cgit v1.2.3