//! `/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 "). 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) } }