use super::{ImageProtocol, TerminalIdentity};
use std::{env, fs, path::Path, process::Command};
pub(super) fn pdf_preview_tools_available() -> bool {
command_exists("pdfinfo") && command_exists("pdftocairo")
}
pub(in crate::app) fn detect_terminal_identity() -> TerminalIdentity {
detect_terminal_identity_with(
real_env_lookup,
query_tmux_client_termname,
query_tmux_client_env,
query_tmux_env,
)
}
fn detect_terminal_identity_with(
env_lookup: impl Fn(&str) -> Option<String>,
tmux_client_term: impl Fn() -> Option<String>,
tmux_client_env_lookup: impl Fn(&str) -> Option<String>,
tmux_env_lookup: impl Fn(&str) -> Option<String>,
) -> TerminalIdentity {
let identity = classify_from_env(&env_lookup);
if identity != TerminalIdentity::Other {
if identity == TerminalIdentity::Kitty
&& let Some(tmux_identity) = recover_direct_graphics_identity_from_tmux(
&env_lookup,
&tmux_client_term,
&tmux_client_env_lookup,
&tmux_env_lookup,
)
{
return tmux_identity;
}
return identity;
}
if env_lookup("TMUX").is_none() {
return identity;
}
if let Some(term) = tmux_client_term()
&& let Some(id) = classify_tmux_client_termname(&term)
{
return id;
}
if let Some(id) = classify_supported_tmux_env(&tmux_client_env_lookup) {
return id;
}
if let Some(id) = classify_supported_tmux_env(&tmux_env_lookup) {
return id;
}
TerminalIdentity::Other
}
fn classify_from_env(env_lookup: &impl Fn(&str) -> Option<String>) -> TerminalIdentity {
let term = env_lookup("TERM").unwrap_or_default().to_ascii_lowercase();
let term_program = env_lookup("TERM_PROGRAM")
.unwrap_or_default()
.to_ascii_lowercase();
let kitty_window_id = env_lookup("KITTY_WINDOW_ID").is_some();
let konsole_dbus = env_lookup("KONSOLE_DBUS_SESSION").is_some()
|| env_lookup("KONSOLE_DBUS_SERVICE").is_some()
|| env_lookup("KONSOLE_DBUS_WINDOW").is_some();
if term.contains("xterm-kitty") || term_program == "kitty" {
TerminalIdentity::Kitty
} else if term.contains("ghostty") || term_program == "ghostty" {
TerminalIdentity::Ghostty
} else if term.contains("wezterm") || term_program == "wezterm" {
TerminalIdentity::WezTerm
} else if term_program.contains("warp") || env_lookup("WARP_SESSION_ID").is_some() {
TerminalIdentity::Warp
} else if term_program == "iterm.app" {
TerminalIdentity::ITerm2
} else if term.contains("alacritty")
|| term_program.contains("alacritty")
|| env_lookup("ALACRITTY_SOCKET").is_some()
{
TerminalIdentity::Alacritty
} else if konsole_dbus {
TerminalIdentity::Konsole
} else if kitty_window_id {
TerminalIdentity::Kitty
} else if term == "foot" || term == "foot-extra" {
TerminalIdentity::Foot
} else if env_lookup("WT_SESSION").is_some() {
TerminalIdentity::WindowsTerminal
} else {
TerminalIdentity::Other
}
}
fn classify_tmux_client_termname(term: &str) -> Option<TerminalIdentity> {
let term = term.to_ascii_lowercase();
if term.contains("xterm-kitty") {
Some(TerminalIdentity::Kitty)
} else if term.contains("ghostty") {
Some(TerminalIdentity::Ghostty)
} else if term == "foot" || term == "foot-extra" {
Some(TerminalIdentity::Foot)
} else {
None
}
}
fn classify_tmux_recovered_identity(
term: &str,
term_program: &str,
kitty_window_id_set: bool,
warp_session_id_set: bool,
konsole_dbus_set: bool,
windows_terminal_session_set: bool,
) -> Option<TerminalIdentity> {
let term = term.to_ascii_lowercase();
let term_program = term_program.to_ascii_lowercase();
if term.contains("xterm-kitty") || term_program == "kitty" {
Some(TerminalIdentity::Kitty)
} else if term.contains("ghostty") || term_program == "ghostty" {
Some(TerminalIdentity::Ghostty)
} else if term.contains("wezterm") || term_program == "wezterm" {
Some(TerminalIdentity::WezTerm)
} else if term_program.contains("warp") || warp_session_id_set {
Some(TerminalIdentity::Warp)
} else if term_program == "iterm.app" {
Some(TerminalIdentity::ITerm2)
} else if konsole_dbus_set {
Some(TerminalIdentity::Konsole)
} else if term == "foot" || term == "foot-extra" {
Some(TerminalIdentity::Foot)
} else if windows_terminal_session_set {
Some(TerminalIdentity::WindowsTerminal)
} else if kitty_window_id_set {
Some(TerminalIdentity::Kitty)
} else {
None
}
}
fn classify_supported_tmux_env(
env_lookup: &impl Fn(&str) -> Option<String>,
) -> Option<TerminalIdentity> {
classify_tmux_recovered_identity(
&env_lookup("TERM").unwrap_or_default(),
&env_lookup("TERM_PROGRAM").unwrap_or_default(),
env_lookup("KITTY_WINDOW_ID").is_some(),
env_lookup("WARP_SESSION_ID").is_some(),
env_lookup("KONSOLE_DBUS_SESSION").is_some()
|| env_lookup("KONSOLE_DBUS_SERVICE").is_some()
|| env_lookup("KONSOLE_DBUS_WINDOW").is_some(),
env_lookup("WT_SESSION").is_some(),
)
}
fn recover_direct_graphics_identity_from_tmux(
env_lookup: &impl Fn(&str) -> Option<String>,
tmux_client_term: &impl Fn() -> Option<String>,
tmux_client_env_lookup: &impl Fn(&str) -> Option<String>,
tmux_env_lookup: &impl Fn(&str) -> Option<String>,
) -> Option<TerminalIdentity> {
env_lookup("TMUX")?;
let term = env_lookup("TERM").unwrap_or_default().to_ascii_lowercase();
let term_program = env_lookup("TERM_PROGRAM")
.unwrap_or_default()
.to_ascii_lowercase();
if !term.contains("tmux") && term_program != "tmux" {
return None;
}
if let Some(client_term) = tmux_client_term()
&& let Some(client_identity) = classify_tmux_client_termname(&client_term)
{
return if client_identity == TerminalIdentity::Kitty {
None
} else {
Some(client_identity)
};
}
if let Some(client_identity) = classify_supported_tmux_env(tmux_client_env_lookup) {
return if client_identity == TerminalIdentity::Kitty {
None
} else {
Some(client_identity)
};
}
if let Some(session_identity) = classify_supported_tmux_env(tmux_env_lookup)
&& session_identity != TerminalIdentity::Kitty
{
return Some(session_identity);
}
None
}
fn real_env_lookup(name: &str) -> Option<String> {
env::var_os(name).map(|v| v.to_string_lossy().into_owned())
}
fn query_tmux_client_termname() -> Option<String> {
let output = Command::new("tmux")
.args(["display-message", "-p", "#{client_termname}"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let trimmed = stdout.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn query_tmux_client_env(name: &str) -> Option<String> {
let pid = query_tmux_client_pid()?;
read_process_env(pid, name)
}
fn query_tmux_client_pid() -> Option<u32> {
let output = Command::new("tmux")
.args(["display-message", "-p", "#{client_pid}"])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
stdout.trim().parse().ok()
}
#[cfg(target_os = "linux")]
fn read_process_env(pid: u32, name: &str) -> Option<String> {
let environ = fs::read(format!("/proc/{pid}/environ")).ok()?;
parse_proc_environ(&environ, name)
}
#[cfg(not(target_os = "linux"))]
fn read_process_env(_: u32, _: &str) -> Option<String> {
None
}
#[cfg(any(target_os = "linux", test))]
fn parse_proc_environ(environ: &[u8], name: &str) -> Option<String> {
let mut prefix = name.as_bytes().to_vec();
prefix.push(b'=');
environ.split(|&byte| byte == 0).find_map(|entry| {
entry
.strip_prefix(prefix.as_slice())
.map(|value| String::from_utf8_lossy(value).into_owned())
})
}
fn query_tmux_env(name: &str) -> Option<String> {
show_environment_value(&[], name).or_else(|| show_environment_value(&["-g"], name))
}
fn show_environment_value(extra_args: &[&str], name: &str) -> Option<String> {
let output = Command::new("tmux")
.arg("show-environment")
.args(extra_args)
.arg(name)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
parse_show_environment_line(&stdout, name)
}
fn parse_show_environment_line(stdout: &str, name: &str) -> Option<String> {
let prefix = format!("{name}=");
let unset = format!("-{name}");
for line in stdout.lines() {
let line = line.trim_end_matches('\r');
if line == unset {
return None;
}
if let Some(value) = line.strip_prefix(&prefix) {
return Some(value.to_string());
}
}
None
}
pub(in crate::app) fn select_image_protocol(
identity: TerminalIdentity,
image_previews_override: bool,
) -> ImageProtocol {
match identity {
TerminalIdentity::Kitty => ImageProtocol::KittyGraphics,
TerminalIdentity::Ghostty => ImageProtocol::KittyGraphics,
TerminalIdentity::Warp => ImageProtocol::KittyDirectGraphics,
TerminalIdentity::Konsole => ImageProtocol::KittyDirectGraphics,
TerminalIdentity::WezTerm | TerminalIdentity::ITerm2 => ImageProtocol::ItermInline,
TerminalIdentity::Foot | TerminalIdentity::WindowsTerminal => ImageProtocol::Sixel,
TerminalIdentity::Other if image_previews_override => ImageProtocol::KittyGraphics,
TerminalIdentity::Alacritty | TerminalIdentity::Other => ImageProtocol::None,
}
}
pub(in crate::app) fn command_exists(program: &str) -> bool {
if program.is_empty() {
return false;
}
let program_path = Path::new(program);
if program_path.components().count() > 1 {
return executable_file_exists(program_path)
|| cfg!(windows) && executable_file_exists(&program_path.with_extension("exe"));
}
env::var_os("PATH").is_some_and(|paths| {
env::split_paths(&paths).any(|dir| {
let candidate = dir.join(program);
executable_file_exists(&candidate)
|| cfg!(windows) && executable_file_exists(&candidate.with_extension("exe"))
})
})
}
fn executable_file_exists(path: &Path) -> bool {
let Ok(metadata) = fs::metadata(path) else {
return false;
};
if !metadata.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
ffi::OsString,
sync::{Mutex, OnceLock},
};
#[cfg(unix)]
use std::{
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
fn temp_root(label: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("elio-inline-image-{label}-{unique}"))
}
fn terminal_env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
struct TerminalEnvGuard {
saved: Vec<(&'static str, Option<OsString>)>,
}
impl TerminalEnvGuard {
fn isolate() -> Self {
const VARS: &[&str] = &[
"TERM",
"TERM_PROGRAM",
"KITTY_WINDOW_ID",
"WARP_SESSION_ID",
"ALACRITTY_SOCKET",
"WT_SESSION",
"KONSOLE_DBUS_SESSION",
"KONSOLE_DBUS_SERVICE",
"KONSOLE_DBUS_WINDOW",
"TMUX",
];
let saved = VARS
.iter()
.map(|name| (*name, env::var_os(name)))
.collect::<Vec<_>>();
for name in VARS {
unsafe {
env::remove_var(name);
}
}
Self { saved }
}
}
impl Drop for TerminalEnvGuard {
fn drop(&mut self) {
for (name, value) in &self.saved {
if let Some(value) = value {
unsafe {
env::set_var(name, value);
}
} else {
unsafe {
env::remove_var(name);
}
}
}
}
}
#[test]
fn detect_terminal_identity_recognizes_iterm2_term_program() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("TERM_PROGRAM", "iTerm.app");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::ITerm2);
}
#[test]
fn select_image_protocol_kitty_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Kitty, false),
ImageProtocol::KittyGraphics
);
assert_eq!(
select_image_protocol(TerminalIdentity::Kitty, true),
ImageProtocol::KittyGraphics
);
}
#[test]
fn select_image_protocol_ghostty_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Ghostty, false),
ImageProtocol::KittyGraphics
);
assert_eq!(
select_image_protocol(TerminalIdentity::Ghostty, true),
ImageProtocol::KittyGraphics
);
}
#[test]
fn select_image_protocol_wezterm_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::WezTerm, false),
ImageProtocol::ItermInline
);
assert_eq!(
select_image_protocol(TerminalIdentity::WezTerm, true),
ImageProtocol::ItermInline
);
}
#[test]
fn select_image_protocol_iterm2_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::ITerm2, false),
ImageProtocol::ItermInline
);
assert_eq!(
select_image_protocol(TerminalIdentity::ITerm2, true),
ImageProtocol::ItermInline
);
}
#[test]
fn detect_terminal_identity_recognizes_konsole_dbus_session() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("KONSOLE_DBUS_SESSION", "/Sessions/7");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Konsole);
}
#[test]
fn detect_terminal_identity_recognizes_konsole_dbus_service() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("KONSOLE_DBUS_SERVICE", "org.kde.konsole-12345");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Konsole);
}
#[test]
fn konsole_dbus_takes_precedence_over_inherited_kitty_window_id() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("KITTY_WINDOW_ID", "1");
env::set_var("KONSOLE_DBUS_SESSION", "/Sessions/9");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Konsole);
}
#[test]
fn select_image_protocol_konsole_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Konsole, false),
ImageProtocol::KittyDirectGraphics
);
assert_eq!(
select_image_protocol(TerminalIdentity::Konsole, true),
ImageProtocol::KittyDirectGraphics
);
}
#[test]
fn wezterm_takes_precedence_over_inherited_konsole_markers() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("TERM_PROGRAM", "WezTerm");
env::set_var("KONSOLE_DBUS_SESSION", "/Sessions/9");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::WezTerm);
}
#[test]
fn select_image_protocol_warp_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Warp, false),
ImageProtocol::KittyDirectGraphics
);
assert_eq!(
select_image_protocol(TerminalIdentity::Warp, true),
ImageProtocol::KittyDirectGraphics
);
}
#[test]
fn warp_markers_take_precedence_over_inherited_kitty_window_id() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("KITTY_WINDOW_ID", "1");
env::set_var("WARP_SESSION_ID", "123");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Warp);
}
#[test]
fn select_image_protocol_alacritty_disabled_and_other_override_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Alacritty, true),
ImageProtocol::None
);
assert_eq!(
select_image_protocol(TerminalIdentity::Other, false),
ImageProtocol::None
);
assert_eq!(
select_image_protocol(TerminalIdentity::Other, true),
ImageProtocol::KittyGraphics
);
}
#[test]
fn detect_terminal_identity_recognizes_foot_term() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("TERM", "foot");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Foot);
}
#[test]
fn detect_terminal_identity_recognizes_foot_extra_term() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("TERM", "foot-extra");
}
assert_eq!(detect_terminal_identity(), TerminalIdentity::Foot);
}
#[test]
fn select_image_protocol_foot_uses_sixel() {
assert_eq!(
select_image_protocol(TerminalIdentity::Foot, false),
ImageProtocol::Sixel
);
assert_eq!(
select_image_protocol(TerminalIdentity::Foot, true),
ImageProtocol::Sixel
);
}
#[test]
fn detect_terminal_identity_recognizes_windows_terminal_wt_session() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("WT_SESSION", "00000000-0000-0000-0000-000000000001");
}
assert_eq!(
detect_terminal_identity(),
TerminalIdentity::WindowsTerminal
);
}
#[test]
fn select_image_protocol_windows_terminal_uses_sixel() {
assert_eq!(
select_image_protocol(TerminalIdentity::WindowsTerminal, false),
ImageProtocol::Sixel
);
assert_eq!(
select_image_protocol(TerminalIdentity::WindowsTerminal, true),
ImageProtocol::Sixel
);
}
#[test]
fn windows_terminal_takes_precedence_over_other_fallback() {
let _lock = terminal_env_lock();
let _guard = TerminalEnvGuard::isolate();
unsafe {
env::set_var("WT_SESSION", "some-guid");
}
assert_eq!(
detect_terminal_identity(),
TerminalIdentity::WindowsTerminal
);
}
#[cfg(unix)]
#[test]
fn command_exists_checks_direct_executable_paths_without_shelling_out() {
use std::os::unix::fs::PermissionsExt;
let root = temp_root("command-exists-direct-path");
fs::create_dir_all(&root).expect("failed to create temp root");
let executable = root.join("demo-tool");
fs::write(&executable, b"#!/bin/sh\nexit 0\n").expect("failed to write test executable");
let mut permissions = fs::metadata(&executable)
.expect("test executable metadata should exist")
.permissions();
permissions.set_mode(0o755);
fs::set_permissions(&executable, permissions).expect("failed to mark test executable");
assert!(command_exists(
executable.to_str().expect("path should be valid utf-8")
));
let not_executable = root.join("demo-data");
fs::write(¬_executable, b"plain data").expect("failed to write plain file");
assert!(!command_exists(
not_executable.to_str().expect("path should be valid utf-8")
));
}
fn no_tmux_client_term() -> Option<String> {
None
}
fn no_tmux_env_lookup(_: &str) -> Option<String> {
None
}
fn tmux_set_only(env: &str) -> impl Fn(&str) -> Option<String> + '_ {
move |name: &str| {
if name == "TMUX" {
Some(env.to_string())
} else {
None
}
}
}
#[test]
fn detect_terminal_identity_inside_tmux_uses_client_termname_for_kitty() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
|| Some("xterm-kitty".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Kitty);
}
#[test]
fn detect_terminal_identity_inside_tmux_uses_client_termname_for_ghostty() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
|| Some("xterm-ghostty".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Ghostty);
}
#[test]
fn detect_terminal_identity_inside_tmux_uses_client_termname_for_foot() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
|| Some("foot".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Foot);
}
#[test]
fn detect_terminal_identity_inside_tmux_uses_client_termname_for_foot_extra() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
|| Some("foot-extra".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Foot);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_kitty_window_id_presence() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "KITTY_WINDOW_ID" {
Some(String::new())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Kitty);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_term_program_ghostty() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "TERM_PROGRAM" {
Some("ghostty".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Ghostty);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_term_program_wezterm() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "TERM_PROGRAM" {
Some("WezTerm".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::WezTerm);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_term_program_iterm2() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "TERM_PROGRAM" {
Some("iTerm.app".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::ITerm2);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_warp() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "WARP_SESSION_ID" {
Some("123".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Warp);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_konsole() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "KONSOLE_DBUS_SESSION" {
Some("/Sessions/3".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Konsole);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_foot_term() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "TERM" {
Some("foot".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Foot);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_live_client_wt_session() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
|name| {
if name == "WT_SESSION" {
Some("live-wt".to_string())
} else {
None
}
},
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::WindowsTerminal);
}
#[test]
fn detect_terminal_identity_inside_tmux_falls_back_to_session_env_wt_session() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
no_tmux_env_lookup,
|name| {
if name == "WT_SESSION" {
Some("session-wt".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::WindowsTerminal);
}
#[test]
fn detect_terminal_identity_inside_tmux_uses_live_client_env_before_session_env() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
no_tmux_client_term,
|name| {
if name == "WARP_SESSION_ID" {
Some("live-client".to_string())
} else {
None
}
},
|name| {
if name == "KITTY_WINDOW_ID" {
Some("stale-session".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Warp);
}
#[test]
fn detect_terminal_identity_inside_tmux_returns_other_for_generic_client_termname() {
let id = detect_terminal_identity_with(
tmux_set_only("/tmp/tmux-1000/default,123,4"),
|| Some("xterm-256color".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Other);
}
#[test]
fn detect_terminal_identity_outside_tmux_skips_tmux_helpers() {
use std::cell::Cell;
let client_calls = Cell::new(0u32);
let client_env_calls = Cell::new(0u32);
let env_calls = Cell::new(0u32);
let id = detect_terminal_identity_with(
|_| None,
|| {
client_calls.set(client_calls.get() + 1);
Some("xterm-kitty".to_string())
},
|_| {
client_env_calls.set(client_env_calls.get() + 1);
Some(String::new())
},
|_| {
env_calls.set(env_calls.get() + 1);
Some(String::new())
},
);
assert_eq!(id, TerminalIdentity::Other);
assert_eq!(client_calls.get(), 0);
assert_eq!(client_env_calls.get(), 0);
assert_eq!(env_calls.get(), 0);
}
#[test]
fn detect_terminal_identity_non_kitty_direct_env_takes_precedence_over_tmux_lookups() {
use std::cell::Cell;
let client_calls = Cell::new(0u32);
let client_env_calls = Cell::new(0u32);
let env_calls = Cell::new(0u32);
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM_PROGRAM" => Some("ghostty".to_string()),
_ => None,
},
|| {
client_calls.set(client_calls.get() + 1);
Some("xterm-kitty".to_string())
},
|_| {
client_env_calls.set(client_env_calls.get() + 1);
None
},
|_| {
env_calls.set(env_calls.get() + 1);
None
},
);
assert_eq!(id, TerminalIdentity::Ghostty);
assert_eq!(
client_calls.get(),
0,
"tmux helpers should not run when non-Kitty direct env detection succeeds"
);
assert_eq!(client_env_calls.get(), 0);
assert_eq!(env_calls.get(), 0);
}
#[test]
fn detect_terminal_identity_recovers_warp_from_tmux_when_stale_kitty_marker_leaks() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
no_tmux_env_lookup,
|name| {
if name == "WARP_SESSION_ID" {
Some("123".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Warp);
}
#[test]
fn detect_terminal_identity_recovers_warp_from_live_client_env_when_stale_kitty_marker_leaks() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
|name| {
if name == "WARP_SESSION_ID" {
Some("live-client".to_string())
} else {
None
}
},
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Warp);
}
#[test]
fn detect_terminal_identity_recovers_konsole_from_tmux_when_stale_kitty_marker_leaks() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
no_tmux_env_lookup,
|name| {
if name == "KONSOLE_DBUS_SESSION" {
Some("/Sessions/3".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Konsole);
}
#[test]
fn detect_terminal_identity_recovers_konsole_from_live_client_env_when_stale_kitty_marker_leaks()
{
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
|name| {
if name == "KONSOLE_DBUS_SESSION" {
Some("/Sessions/live".to_string())
} else {
None
}
},
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Konsole);
}
#[test]
fn detect_terminal_identity_recovers_foot_from_tmux_when_stale_kitty_marker_leaks() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("foot".to_string()),
no_tmux_env_lookup,
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::Foot);
}
#[test]
fn detect_terminal_identity_recovers_wt_from_live_client_env_when_stale_kitty_marker_leaks() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
|name| {
if name == "WT_SESSION" {
Some("live-wt".to_string())
} else {
None
}
},
no_tmux_env_lookup,
);
assert_eq!(id, TerminalIdentity::WindowsTerminal);
}
#[test]
fn detect_terminal_identity_keeps_kitty_when_tmux_client_reports_kitty() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-kitty".to_string()),
no_tmux_env_lookup,
|name| {
if name == "WARP_SESSION_ID" {
Some("stale".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Kitty);
}
#[test]
fn detect_terminal_identity_keeps_live_client_kitty_over_stale_session_warp() {
let id = detect_terminal_identity_with(
|name| match name {
"TMUX" => Some("/tmp/tmux-1000/default,123,4".to_string()),
"TERM" => Some("tmux-256color".to_string()),
"TERM_PROGRAM" => Some("tmux".to_string()),
"KITTY_WINDOW_ID" => Some("1".to_string()),
_ => None,
},
|| Some("xterm-256color".to_string()),
|name| {
if name == "KITTY_WINDOW_ID" {
Some("live-client".to_string())
} else {
None
}
},
|name| {
if name == "WARP_SESSION_ID" {
Some("stale-session".to_string())
} else {
None
}
},
);
assert_eq!(id, TerminalIdentity::Kitty);
}
#[test]
fn parse_show_environment_line_handles_set_unset_and_empty() {
assert_eq!(
parse_show_environment_line("KITTY_WINDOW_ID=42\n", "KITTY_WINDOW_ID"),
Some("42".to_string())
);
assert_eq!(
parse_show_environment_line("KITTY_WINDOW_ID=\n", "KITTY_WINDOW_ID"),
Some(String::new())
);
assert_eq!(
parse_show_environment_line("-KITTY_WINDOW_ID\n", "KITTY_WINDOW_ID"),
None
);
assert_eq!(parse_show_environment_line("", "KITTY_WINDOW_ID"), None);
assert_eq!(
parse_show_environment_line("OTHER=value\nKITTY_WINDOW_ID=7\n", "KITTY_WINDOW_ID"),
Some("7".to_string())
);
}
#[test]
fn parse_proc_environ_handles_present_empty_and_missing_values() {
let environ = b"TERM=xterm-256color\0WARP_SESSION_ID=123\0KITTY_WINDOW_ID=\0";
assert_eq!(
parse_proc_environ(environ, "WARP_SESSION_ID"),
Some("123".to_string())
);
assert_eq!(
parse_proc_environ(environ, "KITTY_WINDOW_ID"),
Some(String::new())
);
assert_eq!(parse_proc_environ(environ, "KONSOLE_DBUS_SESSION"), None);
}
}