fix: keep macos alt, non-us kitty keys, text releases and reply-tail recovery correct after the libghostty switch

This commit is contained in:
Ogulcan Celik
2026-10-06 17:21:23 +03:00
parent 2d9bc8eabf
commit 1717d3eb32
10 changed files with 238 additions and 167 deletions
+17
View File
@@ -3093,6 +3093,23 @@ impl KeyEncoder {
unsafe { ffi::ghostty_key_encoder_setopt_from_terminal(self.raw, terminal.raw()) }
}
/// Whether macOS Option acts as Alt (ESC prefix) rather than a text
/// modifier. `set_from_terminal` resets this to false.
pub fn set_macos_option_as_alt(&mut self, enabled: bool) {
let value = if enabled {
ffi::GhosttyOptionAsAlt_GHOSTTY_OPTION_AS_ALT_TRUE
} else {
ffi::GhosttyOptionAsAlt_GHOSTTY_OPTION_AS_ALT_FALSE
};
unsafe {
ffi::ghostty_key_encoder_setopt(
self.raw,
ffi::GhosttyKeyEncoderOption_GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT,
(&value as *const ffi::GhosttyOptionAsAlt).cast(),
)
}
}
pub fn set_kitty_flags(&mut self, flags: u8) {
unsafe {
ffi::ghostty_key_encoder_setopt(
+8 -18
View File
@@ -126,7 +126,7 @@ fn unix_stdin_reader_loop(
if (framer.has_pending_input() || !pending_palette.is_empty())
&& stdin_read_ready(
&reader,
idle_flush_timeout_ms(&framer, host_mouse_capture_active.load(Ordering::Acquire)),
framer.idle_flush_timeout_ms(host_mouse_capture_active.load(Ordering::Acquire)),
) == Some(false)
{
let had_pending = framer.has_pending_input();
@@ -228,10 +228,8 @@ fn unix_stdin_reader_loop(
return;
}
let timeout_ms = idle_flush_timeout_ms(
&framer,
host_mouse_capture_active.load(Ordering::Acquire),
);
let timeout_ms =
framer.idle_flush_timeout_ms(host_mouse_capture_active.load(Ordering::Acquire));
if stdin_read_ready(&reader, timeout_ms) == Some(false) {
let had_pending = framer.has_pending_input();
let chunks = framer.flush_timeout();
@@ -371,14 +369,6 @@ fn flush_unix_palette_input(
.is_ok()
}
#[cfg(unix)]
fn idle_flush_timeout_ms(
framer: &crate::raw_input::RawInputByteFramer,
host_mouse_capture_active: bool,
) -> i32 {
framer.idle_flush_timeout_ms(host_mouse_capture_active)
}
#[cfg(windows)]
fn windows_stdin_reader_loop(
event_tx: mpsc::Sender<ClientLoopEvent>,
@@ -601,7 +591,7 @@ mod tests {
next: &[u8],
) -> Vec<Vec<u8>> {
let first_wait =
std::time::Duration::from_millis(idle_flush_timeout_ms(framer, mouse_capture) as u64);
std::time::Duration::from_millis(framer.idle_flush_timeout_ms(mouse_capture) as u64);
let mut chunks = Vec::new();
if gap >= first_wait {
chunks.extend(framer.flush_timeout());
@@ -869,23 +859,23 @@ mod tests {
// A lone ESC or ESC[ may still be a key (Escape, Alt+[).
for framer in [&escape, &csi] {
assert_eq!(
idle_flush_timeout_ms(framer, false),
framer.idle_flush_timeout_ms(false),
crate::raw_input::RAW_INPUT_IDLE_FLUSH_TIMEOUT_MS
);
}
assert_eq!(
idle_flush_timeout_ms(&escape, true),
escape.idle_flush_timeout_ms(true),
crate::raw_input::MOUSE_ACTIVE_ESCAPE_SEQUENCE_FLUSH_TIMEOUT_MS
);
assert_eq!(
idle_flush_timeout_ms(&csi, true),
csi.idle_flush_timeout_ms(true),
crate::raw_input::MOUSE_ACTIVE_CSI_INTRODUCER_FLUSH_TIMEOUT_MS
);
// Anything further inside a sequence cannot be a key: wait for the rest.
for framer in [&sgr_mouse, &default_mouse, &unrelated] {
for mouse_capture in [false, true] {
assert_eq!(
idle_flush_timeout_ms(framer, mouse_capture),
framer.idle_flush_timeout_ms(mouse_capture),
crate::raw_input::INCOMPLETE_SEQUENCE_FLUSH_TIMEOUT_MS
);
}
+54 -28
View File
@@ -358,37 +358,24 @@ impl ClientShellState {
self.execute_repeat_plan(lease_key, key, plan, outcome);
}
KeyEventKind::Release => {
// A press that arrived as text ("X") is released by its key
// report ("x" with shifted alternate "X"); match either.
let shifted_lease_key =
key.shifted_codepoint
.and_then(char::from_u32)
.map(|shifted| {
crate::input::InputLeaseKey::new(
LOCAL_INPUT_SOURCE,
&crate::input::TerminalKey::new(
KeyCode::Char(shifted),
key.modifiers,
),
)
});
let forwarded = self.input_leases.remove_forwarded(&lease_key).or_else(|| {
shifted_lease_key
.as_ref()
.and_then(|shifted| self.input_leases.remove_forwarded(shifted))
});
if let Some(lease) = forwarded {
let release = lease
let Some(lease) = self.take_release_lease(&lease_key, &key) else {
return;
};
let crate::input::InputLease::Forwarded(lease) = lease else {
// Herdr consumed the press; its release stays with Herdr.
return;
};
// A native record is released as the recorded key. A VT release
// report already names its key and shifted character exactly.
let release = if key.physical_key_id().is_some() {
lease
.key
.with_modifiers(key.modifiers)
.with_kind(KeyEventKind::Release);
self.push_pane_key(lease.target, release, outcome);
.with_kind(KeyEventKind::Release)
} else {
let _ = self.input_leases.remove(&lease_key);
if let Some(shifted) = shifted_lease_key {
let _ = self.input_leases.remove(&shifted);
}
}
key
};
self.push_pane_key(lease.target, release, outcome);
}
}
}
@@ -1058,6 +1045,45 @@ impl ClientShellState {
.map(crate::protocol::ClientClipboardImageTarget::Pane)
}
/// The tracked press for a release. A press that arrived as text ("A") is
/// reported released as its key ("a", shifted alternate "A"), possibly after
/// Shift was let go, so the shifted alternate and the letter's other case
/// also match.
fn take_release_lease(
&mut self,
lease_key: &crate::input::InputLeaseKey<u8>,
key: &crate::input::TerminalKey,
) -> Option<crate::input::InputLease<ClientInputContext, ClientInputTarget>> {
if let Some(lease) = self.input_leases.remove(lease_key) {
return Some(lease);
}
let KeyCode::Char(c) = key.code else {
return None;
};
fn single(mut chars: impl Iterator<Item = char>) -> Option<char> {
match (chars.next(), chars.next()) {
(Some(only), None) => Some(only),
_ => None,
}
}
let other_case = if c.is_lowercase() {
single(c.to_uppercase())
} else {
single(c.to_lowercase())
}
.filter(|other| *other != c);
key.shifted_codepoint
.and_then(char::from_u32)
.into_iter()
.chain(other_case)
.find_map(|candidate| {
self.input_leases.remove(&crate::input::InputLeaseKey::new(
LOCAL_INPUT_SOURCE,
&crate::input::TerminalKey::new(KeyCode::Char(candidate), key.modifiers),
))
})
}
fn popup_input_target(&self) -> Option<ClientInputTarget> {
self.popup_terminal_id
.as_ref()
+53
View File
@@ -282,6 +282,59 @@ fn invalid_experimental_reload_keeps_input_source_preference() {
assert!(!shell.switch_ascii_input_source_in_prefix);
}
fn pane_key_events(
outcome: &ClientShellInput,
) -> Vec<(
crate::protocol::ClientKeyCode,
crate::protocol::ClientKeyKind,
)> {
outcome
.requests
.iter()
.filter_map(|request| match request {
ClientMessage::ClientShellPaneInput { events, .. } => Some(events),
_ => None,
})
.flatten()
.filter_map(|event| match event {
ClientPaneInputEvent::Key { code, kind, .. } => Some((code.clone(), *kind)),
_ => None,
})
.collect()
}
#[test]
fn text_press_release_survives_shift_released_first_and_never_becomes_a_repeat() {
use crate::protocol::{ClientKeyCode, ClientKeyKind};
let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
state.set_host_reports_key_releases(true);
state.set_snapshot(Box::new(snapshot()));
state.set_pane_surface(surface());
// Shift+A arrives as text; Shift is let go before A, so the release names
// plain `a` with no shifted alternate.
let press = state.handle_input_bytes(b"A");
assert_eq!(
pane_key_events(&press),
[(ClientKeyCode::Char('A'), ClientKeyKind::Press)]
);
let release = state.handle_input_bytes(b"\x1b[97;1:3u");
assert_eq!(
pane_key_events(&release),
[(ClientKeyCode::Char('a'), ClientKeyKind::Release)]
);
// A text press whose release never arrived must not turn the next press
// into a repeat routed to the old target.
let _ = state.handle_input_bytes(b"%");
let again = state.handle_input_bytes(b"%");
assert_eq!(
pane_key_events(&again),
[(ClientKeyCode::Char('%'), ClientKeyKind::Press)]
);
}
#[test]
fn physical_release_uses_the_leased_press_code_with_current_modifiers() {
let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+18 -3
View File
@@ -195,6 +195,9 @@ impl Oracle {
terminal.write(app_output);
let mut encoder = ghostty::KeyEncoder::new().expect("encoder");
encoder.set_from_terminal(&terminal);
// Keystrokes here carry an interpreted Alt, as on Linux; keep the oracle
// identical on macOS, where libghostty would otherwise treat Option as text.
encoder.set_macos_option_as_alt(true);
Self {
_terminal: terminal,
encoder,
@@ -511,10 +514,21 @@ fn run_keyboard_conformance() -> Report {
// the chord without it), so nothing downstream can recover it.
let super_unreportable =
host == HostProfile::Legacy && mods & ghostty::MOD_SUPER != 0;
// Plain text names a character, not a key: which key and Shift state
// produced punctuation depends on the layout. Only a letter's Shift
// is layout independent (its case). Legacy hosts never report
// Shift for non-letters; Kitty hosts send unmodified punctuation
// presses as plain text.
let non_letter_text = def.text.is_some_and(|(base, _)| !base.is_alphabetic());
let plain_text_press =
host_bytes.0.len() == 1 && host_bytes.0[0].is_ascii_graphic();
let layout_unreportable = non_letter_text
&& mods & ghostty::MOD_SHIFT != 0
&& (host == HostProfile::Legacy || plain_text_press);
let ambiguous = expected_by_host_bytes
.get(host_bytes)
.is_some_and(|distinct| distinct.iter().any(|other| *other != expected));
if super_unreportable || ambiguous {
if super_unreportable || layout_unreportable || ambiguous {
tally.host_lossy += 1;
continue;
}
@@ -610,8 +624,9 @@ fn print_report(report: &Report) {
// Ratchet: failure counts may only go down. Lower them when a change fixes
// cases; a rise means a regression in Herdr's input transparency.
// Remaining: Alt+], Alt+Shift+P/X, Alt+^, Alt+_ behind legacy hosts. Their bytes
// also start host replies, so they are never forwarded (#344).
const KEYBOARD_FAILURES_BASELINE: usize = 50;
// also start host replies, so they are never forwarded and the reply-tail
// discard can swallow the next keystroke (#344).
const KEYBOARD_FAILURES_BASELINE: usize = 60;
const MOUSE_FAILURES_BASELINE: usize = 96;
// Remaining: legacy hosts split right after ESC[, which is also Alt+[.
const SPLIT_IDLE_MISMATCH_BASELINE: usize = 207;
+9 -13
View File
@@ -69,8 +69,11 @@ where
lease_key: &InputLeaseKey<Source>,
key: TerminalKey,
) -> TerminalKey {
// A VT text press is always a new press: hosts send text repeats as
// text, and a stale lease (release reported after Shift was let go)
// must not turn the next press into a repeat routed to an old target.
if key.kind != crossterm::event::KeyEventKind::Press
|| (key.generated_text.is_some() && !key.has_physical_identity())
|| (key.generated_text.is_some() && key.physical_key_id().is_none())
{
return key;
}
@@ -172,16 +175,6 @@ where
allowed
}
pub(crate) fn remove_forwarded(
&mut self,
key: &InputLeaseKey<Source>,
) -> Option<ForwardedInputLease<Target>> {
match self.leases.remove(key) {
Some(InputLease::Forwarded(lease)) => Some(lease),
Some(InputLease::Consumed(_)) | None => None,
}
}
#[cfg(test)]
pub(crate) fn contains(&self, key: &InputLeaseKey<Source>) -> bool {
self.leases.contains_key(key)
@@ -375,7 +368,10 @@ mod tests {
leases.plan_repeat(lease_key, &repeated, Some(&context)),
RepeatPlan::Forwarded(10)
));
assert!(leases.remove_forwarded(&lease_key).is_some());
assert!(matches!(
leases.remove(&lease_key),
Some(InputLease::Forwarded(_))
));
}
#[test]
@@ -408,7 +404,7 @@ mod tests {
leases.complete_press(lease_key, &key, Some(&context), Some(&context), Some(10)),
RepeatPlan::Ignore
));
assert_eq!(leases.remove_forwarded(&lease_key), None);
assert!(leases.remove(&lease_key).is_none());
}
#[test]
+1 -1
View File
@@ -13,7 +13,7 @@ pub(crate) use keybindings::{
resolve_non_indexed_action, resolve_prefix_binding, KeybindAction, KeybindDispatch,
KeybindMatch,
};
pub(crate) use lease::{InputLeaseKey, InputLeaseTable, RepeatPlan};
pub(crate) use lease::{InputLease, InputLeaseKey, InputLeaseTable, RepeatPlan};
#[cfg(not(windows))]
pub use model::ime_compatible_keyboard_enhancement_flags;
pub use model::WindowsKeyRecord;
+40 -75
View File
@@ -22,9 +22,11 @@ pub(super) fn ghostty_key_event_from_terminal_key(
let mut mods = ghostty_mods_from_key_modifiers(key.modifiers);
match key.code {
KeyCode::Char(c) => {
// Legacy input reports the produced character ("%", "D"); Kitty
// reports the unshifted key. Both map to the same physical key.
let base = unshifted_char(c);
// An uppercase letter's key is its lowercase letter on every layout.
// Nothing else is inferred: Kitty reports already name the unshifted
// key (`+` on a German layout stays `+`), and legacy input does not
// say which key produced punctuation.
let base = unshifted_letter(c);
if base != c {
mods |= crate::ghostty::MOD_SHIFT;
}
@@ -39,7 +41,15 @@ pub(super) fn ghostty_key_event_from_terminal_key(
// (e.g. Shift+7 = "/" on a German layout, where `base` cannot
// be recovered from the character alone).
let reported_shifted_text = key.generated_text.is_some() && text != " ";
if shifted && (!text.starts_with(base) || reported_shifted_text) {
// A non-letter given with Shift and no reported shifted character
// is the produced character itself ("shift+?" is "?"). Kitty
// hosts report shifted alternates, so a changed character shows
// up there.
let given_shifted_char =
key.shifted_codepoint.is_none() && !base.is_alphabetic() && text != " ";
if shifted
&& (!text.starts_with(base) || reported_shifted_text || given_shifted_char)
{
event.set_consumed_mods(crate::ghostty::MOD_SHIFT);
}
event.set_utf8(&text);
@@ -58,6 +68,17 @@ pub(super) fn ghostty_key_event_from_terminal_key(
Some(event)
}
/// Configure `encoder` for a pane's live terminal modes. Herdr has already
/// decoded Alt from the host, so macOS Option must keep its Alt meaning (ESC
/// prefix) instead of being treated as a text modifier.
pub(super) fn configure_key_encoder(
encoder: &mut crate::ghostty::KeyEncoder,
terminal: &crate::ghostty::Terminal,
) {
encoder.set_from_terminal(terminal);
encoder.set_macos_option_as_alt(true);
}
/// Exception table for panes that negotiated no keyboard protocol. Ghostty sends
/// these chords as escape sequences that plain shells do not understand; keep
/// the classic key instead. libghostty still encodes the result (so Alt keeps
@@ -98,58 +119,29 @@ fn key_text(key: &crate::input::TerminalKey, base: char, shifted: bool) -> Optio
{
return Some(text.clone());
}
let produced = if shifted {
key.shifted_codepoint
.and_then(char::from_u32)
.or_else(|| us_shifted_char(base))
.unwrap_or(base)
} else {
base
// Without a reported shifted character, only a letter's Shift is known.
let produced = match key.shifted_codepoint.and_then(char::from_u32) {
Some(shifted_char) if shifted => shifted_char,
_ if shifted => shifted_letter(base),
_ => base,
};
(!produced.is_control()).then(|| produced.to_string())
}
fn unshifted_char(c: char) -> char {
if c.is_uppercase() {
let mut lower = c.to_lowercase();
if let (Some(lower), None) = (lower.next(), lower.next()) {
return lower;
}
return c;
fn shifted_letter(c: char) -> char {
let mut upper = c.to_uppercase();
match (upper.next(), upper.next()) {
(Some(upper), None) if c.is_lowercase() => upper,
_ => c,
}
ghostty_unshifted_ascii_pair(c).unwrap_or(c)
}
/// US-layout shifted character, used only when the host reported Shift without
/// the shifted alternate.
fn us_shifted_char(base: char) -> Option<char> {
if base.is_ascii_lowercase() {
return Some(base.to_ascii_uppercase());
fn unshifted_letter(c: char) -> char {
let mut lower = c.to_lowercase();
match (lower.next(), lower.next()) {
(Some(lower), None) if c.is_uppercase() => lower,
_ => c,
}
Some(match base {
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
'0' => ')',
'-' => '_',
'=' => '+',
'[' => '{',
']' => '}',
'\\' => '|',
';' => ':',
'\'' => '"',
',' => '<',
'.' => '>',
'/' => '?',
'`' => '~',
_ => return None,
})
}
pub(super) fn ghostty_mods_from_key_modifiers(modifiers: crossterm::event::KeyModifiers) -> u16 {
@@ -464,30 +456,3 @@ fn ghostty_key_from_char(base: char) -> Option<crate::ghostty::ffi::GhosttyKey>
_ => None,
}
}
fn ghostty_unshifted_ascii_pair(c: char) -> Option<char> {
Some(match c {
'!' => '1',
'@' => '2',
'#' => '3',
'$' => '4',
'%' => '5',
'^' => '6',
'&' => '7',
'*' => '8',
'(' => '9',
')' => '0',
'_' => '-',
'+' => '=',
'{' => '[',
'}' => ']',
'|' => '\\',
':' => ';',
'"' => '\'',
'<' => ',',
'>' => '.',
'?' => '/',
'~' => '`',
_ => return None,
})
}
+37 -10
View File
@@ -25,10 +25,10 @@ use super::cursor::CURSOR_POSITION_SETTLE;
use super::cursor::{CursorPositionSettleState, DecscusrTracker};
use super::{
input::{
ghostty_key_event_from_terminal_key, ghostty_mouse_encoder_for_terminal,
ghostty_mouse_event_from_button_kind, ghostty_mouse_event_from_motion_kind,
ghostty_mouse_event_from_wheel_kind, ghostty_mouse_position_for_terminal, legacy_shell_key,
legacy_super_chord,
configure_key_encoder, ghostty_key_event_from_terminal_key,
ghostty_mouse_encoder_for_terminal, ghostty_mouse_event_from_button_kind,
ghostty_mouse_event_from_motion_kind, ghostty_mouse_event_from_wheel_kind,
ghostty_mouse_position_for_terminal, legacy_shell_key, legacy_super_chord,
},
kitty_keyboard::KittyKeyboardTracker,
osc::{
@@ -1169,7 +1169,7 @@ impl GhosttyPaneTerminal {
let initial_default_background = initial_colors.map(|colors| colors.background);
let mut key_encoder =
crate::ghostty::KeyEncoder::new().map_err(|e| std::io::Error::other(e.to_string()))?;
key_encoder.set_from_terminal(&terminal);
configure_key_encoder(&mut key_encoder, &terminal);
Ok(Self {
#[cfg(test)]
scroll_metrics_reads: std::sync::atomic::AtomicUsize::new(0),
@@ -1429,7 +1429,7 @@ impl GhosttyPaneTerminal {
debug!(pane = pane_id.raw(), "processed kitty graphics sequence");
}
if let Ok(mut key_encoder) = self.key_encoder.lock() {
key_encoder.set_from_terminal(&core.terminal);
configure_key_encoder(&mut key_encoder, &core.terminal);
}
let synchronized_output = core
.terminal
@@ -1579,7 +1579,7 @@ impl GhosttyPaneTerminal {
#[cfg(windows)]
windows_recent_fallback::update(&mut core);
if let Ok(mut key_encoder) = self.key_encoder.lock() {
key_encoder.set_from_terminal(&core.terminal);
configure_key_encoder(&mut key_encoder, &core.terminal);
}
}
@@ -1643,7 +1643,7 @@ impl GhosttyPaneTerminal {
}
if let Ok(mut key_encoder) = self.key_encoder.lock() {
key_encoder.set_from_terminal(&core.terminal);
configure_key_encoder(&mut key_encoder, &core.terminal);
}
}
@@ -1666,7 +1666,7 @@ impl GhosttyPaneTerminal {
core.kitty_keyboard.observe(ansi.as_bytes());
core.terminal.write(ansi.as_bytes());
if let Ok(mut key_encoder) = self.key_encoder.lock() {
key_encoder.set_from_terminal(&core.terminal);
configure_key_encoder(&mut key_encoder, &core.terminal);
}
}
@@ -2067,7 +2067,7 @@ impl GhosttyPaneTerminal {
};
let encoded = if negotiated_nothing && legacy_super_chord(&key) {
crate::ghostty::KeyEncoder::new().and_then(|mut encoder| {
encoder.set_from_terminal(&core.terminal);
configure_key_encoder(&mut encoder, &core.terminal);
encoder.set_kitty_flags(KITTY_DISAMBIGUATE);
encoder.encode(&event)
})
@@ -4946,6 +4946,33 @@ mod tests {
}
}
#[test]
fn ghostty_alt_letters_keep_esc_prefix_on_every_platform() {
// libghostty treats macOS Option as a text modifier unless told otherwise;
// Herdr has already decoded Alt from these host bytes.
for (host_bytes, expected) in [
("\x1bf", &b"\x1bf"[..]),
("\x1bb", b"\x1bb"),
("\x1bA", b"\x1bA"),
("\x1b.", b"\x1b."),
] {
let key = crate::input::parse_terminal_key_sequence(host_bytes).expect("alt key");
assert_eq!(
test_encode_key_for_app(b"", key),
expected,
"{host_bytes:?}"
);
}
}
#[test]
fn ghostty_kitty_reports_keep_non_us_key_identity() {
// Ctrl++ on a German layout, where + is an unshifted key. The report
// already names the key; nothing may reinterpret it as US Shift+=.
let key = crate::input::parse_terminal_key_sequence("\x1b[43;5u").expect("kitty key");
assert_eq!(test_encode_key_for_app(b"\x1b[>1u", key), b"\x1b[43;5u");
}
#[test]
fn ghostty_disambiguate_reports_ctrl_letters_as_kitty_keys() {
let (tx, _rx) = mpsc::channel(4);
+1 -19
View File
@@ -110,7 +110,6 @@ pub(crate) struct RawInputFramer {
#[cfg(any(windows, test))]
impl RawInputFramer {
#[cfg(any(windows, test))]
pub(crate) fn for_host_input() -> Self {
Self {
byte_framer: RawInputByteFramer::for_host_input(),
@@ -146,12 +145,10 @@ impl RawInputFramer {
false
}
#[cfg(any(windows, test))]
pub(crate) fn has_pending_bracketed_paste(&self) -> bool {
self.byte_framer.has_pending_bracketed_paste()
}
#[cfg(any(windows, test))]
pub(crate) fn has_pending_default_mouse_sequence(&self) -> bool {
starts_with_incomplete_default_mouse_sequence(&self.byte_framer.buffer)
}
@@ -160,7 +157,6 @@ impl RawInputFramer {
events_from_framed_chunks(self.byte_framer.flush_timeout())
}
#[cfg(any(windows, test))]
pub(crate) fn flush_keyboard_escape(&mut self) -> Vec<RawInputEvent> {
if self.byte_framer.buffer.as_slice() == [ESC] {
self.byte_framer.lone_escape_recently_flushed = true;
@@ -171,7 +167,6 @@ impl RawInputFramer {
}
/// Semantic input ends mouse recovery, unlike another idle interval.
#[cfg(any(windows, test))]
pub(crate) fn flush_interrupted(&mut self) -> Vec<RawInputEvent> {
#[cfg(windows)]
self.byte_framer.flush_lone_escape_on_interruption();
@@ -581,17 +576,6 @@ impl RawInputByteFramer {
chunks.push(std::mem::take(&mut self.buffer));
return chunks;
}
// A lone string introducer with no reply expected is a legacy Alt chord
// that cannot be forwarded safely. Drop just these bytes; entering
// control-string discard would swallow the user's next keystrokes.
if is_escape_key_prefix(&self.buffer)
&& self.buffer.len() == 2
&& !self.awaiting_host_reply()
{
tracing::debug!(bytes = ?self.buffer, "dropping lone string introducer");
self.buffer.clear();
return chunks;
}
if let Some(ControlString::Incomplete { family }) = control_string(&self.buffer) {
tracing::debug!(
@@ -1073,6 +1057,7 @@ fn is_alt_key_introducer(buffer: &[u8]) -> bool {
matches!(buffer, [ESC, b'[' | b'O'])
}
#[cfg(any(unix, test))]
fn is_escape_key_prefix(buffer: &[u8]) -> bool {
match buffer {
[ESC] => true,
@@ -2771,7 +2756,6 @@ mod tests {
#[test]
fn raw_input_byte_framer_discards_split_control_string_after_timeout() {
let mut framer = RawInputByteFramer::default();
framer.host_color_query_sent();
assert!(framer.push(b"\x1b]").is_empty());
assert!(framer.flush_timeout().is_empty());
@@ -2782,7 +2766,6 @@ mod tests {
#[test]
fn raw_input_byte_framer_keeps_discarding_tail_across_timeout() {
let mut framer = RawInputByteFramer::default();
framer.host_color_query_sent();
assert!(framer.push(b"\x1b]").is_empty());
assert!(framer.flush_timeout().is_empty());
@@ -2795,7 +2778,6 @@ mod tests {
#[test]
fn raw_input_byte_framer_releases_discard_on_implausible_tail() {
let mut framer = RawInputByteFramer::default();
framer.host_color_query_sent();
assert!(framer.push(b"\x1b]").is_empty());
assert!(framer.flush_timeout().is_empty());