From efe7c557690157a4e133cd60dd7e2ec1bf6124f7 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Mon, 15 Jun 2026 04:53:46 +0300 Subject: [PATCH] fix: accept alternate f1-f4 key sequences refs #574 --- docs/next/CHANGELOG.md | 1 + src/app/mod.rs | 9 +++++ src/input/parse.rs | 77 +++++++++++++++++++++++++++++++++++++----- src/raw_input.rs | 9 +++++ 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 346193cb..e41c14e0 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -31,6 +31,7 @@ ### Fixed - Config startup and reload now warn about unknown top-level table sections, including a `[toast]` hint that points to `[ui.toast]`, instead of silently ignoring them. - Claude Code session restore now accepts real `/clear`, `/resume`, and compacted session identity changes while still ignoring nested `claude -p` startup sessions that inherit the pane environment. (#620) +- F1-F4 key presses sent as `ESC[11~` through `ESC[14~` now reach pane apps instead of being dropped. (#574) ## [0.6.10] - 2026-06-11 diff --git a/src/app/mod.rs b/src/app/mod.rs index 36fedb71..d45e085b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3971,6 +3971,15 @@ last_pane = "prefix+tab" app.route_client_input(b"\x1b[99;5u".to_vec()); assert_eq!(rx.recv().await.unwrap(), bytes::Bytes::from(vec![3])); + + // iTerm2 and rxvt-style hosts may send F4 as CSI 14~. Normalize it + // through the same semantic key path instead of leaking host bytes. + app.route_client_input(b"\x1b[14~".to_vec()); + + assert_eq!( + rx.recv().await.unwrap(), + bytes::Bytes::from_static(b"\x1bOS") + ); } #[tokio::test] diff --git a/src/input/parse.rs b/src/input/parse.rs index 79f16817..11b873b6 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -151,10 +151,10 @@ fn parse_legacy_special_sequence(data: &str) -> Option { "\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())), - "\x1bOS" => Some(TerminalKey::new(KeyCode::F(4), 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())), @@ -407,16 +407,60 @@ mod tests { #[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("\x1bOP").expect("f1 should parse"), - KeyCode::F(1), + 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[15~").expect("f5 should parse"), - KeyCode::F(5), + 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, @@ -470,6 +514,20 @@ mod tests { 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), @@ -477,6 +535,9 @@ 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] diff --git a/src/raw_input.rs b/src/raw_input.rs index 8925e4a4..fddcb3fd 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -954,6 +954,7 @@ mod tests { (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[14~", KeyCode::F(4), KeyModifiers::empty()), (b"\x1b[49:33;2:1u", KeyCode::Char('1'), KeyModifiers::SHIFT), ]; @@ -987,6 +988,14 @@ mod tests { assert!(matches!(event, RawInputEvent::Unsupported)); } + #[test] + fn modified_rxvt_f_key_alias_stays_unsupported() { + let (event, consumed) = extract_one_event(b"\x1b[14;3~").unwrap(); + + assert_eq!(consumed, 7); + assert!(matches!(event, RawInputEvent::Unsupported)); + } + #[test] fn flushes_lone_escape_after_timeout() { let (tx, mut rx) = mpsc::channel(4);