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 30ff3710a0 (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
This commit is contained in:
Johannes Altmanninger
2025-10-04 06:51:53 +02:00
parent 7d83dc4758
commit 623c14aed0
3 changed files with 25 additions and 7 deletions

View File

@@ -9,6 +9,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 <cmds/fish_config>` would generate invalid configuration (:issue:`11861`).
- Fixed a case where upgrading fish would break old versions of fish that were still running.

View File

@@ -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<u64> {
let number = xtversion.strip_prefix("Zellij(")?.strip_suffix(")")?;
fish_wcstoul(number).ok()
}

View File

@@ -297,6 +297,14 @@ fn ends_with<Suffix: IntoCharIter>(&self, suffix: Suffix) -> bool {
)
}
fn strip_suffix<Suffix: IntoCharIter>(&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();