fix(tui): the Shift+Enter hint follows the protocol, not a brand list

`shift_enter_unavailable` kept its own list of brands and missed every
other reason the Kitty keyboard protocol gets skipped: JetBrains, screen,
tmux below 3.3 and tmux with `extended-keys off` all swallow it, yet the
prompt still advertised Shift+Enter there. It now reads
`kitty_skip_reason` directly. Two skips deliver the modifier another way
and stay available: Apple Terminal, where `route_enter` polls
CoreGraphics, and Windows Terminal, where the console key record carries
SHIFT — a bare ConHost, only optimistically refined to Windows Terminal,
keeps advertising Alt+Enter.

The old list also carried a `VTE_VERSION >= 8200` cutoff, citing VTE
0.82.0 as the first release with the protocol. It is not: at tag 0.82.0
the string "kitty" appears nowhere in `src/parser-seq.py`, `src/vte.cc`
or `src/widget.cc`, and `_vte_keymap_GDK_Return` encodes only Alt
(`ESC CR`), so Shift+Enter leaves a VTE terminal as the same bare CR as
Enter. Every VTE now advertises Alt+Enter, and `/terminal-setup` stops
telling GNOME Terminal users to upgrade to a version that would not
help. It also stops advertising Alt+Enter to WezTerm, which binds that
chord to ToggleFullScreen.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-21 19:19:21 -04:00
1 parent 2e40ebb08b
commit f8cd33d19d
3 files changed
+78 -136

No files matched your search

