#![forbid(unsafe_code)]
use std::env;
use std::str::FromStr;
#[derive(Debug, Clone)]
struct DetectInputs {
no_color: bool,
term: String,
term_program: 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 {
no_color: env::var("NO_COLOR").is_ok(),
term: env::var("TERM").unwrap_or_default(),
term_program: env::var("TERM_PROGRAM").unwrap_or_default(),
colorterm: env::var("COLORTERM").unwrap_or_default(),
in_tmux: env::var("TMUX").is_ok(),
in_screen: env::var("STY").is_ok(),
in_zellij: env::var("ZELLIJ").is_ok(),
wezterm_unix_socket: env::var("WEZTERM_UNIX_SOCKET").is_ok(),
wezterm_pane: env::var("WEZTERM_PANE").is_ok(),
wezterm_executable: env::var("WEZTERM_EXECUTABLE").is_ok(),
kitty_window_id: env::var("KITTY_WINDOW_ID").is_ok(),
wt_session: env::var("WT_SESSION").is_ok(),
}
}
}
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 true_color: bool,
pub colors_256: bool,
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 => Self::basic(),
TerminalProfile::Detected => Self::detect(),
}
}
#[must_use]
pub const fn modern() -> Self {
Self {
profile: TerminalProfile::Modern,
true_color: true,
colors_256: true,
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,
true_color: false,
colors_256: true,
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,
true_color: false,
colors_256: false,
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,
true_color: false,
colors_256: false,
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,
true_color: false,
colors_256: false,
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,
true_color: false,
colors_256: true,
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,
true_color: false,
colors_256: true,
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,
true_color: true,
colors_256: true,
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,
true_color: true,
colors_256: true,
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,
true_color: true,
colors_256: true,
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,
true_color: false,
colors_256: false,
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,
true_color: false,
colors_256: false,
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 true_color(mut self, enabled: bool) -> Self {
self.caps.true_color = enabled;
self
}
pub const fn colors_256(mut self, enabled: bool) -> Self {
self.caps.colors_256 = enabled;
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 in_tmux = env.in_tmux;
let in_screen = env.in_screen;
let in_zellij = env.in_zellij;
let term = env.term.as_str();
let term_program = env.term_program.as_str();
let colorterm = env.colorterm.as_str();
let term_lower = term.to_ascii_lowercase();
let term_program_lower = term_program.to_ascii_lowercase();
let colorterm_lower = colorterm.to_ascii_lowercase();
let term_program_is_wezterm = term_program_lower.contains("wezterm");
let term_is_wezterm = term_lower.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 = term == "dumb" || (term.is_empty() && !is_windows_terminal);
let is_kitty = env.kitty_window_id || term_lower.contains("kitty");
let is_modern_terminal = MODERN_TERMINALS.iter().any(|t| {
let t_lower = t.to_ascii_lowercase();
term_program_lower.contains(&t_lower) || term_lower.contains(&t_lower)
}) || is_windows_terminal;
let true_color = !env.no_color
&& !is_dumb
&& (colorterm_lower.contains("truecolor")
|| colorterm_lower.contains("24bit")
|| is_modern_terminal
|| is_kitty);
let colors_256 = !env.no_color
&& !is_dumb
&& (true_color || term_lower.contains("256color") || term_lower.contains("256"));
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_lower.contains(&t_lower)
}));
let osc8_hyperlinks = !env.no_color && !is_dumb && is_modern_terminal;
let scroll_region = !is_dumb;
let kitty_keyboard = is_kitty
|| KITTY_KEYBOARD_TERMINALS.iter().any(|t| {
let t_lower = t.to_ascii_lowercase();
term_program_lower.contains(&t_lower) || term_lower.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;
Self {
profile: TerminalProfile::Detected,
true_color,
colors_256,
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,
true_color: false,
colors_256: false,
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.true_color || self.colors_256
}
#[must_use]
pub const fn color_depth(&self) -> &'static str {
if self.true_color {
"truecolor"
} else if self.colors_256 {
"256"
} else {
"mono"
}
}
#[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!(!caps.true_color);
assert!(!caps.colors_256);
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.colors_256 = true;
assert!(caps.has_color());
caps.colors_256 = false;
caps.true_color = true;
assert!(caps.has_color());
}
#[test]
fn color_depth_strings() {
let mut caps = TerminalCapabilities::basic();
assert_eq!(caps.color_depth(), "mono");
caps.colors_256 = true;
assert_eq!(caps.color_depth(), "256");
caps.true_color = true;
assert_eq!(caps.color_depth(), "truecolor");
}
#[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(),
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!(caps.true_color, "WT_SESSION implies true color by default");
assert!(caps.colors_256, "truecolor implies 256-color");
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!(caps.true_color, "WT_SESSION implies true color");
assert!(caps.colors_256, "WT_SESSION implies 256-color");
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(),
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!(!caps.true_color, "NO_COLOR must disable true color");
assert!(!caps.colors_256, "NO_COLOR must disable 256-color");
assert!(
!caps.osc8_hyperlinks,
"NO_COLOR must disable OSC 8 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(),
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() {
let env = make_env("dumb", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.true_color);
assert!(!caps.colors_256);
assert!(!caps.sync_output);
assert!(!caps.osc8_hyperlinks);
assert!(!caps.scroll_region);
assert!(!caps.focus_events);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
}
#[test]
fn detect_dumb_overrides_truecolor_env() {
let env = make_env("dumb", "WezTerm", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.true_color, "dumb should override COLORTERM");
assert!(!caps.colors_256);
assert!(!caps.bracketed_paste);
assert!(!caps.mouse_sgr);
assert!(!caps.osc8_hyperlinks);
}
#[test]
fn detect_empty_term_is_dumb() {
let env = make_env("", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.true_color);
assert!(!caps.bracketed_paste);
}
#[test]
fn detect_xterm_256color() {
let env = make_env("xterm-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.colors_256, "xterm-256color implies 256 color");
assert!(!caps.true_color, "256color alone does not imply truecolor");
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!(caps.true_color, "COLORTERM=truecolor enables truecolor");
assert!(caps.colors_256, "truecolor implies 256-color");
}
#[test]
fn detect_colorterm_24bit() {
let env = make_env("xterm-256color", "", "24bit");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.true_color, "COLORTERM=24bit enables 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!(caps.true_color, "Kitty supports 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!(caps.true_color, "kitty TERM implies truecolor");
assert!(caps.kitty_keyboard);
}
#[test]
fn detect_wezterm() {
let env = make_env("xterm-256color", "WezTerm", "truecolor");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.true_color);
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!(caps.true_color, "iTerm2 implies 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!(caps.true_color);
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!(caps.true_color);
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!(caps.true_color);
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!(caps.true_color);
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!(caps.colors_256);
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!(caps.true_color);
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!(!caps.true_color);
assert!(!caps.colors_256);
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.true_color,
"unknown terminal should not assume truecolor"
);
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!(caps.true_color);
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!(caps.true_color);
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!(caps.true_color);
assert!(caps.osc8_hyperlinks);
assert!(caps.focus_events);
}
#[test]
fn detect_linux_console() {
let env = make_env("linux", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.true_color, "linux console doesn't support truecolor");
assert!(!caps.colors_256, "linux console doesn't support 256 colors");
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.scroll_region);
}
#[test]
fn detect_xterm_direct() {
let env = make_env("xterm", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(!caps.true_color, "plain xterm has no truecolor");
assert!(!caps.colors_256, "plain xterm has no 256color");
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
}
#[test]
fn detect_screen_256color() {
let env = make_env("screen-256color", "", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.colors_256, "screen-256color has 256 colors");
assert!(!caps.true_color);
}
#[test]
fn wezterm_without_colorterm() {
let env = make_env("xterm-256color", "WezTerm", "");
let caps = TerminalCapabilities::detect_from_inputs(&env);
assert!(caps.true_color, "WezTerm is modern, implies 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!(caps.true_color);
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!(caps.true_color);
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!(caps.true_color);
}
#[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!(!caps.true_color);
assert!(!caps.colors_256);
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!(!caps.true_color, "COLORTERM=yes is not truecolor");
assert!(caps.colors_256, "TERM=xterm-256color implies 256");
}
#[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!(caps.true_color);
assert!(caps.colors_256);
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!(!caps.true_color);
assert!(caps.colors_256);
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!(!caps.true_color);
assert!(!caps.colors_256);
assert!(caps.scroll_region);
}
#[test]
fn profile_vt100_minimal() {
let caps = TerminalCapabilities::vt100();
assert_eq!(caps.profile(), TerminalProfile::Vt100);
assert!(!caps.true_color);
assert!(!caps.colors_256);
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!(!caps.true_color);
assert!(!caps.colors_256);
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!(caps.true_color);
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!(caps.true_color);
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!(caps.true_color);
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!(!caps.true_color);
assert!(!caps.colors_256);
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!(!caps.true_color);
assert!(!caps.colors_256);
assert!(!caps.sync_output);
assert!(!caps.scroll_region);
assert!(!caps.mouse_sgr);
}
#[test]
fn builder_set_colors() {
let caps = CapabilityProfileBuilder::new()
.true_color(true)
.colors_256(true)
.build();
assert!(caps.true_color);
assert!(caps.colors_256);
}
#[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!(caps.true_color);
assert!(caps.colors_256);
assert!(!caps.sync_output); assert!(caps.osc8_hyperlinks);
assert_eq!(caps.profile(), TerminalProfile::Custom);
}
#[test]
fn builder_chain_multiple() {
let caps = TerminalCapabilities::builder()
.colors_256(true)
.bracketed_paste(true)
.mouse_sgr(true)
.scroll_region(true)
.build();
assert!(caps.colors_256);
assert!(caps.bracketed_paste);
assert!(caps.mouse_sgr);
assert!(caps.scroll_region);
assert!(!caps.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.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!(shared.load().true_color);
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!(!loaded.true_color);
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.true_color;
}
})
})
.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(),
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!(!caps.true_color, "NO_COLOR disables true_color");
prop_assert!(!caps.colors_256, "NO_COLOR disables colors_256");
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");
}
}
}