aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-09 05:10:35 +0300
committerGitHub <noreply@github.com>2026-08-09 05:10:35 +0300
commitd277d2f83b749a38c90eb96ef7cd329cc2171cd7 (patch)
treeec4b516401fc97d8356b85557e407c1d216aa24b
parent2baebb50b0a2c639fb7fc1a4488994c78d4a0b89 (diff)
fix(input): preserve alt-prefixed control keys (#2543)
refs #2514
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/input/parse.rs26
-rw-r--r--src/pane/terminal.rs17
3 files changed, 38 insertions, 6 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md
index 5d123a8c..72d3b0f5 100644
--- a/docs/next/CHANGELOG.md
+++ b/docs/next/CHANGELOG.md
@@ -14,6 +14,7 @@
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.
### Fixed
+- Fish `Ctrl+Alt` keybindings now work in panes after legacy Alt-prefixed control bytes are decoded with both modifiers. (#2514)
- `herdr config check` now reports unknown built-in theme names instead of silently accepting them. (#2452)
- macOS `herdr --remote` clients now keep the accepted bridge socket blocking, preventing an immediate disconnect after the protocol handshake. (#2478, thanks @mathijshenquet)
- Prefix keybindings now preserve Shift in WezTerm Kitty keyboard mode, so commands such as config reload no longer trigger their unshifted action. (#2435)
diff --git a/src/input/parse.rs b/src/input/parse.rs
index 085757c3..b2318339 100644
--- a/src/input/parse.rs
+++ b/src/input/parse.rs
@@ -87,12 +87,9 @@ fn parse_legacy_key_sequence(data: &str) -> Option<TerminalKey> {
_ if data.starts_with('\x1b') => {
let rest = data.strip_prefix('\x1b')?;
if rest.chars().count() == 1 {
- let ch = rest.chars().next()?;
- let mut modifiers = KeyModifiers::ALT;
- if ch.is_ascii_uppercase() {
- modifiers |= KeyModifiers::SHIFT;
- }
- Some(TerminalKey::new(KeyCode::Char(ch), modifiers))
+ let mut key = parse_legacy_key_sequence(rest)?;
+ key.modifiers |= KeyModifiers::ALT;
+ Some(key)
} else {
None
}
@@ -550,6 +547,23 @@ mod tests {
}
#[test]
+ fn parse_legacy_alt_control_letter_composes_modifiers() {
+ let key = parse_terminal_key_sequence("\x1b\x06")
+ .expect("ctrl-alt-f legacy sequence should parse");
+ assert_terminal_key_eq(
+ key.clone(),
+ KeyCode::Char('f'),
+ KeyModifiers::CONTROL | KeyModifiers::ALT,
+ crossterm::event::KeyEventKind::Press,
+ None,
+ );
+ assert_eq!(
+ encode_terminal_key(key, KeyboardProtocol::Legacy),
+ b"\x1b\x06"
+ );
+ }
+
+ #[test]
fn unknown_legacy_ss3_sequence_remains_unsupported() {
assert!(parse_terminal_key_sequence("\x1bOz").is_none());
}
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index 91fcb4fe..a6e5c1da 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -4593,6 +4593,23 @@ mod tests {
}
#[test]
+ fn ghostty_kitty_pane_preserves_legacy_ctrl_alt_letter() {
+ 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[>5u", &tx);
+
+ let mut events = crate::raw_input::parse_raw_input_bytes_sync(b"\x1b\x06");
+ let crate::raw_input::RawInputEvent::Key(key) = events.remove(0) else {
+ panic!("expected key event");
+ };
+ let encoded = pane.encode_terminal_key(key, pane.keyboard_protocol().unwrap());
+
+ assert_eq!(encoded, b"\x1b[102;7u");
+ }
+
+ #[test]
fn ghostty_pane_characterizes_ctrl_backspace_encoding() {
let (tx, _rx) = mpsc::channel(4);
let legacy = GhosttyPaneTerminal::new(