From fa0e380e627644f269cfbd0ed4cf79f15ff74176 Mon Sep 17 00:00:00 2001 From: kangal-bot Date: Mon, 18 May 2026 20:13:19 +0300 Subject: fix: preserve alt backspace in panes (#165) refs #155 Co-authored-by: kangal-bot --- src/app/input/terminal.rs | 27 +++++++++++++++++++++++++++ src/input/encode.rs | 15 +++++++++++++++ src/input/parse.rs | 17 +++++++++++++++++ src/pane/terminal.rs | 14 ++++++++++++++ src/raw_input.rs | 22 ++++++++++++++++++++++ 5 files changed, 95 insertions(+) (limited to 'src') 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), -- cgit v1.2.3