diff options
| author | Ainesh Chatterjee <ainesh@factory.ai> | 2026-09-25 10:04:54 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-25 20:04:54 +0300 |
| commit | 21d0ce60267ad947c081d3d3fba401c859f06dd2 (patch) | |
| tree | 3adac47ca21709aa124596c6da03bda129c0360b /src | |
| parent | 8d95e9bd45338c43091e04ef0c976086bb09163a (diff) | |
fix: preserve shift on ctrl+shift+letter for legacy panes (#4597)
Panes whose app has not enabled the kitty keyboard protocol received
ctrl+shift+letter as the same C0 byte as ctrl+letter. Encode the chord as
CSI-u instead, matching Ghostty's legacy encoder.
refs #4581
Co-authored-by: ain3sh <ainesh.chatterjee@gmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/encode.rs | 61 | ||||
| -rw-r--r-- | src/pane/terminal.rs | 2 |
2 files changed, 58 insertions, 5 deletions
diff --git a/src/input/encode.rs b/src/input/encode.rs index 58543232..91b28fc8 100644 --- a/src/input/encode.rs +++ b/src/input/encode.rs @@ -23,12 +23,12 @@ pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec< return Vec::new(); } - // Super has no legacy character encoding. Preserve the chord with CSI-u - // instead of leaking the unmodified character into the pane. + // Super has no legacy character encoding, and Ctrl+Shift+letter would + // collapse into the same C0 byte as Ctrl+letter. Preserve both chords with + // CSI-u, matching Ghostty's legacy encoder. if matches!(protocol, KeyboardProtocol::Legacy) && key.kind != crossterm::event::KeyEventKind::Release - && matches!(key.code, KeyCode::Char(_)) - && key.modifiers.contains(KeyModifiers::SUPER) + && legacy_chord_needs_csi_u(&key) { if let Some(bytes) = try_encode_csi_u(&key, 0) { return bytes; @@ -81,6 +81,17 @@ pub fn encode_terminal_key(key: TerminalKey, protocol: KeyboardProtocol) -> Vec< encode_legacy(key) } +fn legacy_chord_needs_csi_u(key: &TerminalKey) -> bool { + let KeyCode::Char(ch) = key.code else { + return false; + }; + key.modifiers.contains(KeyModifiers::SUPER) + || (ch.is_ascii_alphabetic() + && key + .modifiers + .contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT)) +} + #[allow(dead_code)] // exercised in input unit tests; production uses TerminalRuntime helpers pub fn encode_cursor_key(code: KeyCode, application_cursor: bool) -> Vec<u8> { match (code, application_cursor) { @@ -626,6 +637,48 @@ mod tests { } #[test] + fn legacy_ctrl_shift_letter_preserves_shift_with_csi_u() { + for ch in ['c', 'C'] { + let key = KeyEvent::new( + KeyCode::Char(ch), + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + ); + assert_eq!( + encode_key(key, KeyboardProtocol::Legacy), + b"\x1b[99;6u", + "ch={ch}" + ); + } + } + + #[test] + fn legacy_ctrl_alt_shift_letter_uses_csi_u() { + let key = KeyEvent::new( + KeyCode::Char('c'), + KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT, + ); + assert_eq!(encode_key(key, KeyboardProtocol::Legacy), b"\x1b[99;8u"); + } + + #[test] + fn legacy_ctrl_shift_letter_from_kitty_host_input_keeps_shift() { + let key = parse_terminal_key_sequence("\x1b[99:67;6:1u").expect("Ctrl+Shift+C"); + assert_eq!( + encode_terminal_key(key, KeyboardProtocol::Legacy), + b"\x1b[99;6u" + ); + } + + #[test] + fn legacy_ctrl_shift_punctuation_keeps_c0_byte() { + let key = KeyEvent::new( + KeyCode::Char('_'), + KeyModifiers::CONTROL | KeyModifiers::SHIFT, + ); + assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![31]); + } + + #[test] fn legacy_ctrl_slash_aliases_ctrl_underscore() { let key = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL); assert_eq!(encode_key(key, KeyboardProtocol::Legacy), vec![31]); diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs index 53d8e32d..d0b9d3ca 100644 --- a/src/pane/terminal.rs +++ b/src/pane/terminal.rs @@ -4959,7 +4959,7 @@ mod tests { let encoded = pane.encode_terminal_key( crate::input::TerminalKey::new( crossterm::event::KeyCode::Char('a'), - crossterm::event::KeyModifiers::CONTROL | crossterm::event::KeyModifiers::SHIFT, + crossterm::event::KeyModifiers::CONTROL, ), crate::input::KeyboardProtocol::Legacy, ); |
