diff --git a/src/app/input/terminal.rs b/src/app/input/terminal.rs index a9af902a..b5ceda12 100644 --- a/src/app/input/terminal.rs +++ b/src/app/input/terminal.rs @@ -486,6 +486,33 @@ mod tests { assert_eq!(app.state.mode, Mode::Terminal); } + #[tokio::test] + async fn alt_backspace_is_forwarded_to_focused_pane() { + let mut app = app_for_mouse_test(); + let mut ws = Workspace::test_new("test"); + let pane_id = ws.tabs[0].root_pane; + let pane_infos = ws.tabs[0].layout.panes(Rect::new(0, 0, 80, 24)); + let info = pane_infos[0].clone(); + let (runtime, mut rx) = crate::pane::PaneRuntime::test_with_channel( + info.inner_rect.width, + info.inner_rect.height, + ); + ws.tabs[0].runtimes.insert(pane_id, runtime); + + app.state.workspaces = vec![ws]; + app.state.active = Some(0); + app.state.selected = 0; + app.state.mode = Mode::Terminal; + app.state.view.pane_infos = pane_infos; + + let key = crate::input::parse_terminal_key_sequence("\x1b\x7f").unwrap(); + app.handle_terminal_key_headless(key); + + let bytes = rx.try_recv().unwrap(); + assert_eq!(bytes.as_ref(), b"\x1b\x7f"); + assert!(rx.try_recv().is_err()); + } + #[tokio::test] async fn page_up_scrolls_plain_shell_pane() { let mut app = app_for_mouse_test(); diff --git a/src/input/encode.rs b/src/input/encode.rs index 81454ddc..ea7949aa 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -523,6 +523,12 @@ mod tests { assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1ba"); } + #[test] + fn legacy_alt_backspace_sends_escape_delete() { + let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT); + assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b\x7f"); + } + #[test] fn application_cursor_keys_use_ss3_sequences() { assert_eq!(encode_cursor_key(KeyCode::Up, true), b"\x1bOA"); @@ -638,6 +644,15 @@ mod tests { ); } + #[test] + fn kitty_alt_backspace_uses_csi_u() { + let key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT); + assert_eq!( + encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), + b"\x1b[127;3u" + ); + } + #[test] fn kitty_plain_ctrl_c_uses_csi_u() { let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL); diff --git a/src/input/parse.rs b/src/input/parse.rs index a7d10ab2..367c0f69 100644 --- a/src/input/parse.rs +++ b/src/input/parse.rs @@ -67,6 +67,7 @@ fn parse_legacy_key_sequence(data: &str) -> Option { "\r" => Some(TerminalKey::new(KeyCode::Enter, KeyModifiers::empty())), "\t" => Some(TerminalKey::new(KeyCode::Tab, KeyModifiers::empty())), "\x1b" => Some(TerminalKey::new(KeyCode::Esc, KeyModifiers::empty())), + "\x1b\x7f" => Some(TerminalKey::new(KeyCode::Backspace, KeyModifiers::ALT)), "\x7f" => Some(TerminalKey::new(KeyCode::Backspace, KeyModifiers::empty())), _ if data.starts_with('\x1b') => { let rest = data.strip_prefix('\x1b')?; @@ -441,6 +442,15 @@ mod tests { assert_eq!(key.shifted_codepoint, Some('L' as u32)); } + #[test] + fn parse_kitty_alt_backspace_sequence() { + let key = parse_terminal_key_sequence("\x1b[127;3u").unwrap(); + assert_eq!(key.code, KeyCode::Backspace); + assert_eq!(key.modifiers, KeyModifiers::ALT); + assert_eq!(key.kind, crossterm::event::KeyEventKind::Press); + assert_eq!(key.shifted_codepoint, None); + } + #[test] fn parse_modify_other_keys_sequence() { let key = parse_terminal_key_sequence("\x1b[27;6;108~").unwrap(); @@ -464,6 +474,13 @@ mod tests { assert_eq!(key.modifiers, KeyModifiers::empty()); } + #[test] + fn parse_legacy_alt_backspace_sequence() { + let key = parse_terminal_key_sequence("\x1b\x7f").unwrap(); + assert_eq!(key.code, KeyCode::Backspace); + assert_eq!(key.modifiers, KeyModifiers::ALT); + } + #[test] fn parse_kitty_modifier_sequence() { let key = parse_terminal_key_sequence("\x1b[57441;2:1u").unwrap(); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index efe9c7ce..fcbedeff 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -1502,6 +1502,20 @@ mod tests { assert_eq!(after, b"\x1b[13;6u"); } + #[test] + fn ghostty_kitty_pane_encodes_parsed_legacy_alt_backspace_as_csi_u() { + let (tx, _rx) = mpsc::channel(4); + let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); + let pane = GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(); + let pane_id = PaneId::from_raw(1); + pane.process_pty_bytes(pane_id, 0, b"\x1b[>1u", &tx); + + let key = crate::input::parse_terminal_key_sequence("\x1b\x7f").unwrap(); + let encoded = pane.encode_terminal_key(key, crate::input::KeyboardProtocol::Legacy); + + assert_eq!(encoded, b"\x1b[127;3u"); + } + #[test] fn ghostty_key_encoders_are_isolated_per_pane() { let (tx, _rx) = mpsc::channel(4); diff --git a/src/raw_input.rs b/src/raw_input.rs index 28a2e561..fa496b95 100644 --- a/src/raw_input.rs +++ b/src/raw_input.rs @@ -673,6 +673,26 @@ mod tests { assert_eq!(key.modifiers, KeyModifiers::ALT); } + #[test] + fn parses_legacy_alt_backspace() { + let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b\x7f").unwrap() else { + panic!("expected key"); + }; + assert_eq!(consumed, 2); + assert_eq!(key.code, KeyCode::Backspace); + assert_eq!(key.modifiers, KeyModifiers::ALT); + } + + #[test] + fn parses_kitty_alt_backspace() { + let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[127;3u").unwrap() else { + panic!("expected key"); + }; + assert_eq!(consumed, 8); + assert_eq!(key.code, KeyCode::Backspace); + assert_eq!(key.modifiers, KeyModifiers::ALT); + } + #[test] fn parses_enhanced_pageup_press() { let (RawInputEvent::Key(key), consumed) = extract_one_event(b"\x1b[5;1:1~").unwrap() else { @@ -704,6 +724,8 @@ mod tests { (b"\x7f", KeyCode::Backspace, KeyModifiers::empty()), (b"\x1b[A", KeyCode::Up, KeyModifiers::empty()), (b"\x1b[1;3A", KeyCode::Up, KeyModifiers::ALT), + (b"\x1b\x7f", KeyCode::Backspace, KeyModifiers::ALT), + (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"\x1b[49:33;2:1u", KeyCode::Char('1'), KeyModifiers::SHIFT), diff --git a/tests/fixtures/keyboard_protocol_corpus.tsv b/tests/fixtures/keyboard_protocol_corpus.tsv index f9b1d734..a709d4d9 100644 --- a/tests/fixtures/keyboard_protocol_corpus.tsv +++ b/tests/fixtures/keyboard_protocol_corpus.tsv @@ -4,6 +4,7 @@ legacy_ctrl_z 1a char:z control press legacy_enter 0d enter - press legacy_tab 09 tab - press legacy_backspace 7f backspace - press +legacy_alt_backspace 1b7f backspace alt press legacy_up 1b5b41 up - press legacy_home 1b5b48 home - press xterm_alt_up 1b5b313b3341 up alt press @@ -12,6 +13,7 @@ xterm_shift_pageup 1b5b353b327e pageup shift press ghostty_enhanced_up_press 1b5b313b313a3141 up - press ghostty_enhanced_up_release 1b5b313b313a3341 up - release modify_other_keys_ctrl_shift_l 1b5b32373b363b3130387e char:l control+shift press +kitty_alt_backspace 1b5b3132373b3375 backspace alt press kitty_shift_letter 1b5b3130383a37363b323a3175 char:l shift press 76 kitty_shift_symbol 1b5b34393a33333b323a3175 char:1 shift press 33 kitty_release_letter 1b5b3130383a37363b323a3375 char:l shift release 76 diff --git a/tests/fixtures/macos_terminal_variants.tsv b/tests/fixtures/macos_terminal_variants.tsv index 24d3840e..c2fd8b53 100644 --- a/tests/fixtures/macos_terminal_variants.tsv +++ b/tests/fixtures/macos_terminal_variants.tsv @@ -1,13 +1,16 @@ # source key bytes_hex code modifiers kind shifted_codepoint +ghostty-macos alt+backspace 1b7f backspace alt press iterm2-macos alt+up 1b5b313b3341 up alt press iterm2-macos alt+down 1b5b313b3342 down alt press iterm2-macos alt+left 1b62 char:b alt press iterm2-macos alt+right 1b66 char:f alt press +iterm2-macos alt+backspace 1b7f backspace alt press iterm2-macos shift+enter 0d enter - press terminal-app-macos alt+up 1b1b5b41 up alt press terminal-app-macos alt+down 1b1b5b42 down alt press terminal-app-macos alt+left 1b62 char:b alt press terminal-app-macos alt+right 1b66 char:f alt press +terminal-app-macos alt+backspace 1b7f backspace alt press terminal-app-macos shift+enter 0d enter - press terminal-app-tmux alt+up 1b1b5b41 up alt press terminal-app-tmux alt+down 1b1b5b42 down alt press