`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.
166 lines
6.6 KiB
Rust
166 lines
6.6 KiB
Rust
//! `/terminal-setup` — diagnose terminal, color/theme, and clipboard setup.
|
|
//!
|
|
//! Runs the same diagnostics engine used for startup warnings and formats
|
|
//! the results as a user-readable message. This gives users an on-demand
|
|
//! way to check their environment and see fix instructions.
|
|
|
|
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
|
|
use crate::terminal::TerminalName;
|
|
|
|
pub struct TerminalSetupCommand;
|
|
|
|
impl SlashCommand for TerminalSetupCommand {
|
|
fn name(&self) -> &str {
|
|
"terminal-setup"
|
|
}
|
|
|
|
fn aliases(&self) -> &[&str] {
|
|
&["terminal-check", "terminal-info"]
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"Check terminal, color, and clipboard setup"
|
|
}
|
|
|
|
fn usage(&self) -> &str {
|
|
"/terminal-setup"
|
|
}
|
|
|
|
fn run(&self, _ctx: &mut CommandExecCtx, _args: &str) -> CommandResult {
|
|
let ctx = crate::terminal::terminal_context();
|
|
let query = crate::diagnostics::LiveTmuxQuery;
|
|
let is_control_mode = crate::terminal::detect_tmux_control_mode(ctx);
|
|
let mut warnings = crate::diagnostics::collect_startup_warnings(
|
|
ctx,
|
|
&query,
|
|
is_control_mode,
|
|
_ctx.screen_mode.is_fullscreen(),
|
|
);
|
|
// Live-environment check, kept out of `collect_startup_warnings` so
|
|
// its tests stay hermetic (same pattern as the WezTerm warning below).
|
|
warnings.extend(crate::diagnostics::diagnose_wayland_data_control_live());
|
|
// WezTerm without the Kitty keyboard protocol: surface the fix
|
|
// alongside the other issues. By the time the user runs
|
|
// /terminal-setup the async XTVERSION reply has landed, so this
|
|
// also catches WezTerm over SSH (env brand Unknown, self-report
|
|
// "WezTerm <version>").
|
|
let wezterm_warning = crate::diagnostics::wezterm_kitty_keyboard_warning(
|
|
ctx,
|
|
crate::app::kitty_flags_pushed(),
|
|
crate::terminal::xtversion::detected(),
|
|
);
|
|
let wezterm_kkp_off = wezterm_warning.is_some();
|
|
warnings.extend(wezterm_warning);
|
|
// Color not in collect_startup_warnings (noisy on limited terminals).
|
|
let color_level = crate::theme::color_support::get();
|
|
warnings.extend(crate::diagnostics::color_support_warning(
|
|
color_level,
|
|
ctx.brand,
|
|
ctx.is_tmux_backed(),
|
|
&ctx.tmux_config_path(),
|
|
));
|
|
let route = crate::clipboard::clipboard_route();
|
|
let is_ssh = kigi_shell::util::clipboard::is_remote_session();
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str("Environment\n");
|
|
out.push_str(&format!(" terminal {}\n", ctx.brand));
|
|
if let Some(v) = crate::terminal::xtversion::detected() {
|
|
out.push_str(&format!(" xtversion {}\n", v));
|
|
}
|
|
out.push_str(&format!(" multiplexer {}\n", ctx.multiplexer));
|
|
if let Some(ref byobu) = ctx.byobu {
|
|
out.push_str(&format!(" byobu {}\n", byobu));
|
|
}
|
|
out.push_str(&format!(
|
|
" ssh {}\n",
|
|
if is_ssh { "yes" } else { "no" }
|
|
));
|
|
out.push_str(&crate::diagnostics::format_color_env_line(color_level));
|
|
out.push_str(&crate::diagnostics::format_themes_env_line(color_level));
|
|
|
|
let kb = ctx.keyboard_capabilities();
|
|
if kb.modifier_delivery.benefits_from_rescue() || kb.enter_needs_rescue() {
|
|
let rescue = if cfg!(target_os = "macos") {
|
|
"OS rescue active"
|
|
} else {
|
|
"OS rescue unavailable on this platform"
|
|
};
|
|
out.push_str(&format!(
|
|
" keyboard {} ({})\n",
|
|
kb.modifier_delivery.label(),
|
|
rescue
|
|
));
|
|
}
|
|
|
|
// Some terminals can't distinguish Shift+Enter from bare Enter at
|
|
// the byte level because the Kitty keyboard protocol isn't
|
|
// 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}: no Kitty keyboard protocol"),
|
|
None => "VTE: no Kitty keyboard protocol".to_owned(),
|
|
}
|
|
} else if matches!(
|
|
ctx.brand,
|
|
TerminalName::VsCode
|
|
| TerminalName::Cursor
|
|
| TerminalName::Windsurf
|
|
| TerminalName::Zed
|
|
) {
|
|
format!("{}: xterm.js can't distinguish Shift+Enter", ctx.brand)
|
|
} else {
|
|
"no Kitty keyboard protocol; Shift+Enter == Enter".to_owned()
|
|
};
|
|
out.push_str(&format!(" newline Alt+Enter ({detail})\n"));
|
|
}
|
|
|
|
out.push_str("\nClipboard routes\n");
|
|
out.push_str(&format!(
|
|
" native {} (tool: {})\n",
|
|
if route.native { "active" } else { "off" },
|
|
kigi_shell::util::clipboard::native_tool_name(),
|
|
));
|
|
out.push_str(&format!(
|
|
" tmux buffer {}\n",
|
|
if route.tmux_buffer { "active" } else { "off" }
|
|
));
|
|
out.push_str(&format!(
|
|
" osc 52 {}\n",
|
|
if route.osc52 { "active" } else { "off" }
|
|
));
|
|
out.push_str(&format!(
|
|
" data-control {}\n",
|
|
crate::clipboard::wayland_data_control_label()
|
|
));
|
|
|
|
if warnings.is_empty() {
|
|
out.push_str("\nNo issues found.\n");
|
|
} else {
|
|
out.push_str(&format!("\n{} issue(s)\n", warnings.len()));
|
|
for w in &warnings {
|
|
out.push_str(&format!("\n [!] {}\n", w.message));
|
|
match (w.fix.as_deref(), w.config_path.as_deref()) {
|
|
(Some(fix), Some(path)) => {
|
|
out.push_str(&format!(" Fix: place `{}` in {}\n", fix, path));
|
|
}
|
|
(Some(fix), None) => {
|
|
out.push_str(&format!(" Fix: run `{}`\n", fix));
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(note) = w.note.as_deref() {
|
|
out.push_str(&format!(" Note: {}\n", note));
|
|
}
|
|
}
|
|
}
|
|
|
|
CommandResult::Message(out)
|
|
}
|
|
}
|