aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-15 04:53:46 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-15 04:53:46 +0300
commitefe7c557690157a4e133cd60dd7e2ec1bf6124f7 (patch)
treed0102e8bc49c291f84118c9612a662a58cf9946d /src
parent3e8f906c6fcc4e9e7be6b3d0c4ee84b804027b5a (diff)
fix: accept alternate f1-f4 key sequences
refs #574
Diffstat (limited to 'src')
-rw-r--r--src/app/mod.rs9
-rw-r--r--src/input/parse.rs77
-rw-r--r--src/raw_input.rs9
3 files changed, 87 insertions, 8 deletions
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<TerminalKey> {
"\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[15~").expect("f5 should parse"),
- KeyCode::F(5),
+ 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[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,
@@ -471,12 +515,29 @@ mod tests {
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),
KeyModifiers::SHIFT,
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),
];
@@ -988,6 +989,14 @@ mod tests {
}
#[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);
let mut buffer = vec![ESC];