fix: preserve windows backspace input

refs #907

refs #920
This commit is contained in:
Ogulcan Celik 2026-07-02 16:22:11 +03:00
parent 885dbcd19e
commit f4d216b63d
3 changed files with 178 additions and 20 deletions

View File

@ -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)
}
@ -1566,6 +1570,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_"

View File

@ -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
}
@ -707,6 +716,83 @@ mod tests {
assert_eq!(encode_key(key, KeyboardProtocol::Kitty { flags: 1 }), b"a");
}
#[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);

View File

@ -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]
@ -2938,6 +2944,47 @@ mod tests {
assert_eq!(encoded, b"\x1b[127;3u");
}
#[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);