Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
1895 lines
55 KiB
Rust
1895 lines
55 KiB
Rust
use super::*;
|
||
|
||
// terminal_name_from_term_program (existing coverage)
|
||
|
||
#[test]
|
||
fn test_terminal_name_from_term_program() {
|
||
assert_eq!(
|
||
terminal_name_from_term_program("iTerm.app"),
|
||
Some(TerminalName::Iterm2)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("WarpTerminal"),
|
||
Some(TerminalName::WarpTerminal)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("Ghostty"),
|
||
Some(TerminalName::Ghostty)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("Apple_Terminal"),
|
||
Some(TerminalName::AppleTerminal)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("zed"),
|
||
Some(TerminalName::Zed)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("WindowsTerminal"),
|
||
Some(TerminalName::WindowsTerminal)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("rio"),
|
||
Some(TerminalName::Rio)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("otty"),
|
||
Some(TerminalName::Otty)
|
||
);
|
||
assert_eq!(
|
||
terminal_name_from_term_program("Otty"),
|
||
Some(TerminalName::Otty)
|
||
);
|
||
assert_eq!(terminal_name_from_term_program("unknown-term"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_otty_from_term_program() {
|
||
let env = env_from(&[("TERM_PROGRAM", "otty")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Otty);
|
||
let env = env_from(&[("TERM_PROGRAM", "Otty")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Otty);
|
||
}
|
||
|
||
#[test]
|
||
fn otty_delivers_ime_as_bracketed_paste_only() {
|
||
assert!(TerminalName::Otty.delivers_ime_as_bracketed_paste());
|
||
for brand in [
|
||
TerminalName::AppleTerminal,
|
||
TerminalName::Ghostty,
|
||
TerminalName::Iterm2,
|
||
TerminalName::Unknown,
|
||
TerminalName::WezTerm,
|
||
TerminalName::Kitty,
|
||
] {
|
||
assert!(
|
||
!brand.delivers_ime_as_bracketed_paste(),
|
||
"{brand:?} must not gate IME bracketed-paste origin"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn otty_is_capability_unclassified_like_unknown() {
|
||
assert!(TerminalName::Otty.is_capability_unclassified());
|
||
assert!(TerminalName::Unknown.is_capability_unclassified());
|
||
assert!(!TerminalName::Ghostty.is_capability_unclassified());
|
||
assert!(!TerminalName::AppleTerminal.is_capability_unclassified());
|
||
}
|
||
|
||
#[test]
|
||
fn otty_skips_kitty_keyboard_like_unknown() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Otty,
|
||
env_brand: TerminalName::Otty,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
|
||
assert!(ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
// detect_terminal_brand_from_env (pure)
|
||
|
||
#[test]
|
||
fn brand_ghostty_from_term_program() {
|
||
let env = env_from(&[("TERM_PROGRAM", "Ghostty")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Ghostty);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_wezterm_from_version_var() {
|
||
let env = env_from(&[("WEZTERM_VERSION", "20240203")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::WezTerm);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_iterm2_from_session_id() {
|
||
let env = env_from(&[("ITERM_SESSION_ID", "w0t0p0")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Iterm2);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_iterm2_from_lc_terminal_over_ssh() {
|
||
// Only LC_TERMINAL survives the SSH hop; ITERM_SESSION_ID / TERM_PROGRAM do not.
|
||
let env = env_from(&[("LC_TERMINAL", "iTerm2")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Iterm2);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_not_iterm2_for_foreign_lc_terminal() {
|
||
// Value-exact gate: a non-iTerm2 LC_TERMINAL value must NOT match
|
||
// (matches on value, not bare presence).
|
||
let env = env_from(&[("LC_TERMINAL", "not-iterm2")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_kitty_from_term() {
|
||
let env = env_from(&[("TERM", "xterm-kitty")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Kitty);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_alacritty_from_term() {
|
||
let env = env_from(&[("TERM", "alacritty")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::Alacritty
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_rio_from_term() {
|
||
let env = env_from(&[("TERM", "rio")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Rio);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_foot_from_term() {
|
||
let env = env_from(&[("TERM", "foot")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_foot_extra_from_term() {
|
||
let env = env_from(&[("TERM", "foot-extra")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_foot_direct_from_term() {
|
||
let env = env_from(&[("TERM", "foot-direct")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Foot);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_not_foot_for_unrelated_term() {
|
||
// Over-match guard: a TERM merely containing "foot" is not foot.
|
||
let env = env_from(&[("TERM", "xterm-footbar")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_not_foot_for_foot_prefixed_out_of_set_term() {
|
||
// Inverse boundary: a `foot`-prefixed value outside the exact set
|
||
// stays Unknown (pins the exact-set decision over `starts_with`).
|
||
let env = env_from(&[("TERM", "foot-extra-256color")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_vte_from_vte_version() {
|
||
let env = env_from(&[("VTE_VERSION", "7402")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Vte);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_terminator_from_term_program() {
|
||
let env = env_from(&[("TERM_PROGRAM", "terminator")]);
|
||
// WHY: canonical per detect-terminal
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::Terminator
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn terminator_vte_version_interaction() {
|
||
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("VTE_VERSION", "8200")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Terminator);
|
||
// WHY: helper covers version + brand
|
||
assert!(ctx.is_vte_based());
|
||
}
|
||
|
||
#[test]
|
||
fn terminator_with_tmux() {
|
||
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("TMUX", "/tmp/tmux")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Terminator);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
|
||
}
|
||
|
||
#[test]
|
||
fn terminator_over_ssh() {
|
||
let env = env_from(&[("TERM_PROGRAM", "terminator"), ("SSH_TTY", "/dev/pts/0")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Terminator);
|
||
assert!(ctx.is_ssh);
|
||
}
|
||
|
||
#[test]
|
||
fn terminator_focus_tracking() {
|
||
let env = env_from(&[("TERM_PROGRAM", "terminator")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
// supports focus like VTE
|
||
assert!(!matches!(
|
||
ctx.brand,
|
||
TerminalName::AppleTerminal | TerminalName::Unknown
|
||
));
|
||
assert!(ctx.is_vte_based());
|
||
}
|
||
|
||
#[test]
|
||
|
||
fn brand_windows_terminal_from_wt_session() {
|
||
let env = env_from(&[("WT_SESSION", "a1b2c3d4-e5f6-7890-abcd-ef1234567890")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::WindowsTerminal
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_unknown_empty_env() {
|
||
let env = env_from(&[]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Unknown);
|
||
}
|
||
|
||
// refine_unknown_brand_for_host
|
||
|
||
#[test]
|
||
fn refine_unknown_brand_defaults_to_wt_only_on_windows() {
|
||
use super::TerminalName::{Unknown, VsCode, WindowsTerminal};
|
||
use crate::host::HostOs::{Linux, Windows};
|
||
let cases = [
|
||
// DefTerm handoff: no WT_SESSION
|
||
(Unknown, Windows, WindowsTerminal),
|
||
(Unknown, Linux, Unknown),
|
||
// never override a positively detected brand
|
||
(VsCode, Windows, VsCode),
|
||
];
|
||
for (brand, host, expected) in cases {
|
||
assert_eq!(refine_unknown_brand_for_host(brand, host), expected);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn env_brand_holds_raw_detection_independent_of_refinement() {
|
||
// `build_*` records the raw detection in both fields.
|
||
let ctx = build_terminal_context_from_env(&env_from(&[]));
|
||
assert_eq!(ctx.brand, TerminalName::Unknown);
|
||
assert_eq!(ctx.env_brand, TerminalName::Unknown);
|
||
|
||
// The fallback refines only `brand`; `env_brand` stays raw so the glyphs
|
||
// legacy-console check and shift_enter_unavailable still treat a bare
|
||
// ConHost conservatively.
|
||
let effective = refine_unknown_brand_for_host(ctx.brand, crate::host::HostOs::Windows);
|
||
assert_eq!(effective, TerminalName::WindowsTerminal);
|
||
assert_eq!(ctx.env_brand, TerminalName::Unknown);
|
||
}
|
||
|
||
#[test]
|
||
fn mouse_reporting_leaks_only_for_jetbrains_on_windows() {
|
||
use crate::host::HostOs;
|
||
// The reported failure: JediTerm on Windows leaks raw X10 mouse bytes.
|
||
assert!(mouse_reporting_leaks(
|
||
TerminalName::JetBrains,
|
||
HostOs::Windows
|
||
));
|
||
// JetBrains on macOS/Linux is fine — crossterm's unix parser decodes the
|
||
// reports, so we must NOT downgrade those to minimal.
|
||
assert!(!mouse_reporting_leaks(
|
||
TerminalName::JetBrains,
|
||
HostOs::Macos
|
||
));
|
||
assert!(!mouse_reporting_leaks(
|
||
TerminalName::JetBrains,
|
||
HostOs::Linux
|
||
));
|
||
// Other Windows terminals deliver native console mouse records — no leak.
|
||
assert!(!mouse_reporting_leaks(
|
||
TerminalName::WindowsTerminal,
|
||
HostOs::Windows
|
||
));
|
||
assert!(!mouse_reporting_leaks(TerminalName::Kitty, HostOs::Windows));
|
||
}
|
||
|
||
// detect_byobu_from_env
|
||
|
||
#[test]
|
||
fn byobu_tmux_explicit_backend() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_screen_explicit_backend() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0")]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_inferred_tmux_from_config_dir() {
|
||
let env = env_from(&[
|
||
("BYOBU_CONFIG_DIR", "/home/user/.byobu"),
|
||
("TMUX", "/tmp/tmux"),
|
||
]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_inferred_screen_from_distro() {
|
||
let env = env_from(&[("BYOBU_DISTRO", "Ubuntu"), ("STY", "12345.pts-0")]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_markers_but_no_mux_markers_returns_none() {
|
||
let env = env_from(&[("BYOBU_CONFIG_DIR", "/home/user/.byobu")]);
|
||
assert_eq!(detect_byobu_from_env(&env), None);
|
||
}
|
||
|
||
#[test]
|
||
fn no_byobu_markers_returns_none() {
|
||
let env = env_from(&[("TMUX", "/tmp/tmux")]);
|
||
assert_eq!(detect_byobu_from_env(&env), None);
|
||
}
|
||
|
||
// detect_multiplexer_from_env
|
||
|
||
#[test]
|
||
fn mux_plain_tmux() {
|
||
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_plain_screen() {
|
||
let env = env_from(&[("STY", "12345.pts-0.host")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Screen);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_zellij() {
|
||
let env = env_from(&[("ZELLIJ", "0")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Zellij);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_zellij_from_session_name() {
|
||
let env = env_from(&[("ZELLIJ_SESSION_NAME", "my-session")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Zellij);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_none_empty_env() {
|
||
let env = env_from(&[]);
|
||
assert_eq!(
|
||
detect_multiplexer_from_env(&env),
|
||
MultiplexerKind::Undetected
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_cmux_from_socket_path() {
|
||
let env = env_from(&[("CMUX_SOCKET_PATH", "/tmp/cmux.sock")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_cmux_from_panel_id() {
|
||
let env = env_from(&[("CMUX_PANEL_ID", "1")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_cmux_from_bundle_id() {
|
||
let env = env_from(&[("CMUX_BUNDLE_ID", "com.cmuxterm.app")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Cmux);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_empty_cmux_socket_alone_is_undetected() {
|
||
// CMUX_SOCKET may be present but empty under cmux; env_get filters empties.
|
||
// Without a non-empty CMUX_* marker, do not classify as Cmux.
|
||
let env = env_from(&[("CMUX_SOCKET", "")]);
|
||
assert_eq!(
|
||
detect_multiplexer_from_env(&env),
|
||
MultiplexerKind::Undetected
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn mux_tmux_nested_inside_cmux_wins() {
|
||
let env = env_from(&[
|
||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||
("CMUX_SOCKET_PATH", "/tmp/cmux.sock"),
|
||
("CMUX_PANEL_ID", "1"),
|
||
]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||
}
|
||
|
||
// ambiguous marker precedence
|
||
|
||
#[test]
|
||
fn tmux_beats_zellij_when_both_set() {
|
||
// tmux pane launched from inside Zellij leaves inherited ZELLIJ var.
|
||
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0"), ("ZELLIJ", "0")]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_screen_beats_tmux_marker() {
|
||
// BYOBU_BACKEND=screen with a stale TMUX var — Byobu backend wins.
|
||
let env = env_from(&[
|
||
("BYOBU_BACKEND", "screen"),
|
||
("TMUX", "/tmp/tmux-old"),
|
||
("STY", "12345.pts-0"),
|
||
]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Screen);
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_tmux_explicit_with_sty_stays_tmux() {
|
||
// BYOBU_BACKEND=tmux + stale STY — Byobu backend wins.
|
||
let env = env_from(&[
|
||
("BYOBU_BACKEND", "tmux"),
|
||
("STY", "old-screen"),
|
||
("TMUX", "/tmp/tmux"),
|
||
]);
|
||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||
}
|
||
|
||
// detect_tmux_meta_from_env
|
||
|
||
#[test]
|
||
fn tmux_meta_populated() {
|
||
let env = env_from(&[
|
||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||
("TMUX_PANE", "%3"),
|
||
]);
|
||
let meta = detect_tmux_meta_from_env(&env);
|
||
assert_eq!(
|
||
meta.tmux_env.as_deref(),
|
||
Some("/tmp/tmux-501/default,12345,0")
|
||
);
|
||
assert_eq!(meta.tmux_pane.as_deref(), Some("%3"));
|
||
}
|
||
|
||
#[test]
|
||
fn tmux_meta_empty_outside_tmux() {
|
||
let env = env_from(&[]);
|
||
let meta = detect_tmux_meta_from_env(&env);
|
||
assert_eq!(meta, TmuxClientMeta::default());
|
||
}
|
||
|
||
// build_terminal_context_from_env (integration)
|
||
|
||
#[test]
|
||
fn context_plain_terminal() {
|
||
let env = env_from(&[("TERM_PROGRAM", "Ghostty")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Ghostty);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
|
||
assert_eq!(ctx.byobu, None);
|
||
assert!(!ctx.is_tmux_backed());
|
||
assert!(!ctx.is_byobu());
|
||
}
|
||
|
||
#[test]
|
||
fn context_iterm2_lc_terminal_version_fallback_over_ssh() {
|
||
// Over SSH, TERM_PROGRAM_VERSION is stripped; version gating falls back to
|
||
// iTerm2's SSH-surviving LC_TERMINAL_VERSION.
|
||
let env = env_from(&[("LC_TERMINAL", "iTerm2"), ("LC_TERMINAL_VERSION", "3.5.6")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Iterm2);
|
||
assert_eq!(ctx.term_program_version.as_deref(), Some("3.5.6"));
|
||
}
|
||
|
||
#[test]
|
||
fn context_plain_tmux() {
|
||
let env = env_from(&[
|
||
("TERM_PROGRAM", "iTerm2"),
|
||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||
("TMUX_PANE", "%0"),
|
||
]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Iterm2);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
|
||
assert_eq!(ctx.byobu, None);
|
||
assert!(ctx.is_tmux_backed());
|
||
assert!(!ctx.is_byobu());
|
||
assert_eq!(ctx.tmux_config_path(), "~/.tmux.conf");
|
||
assert_eq!(
|
||
ctx.tmux_meta.tmux_env.as_deref(),
|
||
Some("/tmp/tmux-501/default,12345,0")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn context_byobu_tmux() {
|
||
let env = env_from(&[
|
||
("BYOBU_BACKEND", "tmux"),
|
||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||
("TMUX_PANE", "%1"),
|
||
]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
|
||
assert_eq!(ctx.byobu, Some(ByobuBackend::Tmux));
|
||
assert!(ctx.is_tmux_backed());
|
||
assert!(ctx.is_byobu());
|
||
assert_eq!(ctx.tmux_config_path(), "~/.byobu/.tmux.conf");
|
||
}
|
||
|
||
#[test]
|
||
fn context_byobu_screen() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0.host")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
|
||
assert_eq!(ctx.byobu, Some(ByobuBackend::Screen));
|
||
assert!(!ctx.is_tmux_backed());
|
||
assert!(ctx.is_byobu());
|
||
}
|
||
|
||
#[test]
|
||
fn context_plain_screen_no_byobu() {
|
||
let env = env_from(&[("STY", "12345.pts-0.host")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
|
||
assert_eq!(ctx.byobu, None);
|
||
assert!(!ctx.is_byobu());
|
||
}
|
||
|
||
#[test]
|
||
fn context_zellij() {
|
||
let env = env_from(&[
|
||
("ZELLIJ", "0"),
|
||
("ZELLIJ_SESSION_NAME", "my-session"),
|
||
("TERM_PROGRAM", "Ghostty"),
|
||
]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::Ghostty);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Zellij);
|
||
assert_eq!(ctx.byobu, None);
|
||
assert!(!ctx.is_tmux_backed());
|
||
}
|
||
|
||
#[test]
|
||
fn context_tmux_inside_zellij() {
|
||
// tmux launched from Zellij: TMUX is set, inherited ZELLIJ var remains.
|
||
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,12345,0"), ("ZELLIJ", "0")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Tmux);
|
||
assert!(ctx.is_tmux_backed());
|
||
}
|
||
|
||
#[test]
|
||
fn context_tmux_meta_not_populated_for_non_tmux() {
|
||
let env = env_from(&[("ZELLIJ", "0")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.tmux_meta, TmuxClientMeta::default());
|
||
}
|
||
|
||
#[test]
|
||
fn context_ambiguous_byobu_screen_with_tmux() {
|
||
// BYOBU_BACKEND=screen + stale TMUX → screen wins via Byobu backend.
|
||
let env = env_from(&[
|
||
("BYOBU_BACKEND", "screen"),
|
||
("TMUX", "/tmp/tmux-old"),
|
||
("STY", "12345.pts-0"),
|
||
]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
|
||
assert_eq!(ctx.byobu, Some(ByobuBackend::Screen));
|
||
}
|
||
|
||
#[test]
|
||
fn context_empty_env_values_ignored() {
|
||
// Empty string values should not trigger detection.
|
||
let env = env_from(&[("TMUX", ""), ("ZELLIJ", ""), ("STY", "")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
|
||
}
|
||
|
||
// determine_alt_screen_policy: fullscreen policy matrix
|
||
|
||
fn plain_ctx() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Ghostty,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn tmux_ctx() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Iterm2,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_meta: TmuxClientMeta {
|
||
tmux_env: Some("/tmp/tmux-501/default,12345,0".to_owned()),
|
||
tmux_pane: Some("%0".to_owned()),
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn zellij_ctx_test() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Ghostty,
|
||
multiplexer: MultiplexerKind::Zellij,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn screen_ctx() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Screen,
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn byobu_tmux_ctx() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
byobu: Some(ByobuBackend::Tmux),
|
||
tmux_meta: TmuxClientMeta {
|
||
tmux_env: Some("/tmp/tmux".to_owned()),
|
||
tmux_pane: Some("%1".to_owned()),
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
fn byobu_screen_ctx() -> TerminalContext {
|
||
TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Screen,
|
||
byobu: Some(ByobuBackend::Screen),
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
// Alt-screen policy matrix (all modes × contexts × CLI override)
|
||
|
||
#[derive(Debug)]
|
||
struct AltScreenCase {
|
||
name: &'static str,
|
||
cli_no_alt: bool,
|
||
mode: AltScreenMode,
|
||
ctx: TerminalContext,
|
||
control: bool,
|
||
expected: bool,
|
||
}
|
||
|
||
#[test]
|
||
fn alt_screen_policy_matrix() {
|
||
let cases = [
|
||
// Auto mode
|
||
AltScreenCase {
|
||
name: "auto_plain_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_tmux_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: tmux_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_tmux_control_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: tmux_ctx(),
|
||
control: true,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_zellij_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: zellij_ctx_test(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_screen_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: screen_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_byobu_tmux_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: byobu_tmux_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_byobu_screen_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: byobu_screen_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "auto_byobu_tmux_control_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: byobu_tmux_ctx(),
|
||
control: true,
|
||
expected: false,
|
||
},
|
||
// Never mode: always inline
|
||
AltScreenCase {
|
||
name: "never_plain_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "never_tmux_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: tmux_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "never_screen_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: screen_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "never_zellij_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: zellij_ctx_test(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "never_byobu_tmux_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: byobu_tmux_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "never_byobu_screen_inline",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Never,
|
||
ctx: byobu_screen_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
// Always mode: forces fullscreen
|
||
AltScreenCase {
|
||
name: "always_plain_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "always_tmux_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: tmux_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "always_tmux_control_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: tmux_ctx(),
|
||
control: true,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "always_zellij_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: zellij_ctx_test(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "always_screen_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: screen_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
AltScreenCase {
|
||
name: "always_byobu_screen_fullscreen",
|
||
cli_no_alt: false,
|
||
mode: AltScreenMode::Always,
|
||
ctx: byobu_screen_ctx(),
|
||
control: false,
|
||
expected: true,
|
||
},
|
||
// CLI --no-alt-screen overrides everything
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_auto",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_always",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Always,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_never",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Never,
|
||
ctx: plain_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_auto_in_tmux",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Auto,
|
||
ctx: tmux_ctx(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_always_in_zellij",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Always,
|
||
ctx: zellij_ctx_test(),
|
||
control: false,
|
||
expected: false,
|
||
},
|
||
AltScreenCase {
|
||
name: "cli_no_alt_overrides_always_in_tmux_control",
|
||
cli_no_alt: true,
|
||
mode: AltScreenMode::Always,
|
||
ctx: tmux_ctx(),
|
||
control: true,
|
||
expected: false,
|
||
},
|
||
];
|
||
|
||
for case in &cases {
|
||
let result =
|
||
determine_alt_screen_policy(case.cli_no_alt, case.mode, &case.ctx, case.control);
|
||
assert_eq!(
|
||
result, case.expected,
|
||
"alt_screen_policy failed on case '{}'",
|
||
case.name
|
||
);
|
||
}
|
||
}
|
||
|
||
// Windows Terminal context integration
|
||
|
||
#[test]
|
||
fn context_windows_terminal() {
|
||
let env = env_from(&[("WT_SESSION", "a1b2c3d4-e5f6-7890-abcd-ef1234567890")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::WindowsTerminal);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
|
||
assert!(!ctx.is_ssh);
|
||
}
|
||
|
||
// Terminal brand detection edge cases
|
||
|
||
// Extended environment matrix (final hardening)
|
||
|
||
// Byobu-screen: auto keeps fullscreen (screen is not auto-disabled)
|
||
|
||
#[test]
|
||
fn auto_byobu_screen_is_fullscreen() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Screen,
|
||
byobu: Some(ByobuBackend::Screen),
|
||
..Default::default()
|
||
};
|
||
assert!(determine_alt_screen_policy(
|
||
false,
|
||
AltScreenMode::Auto,
|
||
&ctx,
|
||
false
|
||
));
|
||
}
|
||
|
||
// Terminal brand detection edge cases
|
||
|
||
#[test]
|
||
fn brand_vscode_from_term_program() {
|
||
let env = env_from(&[("TERM_PROGRAM", "vscode")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_kitty_from_window_id() {
|
||
let env = env_from(&[("KITTY_WINDOW_ID", "1")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Kitty);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_alacritty_from_socket() {
|
||
let env = env_from(&[("ALACRITTY_SOCKET", "/tmp/alacritty.sock")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::Alacritty
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_apple_terminal_from_session_id() {
|
||
let env = env_from(&[("TERM_SESSION_ID", "w0t0p0:ABC123")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::AppleTerminal
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_term_program_takes_precedence_over_other_vars() {
|
||
// TERM_PROGRAM should win even when other brand-specific vars are set.
|
||
let env = env_from(&[
|
||
("TERM_PROGRAM", "Ghostty"),
|
||
("WEZTERM_VERSION", "20240203"),
|
||
("ITERM_SESSION_ID", "w0t0p0"),
|
||
]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Ghostty);
|
||
}
|
||
|
||
// IDE family detection (VS Code forks / xterm.js embeds)
|
||
|
||
#[test]
|
||
fn brand_cursor_from_cursor_trace_id() {
|
||
// Cursor sets a unique CURSOR_TRACE_ID; it also sets TERM_PROGRAM=vscode
|
||
// (since it's a VS Code fork), so we must detect it before the
|
||
// TERM_PROGRAM lookup.
|
||
let env = env_from(&[
|
||
("CURSOR_TRACE_ID", "abcdef0123456789"),
|
||
("TERM_PROGRAM", "vscode"),
|
||
]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Cursor);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_cursor_from_askpass_main() {
|
||
// Cursor exposes VSCODE_GIT_ASKPASS_MAIN pointing at its install path.
|
||
let env = env_from(&[
|
||
(
|
||
"VSCODE_GIT_ASKPASS_MAIN",
|
||
"/Applications/Cursor.app/Contents/Resources/app/extensions/git/dist/askpass-main.js",
|
||
),
|
||
("TERM_PROGRAM", "vscode"),
|
||
]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Cursor);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_windsurf_from_askpass_main() {
|
||
// Another VS Code fork; same askpass path substring pattern.
|
||
let env = env_from(&[
|
||
(
|
||
"VSCODE_GIT_ASKPASS_MAIN",
|
||
"/Applications/Windsurf.app/Contents/Resources/app/extensions/git/dist/askpass-main.js",
|
||
),
|
||
("TERM_PROGRAM", "vscode"),
|
||
]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Windsurf);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_zed_from_term_program() {
|
||
let env = env_from(&[("TERM_PROGRAM", "zed")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Zed);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_vscode_when_no_ide_markers() {
|
||
// Pure VS Code: TERM_PROGRAM=vscode and no CURSOR_TRACE_ID / matching
|
||
// askpass marker. Should remain VsCode, not collapse to anything else.
|
||
let env = env_from(&[("TERM_PROGRAM", "vscode")]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_vscode_when_askpass_does_not_match_ide() {
|
||
// Pure VS Code askpass path (no "cursor"/"windsurf") — brand is VsCode
|
||
// even when TERM_PROGRAM is also set.
|
||
let env = env_from(&[
|
||
("VSCODE_GIT_ASKPASS_MAIN", "/usr/local/bin/askpass-main.js"),
|
||
("TERM_PROGRAM", "vscode"),
|
||
]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_vscode_from_askpass_without_term_program() {
|
||
// Remote SSH / tmux: TERM_PROGRAM missing or overwritten, but the VS Code
|
||
// remote agent still injects VSCODE_GIT_ASKPASS_MAIN into the pane env.
|
||
let env = env_from(&[(
|
||
"VSCODE_GIT_ASKPASS_MAIN",
|
||
"/home/user/.vscode-server/bin/abc/askpass",
|
||
)]);
|
||
assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::VsCode);
|
||
}
|
||
|
||
// Zellij detection from ZELLIJ_VERSION (no ZELLIJ or SESSION_NAME)
|
||
|
||
#[test]
|
||
fn mux_zellij_not_from_version_only() {
|
||
// ZELLIJ_VERSION alone does not trigger Zellij detection — it must be
|
||
// ZELLIJ or ZELLIJ_SESSION_NAME.
|
||
let env = env_from(&[("ZELLIJ_VERSION", "0.43.1")]);
|
||
assert_eq!(
|
||
detect_multiplexer_from_env(&env),
|
||
MultiplexerKind::Undetected
|
||
);
|
||
}
|
||
|
||
// Byobu inference edge cases
|
||
|
||
#[test]
|
||
fn byobu_unknown_backend_string_with_tmux() {
|
||
// Unknown BYOBU_BACKEND value falls through to mux-marker inference.
|
||
let env = env_from(&[("BYOBU_BACKEND", "unknown"), ("TMUX", "/tmp/tmux")]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Tmux));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_unknown_backend_string_with_sty() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "fish"), ("STY", "12345.pts-0")]);
|
||
assert_eq!(detect_byobu_from_env(&env), Some(ByobuBackend::Screen));
|
||
}
|
||
|
||
#[test]
|
||
fn byobu_unknown_backend_no_mux_returns_none() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "unknown")]);
|
||
assert_eq!(detect_byobu_from_env(&env), None);
|
||
}
|
||
|
||
// Context-level edge cases
|
||
|
||
#[test]
|
||
fn context_sty_takes_screen_when_no_tmux_or_zellij() {
|
||
let env = env_from(&[("STY", "12345.pts-0.host")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
|
||
assert_eq!(ctx.byobu, None);
|
||
assert!(!ctx.is_tmux_backed());
|
||
}
|
||
|
||
#[test]
|
||
fn context_screen_does_not_populate_tmux_meta() {
|
||
let env = env_from(&[("STY", "12345.pts-0.host"), ("TMUX_PANE", "%stale")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Screen);
|
||
// tmux_meta should be default since multiplexer is not Tmux.
|
||
assert_eq!(ctx.tmux_meta, TmuxClientMeta::default());
|
||
}
|
||
|
||
#[test]
|
||
fn context_tmux_config_path_is_standard_for_plain_tmux() {
|
||
let env = env_from(&[("TMUX", "/tmp/tmux-501/default,1,0")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.tmux_config_path(), "~/.tmux.conf");
|
||
}
|
||
|
||
#[test]
|
||
fn context_tmux_config_path_is_byobu_for_byobu_tmux() {
|
||
let env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.tmux_config_path(), "~/.byobu/.tmux.conf");
|
||
}
|
||
|
||
#[test]
|
||
fn context_is_byobu_returns_true_for_both_backends() {
|
||
let tmux_env = env_from(&[("BYOBU_BACKEND", "tmux"), ("TMUX", "/tmp/tmux")]);
|
||
let screen_env = env_from(&[("BYOBU_BACKEND", "screen"), ("STY", "12345.pts-0")]);
|
||
|
||
assert!(build_terminal_context_from_env(&tmux_env).is_byobu());
|
||
assert!(build_terminal_context_from_env(&screen_env).is_byobu());
|
||
}
|
||
|
||
#[test]
|
||
fn context_is_byobu_returns_false_without_byobu_markers() {
|
||
let env = env_from(&[("TMUX", "/tmp/tmux")]);
|
||
assert!(!build_terminal_context_from_env(&env).is_byobu());
|
||
}
|
||
|
||
// parse_tmux_major_minor: version string parsing
|
||
|
||
#[test]
|
||
fn parse_tmux_version_standard() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 3.4"), Some((3, 4)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_with_letter_suffix() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 3.3a"), Some((3, 3)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_old() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 2.9"), Some((2, 9)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_next_gen() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 4.0"), Some((4, 0)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_missing_prefix() {
|
||
assert_eq!(parse_tmux_major_minor("3.4"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_empty() {
|
||
assert_eq!(parse_tmux_major_minor(""), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_garbage() {
|
||
assert_eq!(parse_tmux_major_minor("not a version"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_trailing_dot() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 3."), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_tmux_version_no_minor() {
|
||
assert_eq!(parse_tmux_major_minor("tmux 3"), None);
|
||
}
|
||
|
||
// parse_semver_major_minor: TERM_PROGRAM_VERSION parsing
|
||
|
||
#[test]
|
||
fn parse_semver_standard() {
|
||
assert_eq!(parse_semver_major_minor("3.6.0"), Some((3, 6)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_semver_two_part() {
|
||
assert_eq!(parse_semver_major_minor("1.2"), Some((1, 2)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_semver_high_version() {
|
||
assert_eq!(parse_semver_major_minor("4.0.0"), Some((4, 0)));
|
||
}
|
||
|
||
#[test]
|
||
fn parse_semver_empty() {
|
||
assert_eq!(parse_semver_major_minor(""), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_semver_garbage() {
|
||
assert_eq!(parse_semver_major_minor("garbage"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_semver_major_only() {
|
||
assert_eq!(parse_semver_major_minor("3"), None);
|
||
}
|
||
|
||
// graphics_protocol_skip_reason
|
||
|
||
#[test]
|
||
fn graphics_protocol_skip_reason_tmux() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Kitty,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.graphics_protocol_skip_reason(), Some("tmux"));
|
||
}
|
||
|
||
#[test]
|
||
fn graphics_protocol_skip_reason_plain_kitty() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Kitty,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.graphics_protocol_skip_reason(), None);
|
||
}
|
||
|
||
// kitty_skip_reason: Kitty keyboard protocol skip-reason matrix
|
||
|
||
#[test]
|
||
fn kitty_skip_vscode() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::VsCode,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_cursor() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Cursor,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_windsurf() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Windsurf,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_zed() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Zed,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_apple_terminal() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::AppleTerminal,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("apple_terminal"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_screen() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Screen,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("screen"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_old_tmux() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.2".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_tmux_no_version() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: None,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_modern_tmux() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.3".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_modern_tmux_letter() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.3a".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_tmux_4() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 4.0".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_unknown_terminal_no_multiplexer() {
|
||
// Unknown brand with no multiplexer = no positive evidence of KKP
|
||
// support. Catches VSCode-over-SSH, bare Docker containers, etc.
|
||
let ctx = TerminalContext::default();
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_known_good_terminal() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Ghostty,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_foot_no_multiplexer() {
|
||
// Core fix: a bare foot window negotiates KKP (no skip reason).
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Foot,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_available_on_foot() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Foot,
|
||
env_brand: TerminalName::Foot,
|
||
..Default::default()
|
||
};
|
||
assert!(!ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
#[test]
|
||
fn foot_ctrl_dot_is_reliable() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Foot,
|
||
..Default::default()
|
||
};
|
||
assert!(!ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_foot_over_old_tmux_uses_mux_logic() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Foot,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.0".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_old"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_zellij() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Zellij,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_byobu_screen() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Screen,
|
||
byobu: Some(ByobuBackend::Screen),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("screen"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_windows_terminal() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::WindowsTerminal,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("windows_terminal"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vscode_over_tmux() {
|
||
// When both brand and multiplexer would skip, brand wins.
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::VsCode,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.0".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vscode"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vte_version() {
|
||
// VTE does not support 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()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vte_brand() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Vte,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
|
||
}
|
||
|
||
// shift_enter_unavailable: VTE version gating for 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.
|
||
|
||
#[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());
|
||
}
|
||
|
||
#[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());
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_available_kkp_terminals() {
|
||
// Terminals that negotiate the Kitty keyboard protocol (or, for Apple
|
||
// Terminal, recover the modifier via CoreGraphics) deliver a usable
|
||
// Shift+Enter and must never report it as unavailable.
|
||
for brand in [
|
||
TerminalName::Ghostty,
|
||
TerminalName::Kitty,
|
||
TerminalName::WezTerm,
|
||
TerminalName::AppleTerminal,
|
||
] {
|
||
let ctx = TerminalContext {
|
||
brand,
|
||
// lockstep with brand (no Windows refinement)
|
||
env_brand: brand,
|
||
..Default::default()
|
||
};
|
||
assert!(
|
||
!ctx.shift_enter_unavailable(),
|
||
"{brand:?} must not report shift_enter_unavailable"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_unavailable_vscode_family() {
|
||
// VS Code's xterm.js (and VS Code-family / xterm.js IDE forks) never
|
||
// negotiate KKP, so Shift+Enter arrives as a bare CR identical to
|
||
// Enter. The UI must advertise Alt+Enter instead.
|
||
for brand in [
|
||
TerminalName::VsCode,
|
||
TerminalName::Cursor,
|
||
TerminalName::Windsurf,
|
||
TerminalName::Zed,
|
||
] {
|
||
let ctx = TerminalContext {
|
||
brand,
|
||
..Default::default()
|
||
};
|
||
assert!(
|
||
ctx.shift_enter_unavailable(),
|
||
"{brand:?} (xterm.js) must report shift_enter_unavailable"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_unavailable_unknown_no_multiplexer() {
|
||
// The common VS Code-over-SSH shape: TERM_PROGRAM isn't forwarded so
|
||
// the brand falls back to Unknown, and the pager skips KKP. Shift+Enter
|
||
// is indistinguishable from Enter — advertise Alt+Enter.
|
||
let ctx = TerminalContext::default();
|
||
assert_eq!(ctx.brand, TerminalName::Unknown);
|
||
assert_eq!(ctx.env_brand, TerminalName::Unknown);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
|
||
assert!(ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_unavailable_windows_refined_brand_stays_env_unknown() {
|
||
// Native Windows DefTerm / bare ConHost: `brand` is refined to WT for
|
||
// capabilities/label, but `env_brand` stays Unknown. KKP is skipped
|
||
// (WT is in the skip list; ConHost has no KKP either), so Shift+Enter
|
||
// is unreliable — advertise Alt+Enter, same as an unrefined Unknown.
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::WindowsTerminal,
|
||
env_brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert!(ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_available_positively_detected_windows_terminal() {
|
||
// WT detected via WT_SESSION/TERM_PROGRAM: both brand fields are WT.
|
||
// The Unknown gate must not fire; Shift+Enter follows the WT path
|
||
// (KKP is skipped for WT, but that is a separate, pre-existing choice).
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::WindowsTerminal,
|
||
env_brand: TerminalName::WindowsTerminal,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert!(!ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
#[test]
|
||
fn shift_enter_available_unknown_with_multiplexer() {
|
||
// Unknown brand but inside modern tmux: KKP is negotiated through the
|
||
// multiplexer, so Shift+Enter works and must not be flagged.
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
env_brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Tmux,
|
||
tmux_version: Some("tmux 3.4".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert!(!ctx.shift_enter_unavailable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_on_vte() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Vte,
|
||
..Default::default()
|
||
};
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_when_vte_version_set() {
|
||
let ctx = TerminalContext {
|
||
vte_version: Some("7402".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_reliable_on_kitty() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Kitty,
|
||
..Default::default()
|
||
};
|
||
assert!(!ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_on_windows_terminal() {
|
||
// WT propagates WT_SESSION into WSL via WSLENV, so this also covers
|
||
// WSL-inside-WT — `Ctrl+.` can't survive WT's ConPTY pipeline either
|
||
// way. (Pure WSL with no WT_SESSION is caught by `is_wsl()` in the
|
||
// consumer, not here.)
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::WindowsTerminal,
|
||
..Default::default()
|
||
};
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_on_vscode_forks() {
|
||
// VS Code's integrated terminal swallows `Ctrl+.` for its own command
|
||
// palette. VS Code-family forks and other xterm.js IDE embeds inherit
|
||
// the same behavior (forks share the keymap; others mirror it).
|
||
// Preference is keyed off `kitty_skip_reason` ("vscode").
|
||
for brand in [
|
||
TerminalName::VsCode,
|
||
TerminalName::Cursor,
|
||
TerminalName::Windsurf,
|
||
TerminalName::Zed,
|
||
] {
|
||
let ctx = TerminalContext {
|
||
brand,
|
||
..Default::default()
|
||
};
|
||
assert!(
|
||
ctx.ctrl_dot_unreliable(),
|
||
"expected ctrl_dot_unreliable on {brand:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_when_tmux_extended_keys_off() {
|
||
// iTerm2+tmux still used to advertise Ctrl+. while KKP was
|
||
// skipped for `extended-keys off`. Preference must follow the skip.
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Iterm2,
|
||
tmux_version: Some("tmux 3.6a".to_owned()),
|
||
tmux_extended_keys: Some("off".to_owned()),
|
||
..tmux_ctx()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("tmux_extended_keys_off"));
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_reliable_on_iterm2_tmux_extended_keys_on() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Iterm2,
|
||
tmux_version: Some("tmux 3.6a".to_owned()),
|
||
tmux_extended_keys: Some("on".to_owned()),
|
||
..tmux_ctx()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||
assert!(!ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_on_unknown_no_multiplexer() {
|
||
// Same KKP-skip path as VS Code SSH / unbranded hosts.
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::Unknown,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("unknown_no_multiplexer"));
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
// tmux extended-keys interaction with kitty_skip_reason
|
||
|
||
fn extended_keys_ctx(version: &str, extended_keys: Option<&str>) -> TerminalContext {
|
||
TerminalContext {
|
||
tmux_version: Some(version.to_owned()),
|
||
tmux_extended_keys: extended_keys.map(str::to_owned),
|
||
..tmux_ctx()
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_modern_tmux_extended_keys_off() {
|
||
assert_eq!(
|
||
extended_keys_ctx("tmux 3.4", Some("off")).kitty_skip_reason(),
|
||
Some("tmux_extended_keys_off"),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_allowed_modern_tmux_extended_keys_non_off_values() {
|
||
// `on`/`always`/empty/uppercase/None must all NOT trigger the skip:
|
||
// the comparison against `"off"` is exact and case-sensitive.
|
||
for val in [Some("on"), Some("always"), Some(""), Some("OFF"), None] {
|
||
assert_eq!(
|
||
extended_keys_ctx("tmux 3.4", val).kitty_skip_reason(),
|
||
None,
|
||
"value {val:?} must not trigger skip",
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_old_tmux_takes_precedence_over_extended_keys() {
|
||
assert_eq!(
|
||
extended_keys_ctx("tmux 3.2", Some("off")).kitty_skip_reason(),
|
||
Some("tmux_old"),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vte_takes_precedence_over_tmux_extended_keys_off() {
|
||
// VTE strips kitty CSI-u so `/terminal-setup` should recommend
|
||
// switching terminal emulators rather than editing tmux.conf.
|
||
let mut ctx = extended_keys_ctx("tmux 3.4", Some("off"));
|
||
ctx.vte_version = Some("7402".to_owned());
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vte_takes_precedence_over_screen() {
|
||
let ctx = TerminalContext {
|
||
multiplexer: MultiplexerKind::Screen,
|
||
vte_version: Some("7402".to_owned()),
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_vte_takes_precedence_over_tmux_old() {
|
||
let mut ctx = extended_keys_ctx("tmux 3.2", None);
|
||
ctx.vte_version = Some("7402".to_owned());
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("vte"));
|
||
}
|
||
|
||
// JetBrains JediTerm detection
|
||
|
||
#[test]
|
||
fn brand_jetbrains_from_terminal_emulator() {
|
||
let env = env_from(&[("TERMINAL_EMULATOR", "JetBrains-JediTerm")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::JetBrains
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_jetbrains_case_insensitive() {
|
||
let env = env_from(&[("TERMINAL_EMULATOR", "jetbrains-jediterm")]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::JetBrains
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn brand_jetbrains_beats_term_session_id() {
|
||
// JediTerm on some platforms may set TERM_SESSION_ID, which would
|
||
// otherwise be detected as Apple Terminal. JetBrains must win.
|
||
let env = env_from(&[
|
||
("TERMINAL_EMULATOR", "JetBrains-JediTerm"),
|
||
("TERM_SESSION_ID", "w0t0p0:ABC123"),
|
||
]);
|
||
assert_eq!(
|
||
detect_terminal_brand_from_env(&env),
|
||
TerminalName::JetBrains
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn context_jetbrains_terminal() {
|
||
let env = env_from(&[("TERMINAL_EMULATOR", "JetBrains-JediTerm")]);
|
||
let ctx = build_terminal_context_from_env(&env);
|
||
assert_eq!(ctx.brand, TerminalName::JetBrains);
|
||
assert_eq!(ctx.multiplexer, MultiplexerKind::Undetected);
|
||
assert!(!ctx.is_ssh);
|
||
}
|
||
|
||
#[test]
|
||
fn kitty_skip_jetbrains() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::JetBrains,
|
||
..Default::default()
|
||
};
|
||
assert_eq!(ctx.kitty_skip_reason(), Some("jetbrains"));
|
||
}
|
||
|
||
#[test]
|
||
fn ctrl_dot_unreliable_on_jetbrains() {
|
||
let ctx = TerminalContext {
|
||
brand: TerminalName::JetBrains,
|
||
..Default::default()
|
||
};
|
||
assert!(ctx.ctrl_dot_unreliable());
|
||
}
|
||
|
||
#[test]
|
||
fn repaints_pane_out_of_band_per_arm() {
|
||
// Embedded-editor :terminal (libvterm) can repaint our pane with no PTY resize.
|
||
let editor = TerminalContext {
|
||
embedded_editor: Some(EmbeddedEditor::Neovim),
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert!(editor.repaints_pane_out_of_band());
|
||
|
||
// Each multiplexer arm fires independently.
|
||
for mux in [
|
||
MultiplexerKind::Tmux,
|
||
MultiplexerKind::Screen,
|
||
MultiplexerKind::Zellij,
|
||
] {
|
||
let ctx = TerminalContext {
|
||
multiplexer: mux,
|
||
..Default::default()
|
||
};
|
||
assert!(
|
||
ctx.repaints_pane_out_of_band(),
|
||
"multiplexer {mux:?} should report out-of-band repaint"
|
||
);
|
||
}
|
||
|
||
// Plain terminal: no editor, no multiplexer -> no heal (no flicker regression).
|
||
let plain = TerminalContext {
|
||
embedded_editor: None,
|
||
multiplexer: MultiplexerKind::Undetected,
|
||
..Default::default()
|
||
};
|
||
assert!(!plain.repaints_pane_out_of_band());
|
||
}
|