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:
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,
|
||||
|
||||
Reference in new issue
Block a user