#![forbid(unsafe_code)]
use std::env;
use std::ffi::OsString;
use std::str::FromStr;
fn normalize_terminal_env_value(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
fn is_dumb_terminal(term: &str, windows_terminal: bool) -> bool {
term == "dumb" || (term.is_empty() && !windows_terminal)
}
fn colorterm_declares_true_color(colorterm: &str) -> bool {
matches!(colorterm, "truecolor" | "24bit")
}
fn term_declares_true_color(term: &str) -> bool {
let direct_color = term.rsplit_once("-direct").is_some_and(|(base, suffix)| {
!base.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit())
});
direct_color || term.ends_with("-truecolor") || term == "24bit" || term.ends_with("-24bit")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[repr(u8)]
pub enum ColorDepth {
#[default]
Mono,
Ansi16,
Ansi256,
TrueColor,
}
impl ColorDepth {
#[must_use]
pub fn detect() -> Self {
TerminalCapabilities::detect().color_depth
}
#[must_use]
pub fn detect_from_env(
no_color: Option<&str>,
colorterm: Option<&str>,
term: Option<&str>,
) -> Self {
let term = normalize_terminal_env_value(term.unwrap_or_default());
let colorterm = normalize_terminal_env_value(colorterm.unwrap_or_default());
Self::detect_normalized(
no_color.is_some(),
is_dumb_terminal(&term, false),
&colorterm,
&term,
false,
)
}
fn detect_normalized(
no_color: bool,
is_dumb: bool,
colorterm: &str,
term: &str,
inferred_true_color: bool,
) -> Self {
if no_color || is_dumb || term == "vt100" {
Self::Mono
} else if term == "linux" {
Self::Ansi16
} else if colorterm_declares_true_color(colorterm)
|| term_declares_true_color(term)
|| inferred_true_color
{
Self::TrueColor
} else if term.contains("256color") || term.contains("256") {
Self::Ansi256
} else {
Self::Ansi16
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Mono => "mono",
Self::Ansi16 => "ansi16",
Self::Ansi256 => "ansi256",
Self::TrueColor => "truecolor",
}
}
#[must_use]
pub const fn supports_color(self) -> bool {
!matches!(self, Self::Mono)
}
#[must_use]
pub const fn supports_256_colors(self) -> bool {
matches!(self, Self::Ansi256 | Self::TrueColor)
}
#[must_use]
pub const fn supports_true_color(self) -> bool {
matches!(self, Self::TrueColor)
}
}
impl std::fmt::Display for ColorDepth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
struct DetectInputs {
no_color: bool,
term: String,
term_program: String,
lc_terminal: String,
colorterm: String,
in_tmux: bool,
in_screen: bool,
in_zellij: bool,
wezterm_unix_socket: bool,
wezterm_pane: bool,
wezterm_executable: bool,
kitty_window_id: bool,
wt_session: bool,
}
impl DetectInputs {
fn from_env() -> Self {
Self::from_env_with(|key| env::var_os(key))
}
fn from_env_with<F>(get_env: F) -> Self
where
F: Fn(&str) -> Option<OsString>,
{
let text = |key| {
get_env(key)
.and_then(|value| value.into_string().ok())
.unwrap_or_default()
};
let utf8_present = |key| get_env(key).is_some_and(|value| value.into_string().is_ok());
Self {
no_color: get_env("NO_COLOR").is_some(),
term: text("TERM"),
term_program: text("TERM_PROGRAM"),
lc_terminal: text("LC_TERMINAL"),
colorterm: text("COLORTERM"),
in_tmux: utf8_present("TMUX"),
in_screen: utf8_present("STY"),
in_zellij: utf8_present("ZELLIJ"),
wezterm_unix_socket: utf8_present("WEZTERM_UNIX_SOCKET"),
wezterm_pane: utf8_present("WEZTERM_PANE"),
wezterm_executable: utf8_present("WEZTERM_EXECUTABLE"),
kitty_window_id: utf8_present("KITTY_WINDOW_ID"),
wt_session: utf8_present("WT_SESSION"),
}
}
}
const MODERN_TERMINALS: &[&str] = &[
"iTerm.app",
"WezTerm",
"Alacritty",
"Ghostty",
"kitty",
"Rio",
"Hyper",
"Contour",
"vscode",
"Black Box",
];
const KITTY_KEYBOARD_TERMINALS: &[&str] = &[
"iTerm.app",
"WezTerm",
"Alacritty",
"Ghostty",
"Rio",
"kitty",
"foot",
"Black Box",
];
const SYNC_OUTPUT_TERMINALS: &[&str] = &["Alacritty", "Ghostty", "kitty", "Contour"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TerminalProfile {
Modern,
Xterm256Color,
Xterm,
Vt100,
Dumb,
Screen,
Tmux,
Zellij,
WindowsConsole,
Kitty,
LinuxConsole,
Custom,
Detected,
}
impl TerminalProfile {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Modern => "modern",
Self::Xterm256Color => "xterm-256color",
Self::Xterm => "xterm",
Self::Vt100 => "vt100",
Self::Dumb => "dumb",
Self::Screen => "screen",
Self::Tmux => "tmux",
Self::Zellij => "zellij",
Self::WindowsConsole => "windows-console",
Self::Kitty => "kitty",
Self::LinuxConsole => "linux",
Self::Custom => "custom",
Self::Detected => "detected",
}
}
#[must_use]
pub const fn all_predefined() -> &'static [Self] {
&[
Self::Modern,
Self::Xterm256Color,
Self::Xterm,
Self::Vt100,
Self::Dumb,
Self::Screen,
Self::Tmux,
Self::Zellij,
Self::WindowsConsole,
Self::Kitty,
Self::LinuxConsole,
]
}
}
impl std::str::FromStr for TerminalProfile {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"modern" => Ok(Self::Modern),
"xterm-256color" | "xterm256color" | "xterm-256" => Ok(Self::Xterm256Color),
"xterm" => Ok(Self::Xterm),
"vt100" => Ok(Self::Vt100),
"dumb" => Ok(Self::Dumb),
"screen" | "screen-256color" => Ok(Self::Screen),
"tmux" | "tmux-256color" => Ok(Self::Tmux),
"zellij" => Ok(Self::Zellij),
"windows-console" | "windows" | "conhost" => Ok(Self::WindowsConsole),
"kitty" | "xterm-kitty" => Ok(Self::Kitty),
"linux" | "linux-console" => Ok(Self::LinuxConsole),
"custom" => Ok(Self::Custom),
"detected" | "auto" => Ok(Self::Detected),
_ => Err(()),
}
}
}
impl std::fmt::Display for TerminalProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalCapabilities {
profile: TerminalProfile,
pub color_depth: ColorDepth,
pub unicode_box_drawing: bool,
pub unicode_emoji: bool,
pub double_width: bool,
pub sync_output: bool,
pub osc8_hyperlinks: bool,
pub scroll_region: bool,
pub in_tmux: bool,
pub in_screen: bool,
pub in_zellij: bool,
pub in_wezterm_mux: bool,
pub kitty_keyboard: bool,
pub focus_events: bool,
pub bracketed_paste: bool,
pub mouse_sgr: bool,
pub osc52_clipboard: bool,
}
impl Default for TerminalCapabilities {
fn default() -> Self {
Self::basic()
}
}
impl TerminalCapabilities {
#[must_use]
pub const fn profile(&self) -> TerminalProfile {
self.profile
}
#[must_use]
pub fn profile_name(&self) -> Option<&'static str> {
match self.profile {
TerminalProfile::Detected => None,
p => Some(p.as_str()),
}
}
#[must_use]
pub fn from_profile(profile: TerminalProfile) -> Self {
match profile {
TerminalProfile::Modern => Self::modern(),
TerminalProfile::Xterm256Color => Self::xterm_256color(),
TerminalProfile::Xterm => Self::xterm(),
TerminalProfile::Vt100 => Self::vt100(),
TerminalProfile::Dumb => Self::dumb(),
TerminalProfile::Screen => Self::screen(),
TerminalProfile::Tmux => Self::tmux(),
TerminalProfile::Zellij => Self::zellij(),
TerminalProfile::WindowsConsole => Self::windows_console(),
TerminalProfile::Kitty => Self::kitty(),
TerminalProfile::LinuxConsole => Self::linux_console(),
TerminalProfile::Custom => {
let mut caps = Self::basic();
caps.profile = TerminalProfile::Custom;
caps
}
TerminalProfile::Detected => Self::detect(),
}
}
#[must_use]
pub const fn modern() -> Self {
Self {
profile: TerminalProfile::Modern,
color_depth: ColorDepth::TrueColor,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: true,
osc8_hyperlinks: true,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: true,
focus_events: true,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: true,
}
}
#[must_use]
pub const fn xterm_256color() -> Self {
Self {
profile: TerminalProfile::Xterm256Color,
color_depth: ColorDepth::Ansi256,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn xterm() -> Self {
Self {
profile: TerminalProfile::Xterm,
color_depth: ColorDepth::Ansi16,
unicode_box_drawing: true,
unicode_emoji: false,
double_width: true,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn vt100() -> Self {
Self {
profile: TerminalProfile::Vt100,
color_depth: ColorDepth::Mono,
unicode_box_drawing: false,
unicode_emoji: false,
double_width: false,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: false,
mouse_sgr: false,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn dumb() -> Self {
Self {
profile: TerminalProfile::Dumb,
color_depth: ColorDepth::Mono,
unicode_box_drawing: false,
unicode_emoji: false,
double_width: false,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: false,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: false,
mouse_sgr: false,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn screen() -> Self {
Self {
profile: TerminalProfile::Screen,
color_depth: ColorDepth::Ansi256,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: true,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn tmux() -> Self {
Self {
profile: TerminalProfile::Tmux,
color_depth: ColorDepth::Ansi256,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: true,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn zellij() -> Self {
Self {
profile: TerminalProfile::Zellij,
color_depth: ColorDepth::TrueColor,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: true,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: true,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
#[must_use]
pub const fn windows_console() -> Self {
Self {
profile: TerminalProfile::WindowsConsole,
color_depth: ColorDepth::TrueColor,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: false,
osc8_hyperlinks: true,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: true,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: true,
}
}
#[must_use]
pub const fn kitty() -> Self {
Self {
profile: TerminalProfile::Kitty,
color_depth: ColorDepth::TrueColor,
unicode_box_drawing: true,
unicode_emoji: true,
double_width: true,
sync_output: true,
osc8_hyperlinks: true,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: true,
focus_events: true,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: true,
}
}
#[must_use]
pub const fn linux_console() -> Self {
Self {
profile: TerminalProfile::LinuxConsole,
color_depth: ColorDepth::Ansi16,
unicode_box_drawing: true,
unicode_emoji: false,
double_width: false,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: true,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: true,
mouse_sgr: true,
osc52_clipboard: false,
}
}
pub fn builder() -> CapabilityProfileBuilder {
CapabilityProfileBuilder::new()
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct CapabilityProfileBuilder {
caps: TerminalCapabilities,
}
impl Default for CapabilityProfileBuilder {
fn default() -> Self {
Self::new()
}
}
impl CapabilityProfileBuilder {
pub fn new() -> Self {
Self {
caps: TerminalCapabilities {
profile: TerminalProfile::Custom,
color_depth: ColorDepth::Mono,
unicode_box_drawing: false,
unicode_emoji: false,
double_width: false,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: false,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: false,
mouse_sgr: false,
osc52_clipboard: false,
},
}
}
pub fn from_profile(profile: TerminalProfile) -> Self {
let mut caps = TerminalCapabilities::from_profile(profile);
caps.profile = TerminalProfile::Custom;
Self { caps }
}
#[must_use]
pub fn build(self) -> TerminalCapabilities {
self.caps
}
pub const fn color_depth(mut self, depth: ColorDepth) -> Self {
self.caps.color_depth = depth;
self
}
pub const fn sync_output(mut self, enabled: bool) -> Self {
self.caps.sync_output = enabled;
self
}
pub const fn osc8_hyperlinks(mut self, enabled: bool) -> Self {
self.caps.osc8_hyperlinks = enabled;
self
}
pub const fn scroll_region(mut self, enabled: bool) -> Self {
self.caps.scroll_region = enabled;
self
}
pub const fn in_tmux(mut self, enabled: bool) -> Self {
self.caps.in_tmux = enabled;
self
}
pub const fn in_screen(mut self, enabled: bool) -> Self {
self.caps.in_screen = enabled;
self
}
pub const fn in_zellij(mut self, enabled: bool) -> Self {
self.caps.in_zellij = enabled;
self
}
pub const fn in_wezterm_mux(mut self, enabled: bool) -> Self {
self.caps.in_wezterm_mux = enabled;
self
}
pub const fn kitty_keyboard(mut self, enabled: bool) -> Self {
self.caps.kitty_keyboard = enabled;
self
}
pub const fn focus_events(mut self, enabled: bool) -> Self {
self.caps.focus_events = enabled;
self
}
pub const fn bracketed_paste(mut self, enabled: bool) -> Self {
self.caps.bracketed_paste = enabled;
self
}
pub const fn mouse_sgr(mut self, enabled: bool) -> Self {
self.caps.mouse_sgr = enabled;
self
}
pub const fn osc52_clipboard(mut self, enabled: bool) -> Self {
self.caps.osc52_clipboard = enabled;
self
}
}
impl TerminalCapabilities {
#[must_use]
pub fn detect() -> Self {
let value = env::var("FTUI_TEST_PROFILE").ok();
Self::detect_with_test_profile_override(value.as_deref())
}
fn detect_with_test_profile_override(value: Option<&str>) -> Self {
if let Some(value) = value
&& let Ok(profile) = TerminalProfile::from_str(value.trim())
&& profile != TerminalProfile::Detected
{
return Self::from_profile(profile);
}
let env = DetectInputs::from_env();
Self::detect_from_inputs(&env)
}
fn detect_from_inputs(env: &DetectInputs) -> Self {
let term = normalize_terminal_env_value(&env.term);
let term_program = {
let program = normalize_terminal_env_value(&env.term_program);
if program.is_empty()
&& normalize_terminal_env_value(&env.lc_terminal).contains("iterm")
{
"iterm.app".to_string()
} else {
program
}
};
let colorterm = normalize_terminal_env_value(&env.colorterm);
let in_tmux = env.in_tmux || term.starts_with("tmux");
let in_screen = env.in_screen || term.starts_with("screen");
let in_zellij = env.in_zellij;
let term_program_is_wezterm = term_program.contains("wezterm");
let term_is_wezterm = term.contains("wezterm");
let in_wezterm_mux = term_program_is_wezterm
|| term_is_wezterm
|| env.wezterm_unix_socket
|| env.wezterm_pane
|| env.wezterm_executable;
let in_any_mux = in_tmux || in_screen || in_zellij || in_wezterm_mux;
let is_windows_terminal = env.wt_session;
let is_dumb = is_dumb_terminal(&term, is_windows_terminal);
let is_kitty = env.kitty_window_id || term.contains("kitty");
let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
let t_lower = t.to_ascii_lowercase();
term_program.contains(&t_lower) || term.contains(&t_lower)
}) || is_windows_terminal;
let color_depth = ColorDepth::detect_normalized(
env.no_color,
is_dumb,
&colorterm,
&term,
is_modern_terminal || is_kitty,
);
let is_wezterm = term_program_is_wezterm || term_is_wezterm || env.wezterm_executable;
let sync_output = !is_dumb
&& !is_wezterm
&& (is_kitty
|| SYNC_OUTPUT_TERMINALS.iter().any(|t| {
let t_lower = t.to_ascii_lowercase();
term_program.contains(&t_lower) || term.contains(&t_lower)
}));
let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
let scroll_region = !is_dumb;
let kitty_keyboard = !is_dumb
&& (is_kitty
|| KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
let t_lower = t.to_ascii_lowercase();
term_program.contains(&t_lower) || term.contains(&t_lower)
}));
let focus_events = !is_dumb && (is_modern_terminal || is_kitty);
let bracketed_paste = !is_dumb;
let mouse_sgr = !is_dumb;
let osc52_clipboard = !is_dumb && !in_any_mux && (is_modern_terminal || is_kitty);
let unicode_box_drawing = !is_dumb;
let unicode_emoji = !is_dumb && (is_modern_terminal || is_kitty);
let double_width = !is_dumb && term != "linux";
Self {
profile: TerminalProfile::Detected,
color_depth,
unicode_box_drawing,
unicode_emoji,
double_width,
sync_output,
osc8_hyperlinks,
scroll_region,
in_tmux,
in_screen,
in_zellij,
in_wezterm_mux,
kitty_keyboard,
focus_events,
bracketed_paste,
mouse_sgr,
osc52_clipboard,
}
}
#[must_use]
pub const fn basic() -> Self {
Self {
profile: TerminalProfile::Dumb,
color_depth: ColorDepth::Mono,
unicode_box_drawing: false,
unicode_emoji: false,
double_width: false,
sync_output: false,
osc8_hyperlinks: false,
scroll_region: false,
in_tmux: false,
in_screen: false,
in_zellij: false,
in_wezterm_mux: false,
kitty_keyboard: false,
focus_events: false,
bracketed_paste: false,
mouse_sgr: false,
osc52_clipboard: false,
}
}
#[must_use]
#[inline]
pub const fn in_any_mux(&self) -> bool {
self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux
}
#[must_use]
#[inline]
pub const fn has_color(&self) -> bool {
self.color_depth.supports_color()
}
#[must_use]
pub const fn supports_256_colors(&self) -> bool {
self.color_depth.supports_256_colors()
}
#[must_use]
pub const fn supports_true_color(&self) -> bool {
self.color_depth.supports_true_color()
}
#[must_use]
#[inline]
pub const fn use_sync_output(&self) -> bool {
if self.in_tmux || self.in_screen || self.in_zellij || self.in_wezterm_mux {
return false;
}
self.sync_output
}
#[must_use]
#[inline]
pub const fn use_scroll_region(&self) -> bool {
if self.in_any_mux() {
return false;
}
self.scroll_region
}
#[must_use]
#[inline]
pub const fn use_hyperlinks(&self) -> bool {
if self.in_any_mux() {
return false;
}
self.osc8_hyperlinks
}
#[must_use]
#[inline]
pub const fn use_clipboard(&self) -> bool {
if self.in_any_mux() {
return false;
}
self.osc52_clipboard
}
#[must_use]
#[inline]
pub const fn needs_passthrough_wrap(&self) -> bool {
self.in_tmux || self.in_screen
}
}
pub struct SharedCapabilities {
inner: crate::read_optimized::ArcSwapStore<TerminalCapabilities>,
}
impl SharedCapabilities {
pub fn new(caps: TerminalCapabilities) -> Self {
Self {
inner: crate::read_optimized::ArcSwapStore::new(caps),
}
}
pub fn detect() -> Self {
Self::new(TerminalCapabilities::detect())
}
#[inline]
pub fn load(&self) -> TerminalCapabilities {
crate::read_optimized::ReadOptimized::load(&self.inner)
}
#[inline]
pub fn store(&self, caps: TerminalCapabilities) {
crate::read_optimized::ReadOptimized::store(&self.inner, caps);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn detect_with_override(value: Option<&str>) -> TerminalCapabilities {
TerminalCapabilities::detect_with_test_profile_override(value)
}
#[test]
fn basic_is_minimal() {
let caps = TerminalCapabilities::basic();
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.sync_output);
assert!(!caps.osc8_hyperlinks);
assert!(!caps.scroll_region);
assert!(!caps.in_tmux);
assert!(!caps.in_screen);
assert!(!caps.in_zellij);
assert!(!caps.kitty_keyboard);
assert!(!caps.focus_events);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
assert!(!caps.osc52_clipboard);
}
#[test]
fn basic_is_default() {
let basic = TerminalCapabilities::basic();
let default = TerminalCapabilities::default();
assert_eq!(basic, default);
}
#[test]
fn in_any_mux_logic() {
let mut caps = TerminalCapabilities::basic();
assert!(!caps.in_any_mux());
caps.in_tmux = true;
assert!(caps.in_any_mux());
caps.in_tmux = false;
caps.in_screen = true;
assert!(caps.in_any_mux());
caps.in_screen = false;
caps.in_zellij = true;
assert!(caps.in_any_mux());
caps.in_zellij = false;
caps.in_wezterm_mux = true;
assert!(caps.in_any_mux());
}
#[test]
fn has_color_logic() {
let mut caps = TerminalCapabilities::basic();
assert!(!caps.has_color());
caps.color_depth = ColorDepth::Ansi16;
assert!(caps.has_color());
caps.color_depth = ColorDepth::TrueColor;
assert!(caps.has_color());
}
#[test]
fn color_depth_identifiers() {
let mut caps = TerminalCapabilities::basic();
assert_eq!(caps.color_depth.as_str(), "mono");
caps.color_depth = ColorDepth::Ansi16;
assert_eq!(caps.color_depth.as_str(), "ansi16");
caps.color_depth = ColorDepth::Ansi256;
assert_eq!(caps.color_depth.as_str(), "ansi256");
caps.color_depth = ColorDepth::TrueColor;
assert_eq!(caps.color_depth.as_str(), "truecolor");
}
#[test]
fn explicit_color_depth_detection_contract() {
let cases = [
(Some(""), Some("truecolor"), Some("xterm"), ColorDepth::Mono),
(None, Some("truecolor"), Some("dumb"), ColorDepth::Mono),
(None, Some("truecolor"), Some(" DUMB "), ColorDepth::Mono),
(None, Some("truecolor"), Some("vt100"), ColorDepth::Mono),
(None, None, Some("xterm"), ColorDepth::Ansi16),
(None, None, Some("xterm-256color"), ColorDepth::Ansi256),
(None, Some("24bit"), Some("xterm"), ColorDepth::TrueColor),
(None, None, Some(" XTERM-DIRECT "), ColorDepth::TrueColor),
(None, None, Some("xterm-direct2"), ColorDepth::TrueColor),
(None, None, Some("xterm-direct16"), ColorDepth::TrueColor),
(None, None, Some("xterm-direct256"), ColorDepth::TrueColor),
(None, None, Some("xterm-truecolor"), ColorDepth::TrueColor),
(None, None, Some("xterm-24bit"), ColorDepth::TrueColor),
(None, None, Some("xterm-no24bit"), ColorDepth::Ansi16),
(
None,
Some("nottruecolor"),
Some("xterm"),
ColorDepth::Ansi16,
),
(None, Some("no24bit"), Some("xterm"), ColorDepth::Ansi16),
(None, Some("truecolor"), Some("linux"), ColorDepth::Ansi16),
];
for (no_color, colorterm, term, expected) in cases {
assert_eq!(
ColorDepth::detect_from_env(no_color, colorterm, term),
expected,
"NO_COLOR={no_color:?} COLORTERM={colorterm:?} TERM={term:?}"
);
}
}
#[test]
fn detect_does_not_panic() {
let _caps = TerminalCapabilities::detect();
}
#[test]
fn windows_terminal_not_dumb_when_term_missing() {
let env = DetectInputs {
no_color: false,
term: String::new(),
term_program: String::new(),
lc_terminal: String::new(),
colorterm: String::new(),
in_tmux: false,
in_screen: false,
in_zellij: false,
wezterm_unix_socket: false,
wezterm_pane: false,
wezterm_executable: false,
kitty_window_id: false,
wt_session: true,
};
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(
caps.osc8_hyperlinks,
"WT_SESSION implies OSC 8 hyperlink support by default"
);
assert!(
caps.bracketed_paste,
"WT_SESSION should not be treated as dumb"
);
assert!(caps.mouse_sgr, "WT_SESSION should not be treated as dumb");
}
#[test]
#[cfg(target_os = "windows")]
fn detect_windows_terminal_from_wt_session() {
let mut env = make_env("", "", "");
env.wt_session = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks, "WT_SESSION implies OSC 8 support");
}
#[test]
fn no_color_disables_color_and_links() {
let env = DetectInputs {
no_color: true,
term: "xterm-256color".to_string(),
term_program: "WezTerm".to_string(),
lc_terminal: String::new(),
colorterm: "truecolor".to_string(),
in_tmux: false,
in_screen: false,
in_zellij: false,
wezterm_unix_socket: false,
wezterm_pane: false,
wezterm_executable: false,
kitty_window_id: false,
wt_session: false,
};
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(
!caps.osc8_hyperlinks,
"NO_COLOR must disable OSC 8 hyperlinks"
);
}
#[cfg(unix)]
#[test]
fn non_utf8_no_color_presence_disables_color_without_global_env_mutation() {
use std::os::unix::ffi::OsStringExt as _;
let non_utf8 = OsString::from_vec(vec![0xff]);
let env = DetectInputs::from_env_with(|key| match key {
"NO_COLOR" => Some(non_utf8.clone()),
"TERM" => Some(OsString::from("xterm-direct")),
"TERM_PROGRAM" => Some(OsString::from("WezTerm")),
"COLORTERM" => Some(OsString::from("truecolor")),
_ => None,
});
assert!(env.no_color, "NO_COLOR is a presence-only contract");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.osc8_hyperlinks);
}
#[test]
fn use_sync_output_disabled_in_tmux() {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
assert!(caps.use_sync_output());
caps.in_tmux = true;
assert!(!caps.use_sync_output());
}
#[test]
fn use_sync_output_disabled_in_screen() {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_screen = true;
assert!(!caps.use_sync_output());
}
#[test]
fn use_sync_output_disabled_in_zellij() {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_zellij = true;
assert!(!caps.use_sync_output());
}
#[test]
fn use_sync_output_disabled_in_wezterm_mux() {
let mut caps = TerminalCapabilities::basic();
caps.sync_output = true;
caps.in_wezterm_mux = true;
assert!(!caps.use_sync_output());
}
#[test]
fn use_scroll_region_disabled_in_mux() {
let mut caps = TerminalCapabilities::basic();
caps.scroll_region = true;
assert!(caps.use_scroll_region());
caps.in_tmux = true;
assert!(!caps.use_scroll_region());
caps.in_tmux = false;
caps.in_screen = true;
assert!(!caps.use_scroll_region());
caps.in_screen = false;
caps.in_zellij = true;
assert!(!caps.use_scroll_region());
caps.in_zellij = false;
caps.in_wezterm_mux = true;
assert!(!caps.use_scroll_region());
}
#[test]
fn use_hyperlinks_disabled_in_mux() {
let mut caps = TerminalCapabilities::basic();
caps.osc8_hyperlinks = true;
assert!(caps.use_hyperlinks());
caps.in_tmux = true;
assert!(!caps.use_hyperlinks());
caps.in_tmux = false;
caps.in_wezterm_mux = true;
assert!(!caps.use_hyperlinks());
}
#[test]
fn use_clipboard_disabled_in_mux() {
let mut caps = TerminalCapabilities::basic();
caps.osc52_clipboard = true;
assert!(caps.use_clipboard());
caps.in_screen = true;
assert!(!caps.use_clipboard());
caps.in_screen = false;
caps.in_wezterm_mux = true;
assert!(!caps.use_clipboard());
}
#[test]
fn needs_passthrough_wrap_only_for_tmux_screen() {
let mut caps = TerminalCapabilities::basic();
assert!(!caps.needs_passthrough_wrap());
caps.in_tmux = true;
assert!(caps.needs_passthrough_wrap());
caps.in_tmux = false;
caps.in_screen = true;
assert!(caps.needs_passthrough_wrap());
caps.in_screen = false;
caps.in_zellij = true;
assert!(!caps.needs_passthrough_wrap());
}
#[test]
fn policies_return_false_when_capability_absent() {
let caps = TerminalCapabilities::basic();
assert!(!caps.use_sync_output());
assert!(!caps.use_scroll_region());
assert!(!caps.use_hyperlinks());
assert!(!caps.use_clipboard());
}
fn make_env(term: &str, term_program: &str, colorterm: &str) -> DetectInputs {
DetectInputs {
no_color: false,
term: term.to_string(),
term_program: term_program.to_string(),
lc_terminal: String::new(),
colorterm: colorterm.to_string(),
in_tmux: false,
in_screen: false,
in_zellij: false,
wezterm_unix_socket: false,
wezterm_pane: false,
wezterm_executable: false,
kitty_window_id: false,
wt_session: false,
}
}
#[test]
fn detect_dumb_terminal() {
for term in ["dumb", "DUMB", " dumb "] {
let env = make_env(term, " WezTerm ", " TRUECOLOR ");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono, "TERM={term:?}");
assert!(!caps.sync_output, "TERM={term:?}");
assert!(!caps.osc8_hyperlinks, "TERM={term:?}");
assert!(!caps.scroll_region, "TERM={term:?}");
assert!(!caps.focus_events, "TERM={term:?}");
assert!(!caps.bracketed_paste, "TERM={term:?}");
assert!(!caps.mouse_sgr, "TERM={term:?}");
}
}
#[test]
fn detect_dumb_overrides_truecolor_env() {
let env = make_env("dumb", "WezTerm", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
assert!(!caps.osc8_hyperlinks);
}
#[test]
fn detect_dumb_disables_kitty_keyboard() {
for term_program in ["kitty", "ghostty", "iTerm.app", "WezTerm"] {
let env = make_env("dumb", term_program, "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
!caps.kitty_keyboard,
"TERM=dumb + TERM_PROGRAM={term_program} must disable kitty keyboard"
);
}
let env = make_env("xterm-kitty", "kitty", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.kitty_keyboard);
}
#[test]
fn detect_term_identity_counts_as_mux_evidence() {
let env = make_env("tmux-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_tmux, "TERM=tmux-256color implies tmux");
assert!(caps.in_any_mux());
assert!(!caps.use_scroll_region());
let env = make_env("screen-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_screen, "TERM=screen-256color implies screen");
assert!(caps.in_any_mux());
assert!(!caps.use_scroll_region());
let env = make_env("xterm-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.in_tmux);
assert!(!caps.in_screen);
}
#[test]
fn from_profile_custom_roundtrips() {
let caps = TerminalCapabilities::from_profile(TerminalProfile::Custom);
assert_eq!(caps.profile(), TerminalProfile::Custom);
let basic = TerminalCapabilities::basic();
assert_eq!(caps.color_depth, basic.color_depth);
assert_eq!(caps.scroll_region, basic.scroll_region);
}
#[test]
fn detect_empty_term_is_dumb() {
let env = make_env("", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.bracketed_paste);
}
#[test]
fn detect_xterm_256color() {
let env = make_env("xterm-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.scroll_region);
}
#[test]
fn detect_colorterm_truecolor() {
let env = make_env("xterm-256color", "", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
}
#[test]
fn detect_colorterm_24bit() {
let env = make_env("xterm-256color", "", "24bit");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
}
#[test]
fn detect_kitty_by_window_id() {
let mut env = make_env("xterm-kitty", "", "");
env.kitty_window_id = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(
caps.kitty_keyboard,
"Kitty supports kitty keyboard protocol"
);
assert!(caps.sync_output, "Kitty supports sync output");
}
#[test]
fn detect_kitty_by_term() {
let env = make_env("xterm-kitty", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.kitty_keyboard);
}
#[test]
fn detect_alacritty_via_term_only() {
let env = make_env("alacritty", "", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output, "Alacritty implements DEC 2026");
assert!(caps.use_sync_output());
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(!caps.in_any_mux());
}
#[test]
fn detect_iterm2_via_lc_terminal_when_term_program_missing() {
let mut env = make_env("xterm-256color", "", "");
env.lc_terminal = "iTerm2".to_string();
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor, "modern terminal");
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(
!caps.sync_output,
"not allowlisted: left to the DECRPM probe"
);
let mut env = make_env("xterm-256color", "Apple_Terminal", "");
env.lc_terminal = "iTerm2".to_string();
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
!caps.osc8_hyperlinks,
"Apple Terminal is not on the allowlist"
);
}
#[test]
fn detect_wezterm() {
let env = make_env("xterm-256color", "WezTerm", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(
caps.in_wezterm_mux,
"WezTerm identity is treated as conservative mux evidence"
);
assert!(caps.in_any_mux());
assert!(
!caps.sync_output,
"WezTerm sync output is hard-disabled as a safety fallback"
);
assert!(caps.osc8_hyperlinks, "WezTerm supports hyperlinks");
assert!(caps.kitty_keyboard, "WezTerm supports kitty keyboard");
assert!(caps.focus_events);
assert!(
!caps.osc52_clipboard,
"conservative mux policy should disable raw OSC52 detection"
);
assert!(!caps.use_scroll_region());
assert!(!caps.use_hyperlinks());
assert!(!caps.use_clipboard());
}
#[test]
fn detect_wezterm_mux_socket_disables_sync_policy() {
let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
env.wezterm_unix_socket = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.sync_output);
assert!(caps.in_wezterm_mux, "wezterm mux marker should be detected");
assert!(
caps.in_any_mux(),
"wezterm mux must participate in in_any_mux()"
);
assert!(
!caps.use_sync_output(),
"policy must suppress sync output in wezterm mux sessions"
);
assert!(
!caps.use_scroll_region(),
"policy must suppress scroll region in wezterm mux sessions"
);
assert!(
!caps.use_hyperlinks(),
"policy must suppress hyperlinks in wezterm mux sessions"
);
assert!(
!caps.use_clipboard(),
"policy must suppress clipboard in wezterm mux sessions"
);
}
#[test]
fn detect_wezterm_mux_socket_without_term_program_disables_sync_policy() {
let mut env = make_env("xterm-256color", "", "truecolor");
env.wezterm_unix_socket = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"socket marker alone must detect wezterm mux"
);
assert!(
caps.in_any_mux(),
"wezterm mux must participate in in_any_mux()"
);
assert!(
!caps.use_sync_output(),
"policy must suppress sync output when wezterm mux socket is present"
);
}
#[test]
fn detect_wezterm_mux_pane_disables_sync_policy() {
let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
env.wezterm_pane = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.sync_output);
assert!(
caps.in_wezterm_mux,
"wezterm pane marker should be detected"
);
assert!(
caps.in_any_mux(),
"wezterm mux must participate in in_any_mux()"
);
assert!(
!caps.use_sync_output(),
"policy must suppress sync output when wezterm pane marker is present"
);
}
#[test]
fn detect_wezterm_mux_pane_without_term_program_disables_sync_policy() {
let mut env = make_env("xterm-256color", "", "truecolor");
env.wezterm_pane = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"pane marker alone must detect wezterm mux"
);
assert!(
caps.in_any_mux(),
"wezterm mux must participate in in_any_mux()"
);
assert!(
!caps.use_sync_output(),
"policy must suppress sync output when wezterm pane marker is present"
);
}
#[test]
fn detect_wezterm_executable_without_term_program_is_conservative_mux() {
let mut env = make_env("xterm-256color", "", "truecolor");
env.wezterm_executable = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"WEZTERM_EXECUTABLE fallback should conservatively mark mux context"
);
assert!(
!caps.use_sync_output(),
"fallback mux detection must suppress sync output policy"
);
}
#[test]
fn detect_wezterm_executable_overrides_explicit_non_wezterm_program() {
let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
env.wezterm_executable = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"WEZTERM_EXECUTABLE should conservatively force wezterm mux policy"
);
assert!(
!caps.sync_output,
"raw sync_output capability should be disabled under conservative wezterm marker handling"
);
assert!(
!caps.use_sync_output(),
"mux policy should disable sync output under WEZTERM_EXECUTABLE marker"
);
}
#[test]
fn detect_wezterm_socket_overrides_explicit_non_wezterm_program() {
let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
env.wezterm_unix_socket = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
);
assert!(
!caps.use_sync_output(),
"mux policy should disable sync output with socket marker"
);
}
#[test]
fn detect_wezterm_pane_overrides_explicit_non_wezterm_program() {
let mut env = make_env("xterm-ghostty", "Ghostty", "truecolor");
env.wezterm_pane = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"WEZTERM_PANE should conservatively force wezterm mux policy"
);
assert!(
!caps.use_sync_output(),
"mux policy should disable sync output with pane marker"
);
}
#[test]
fn detect_wezterm_socket_overrides_explicit_non_wezterm_term_identity() {
let mut env = make_env("xterm-ghostty", "", "truecolor");
env.wezterm_unix_socket = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.in_wezterm_mux,
"WEZTERM_UNIX_SOCKET should conservatively force wezterm mux policy"
);
assert!(
!caps.use_sync_output(),
"mux policy should disable sync output with socket marker"
);
}
#[test]
#[cfg(target_os = "macos")]
fn detect_iterm2_from_term_program() {
let env = make_env("xterm-256color", "iTerm.app", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks, "iTerm2 supports OSC 8 hyperlinks");
}
#[test]
fn detect_alacritty() {
let env = make_env("alacritty", "Alacritty", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
}
#[test]
fn detect_ghostty() {
let env = make_env("xterm-ghostty", "Ghostty", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
}
#[test]
fn detect_iterm() {
let env = make_env("xterm-256color", "iTerm.app", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
}
#[test]
fn detect_vscode_terminal() {
let env = make_env("xterm-256color", "vscode", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
assert!(caps.focus_events);
}
#[test]
fn detect_in_tmux() {
let mut env = make_env("screen-256color", "", "");
env.in_tmux = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_tmux);
assert!(caps.in_any_mux());
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
assert!(!caps.osc52_clipboard, "clipboard disabled in tmux");
}
#[test]
fn detect_in_screen() {
let mut env = make_env("screen", "", "");
env.in_screen = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_screen);
assert!(caps.in_any_mux());
assert!(caps.needs_passthrough_wrap());
}
#[test]
fn detect_in_zellij() {
let mut env = make_env("xterm-256color", "", "truecolor");
env.in_zellij = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_zellij);
assert!(caps.in_any_mux());
assert!(
!caps.needs_passthrough_wrap(),
"Zellij handles passthrough natively"
);
assert!(!caps.osc52_clipboard, "clipboard disabled in mux");
}
#[test]
fn detect_modern_terminal_in_tmux() {
let mut env = make_env("screen-256color", "WezTerm", "truecolor");
env.in_tmux = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(!caps.sync_output);
assert!(!caps.use_sync_output());
assert!(!caps.use_hyperlinks());
assert!(!caps.use_scroll_region());
}
#[test]
fn no_color_overrides_everything() {
let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
env.no_color = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.osc8_hyperlinks);
assert!(!caps.sync_output);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn unknown_term_program() {
let env = make_env("xterm", "SomeUnknownTerminal", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
!caps.supports_true_color(),
"unknown terminal should not assume truecolor"
);
assert_eq!(caps.color_depth, ColorDepth::Ansi16);
assert!(!caps.osc8_hyperlinks);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.scroll_region);
}
#[test]
fn all_mux_flags_simultaneous() {
let mut env = make_env("screen", "", "");
env.in_tmux = true;
env.in_screen = true;
env.in_zellij = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.in_any_mux());
assert!(caps.needs_passthrough_wrap());
assert!(!caps.use_sync_output());
assert!(!caps.use_hyperlinks());
assert!(!caps.use_clipboard());
}
#[test]
fn detect_rio() {
let env = make_env("xterm-256color", "Rio", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
}
#[test]
fn detect_contour() {
let env = make_env("xterm-256color", "Contour", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
assert!(caps.osc8_hyperlinks);
assert!(caps.focus_events);
}
#[test]
fn detect_foot() {
let env = make_env("foot", "foot", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.kitty_keyboard, "foot supports kitty keyboard");
}
#[test]
fn detect_hyper() {
let env = make_env("xterm-256color", "Hyper", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
assert!(caps.focus_events);
}
#[test]
fn detect_linux_console() {
let env = make_env("linux", "", "");
let detected = TerminalCapabilities::detect_from_inputs(&env);
let mut expected = TerminalCapabilities::linux_console();
expected.profile = TerminalProfile::Detected;
assert_eq!(
detected, expected,
"the predefined Linux console profile must match TERM=linux detection"
);
let policy = crate::glyph_policy::GlyphPolicy::from_env_with(
|key| (key == "TERM").then(|| "linux".to_string()),
&detected,
);
assert_eq!(policy.mode, crate::glyph_policy::GlyphMode::Ascii);
assert!(!policy.double_width);
}
#[test]
fn detect_linux_console_caps_color_despite_conflicting_positive_signals() {
let expected = TerminalCapabilities::linux_console().color_depth;
for (term_program, colorterm) in [
("WezTerm", "truecolor"),
("kitty", "24bit"),
("Ghostty", ""),
] {
let env = make_env(" linux ", term_program, colorterm);
let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some(" linux "));
assert_eq!(detected, expected, "TERM_PROGRAM={term_program:?}");
assert_eq!(explicit, expected, "COLORTERM={colorterm:?}");
}
}
#[test]
fn detect_direct_color_term_variants() {
for term in [
"xterm-direct",
"xterm-direct2",
"xterm-direct16",
"xterm-direct256",
"xterm-truecolor",
"xterm-24bit",
" XTERM-DIRECT ",
] {
let env = make_env(term, "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor, "TERM={term:?}");
assert!(caps.bracketed_paste, "TERM={term:?}");
assert!(caps.mouse_sgr, "TERM={term:?}");
}
}
#[test]
fn direct_color_detection_rejects_negated_and_partial_tokens() {
for term in [
"xterm-indirect",
"xterm-direct-color",
"xterm-no24bit",
"xterm-24bit-disabled",
] {
let env = make_env(term, "", "");
let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
let explicit = ColorDepth::detect_from_env(None, None, Some(term));
assert_eq!(detected, ColorDepth::Ansi16, "TERM={term:?}");
assert_eq!(explicit, detected, "TERM={term:?}");
}
for colorterm in [
"nottruecolor",
"truecolor-disabled",
"no24bit",
"24bit-disabled",
] {
let env = make_env("xterm", "", colorterm);
let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
let explicit = ColorDepth::detect_from_env(None, Some(colorterm), Some("xterm"));
assert_eq!(detected, ColorDepth::Ansi16, "COLORTERM={colorterm:?}");
assert_eq!(explicit, detected, "COLORTERM={colorterm:?}");
}
}
#[test]
fn detect_plain_xterm_is_ansi16() {
let env = make_env("xterm", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Ansi16);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn explicit_and_full_color_detection_agree() {
for (no_color, colorterm, term) in [
(false, "", "xterm"),
(false, "", "xterm-256color"),
(false, "", " XTERM-DIRECT "),
(false, "", "xterm-direct2"),
(false, "", "xterm-direct16"),
(false, "", "xterm-direct256"),
(false, "24BIT", "xterm"),
(false, "no24bit", "xterm"),
(false, "truecolor", "linux"),
(false, "truecolor", " VT100 "),
(false, "truecolor", " DUMB "),
(true, "truecolor", "xterm-direct"),
] {
let mut env = make_env(term, "", colorterm);
env.no_color = no_color;
let detected = TerminalCapabilities::detect_from_inputs(&env).color_depth;
let explicit =
ColorDepth::detect_from_env(no_color.then_some(""), Some(colorterm), Some(term));
assert_eq!(
detected, explicit,
"NO_COLOR={no_color} COLORTERM={colorterm:?} TERM={term:?}"
);
}
}
#[test]
fn detect_vt100_is_monochrome() {
let env = make_env("vt100", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
}
#[test]
fn detect_screen_256color() {
let env = make_env("screen-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
}
#[test]
fn wezterm_without_colorterm() {
let env = make_env("xterm-256color", "WezTerm", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(!caps.sync_output);
assert!(caps.osc8_hyperlinks);
}
#[test]
fn alacritty_via_term_only() {
let env = make_env("alacritty", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
}
#[test]
fn kitty_via_term_without_window_id() {
let env = make_env("xterm-kitty", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.kitty_keyboard);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
}
#[test]
fn kitty_window_id_with_generic_term() {
let mut env = make_env("xterm-256color", "", "");
env.kitty_window_id = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.kitty_keyboard);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
}
#[test]
fn use_clipboard_enabled_when_no_mux_and_modern() {
let env = make_env("xterm-256color", "Alacritty", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.osc52_clipboard);
assert!(caps.use_clipboard());
}
#[test]
fn use_clipboard_disabled_in_tmux_even_if_detected() {
let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
env.in_tmux = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.osc52_clipboard);
assert!(!caps.use_clipboard());
}
#[test]
fn scroll_region_enabled_for_basic_xterm() {
let env = make_env("xterm", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.scroll_region);
assert!(caps.use_scroll_region());
}
#[test]
fn no_color_preserves_non_visual_features() {
let mut env = make_env("xterm-256color", "WezTerm", "truecolor");
env.no_color = true;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.osc8_hyperlinks);
assert!(!caps.sync_output);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn colorterm_yes_not_truecolor() {
let env = make_env("xterm-256color", "", "yes");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
}
#[test]
fn profile_enum_as_str() {
assert_eq!(TerminalProfile::Modern.as_str(), "modern");
assert_eq!(TerminalProfile::Xterm256Color.as_str(), "xterm-256color");
assert_eq!(TerminalProfile::Vt100.as_str(), "vt100");
assert_eq!(TerminalProfile::Dumb.as_str(), "dumb");
assert_eq!(TerminalProfile::Tmux.as_str(), "tmux");
assert_eq!(TerminalProfile::Screen.as_str(), "screen");
assert_eq!(TerminalProfile::Kitty.as_str(), "kitty");
}
#[test]
fn profile_enum_from_str() {
use std::str::FromStr;
assert_eq!(
TerminalProfile::from_str("modern"),
Ok(TerminalProfile::Modern)
);
assert_eq!(
TerminalProfile::from_str("xterm-256color"),
Ok(TerminalProfile::Xterm256Color)
);
assert_eq!(
TerminalProfile::from_str("xterm256color"),
Ok(TerminalProfile::Xterm256Color)
);
assert_eq!(TerminalProfile::from_str("DUMB"), Ok(TerminalProfile::Dumb));
assert!(TerminalProfile::from_str("unknown").is_err());
}
#[test]
fn profile_all_predefined() {
let all = TerminalProfile::all_predefined();
assert!(all.len() >= 10);
assert!(all.contains(&TerminalProfile::Modern));
assert!(all.contains(&TerminalProfile::Dumb));
assert!(!all.contains(&TerminalProfile::Custom));
assert!(!all.contains(&TerminalProfile::Detected));
}
#[test]
fn profile_modern_has_all_features() {
let caps = TerminalCapabilities::modern();
assert_eq!(caps.profile(), TerminalProfile::Modern);
assert_eq!(caps.profile_name(), Some("modern"));
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
assert!(caps.osc8_hyperlinks);
assert!(caps.scroll_region);
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.osc52_clipboard);
assert!(!caps.in_any_mux());
}
#[test]
fn profile_xterm_256color() {
let caps = TerminalCapabilities::xterm_256color();
assert_eq!(caps.profile(), TerminalProfile::Xterm256Color);
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
assert!(!caps.sync_output);
assert!(!caps.osc8_hyperlinks);
assert!(caps.scroll_region);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn profile_xterm_basic() {
let caps = TerminalCapabilities::xterm();
assert_eq!(caps.profile(), TerminalProfile::Xterm);
assert_eq!(caps.color_depth, ColorDepth::Ansi16);
assert!(caps.scroll_region);
}
#[test]
fn profile_vt100_minimal() {
let caps = TerminalCapabilities::vt100();
assert_eq!(caps.profile(), TerminalProfile::Vt100);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(caps.scroll_region);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
}
#[test]
fn profile_dumb_no_features() {
let caps = TerminalCapabilities::dumb();
assert_eq!(caps.profile(), TerminalProfile::Dumb);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.scroll_region);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
assert!(!caps.use_sync_output());
assert!(!caps.use_scroll_region());
}
#[test]
fn profile_tmux_mux_flags() {
let caps = TerminalCapabilities::tmux();
assert_eq!(caps.profile(), TerminalProfile::Tmux);
assert!(caps.in_tmux);
assert!(!caps.in_screen);
assert!(!caps.in_zellij);
assert!(caps.in_any_mux());
assert!(!caps.use_sync_output());
assert!(!caps.use_scroll_region());
assert!(!caps.use_hyperlinks());
}
#[test]
fn profile_screen_mux_flags() {
let caps = TerminalCapabilities::screen();
assert_eq!(caps.profile(), TerminalProfile::Screen);
assert!(!caps.in_tmux);
assert!(caps.in_screen);
assert!(caps.in_any_mux());
assert!(caps.needs_passthrough_wrap());
}
#[test]
fn profile_zellij_mux_flags() {
let caps = TerminalCapabilities::zellij();
assert_eq!(caps.profile(), TerminalProfile::Zellij);
assert!(caps.in_zellij);
assert!(caps.in_any_mux());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.focus_events);
assert!(!caps.needs_passthrough_wrap());
}
#[test]
fn profile_kitty_full_features() {
let caps = TerminalCapabilities::kitty();
assert_eq!(caps.profile(), TerminalProfile::Kitty);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.sync_output);
assert!(caps.kitty_keyboard);
assert!(caps.osc8_hyperlinks);
}
#[test]
fn profile_windows_console() {
let caps = TerminalCapabilities::windows_console();
assert_eq!(caps.profile(), TerminalProfile::WindowsConsole);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.osc8_hyperlinks);
assert!(caps.focus_events);
}
#[test]
fn profile_linux_console() {
let caps = TerminalCapabilities::linux_console();
assert_eq!(caps.profile(), TerminalProfile::LinuxConsole);
assert_eq!(caps.color_depth, ColorDepth::Ansi16);
assert!(!caps.double_width);
assert!(caps.scroll_region);
}
#[test]
fn from_profile_roundtrip() {
for profile in TerminalProfile::all_predefined() {
let caps = TerminalCapabilities::from_profile(*profile);
assert_eq!(caps.profile(), *profile);
}
}
#[test]
fn detected_profile_has_none_name() {
let caps = detect_with_override(None);
assert_eq!(caps.profile(), TerminalProfile::Detected);
assert_eq!(caps.profile_name(), None);
}
#[test]
fn detect_respects_test_profile_env() {
let caps = detect_with_override(Some("dumb"));
assert_eq!(caps.profile(), TerminalProfile::Dumb);
}
#[test]
fn detect_ignores_invalid_test_profile() {
let caps = detect_with_override(Some("not-a-real-profile"));
assert_eq!(caps.profile(), TerminalProfile::Detected);
}
#[test]
fn basic_has_dumb_profile() {
let caps = TerminalCapabilities::basic();
assert_eq!(caps.profile(), TerminalProfile::Dumb);
}
#[test]
fn builder_starts_empty() {
let caps = CapabilityProfileBuilder::new().build();
assert_eq!(caps.profile(), TerminalProfile::Custom);
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.sync_output);
assert!(!caps.scroll_region);
assert!(!caps.mouse_sgr);
}
#[test]
fn builder_sets_one_canonical_color_depth() {
let caps = CapabilityProfileBuilder::new()
.color_depth(ColorDepth::TrueColor)
.build();
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
}
#[test]
fn builder_set_advanced() {
let caps = CapabilityProfileBuilder::new()
.sync_output(true)
.osc8_hyperlinks(true)
.scroll_region(true)
.build();
assert!(caps.sync_output);
assert!(caps.osc8_hyperlinks);
assert!(caps.scroll_region);
}
#[test]
fn builder_set_mux() {
let caps = CapabilityProfileBuilder::new()
.in_tmux(true)
.in_screen(false)
.in_zellij(false)
.build();
assert!(caps.in_tmux);
assert!(!caps.in_screen);
assert!(caps.in_any_mux());
}
#[test]
fn builder_set_input() {
let caps = CapabilityProfileBuilder::new()
.kitty_keyboard(true)
.focus_events(true)
.bracketed_paste(true)
.mouse_sgr(true)
.build();
assert!(caps.kitty_keyboard);
assert!(caps.focus_events);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn builder_set_clipboard() {
let caps = CapabilityProfileBuilder::new()
.osc52_clipboard(true)
.build();
assert!(caps.osc52_clipboard);
}
#[test]
fn builder_from_profile() {
let caps = CapabilityProfileBuilder::from_profile(TerminalProfile::Modern)
.sync_output(false) .build();
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
assert_eq!(caps.profile(), TerminalProfile::Custom);
}
#[test]
fn builder_chain_multiple() {
let caps = TerminalCapabilities::builder()
.color_depth(ColorDepth::Ansi256)
.bracketed_paste(true)
.mouse_sgr(true)
.scroll_region(true)
.build();
assert_eq!(caps.color_depth, ColorDepth::Ansi256);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.scroll_region);
assert!(!caps.supports_true_color());
assert!(!caps.sync_output);
}
#[test]
fn builder_default() {
let builder = CapabilityProfileBuilder::default();
let caps = builder.build();
assert_eq!(caps.profile(), TerminalProfile::Custom);
}
#[test]
fn mux_compatibility_matrix() {
{
let caps = TerminalCapabilities::modern();
assert!(
caps.use_sync_output(),
"baseline: sync_output should be enabled"
);
assert!(
caps.use_scroll_region(),
"baseline: scroll_region should be enabled"
);
assert!(
caps.use_hyperlinks(),
"baseline: hyperlinks should be enabled"
);
assert!(!caps.needs_passthrough_wrap(), "baseline: no wrap needed");
}
{
let caps = TerminalCapabilities::tmux();
assert!(!caps.use_sync_output(), "tmux: sync_output disabled");
assert!(!caps.use_scroll_region(), "tmux: scroll_region disabled");
assert!(!caps.use_hyperlinks(), "tmux: hyperlinks disabled");
assert!(caps.needs_passthrough_wrap(), "tmux: needs wrap");
}
{
let caps = TerminalCapabilities::screen();
assert!(!caps.use_sync_output(), "screen: sync_output disabled");
assert!(!caps.use_scroll_region(), "screen: scroll_region disabled");
assert!(!caps.use_hyperlinks(), "screen: hyperlinks disabled");
assert!(caps.needs_passthrough_wrap(), "screen: needs wrap");
}
{
let caps = TerminalCapabilities::zellij();
assert!(!caps.use_sync_output(), "zellij: sync_output disabled");
assert!(!caps.use_scroll_region(), "zellij: scroll_region disabled");
assert!(!caps.use_hyperlinks(), "zellij: hyperlinks disabled");
assert!(
!caps.needs_passthrough_wrap(),
"zellij: no wrap needed (native passthrough)"
);
}
{
let caps = TerminalCapabilities::builder()
.in_wezterm_mux(true)
.sync_output(true)
.scroll_region(true)
.osc8_hyperlinks(true)
.build();
assert!(!caps.use_sync_output(), "wezterm mux: sync_output disabled");
assert!(
!caps.use_scroll_region(),
"wezterm mux: scroll_region disabled"
);
assert!(!caps.use_hyperlinks(), "wezterm mux: hyperlinks disabled");
assert!(
!caps.needs_passthrough_wrap(),
"wezterm mux: no wrap needed"
);
}
}
#[test]
fn modern_terminal_in_mux_matrix() {
for (mux_name, in_tmux, in_screen, in_zellij, in_wezterm_mux) in [
("tmux", true, false, false, false),
("screen", false, true, false, false),
("zellij", false, false, true, false),
("wezterm-mux", false, false, false, true),
] {
let mut env = make_env("screen-256color", "WezTerm", "truecolor");
env.in_tmux = in_tmux;
env.in_screen = in_screen;
env.in_zellij = in_zellij;
env.wezterm_unix_socket = in_wezterm_mux;
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(
caps.supports_true_color(),
"{mux_name}: true_color detection should work"
);
assert!(
!caps.sync_output,
"{mux_name}: sync_output hard-disabled for WezTerm safety"
);
assert!(
!caps.use_sync_output(),
"{mux_name}: use_sync_output() should be false"
);
assert!(
!caps.use_scroll_region(),
"{mux_name}: use_scroll_region() should be false"
);
assert!(
!caps.use_hyperlinks(),
"{mux_name}: use_hyperlinks() should be false"
);
}
}
#[test]
fn profile_mux_invariant_matrix() {
for profile in TerminalProfile::all_predefined() {
let caps = TerminalCapabilities::from_profile(*profile);
let name = profile.as_str();
let expected_mux =
caps.in_tmux || caps.in_screen || caps.in_zellij || caps.in_wezterm_mux;
assert_eq!(
caps.in_any_mux(),
expected_mux,
"{name}: in_any_mux() should match individual flags"
);
if caps.in_any_mux() {
assert!(
!caps.use_sync_output(),
"{name}: mux should disable use_sync_output()"
);
assert!(
!caps.use_scroll_region(),
"{name}: mux should disable use_scroll_region()"
);
assert!(
!caps.use_hyperlinks(),
"{name}: mux should disable use_hyperlinks()"
);
}
if caps.in_tmux || caps.in_screen {
assert!(
caps.needs_passthrough_wrap(),
"{name}: tmux/screen should need passthrough wrap"
);
} else if caps.in_zellij {
assert!(
!caps.needs_passthrough_wrap(),
"{name}: zellij should NOT need passthrough wrap"
);
}
}
}
#[test]
fn fallback_ordering_matrix() {
use crate::inline_mode::InlineStrategy;
let caps_full = TerminalCapabilities::builder()
.sync_output(true)
.scroll_region(true)
.build();
assert_eq!(
InlineStrategy::select(&caps_full),
InlineStrategy::ScrollRegion,
"full capabilities should use ScrollRegion"
);
let caps_hybrid = TerminalCapabilities::builder()
.sync_output(false)
.scroll_region(true)
.build();
assert_eq!(
InlineStrategy::select(&caps_hybrid),
InlineStrategy::Hybrid,
"scroll without sync should use Hybrid"
);
let caps_none = TerminalCapabilities::builder()
.sync_output(false)
.scroll_region(false)
.build();
assert_eq!(
InlineStrategy::select(&caps_none),
InlineStrategy::OverlayRedraw,
"no capabilities should use OverlayRedraw"
);
let caps_tmux = TerminalCapabilities::tmux();
assert_eq!(
InlineStrategy::select(&caps_tmux),
InlineStrategy::OverlayRedraw,
"tmux should force OverlayRedraw"
);
}
#[test]
fn terminal_mux_strategy_matrix() {
use crate::inline_mode::InlineStrategy;
struct TestCase {
name: &'static str,
profile: TerminalProfile,
expected: InlineStrategy,
}
let cases = [
TestCase {
name: "modern (no mux)",
profile: TerminalProfile::Modern,
expected: InlineStrategy::ScrollRegion,
},
TestCase {
name: "kitty (no mux)",
profile: TerminalProfile::Kitty,
expected: InlineStrategy::ScrollRegion,
},
TestCase {
name: "xterm-256color (no mux)",
profile: TerminalProfile::Xterm256Color,
expected: InlineStrategy::Hybrid, },
TestCase {
name: "xterm (no mux)",
profile: TerminalProfile::Xterm,
expected: InlineStrategy::Hybrid,
},
TestCase {
name: "vt100 (no mux)",
profile: TerminalProfile::Vt100,
expected: InlineStrategy::Hybrid,
},
TestCase {
name: "dumb",
profile: TerminalProfile::Dumb,
expected: InlineStrategy::OverlayRedraw, },
TestCase {
name: "tmux",
profile: TerminalProfile::Tmux,
expected: InlineStrategy::OverlayRedraw,
},
TestCase {
name: "screen",
profile: TerminalProfile::Screen,
expected: InlineStrategy::OverlayRedraw,
},
TestCase {
name: "zellij",
profile: TerminalProfile::Zellij,
expected: InlineStrategy::OverlayRedraw,
},
];
for case in cases {
let caps = TerminalCapabilities::from_profile(case.profile);
let actual = InlineStrategy::select(&caps);
assert_eq!(
actual, case.expected,
"{}: expected {:?}, got {:?}",
case.name, case.expected, actual
);
}
}
#[test]
fn shared_caps_load_returns_initial() {
let shared = SharedCapabilities::new(TerminalCapabilities::modern());
assert_eq!(shared.load().color_depth, ColorDepth::TrueColor);
assert!(shared.load().sync_output);
}
#[test]
fn shared_caps_store_replaces_value() {
let shared = SharedCapabilities::new(TerminalCapabilities::modern());
shared.store(TerminalCapabilities::dumb());
let loaded = shared.load();
assert_eq!(loaded.color_depth, ColorDepth::Mono);
assert!(!loaded.sync_output);
}
#[test]
fn shared_caps_concurrent_read_write() {
use std::sync::{Arc, Barrier};
use std::thread;
let shared = Arc::new(SharedCapabilities::new(TerminalCapabilities::basic()));
let barrier = Arc::new(Barrier::new(5));
let readers: Vec<_> = (0..4)
.map(|_| {
let s = Arc::clone(&shared);
let b = Arc::clone(&barrier);
thread::spawn(move || {
b.wait();
for _ in 0..10_000 {
let caps = s.load();
let _ = caps.use_sync_output();
let _ = caps.color_depth;
}
})
})
.collect();
let writer = {
let s = Arc::clone(&shared);
let b = Arc::clone(&barrier);
thread::spawn(move || {
b.wait();
for i in 0..1_000 {
if i % 2 == 0 {
s.store(TerminalCapabilities::modern());
} else {
s.store(TerminalCapabilities::dumb());
}
}
})
};
writer.join().unwrap();
for h in readers {
h.join().unwrap();
}
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_in_any_mux_consistent(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.build();
let expected = in_tmux || in_screen || in_zellij || in_wezterm_mux;
prop_assert_eq!(caps.in_any_mux(), expected);
}
#[test]
fn prop_mux_disables_sync_output(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
sync_output in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.sync_output(sync_output)
.build();
if caps.in_any_mux() {
prop_assert!(!caps.use_sync_output(), "mux should disable sync_output policy");
}
}
#[test]
fn prop_mux_disables_scroll_region(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
scroll_region in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.scroll_region(scroll_region)
.build();
if caps.in_any_mux() {
prop_assert!(!caps.use_scroll_region(), "mux should disable scroll_region policy");
}
}
#[test]
fn prop_mux_disables_hyperlinks(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
osc8_hyperlinks in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.osc8_hyperlinks(osc8_hyperlinks)
.build();
if caps.in_any_mux() {
prop_assert!(!caps.use_hyperlinks(), "mux should disable hyperlinks policy");
}
}
#[test]
fn prop_passthrough_wrap_logic(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.build();
let expected = in_tmux || in_screen; prop_assert_eq!(caps.needs_passthrough_wrap(), expected);
}
#[test]
fn prop_policy_false_when_capability_off(
in_tmux in any::<bool>(),
in_screen in any::<bool>(),
in_zellij in any::<bool>(),
in_wezterm_mux in any::<bool>(),
) {
let caps = TerminalCapabilities::builder()
.in_tmux(in_tmux)
.in_screen(in_screen)
.in_zellij(in_zellij)
.in_wezterm_mux(in_wezterm_mux)
.sync_output(false)
.scroll_region(false)
.osc8_hyperlinks(false)
.osc52_clipboard(false)
.build();
prop_assert!(!caps.use_sync_output(), "sync_output=false implies use_sync_output()=false");
prop_assert!(!caps.use_scroll_region(), "scroll_region=false implies use_scroll_region()=false");
prop_assert!(!caps.use_hyperlinks(), "osc8_hyperlinks=false implies use_hyperlinks()=false");
prop_assert!(!caps.use_clipboard(), "osc52_clipboard=false implies use_clipboard()=false");
}
#[test]
fn prop_no_color_preserves_non_visual(no_color in any::<bool>()) {
let env = DetectInputs {
no_color,
term: "xterm-256color".to_string(),
term_program: "WezTerm".to_string(),
lc_terminal: String::new(),
colorterm: "truecolor".to_string(),
in_tmux: false,
in_screen: false,
in_zellij: false,
wezterm_unix_socket: false,
wezterm_pane: false,
wezterm_executable: false,
kitty_window_id: false,
wt_session: false,
};
let caps = TerminalCapabilities::detect_from_inputs(&env);
if no_color {
prop_assert_eq!(caps.color_depth, ColorDepth::Mono);
prop_assert!(!caps.osc8_hyperlinks, "NO_COLOR disables hyperlinks");
}
prop_assert!(
!caps.sync_output,
"WezTerm sync_output stays disabled despite NO_COLOR"
);
prop_assert!(caps.bracketed_paste, "bracketed_paste preserved despite NO_COLOR");
}
}
}