fix: send ctrl+shift+letter to plain shells as its control byte

This commit is contained in:
Ogulcan Celik
2026-10-06 23:49:04 +03:00
parent 5adbbf427f
commit 26a3cf0257
3 changed files with 54 additions and 0 deletions
@@ -252,6 +252,19 @@ fn legacy_shell_exception(pane_mode: &[u8], key: &str, mods: u16) -> Option<Vec<
("m", ghostty::MOD_CTRL) => b"\r",
("i", ghostty::MOD_CTRL) | ("tab", ghostty::MOD_CTRL) => b"\t",
("[", ghostty::MOD_CTRL) => b"\x1b",
// Real terminals (lab: Alacritty, WezTerm) send the control byte.
(letter, m)
if m == ghostty::MOD_CTRL | ghostty::MOD_SHIFT
&& letter.len() == 1
&& letter.as_bytes()[0].is_ascii_lowercase() =>
{
let control = [letter.as_bytes()[0] & 0x1f];
return Some(if alt {
[b"\x1b".as_slice(), &control].concat()
} else {
control.to_vec()
});
}
_ => return None,
};
Some(if alt {
+18
View File
@@ -118,6 +118,24 @@ pub(super) fn legacy_shell_key(key: crate::input::TerminalKey) -> crate::input::
KeyCode::Tab
}
(KeyCode::Char('['), KeyModifiers::CONTROL) => KeyCode::Esc,
// Ghostty reports Ctrl+Shift+letter as CSI u; every terminal sends the
// plain control byte, which is all a shell can read.
// WezTerm names it Ctrl plus an uppercase letter, without Shift.
(KeyCode::Char(c), chord)
if chord - KeyModifiers::SHIFT == KeyModifiers::CONTROL
&& c.is_ascii_alphabetic()
&& (chord.contains(KeyModifiers::SHIFT) || c.is_ascii_uppercase()) =>
{
// Ctrl+I and Ctrl+M then follow the rows above (Tab, Enter).
return legacy_shell_key(
crate::input::TerminalKey::new(
KeyCode::Char(c.to_ascii_lowercase()),
KeyModifiers::CONTROL | alt,
)
.with_kind(key.kind)
.with_repeat_count(key.repeat_count),
);
}
_ => return key,
};
crate::input::TerminalKey::new(classic, alt)
+23
View File
@@ -5001,6 +5001,29 @@ mod tests {
}
}
#[test]
fn ctrl_shift_letters_reach_plain_shells_as_control_bytes() {
// Lab capture: Alacritty and WezTerm (with or without Kitty keyboard)
// send 0x01 for Ctrl+Shift+A; Ghostty's CSI u prints as garbage in a shell.
for (host_bytes, plain, kitty) in [
("\x1b[97;6u", &b"\x01"[..], &b"\x1b[97;6u"[..]),
("\x1b[65;5u", b"\x01", b"\x1b[97;6u"),
("\x1b[97;8u", b"\x1b\x01", b"\x1b[97;8u"),
] {
let key = crate::input::parse_terminal_key_sequence(host_bytes).expect("kitty key");
assert_eq!(
test_encode_key_for_app(b"", key.clone()),
plain,
"{host_bytes:?} in a plain shell"
);
assert_eq!(
test_encode_key_for_app(b"\x1b[>1u", key),
kitty,
"{host_bytes:?} in a Kitty pane"
);
}
}
#[test]
fn legacy_alt_prefixes_the_produced_non_ascii_text() {
// A legacy host's Alt+Shift+\u{f6}. libghostty on macOS would prefix the