@@ -375,7 +375,6 @@ impl TerminalContext {
return Some("apple_terminal");
}
if self.is_vte_based() {
// WHY: central helper replaces VTE duplication
return Some("vte");
}
if self.brand == TerminalName::WindowsTerminal {
@@ -429,74 +428,24 @@ impl TerminalContext {
}
/// Whether the running terminal cannot distinguish `Shift+Enter` from
/// bare `Enter` at the byte level, so the UI should advertise
/// `Alt+Enter` for newline insertion instead.
/// bare `Enter`, so the UI should advertise `Alt+Enter` (delivered as
/// `ESC`+`CR`) for newline insertion instead.
///
/// Distinguishing `Shift+Enter` requires the Kitty keyboard protocol
/// (KKP) to be negotiated. This returns `true` for the environments
/// where the pager cannot rely on KKP for a usable `Shift+Enter`:
///
/// 1. **Legacy VTE** (GNOME Terminal, Ptyxis, kgx, Tilix, etc.) whose
/// `VTE_VERSION` is below `8200` (VTE 0.82.0, the first release with
/// KKP, merged in
/// [MR !14](https://gitlab.gnome.org/GNOME/vte/-/merge_requests/14)).
/// Also true when the brand is detected as VTE but `VTE_VERSION` is
/// missing or unparseable — we conservatively assume old.
/// 2. **VS Code's integrated terminal (xterm.js) and VS Code-family /
/// xterm.js IDE forks**. xterm.js only partially implements KKP —
/// it mis-encodes shifted printable keys — so the pager deliberately
/// never negotiates KKP for them (see [`Self::kitty_skip_reason`]
/// `== "vscode"` and [xterm.js#5823](https://github.com/xtermjs/xterm.js/issues/5823)).
/// Without KKP, xterm.js sends a bare `CR` for `Shift+Enter`,
/// byte-for-byte identical to `Enter`.
/// 3. **Unidentified terminals with no multiplexer**, where the pager
/// also skips KKP (no positive evidence of support — typically
/// VS Code's xterm.js reached over SSH, where `TERM_PROGRAM` isn't
/// forwarded and the brand falls back to `Unknown`).
///
/// In every case `Alt+Enter` (delivered as `ESC`+`CR`) is the reliable
/// newline chord and is what the UI advertises.
/// Follows [`Self::kitty_skip_reason`] rather than a second brand list:
/// when the protocol is not negotiated the modifier is lost, and the two
/// drifted once already (VTE advertised a chord that never arrived — its
/// `Return` keymap encodes only Alt, through 0.82.0 at least, and no VTE
/// release carries the protocol). Two skips still deliver it — Apple
/// Terminal drops the flags but `route_enter` polls CoreGraphics, and
/// Windows console key records carry SHIFT with no protocol at all.
pub fn shift_enter_unavailable(&self) -> bool {
// WHY: central helper + version gating
let is_vte = self.is_vte_based();
if is_vte {
return match self
.vte_version
.as_deref()
.and_then(|v| v.parse::<u32>().ok())
{
Some(ver) => ver < 8200,
// Brand=Vte but no parseable version — conservative: assume old.
None => true,
};
match self.kitty_skip_reason() {
None | Some("apple_terminal") => false,
// A bare ConHost is only optimistically refined to Windows
// Terminal; it still advertises Alt+Enter.
Some("windows_terminal") => self.env_brand.is_capability_unclassified(),
Some(_) => true,
}
// VS Code / xterm.js and its forks: KKP is never negotiated, so
// Shift+Enter arrives as a bare CR indistinguishable from Enter.
if matches!(
self.brand,
TerminalName::VsCode
| TerminalName::Cursor
| TerminalName::Windsurf
| TerminalName::Zed
) {
return true;
}
// Unidentified / unclassified brand with no multiplexer: KKP is
// skipped (see `kitty_skip_reason`). This is the common
// VS Code-over-SSH shape (brand falls back to Unknown). On native
// Windows the effective `brand` is refined to WindowsTerminal, so
// consult `env_brand` — a bare ConHost is still env-Unknown and
// must advertise Alt+Enter even though we optimistically treat it
// as WT for capabilities.
if self.env_brand.is_capability_unclassified()
&& self.multiplexer == MultiplexerKind::Undetected
{
return true;
}
false
}
/// True when `Ctrl+.` cannot be delivered reliably as a shortcuts primary.
@@ -1443,7 +1443,7 @@ fn kitty_skip_vscode_over_tmux() {
#[test]
fn kitty_skip_vte_version() {
// VTE does not support Kitty keyboard protocol and crossterm's probe
// VTE below 0.82 has no Kitty keyboard protocol and crossterm's probe
// can false-positive on it. https://gitlab.gnome.org/GNOME/vte/-/issues/2601
let ctx = TerminalContext {
vte_version: Some("7402".to_owned()),
@@ -1461,73 +1461,67 @@ fn kitty_skip_vte_brand() {
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
}
// shift_enter_unavailable: VTE version gating for Shift+Enter
// shift_enter_unavailable: VTE never delivers Shift+Enter
//
// VTE 0.82.0 (= VTE_VERSION 8200) is the first release containing the
// Kitty keyboard protocol; earlier versions cannot distinguish
// Shift+Enter from bare Enter, so the UI should advertise Alt+Enter
// for newline insertion instead.
// No VTE release carries the Kitty keyboard protocol: at 0.82.0 the word
// "kitty" appears nowhere in the parser or widget, and
// `_vte_keymap_GDK_Return` encodes only Alt (`ESC CR`), so Shift+Enter is
// the same bare CR as Enter. The UI advertises Alt+Enter instead.
#[test]
fn shift_enter_unavailable_legacy_vte_version() {
// VTE 0.64.2 (real user report) — well below the KKP cutoff.
let ctx = TerminalContext {
vte_version: Some("6402".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
fn shift_enter_unavailable_on_every_vte() {
for version in [Some("6402"), Some("8200"), Some("8401"), None] {
let ctx = TerminalContext {
brand: TerminalName::Vte,
env_brand: TerminalName::Vte,
vte_version: version.map(str::to_owned),
..Default::default()
};
assert_eq!(ctx.kitty_skip_reason(), Some("vte"), "VTE {version:?}");
assert!(ctx.shift_enter_unavailable(), "VTE {version:?}");
}
}
#[test]
fn shift_enter_unavailable_just_below_cutoff() {
// VTE 0.81.99 — anything below 8200 must return true.
let ctx = TerminalContext {
vte_version: Some("8199".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_modern_vte() {
// VTE 0.82.0 — first release with KKP.
let ctx = TerminalContext {
vte_version: Some("8200".to_owned()),
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_available_future_vte() {
// VTE 0.84.1 — well above the cutoff.
let ctx = TerminalContext {
vte_version: Some("8401".to_owned()),
..Default::default()
};
assert!(!ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_vte_brand_no_version() {
// Brand detected as VTE but VTE_VERSION missing — conservative: old.
let ctx = TerminalContext {
brand: TerminalName::Vte,
vte_version: None,
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
}
#[test]
fn shift_enter_unavailable_unparseable_version() {
// Garbage in VTE_VERSION — conservative: old.
let ctx = TerminalContext {
brand: TerminalName::Vte,
vte_version: Some("not-a-number".to_owned()),
..Default::default()
};
assert!(ctx.shift_enter_unavailable());
fn shift_enter_unavailable_follows_every_kitty_skip() {
// A skipped protocol means the modifier never arrives, whatever the
// reason — JediTerm, screen and a multiplexer that eats extended keys
// included. Apple Terminal is the one skip with another way in.
for ctx in [
TerminalContext {
brand: TerminalName::JetBrains,
env_brand: TerminalName::JetBrains,
..Default::default()
},
TerminalContext {
brand: TerminalName::Kitty,
env_brand: TerminalName::Kitty,
multiplexer: MultiplexerKind::Screen,
..Default::default()
},
TerminalContext {
brand: TerminalName::Kitty,
env_brand: TerminalName::Kitty,
multiplexer: MultiplexerKind::Tmux,
tmux_version: Some("3.2".to_owned()),
..Default::default()
},
// A VTE that speaks the protocol still loses it to the multiplexer.
TerminalContext {
brand: TerminalName::Vte,
env_brand: TerminalName::Vte,
vte_version: Some("8200".to_owned()),
multiplexer: MultiplexerKind::Screen,
..Default::default()
},
] {
assert!(ctx.kitty_skip_reason().is_some());
assert!(
ctx.shift_enter_unavailable(),
"{:?} skips the protocol, so Shift+Enter cannot arrive",
ctx.brand
);
}
}
#[test]
@@ -96,16 +96,15 @@ impl SlashCommand for TerminalSetupCommand {
// Some terminals can't distinguish Shift+Enter from bare Enter at
// the byte level because the Kitty keyboard protocol isn't
// negotiated (VTE < 0.82, or VS Code's xterm.js which mis-encodes
// shifted keys). Point users at Alt+Enter, which is reliably
// delivered as ESC+CR. Suppressed when the WezTerm warning fired:
// stock WezTerm binds Alt+Enter to ToggleFullScreen, so advertising
// it would contradict that warning's `\`+Enter guidance.
if ctx.shift_enter_unavailable() && !wezterm_kkp_off {
// negotiated (VTE, VS Code's xterm.js, a multiplexer that eats
// extended keys). Point users at Alt+Enter, which is reliably
// delivered as ESC+CR. Never for WezTerm: it binds Alt+Enter to
// ToggleFullScreen, so its own warning's `\`+Enter guidance stands.
if ctx.shift_enter_unavailable() && !wezterm_kkp_off && ctx.brand != TerminalName::WezTerm {
let detail = if ctx.vte_version.is_some() || ctx.brand == TerminalName::Vte {
match ctx.vte_version.as_deref() {
Some(v) => format!("VTE {v}; need >= 8200 for Shift+Enter"),
None => "legacy VTE; need VTE >= 0.82 for Shift+Enter".to_owned(),
Some(v) => format!("VTE {v}: no Kitty keyboard protocol"),
None => "VTE: no Kitty keyboard protocol".to_owned(),
}
} else if matches!(
ctx.brand,