mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
fix: occasional stall when querying host terminal (introduced in 0.44.1) (#5149)
* fix(client): unwedge termwiz on unrecognized CSI sequences * rustfmt * add pr
This commit is contained in:
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
## [Unreleased]
|
||||
* feat: allow tabs to have different sizes if clients aren't focused on the same one (https://github.com/zellij-org/zellij/pull/5133)
|
||||
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
|
||||
* fix: occasional stall before prompt/program-run (https://github.com/zellij-org/zellij/pull/5149)
|
||||
|
||||
## [0.44.2] - 2026-05-05
|
||||
* fix: idle CPU + disk i/o, CommandChanged plugin Event, GetSessionList plugin command (https://github.com/zellij-org/zellij/pull/5063)
|
||||
|
||||
@@ -817,3 +817,61 @@ fn host_theme_dsr_997_unknown_param_dropped() {
|
||||
"unknown ?997;N value must not classify"
|
||||
);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Regression test: a Kitty keyboard-protocol event arriving on the
|
||||
// client's stdin must not wedge the parser. Before the fix, such an
|
||||
// event (`\x1b[<keycode>;<mods>u`) sat at the head of termwiz's
|
||||
// internal buffer because parse_csi_report rejects the unwhitelisted
|
||||
// final byte 'u' AND the keymap returned `Found::NeedData` (it's a
|
||||
// possible prefix of registered key sequences). Every host reply that
|
||||
// arrived behind it was silently swallowed, stalling host-color
|
||||
// forwards until session timeout. This was confirmed via WEDGE-CANDIDATE
|
||||
// log entries from three independent users on Kitty, Ghostty, and
|
||||
// Konsole — all triggered by the same `\x1b[<digits>;<digits>u` shape.
|
||||
//
|
||||
// Captured triggers from real logs:
|
||||
// `\x1b[57414;129u` (Kitty user, repro-logs/third-commit/zellij(4).log)
|
||||
// `\x1b[27;129u` (Ghostty user, ...zellij(5).log)
|
||||
// `\x1b[102;133u` (Konsole user, ...zellij_0.45_new-2.log)
|
||||
// =====================================================================
|
||||
|
||||
#[test]
|
||||
fn kitty_kbd_event_does_not_wedge_subsequent_forward_reply() {
|
||||
let kbd_events: Vec<&[u8]> = vec![
|
||||
b"\x1b[57414;129u", // captured from Kitty user
|
||||
b"\x1b[27;129u", // captured from Ghostty user
|
||||
b"\x1b[102;133u", // captured from Konsole user
|
||||
b"\x1b[A", // legacy CSI key (not Kitty kbd) — sanity
|
||||
];
|
||||
|
||||
for kbd in &kbd_events {
|
||||
let pretty = String::from_utf8_lossy(kbd).replace('\x1b', "ESC");
|
||||
let mut parser = StdinAnsiParser::new();
|
||||
|
||||
// Pre-wedge: keypress arrives before the forward goes out.
|
||||
let _ = parser.feed(kbd);
|
||||
|
||||
// Forward dispatched — slot opens for token=42.
|
||||
parser.open_forward(42);
|
||||
|
||||
// Host's reply arrives in two chunks (matches captured wire:
|
||||
// OSC 11 reply 25B, then DA1 barrier 10B).
|
||||
let _ = parser.feed(b"\x1b]11;rgb:1e1e/1e1e/2e2e\x1b\\");
|
||||
let out = parser.feed(b"\x1b[?62;52;c");
|
||||
|
||||
let (token, bytes) = out.completed_forward.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"BARRIER never closed slot for kbd-event prelude {:?} — \
|
||||
termwiz wedge regression",
|
||||
pretty
|
||||
)
|
||||
});
|
||||
assert_eq!(token, 42, "wrong token closed for prelude {:?}", pretty);
|
||||
assert!(
|
||||
!bytes.is_empty(),
|
||||
"OSC payload was lost behind the kbd event for prelude {:?}",
|
||||
pretty
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,6 +838,38 @@ fn parse_sgr_mouse(buf: &[u8]) -> Option<(InputEvent, usize)> {
|
||||
/// `ESC [` for two distinct cases — not currently disambiguated here:
|
||||
/// - truly malformed / unsupported sequence, or
|
||||
/// - incomplete input; caller handles incompleteness via `maybe_more`.
|
||||
/// Return `Some(len)` if `buf` starts with a structurally complete CSI
|
||||
/// sequence (`\x1b[ <params>* <intermediates>* <final>` per ECMA-48 §5.4),
|
||||
/// regardless of whether the final byte is one we have a use for. Used by
|
||||
/// `process_bytes` to advance past CSI sequences the keymap doesn't
|
||||
/// recognise — most importantly Kitty keyboard-protocol events
|
||||
/// `\x1b[<keycode>;<mods>u`. Without this, the keymap returns
|
||||
/// `Found::NeedData` (it sees the bytes as a possible prefix of a longer
|
||||
/// registered key) and the parser wedges holding bytes that will never
|
||||
/// extend into anything.
|
||||
///
|
||||
/// Returns `None` if the buffer doesn't start with `\x1b[`, contains a
|
||||
/// non-CSI byte, or hasn't yet received its final byte.
|
||||
fn complete_csi_len(buf: &[u8]) -> Option<usize> {
|
||||
if buf.get(0) != Some(&0x1b) || buf.get(1) != Some(&b'[') {
|
||||
return None;
|
||||
}
|
||||
let mut i = 2;
|
||||
let max_scan = buf.len().min(256);
|
||||
while i < max_scan {
|
||||
let b = buf[i];
|
||||
match b {
|
||||
// Parameters (digits, ;, :, ?, <, =, >) and intermediates (space..'/').
|
||||
0x30..=0x3F | 0x20..=0x2F => i += 1,
|
||||
// Any byte in the final-byte range terminates a CSI sequence.
|
||||
0x40..=0x7E => return Some(i + 1),
|
||||
// Anything else means this isn't a well-formed CSI.
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_csi_report(buf: &[u8]) -> Option<(InputEvent, usize)> {
|
||||
if buf.get(0) != Some(&0x1b) || buf.get(1) != Some(&b'[') {
|
||||
return None;
|
||||
@@ -1701,6 +1733,42 @@ impl InputParser {
|
||||
self.buf.advance(len);
|
||||
},
|
||||
(Found::Ambiguous(_, _), true) | (Found::NeedData, true) => {
|
||||
// The keymap is signalling "this buffer
|
||||
// could still grow into a registered key,
|
||||
// give me more bytes." That verdict is
|
||||
// wrong when the buffer already holds a
|
||||
// structurally complete CSI sequence whose
|
||||
// final byte isn't in the keymap — most
|
||||
// importantly Kitty keyboard-protocol
|
||||
// events `\x1b[<keycode>;<mods>u`, which
|
||||
// never grow into anything the keymap
|
||||
// knows. Returning here would wedge
|
||||
// `self.buf` indefinitely, swallowing every
|
||||
// host reply that arrives behind it (the
|
||||
// OSC + DA1 bytes for a forwarded
|
||||
// `OSC 11;?` query among them) and stalling
|
||||
// host-color forwards until session exit.
|
||||
//
|
||||
// Both `Ambiguous(_, true)` and
|
||||
// `NeedData(true)` reach this point in
|
||||
// practice: for `\x1b[<digits>;<digits>u`
|
||||
// the trie reports `Ambiguous(1, Escape)`
|
||||
// (it has ESC alone as a match and ESC[…]
|
||||
// as longer prefixes), so the fix must
|
||||
// cover both verdicts.
|
||||
//
|
||||
// Skip past the unrecognised CSI without
|
||||
// emitting an event; callers that need
|
||||
// keyboard dispatch (kitty_parser, the
|
||||
// separate `input_parser` instance fed the
|
||||
// residue from `StdinAnsiParser`) see the
|
||||
// same bytes via `strip_replies`, which
|
||||
// already treats unwhitelisted-final CSIs
|
||||
// as `Malformed` and pushes them through.
|
||||
if let Some(len) = complete_csi_len(self.buf.as_slice()) {
|
||||
self.buf.advance(len);
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
},
|
||||
(Found::None, _) | (Found::NeedData, false) => {
|
||||
|
||||
Reference in New Issue
Block a user