aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 16:22:11 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-02 16:22:11 +0300
commitf4d216b63d010987775dc6eaf92afa72f86f2816 (patch)
tree3af6b07d744a581231a6dabf49e658c6640fdfed
parent885dbcd19e5dc1ba3ac2fe96ce714cf6906a458c (diff)
fix: preserve windows backspace input
refs #907 refs #920
-rw-r--r--src/client/input/windows_vti.rs25
-rw-r--r--src/input/encode.rs90
-rw-r--r--src/pane/terminal.rs83
3 files changed, 178 insertions, 20 deletions
diff --git a/src/client/input/windows_vti.rs b/src/client/input/windows_vti.rs
index 49e77909..6404e586 100644
--- a/src/client/input/windows_vti.rs
+++ b/src/client/input/windows_vti.rs
@@ -497,6 +497,10 @@ impl WindowsInputMapper {
return None;
}
+ if record.virtual_key_code != 0 && (record.unicode < 0x20 || record.unicode == 0x7f) {
+ return None;
+ }
+
self.synthetic_utf16_unit_to_bytes(record.unicode)
}
@@ -1567,6 +1571,27 @@ mod tests {
}
#[test]
+ fn vti_win32_input_mode_backspace_stays_backspace() {
+ let records = "\x1b[8;14;8;1;0;1_\x1b[8;14;8;0;0;1_".chars().map(key_char);
+
+ assert_eq!(
+ translate(records),
+ vec![
+ crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Backspace,
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Press,
+ },
+ crate::protocol::ClientInputEvent::Key {
+ code: crate::protocol::ClientKeyCode::Backspace,
+ modifiers: 0,
+ kind: crate::protocol::ClientKeyKind::Release,
+ },
+ ]
+ );
+ }
+
+ #[test]
fn vti_win32_input_mode_ctrl_j_preserves_lf_control_key() {
let records = "\x1b[74;36;10;1;8;1_\x1b[74;36;10;0;8;1_"
.chars()
diff --git a/src/input/encode.rs b/src/input/encode.rs
index 6323f1ee..a9eb19f0 100644
--- a/src/input/encode.rs
+++ b/src/input/encode.rs
@@ -4,6 +4,7 @@ use super::{KeyboardProtocol, MouseProtocolEncoding, TerminalKey};
const KITTY_FLAG_REPORT_EVENT_TYPES: u16 = 0b0000_0010;
const KITTY_FLAG_REPORT_ALTERNATE_KEYS: u16 = 0b0000_0100;
+const KITTY_FLAG_REPORT_ALL_KEYS: u16 = 0b0000_1000;
/// Encode a key event for a PTY child using the pane's negotiated keyboard protocol.
#[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers
@@ -158,9 +159,17 @@ fn push_mouse_codepoint(bytes: &mut Vec<u8>, value: u32) -> Option<()> {
fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option<Vec<u8>> {
let mods = key.modifiers;
let event_suffix = kitty_event_suffix(key, flags);
+ let report_all_keys = flags & KITTY_FLAG_REPORT_ALL_KEYS != 0;
+
+ if !report_all_keys
+ && key.modifiers.is_empty()
+ && matches!(key.code, KeyCode::Enter | KeyCode::Tab | KeyCode::Backspace)
+ {
+ return None;
+ }
// Unmodified keys use legacy encoding (more compatible)
- if mods.is_empty() && event_suffix.is_none() {
+ if mods.is_empty() && event_suffix.is_none() && !report_all_keys {
return None;
}
@@ -180,7 +189,7 @@ fn try_encode_csi_u(key: &TerminalKey, flags: u16) -> Option<Vec<u8>> {
| KeyCode::Insert
| KeyCode::Delete
| KeyCode::F(_)
- if event_suffix.is_none() =>
+ if event_suffix.is_none() && !report_all_keys =>
{
return None; // let legacy handle these
}
@@ -708,6 +717,83 @@ mod tests {
}
#[test]
+ fn kitty_report_event_types_keeps_basic_compatibility_keys_legacy() {
+ let cases = [
+ (KeyCode::Enter, b"\r".as_slice()),
+ (KeyCode::Tab, b"\t".as_slice()),
+ (KeyCode::Backspace, b"\x7f".as_slice()),
+ ];
+
+ for (code, expected) in cases {
+ let press = KeyEvent::new_with_kind(
+ code,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Press,
+ );
+ assert_eq!(
+ encode_key(press, KeyboardProtocol::Kitty { flags: 3 }),
+ expected,
+ "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS"
+ );
+
+ let repeat = KeyEvent::new_with_kind(
+ code,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Repeat,
+ );
+ assert_eq!(
+ encode_key(repeat, KeyboardProtocol::Kitty { flags: 3 }),
+ expected,
+ "{code:?} repeat should stay legacy-compatible without REPORT_ALL_KEYS"
+ );
+
+ let release = KeyEvent::new_with_kind(
+ code,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Release,
+ );
+ assert_eq!(
+ encode_key(release, KeyboardProtocol::Kitty { flags: 3 }),
+ b"",
+ "{code:?} release should not fall back to legacy bytes"
+ );
+ }
+ }
+
+ #[test]
+ fn kitty_report_all_keys_encodes_basic_compatibility_keys_with_events() {
+ let enter_press = KeyEvent::new_with_kind(
+ KeyCode::Enter,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Press,
+ );
+ assert_eq!(
+ encode_key(enter_press, KeyboardProtocol::Kitty { flags: 9 }),
+ b"\x1b[13;1u"
+ );
+
+ let backspace_press = KeyEvent::new_with_kind(
+ KeyCode::Backspace,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Press,
+ );
+ assert_eq!(
+ encode_key(backspace_press, KeyboardProtocol::Kitty { flags: 11 }),
+ b"\x1b[127;1:1u"
+ );
+
+ let backspace_release = KeyEvent::new_with_kind(
+ KeyCode::Backspace,
+ KeyModifiers::empty(),
+ crossterm::event::KeyEventKind::Release,
+ );
+ assert_eq!(
+ encode_key(backspace_release, KeyboardProtocol::Kitty { flags: 11 }),
+ b"\x1b[127;1:3u"
+ );
+ }
+
+ #[test]
fn kitty_shift_tab() {
let key = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT);
assert_eq!(
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 708a7d28..79b4a19e 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -2689,30 +2689,36 @@ mod tests {
}
#[test]
- fn ghostty_release_still_encoded_for_report_event_pane() {
+ fn ghostty_report_event_pane_keeps_basic_compatibility_keys_legacy() {
let (tx, _rx) = mpsc::channel(4);
// Push kitty flags including REPORT_EVENT_TYPES (0b10) + DISAMBIGUATE (0b1).
let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
terminal.write(b"\x1b[>3u");
let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
- let release = pane.encode_terminal_key(
- crate::input::TerminalKey::new(
- crossterm::event::KeyCode::Enter,
- crossterm::event::KeyModifiers::empty(),
- )
- .with_kind(crossterm::event::KeyEventKind::Release),
- pane.keyboard_protocol().unwrap(),
- );
- let parsed =
- crate::input::parse_terminal_key_sequence(std::str::from_utf8(&release).unwrap())
- .unwrap();
- assert_eq!(parsed.code, crossterm::event::KeyCode::Enter);
- assert_eq!(
- parsed.kind,
- crossterm::event::KeyEventKind::Release,
- "report-event pane should encode a release, got {release:?}"
- );
+ for (code, expected) in [
+ (crossterm::event::KeyCode::Enter, b"\r".as_slice()),
+ (crossterm::event::KeyCode::Backspace, b"\x7f".as_slice()),
+ ] {
+ let press = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty()),
+ pane.keyboard_protocol().unwrap(),
+ );
+ assert_eq!(
+ press, expected,
+ "{code:?} press should stay legacy-compatible without REPORT_ALL_KEYS"
+ );
+
+ let release = pane.encode_terminal_key(
+ crate::input::TerminalKey::new(code, crossterm::event::KeyModifiers::empty())
+ .with_kind(crossterm::event::KeyEventKind::Release),
+ pane.keyboard_protocol().unwrap(),
+ );
+ assert!(
+ release.is_empty(),
+ "{code:?} release should not fall back to legacy bytes, got {release:?}"
+ );
+ }
}
#[test]
@@ -2939,6 +2945,47 @@ mod tests {
}
#[test]
+ fn ghostty_pane_characterizes_ctrl_backspace_encoding() {
+ let (tx, _rx) = mpsc::channel(4);
+ let legacy = GhosttyPaneTerminal::new(
+ crate::ghostty::Terminal::new(80, 24, 0).unwrap(),
+ tx.clone(),
+ )
+ .unwrap();
+
+ let ctrl_backspace = crate::input::TerminalKey::new(
+ crossterm::event::KeyCode::Backspace,
+ crossterm::event::KeyModifiers::CONTROL,
+ );
+ assert_eq!(
+ legacy.encode_terminal_key(ctrl_backspace, crate::input::KeyboardProtocol::Legacy),
+ b"\x08"
+ );
+
+ let plain_backspace = crate::input::TerminalKey::new(
+ crossterm::event::KeyCode::Backspace,
+ crossterm::event::KeyModifiers::empty(),
+ );
+ assert_eq!(
+ legacy.encode_terminal_key(plain_backspace, crate::input::KeyboardProtocol::Legacy),
+ b"\x7f"
+ );
+
+ let kitty = GhosttyPaneTerminal::new(
+ crate::ghostty::Terminal::new(80, 24, 0).unwrap(),
+ tx.clone(),
+ )
+ .unwrap();
+ let pane_id = PaneId::from_raw(1);
+ kitty.process_pty_bytes(pane_id, 0, b"\x1b[>1u", &tx);
+
+ assert_eq!(
+ kitty.encode_terminal_key(ctrl_backspace, crate::input::KeyboardProtocol::Legacy),
+ b"\x1b[127;5u"
+ );
+ }
+
+ #[test]
fn ghostty_key_encoders_are_isolated_per_pane() {
let (tx, _rx) = mpsc::channel(4);
let first = GhosttyPaneTerminal::new(