use super::{ImageProtocol, TerminalIdentity};
use std::{env, fs, path::Path};
pub(super) fn pdf_preview_tools_available() -> bool {
command_exists("pdfinfo") && command_exists("pdftocairo")
}
pub(in crate::app) fn detect_terminal_identity() -> TerminalIdentity {
let term = env::var("TERM").unwrap_or_default().to_ascii_lowercase();
let term_program = env::var("TERM_PROGRAM")
.unwrap_or_default()
.to_ascii_lowercase();
let kitty_window_id = env::var_os("KITTY_WINDOW_ID").is_some();
if kitty_window_id || 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::var_os("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::var_os("ALACRITTY_SOCKET").is_some()
{
TerminalIdentity::Alacritty
} else if term == "foot" || term == "foot-extra" {
TerminalIdentity::Foot
} else if env::var_os("WT_SESSION").is_some() {
TerminalIdentity::WindowsTerminal
} else {
TerminalIdentity::Other
}
}
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::KittyGraphics,
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,
fs,
path::PathBuf,
sync::{Mutex, OnceLock},
time::{SystemTime, UNIX_EPOCH},
};
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",
];
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 select_image_protocol_warp_always_enabled() {
assert_eq!(
select_image_protocol(TerminalIdentity::Warp, false),
ImageProtocol::KittyGraphics
);
assert_eq!(
select_image_protocol(TerminalIdentity::Warp, true),
ImageProtocol::KittyGraphics
);
}
#[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")
));
}
}