From 695f225cb4a3b01fe9a605142c27b9964f80e451 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 4 Oct 2025 06:51:53 +0200 Subject: [PATCH] Kitty keyboard protocol is non-functional on old versions of Zellij try_readb() uses a high timeout when the kitty keyboard protocol is enabled, because in that case it should basically never be necessary to interpret \e as escape key, see 30ff3710a06 (Increase timeout when reading escape sequences inside paste/kitty kbd, 2025-07-24). Zellij before commit 0075548a (fix(terminal): support kitty keyboard protocol setting with "=" (#3942), 2025-01-17) fails to enable kitty keyboard protocol, so it sends the raw escape bytes, causing us to wait 300ms. Closes #11868 (cherry picked from commit 623c14aed0a4c35cc687ba7bb09e0f7a339d89a6) --- CHANGELOG.rst | 1 + src/tty_handoff.rs | 23 ++++++++++++++++------- src/wchar_ext.rs | 8 ++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f1d59275c..bcfb71589 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,7 @@ This release fixes the following regressions identified in 4.1.0: - Fixed spurious error output when completing remote file paths for ``scp`` (:issue:`11860`). - Fixed an issue where focus events (currently only enabled in ``tmux``) would cause multiline prompts to be redrawn in the wrong line (:issue:`11870`). - Stopped printing output that would cause a glitch on old versions of Midnight Commander (:issue:`11869`). +- Added a workaround for old versions of Zellij where :kbd:`escape` processing was delayed (:issue:`11868`). - Fixed a case where the :doc:`web-based configuration tool ` would generate invalid configuration (:issue:`11861`). - Fixed a case where upgrading fish would break old versions of fish that were still running. diff --git a/src/tty_handoff.rs b/src/tty_handoff.rs index 59fe388b8..895788b5f 100644 --- a/src/tty_handoff.rs +++ b/src/tty_handoff.rs @@ -18,7 +18,7 @@ use crate::threads::assert_is_main_thread; use crate::wchar::prelude::*; use crate::wchar_ext::ToWString; -use crate::wutil::{perror, wcstoi}; +use crate::wutil::{fish_wcstoul, perror, wcstoi}; use libc::{EINVAL, ENOTTY, EPERM, STDIN_FILENO, WNOHANG}; use once_cell::sync::OnceCell; use std::mem::MaybeUninit; @@ -62,10 +62,12 @@ pub fn xtversion() -> Option<&'static wstr> { #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TtyQuirks { None, - // Running Midnight Commander which can't parse CSI/OSC/DCS yet. - MidnightCommander, + // Running Midnight Commander which can't parse CSI yet. + PreCsiMidnightCommander, // Running in iTerm2 before 3.5.12, which causes issues when using the kitty keyboard protocol. PreKittyIterm2, + // Running in Zellij before 0.42.0 which fails to activate the kitty keyboard protocol. + PreKittyZellij, // Whether we are running under tmux. Tmux, // Whether we are running under WezTerm. @@ -79,9 +81,11 @@ fn detect(vars: &dyn Environment, xtversion: &wstr) -> Self { if vars.get(MIDNIGHT_COMMANDER_SID).is_some() && vars.get(L!("__mc_kitty_keyboard")).is_none() { - MidnightCommander + PreCsiMidnightCommander } else if get_iterm2_version(xtversion).is_some_and(|v| v < (3, 5, 12)) { PreKittyIterm2 + } else if get_zellij_version(xtversion).is_some_and(|v| v < 4200) { + PreKittyZellij } else if xtversion.starts_with(L!("tmux ")) { Tmux } else if xtversion.starts_with(L!("WezTerm ")) { @@ -156,11 +160,11 @@ impl TtyQuirks { // Determine which keyboard protocol. // This is used from a signal handler. fn safe_get_supported_protocol(&self) -> ProtocolKind { - use TtyQuirks::{MidnightCommander, PreKittyIterm2, Wezterm}; - if *self == MidnightCommander { + use TtyQuirks::{PreCsiMidnightCommander, PreKittyIterm2, PreKittyZellij, Wezterm}; + if *self == PreCsiMidnightCommander { return ProtocolKind::None; } - if *self == PreKittyIterm2 { + if matches!(*self, PreKittyIterm2 | PreKittyZellij) { return ProtocolKind::Other; } match KITTY_KEYBOARD_SUPPORTED.get() { @@ -631,3 +635,8 @@ fn get_iterm2_version(xtversion: &wstr) -> Option<(u32, u32, u32)> { wcstoi(parts.next()?).ok()?, )) } + +fn get_zellij_version(xtversion: &wstr) -> Option { + let number = xtversion.strip_prefix("Zellij(")?.strip_suffix(")")?; + fish_wcstoul(number).ok() +} diff --git a/src/wchar_ext.rs b/src/wchar_ext.rs index 89c96254d..5484d1eb6 100644 --- a/src/wchar_ext.rs +++ b/src/wchar_ext.rs @@ -297,6 +297,14 @@ fn ends_with(&self, suffix: Suffix) -> bool { ) } + fn strip_suffix(&self, suffix: Suffix) -> Option<&wstr> { + // Note the above trait requires a double ended iterator. + let iter = suffix.chars().rev(); + let suffix_len = iter.clone().count(); + iter_prefixes_iter(iter, self.as_char_slice().iter().rev().copied()) + .then(|| self.slice_to(self.char_count() - suffix_len)) + } + fn trim_matches(&self, pat: char) -> &wstr { let slice = self.as_char_slice(); let leading_count = slice.chars().take_while(|&c| c == pat).count();