#![forbid(unsafe_code)]
use crate::terminal_capabilities::{ColorDepth, TerminalCapabilities};
use std::cell::RefCell;
#[derive(Debug, Clone, Default)]
pub struct CapabilityOverride {
pub color_depth: Option<ColorDepth>,
pub unicode_box_drawing: Option<bool>,
pub unicode_emoji: Option<bool>,
pub double_width: Option<bool>,
pub sync_output: Option<bool>,
pub osc8_hyperlinks: Option<bool>,
pub scroll_region: Option<bool>,
pub in_tmux: Option<bool>,
pub in_screen: Option<bool>,
pub in_zellij: Option<bool>,
pub in_wezterm_mux: Option<bool>,
pub kitty_keyboard: Option<bool>,
pub focus_events: Option<bool>,
pub bracketed_paste: Option<bool>,
pub mouse_sgr: Option<bool>,
pub osc52_clipboard: Option<bool>,
}
impl CapabilityOverride {
#[must_use]
pub const fn new() -> Self {
Self {
color_depth: None,
unicode_box_drawing: None,
unicode_emoji: None,
double_width: None,
sync_output: None,
osc8_hyperlinks: None,
scroll_region: None,
in_tmux: None,
in_screen: None,
in_zellij: None,
in_wezterm_mux: None,
kitty_keyboard: None,
focus_events: None,
bracketed_paste: None,
mouse_sgr: None,
osc52_clipboard: None,
}
}
#[must_use]
pub const fn dumb() -> Self {
Self {
color_depth: Some(ColorDepth::Mono),
unicode_box_drawing: Some(false),
unicode_emoji: Some(false),
double_width: Some(false),
sync_output: Some(false),
osc8_hyperlinks: Some(false),
scroll_region: Some(false),
in_tmux: Some(false),
in_screen: Some(false),
in_zellij: Some(false),
in_wezterm_mux: Some(false),
kitty_keyboard: Some(false),
focus_events: Some(false),
bracketed_paste: Some(false),
mouse_sgr: Some(false),
osc52_clipboard: Some(false),
}
}
#[must_use]
pub const fn modern() -> Self {
Self {
color_depth: Some(ColorDepth::TrueColor),
unicode_box_drawing: Some(true),
unicode_emoji: Some(true),
double_width: Some(true),
sync_output: Some(true),
osc8_hyperlinks: Some(true),
scroll_region: Some(true),
in_tmux: Some(false),
in_screen: Some(false),
in_zellij: Some(false),
in_wezterm_mux: Some(false),
kitty_keyboard: Some(true),
focus_events: Some(true),
bracketed_paste: Some(true),
mouse_sgr: Some(true),
osc52_clipboard: Some(true),
}
}
#[must_use]
pub const fn tmux() -> Self {
Self {
color_depth: None,
unicode_box_drawing: None,
unicode_emoji: None,
double_width: None,
sync_output: Some(false),
osc8_hyperlinks: Some(false),
scroll_region: Some(true),
in_tmux: Some(true),
in_screen: Some(false),
in_zellij: Some(false),
in_wezterm_mux: Some(false),
kitty_keyboard: Some(false),
focus_events: Some(false),
bracketed_paste: Some(true),
mouse_sgr: Some(true),
osc52_clipboard: Some(false),
}
}
#[must_use]
pub const fn color_depth(mut self, value: Option<ColorDepth>) -> Self {
self.color_depth = value;
self
}
#[must_use]
pub const fn unicode_box_drawing(mut self, value: Option<bool>) -> Self {
self.unicode_box_drawing = value;
self
}
#[must_use]
pub const fn unicode_emoji(mut self, value: Option<bool>) -> Self {
self.unicode_emoji = value;
self
}
#[must_use]
pub const fn double_width(mut self, value: Option<bool>) -> Self {
self.double_width = value;
self
}
#[must_use]
pub const fn sync_output(mut self, value: Option<bool>) -> Self {
self.sync_output = value;
self
}
#[must_use]
pub const fn osc8_hyperlinks(mut self, value: Option<bool>) -> Self {
self.osc8_hyperlinks = value;
self
}
#[must_use]
pub const fn scroll_region(mut self, value: Option<bool>) -> Self {
self.scroll_region = value;
self
}
#[must_use]
pub const fn in_tmux(mut self, value: Option<bool>) -> Self {
self.in_tmux = value;
self
}
#[must_use]
pub const fn in_screen(mut self, value: Option<bool>) -> Self {
self.in_screen = value;
self
}
#[must_use]
pub const fn in_zellij(mut self, value: Option<bool>) -> Self {
self.in_zellij = value;
self
}
#[must_use]
pub const fn in_wezterm_mux(mut self, value: Option<bool>) -> Self {
self.in_wezterm_mux = value;
self
}
#[must_use]
pub const fn kitty_keyboard(mut self, value: Option<bool>) -> Self {
self.kitty_keyboard = value;
self
}
#[must_use]
pub const fn focus_events(mut self, value: Option<bool>) -> Self {
self.focus_events = value;
self
}
#[must_use]
pub const fn bracketed_paste(mut self, value: Option<bool>) -> Self {
self.bracketed_paste = value;
self
}
#[must_use]
pub const fn mouse_sgr(mut self, value: Option<bool>) -> Self {
self.mouse_sgr = value;
self
}
#[must_use]
pub const fn osc52_clipboard(mut self, value: Option<bool>) -> Self {
self.osc52_clipboard = value;
self
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.color_depth.is_none()
&& self.unicode_box_drawing.is_none()
&& self.unicode_emoji.is_none()
&& self.double_width.is_none()
&& self.sync_output.is_none()
&& self.osc8_hyperlinks.is_none()
&& self.scroll_region.is_none()
&& self.in_tmux.is_none()
&& self.in_screen.is_none()
&& self.in_zellij.is_none()
&& self.in_wezterm_mux.is_none()
&& self.kitty_keyboard.is_none()
&& self.focus_events.is_none()
&& self.bracketed_paste.is_none()
&& self.mouse_sgr.is_none()
&& self.osc52_clipboard.is_none()
}
#[must_use]
pub fn apply_to(&self, mut caps: TerminalCapabilities) -> TerminalCapabilities {
if let Some(v) = self.color_depth {
caps.color_depth = v;
}
if let Some(v) = self.unicode_box_drawing {
caps.unicode_box_drawing = v;
}
if let Some(v) = self.unicode_emoji {
caps.unicode_emoji = v;
}
if let Some(v) = self.double_width {
caps.double_width = v;
}
if let Some(v) = self.sync_output {
caps.sync_output = v;
}
if let Some(v) = self.osc8_hyperlinks {
caps.osc8_hyperlinks = v;
}
if let Some(v) = self.scroll_region {
caps.scroll_region = v;
}
if let Some(v) = self.in_tmux {
caps.in_tmux = v;
}
if let Some(v) = self.in_screen {
caps.in_screen = v;
}
if let Some(v) = self.in_zellij {
caps.in_zellij = v;
}
if let Some(v) = self.in_wezterm_mux {
caps.in_wezterm_mux = v;
}
if let Some(v) = self.kitty_keyboard {
caps.kitty_keyboard = v;
}
if let Some(v) = self.focus_events {
caps.focus_events = v;
}
if let Some(v) = self.bracketed_paste {
caps.bracketed_paste = v;
}
if let Some(v) = self.mouse_sgr {
caps.mouse_sgr = v;
}
if let Some(v) = self.osc52_clipboard {
caps.osc52_clipboard = v;
}
caps
}
}
thread_local! {
static OVERRIDE_STACK: RefCell<Vec<CapabilityOverride>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
pub struct OverrideGuard {
_marker: std::marker::PhantomData<*const ()>,
}
impl Drop for OverrideGuard {
fn drop(&mut self) {
OVERRIDE_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
}
#[must_use = "the override is removed when the guard is dropped"]
pub fn push_override(over: CapabilityOverride) -> OverrideGuard {
OVERRIDE_STACK.with(|stack| {
stack.borrow_mut().push(over);
});
OverrideGuard {
_marker: std::marker::PhantomData,
}
}
pub fn with_capability_override<F, R>(over: CapabilityOverride, f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = push_override(over);
f()
}
#[must_use]
pub fn current_capabilities() -> TerminalCapabilities {
let base = TerminalCapabilities::detect();
current_capabilities_with_base(base)
}
#[must_use]
pub fn current_capabilities_with_base(base: TerminalCapabilities) -> TerminalCapabilities {
OVERRIDE_STACK.with(|stack| {
let stack = stack.borrow();
stack.iter().fold(base, |caps, over| over.apply_to(caps))
})
}
#[must_use]
pub fn has_active_overrides() -> bool {
OVERRIDE_STACK.with(|stack| !stack.borrow().is_empty())
}
#[must_use]
pub fn override_depth() -> usize {
OVERRIDE_STACK.with(|stack| stack.borrow().len())
}
pub fn clear_all_overrides() {
OVERRIDE_STACK.with(|stack| {
stack.borrow_mut().clear();
});
}
fn policy_switch(value: Option<String>) -> Option<bool> {
let value = value?;
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "on" | "yes" => Some(true),
"0" | "false" | "off" | "no" => Some(false),
_ => None,
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PolicyOverrides {
pub sync_output: Option<bool>,
pub scroll_region: Option<bool>,
}
impl PolicyOverrides {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.sync_output.is_none() && self.scroll_region.is_none()
}
}
pub fn apply_env_policy_overrides_with<F>(
caps: &mut TerminalCapabilities,
get_env: F,
) -> PolicyOverrides
where
F: Fn(&str) -> Option<String>,
{
let overrides = PolicyOverrides {
sync_output: policy_switch(get_env("FTUI_SYNC_OUTPUT")),
scroll_region: policy_switch(get_env("FTUI_SCROLL_REGION")),
};
if let Some(enabled) = overrides.sync_output {
caps.sync_output = enabled;
if enabled {
caps.in_wezterm_mux = false;
}
}
if let Some(enabled) = overrides.scroll_region {
caps.scroll_region = enabled;
if enabled {
caps.in_wezterm_mux = false;
}
}
overrides
}
pub fn apply_env_policy_overrides(caps: &mut TerminalCapabilities) -> PolicyOverrides {
apply_env_policy_overrides_with(caps, |key| std::env::var(key).ok())
}
impl TerminalCapabilities {
#[must_use]
pub fn with_overrides() -> Self {
let mut caps = current_capabilities();
let _applied = apply_env_policy_overrides(&mut caps);
caps
}
#[must_use]
pub fn with_overrides_from(self, base: Self) -> Self {
current_capabilities_with_base(base)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env_from<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
move |key| {
pairs
.iter()
.find(|(k, _)| *k == key)
.map(|(_, v)| (*v).to_string())
}
}
#[test]
fn env_policy_switch_parses_common_spellings() {
for on in ["1", "true", "ON", " yes "] {
assert_eq!(policy_switch(Some(on.to_string())), Some(true), "{on:?}");
}
for off in ["0", "false", "Off", "no"] {
assert_eq!(policy_switch(Some(off.to_string())), Some(false), "{off:?}");
}
assert_eq!(policy_switch(Some("maybe".to_string())), None);
assert_eq!(policy_switch(None), None);
}
#[test]
fn env_policy_override_forces_sync_output_on_and_lifts_wezterm_gate() {
let mut caps = TerminalCapabilities::xterm_256color();
caps.in_wezterm_mux = true;
assert!(!caps.use_sync_output());
apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
assert!(caps.sync_output);
assert!(
!caps.in_wezterm_mux,
"explicit opt-in outranks the identity gate"
);
assert!(caps.use_sync_output());
}
#[test]
fn env_policy_override_never_lifts_real_multiplexer_evidence() {
let mut caps = TerminalCapabilities::modern();
caps.in_tmux = true;
apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
assert!(caps.sync_output);
assert!(caps.in_tmux);
assert!(!caps.use_sync_output(), "tmux policy still wins");
}
#[test]
fn env_policy_override_forces_off_and_ignores_unset_or_junk() {
let mut caps = TerminalCapabilities::modern();
apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "0")]));
assert!(!caps.sync_output);
assert!(caps.scroll_region, "unrelated switch untouched");
let mut caps = TerminalCapabilities::modern();
apply_env_policy_overrides_with(
&mut caps,
env_from(&[("FTUI_SYNC_OUTPUT", "banana"), ("FTUI_SCROLL_REGION", "0")]),
);
assert!(caps.sync_output, "junk value ignored");
assert!(!caps.scroll_region);
assert!(!caps.use_scroll_region());
}
#[test]
fn env_policy_override_reports_which_switches_applied() {
let mut caps = TerminalCapabilities::modern();
let none = apply_env_policy_overrides_with(&mut caps, env_from(&[]));
assert!(none.is_empty());
assert_eq!(none, PolicyOverrides::default());
let mut caps = TerminalCapabilities::modern();
let applied = apply_env_policy_overrides_with(
&mut caps,
env_from(&[("FTUI_SYNC_OUTPUT", "off"), ("FTUI_SCROLL_REGION", "yes")]),
);
assert_eq!(applied.sync_output, Some(false));
assert_eq!(applied.scroll_region, Some(true));
assert!(!applied.is_empty());
assert!(!caps.sync_output);
assert!(caps.scroll_region);
let mut caps = TerminalCapabilities::modern();
let junk =
apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "maybe")]));
assert!(junk.is_empty(), "unparsable switch reports nothing applied");
}
#[test]
fn override_new_is_empty() {
let over = CapabilityOverride::new();
assert!(over.is_empty());
}
#[test]
fn override_dumb_disables_all() {
let over = CapabilityOverride::dumb();
assert!(!over.is_empty());
assert_eq!(over.color_depth, Some(ColorDepth::Mono));
assert_eq!(over.sync_output, Some(false));
assert_eq!(over.mouse_sgr, Some(false));
}
#[test]
fn override_modern_enables_all() {
let over = CapabilityOverride::modern();
assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
assert_eq!(over.sync_output, Some(true));
assert_eq!(over.kitty_keyboard, Some(true));
assert_eq!(over.in_tmux, Some(false));
}
#[test]
fn override_tmux_sets_mux() {
let over = CapabilityOverride::tmux();
assert_eq!(over.in_tmux, Some(true));
assert_eq!(over.sync_output, Some(false));
assert_eq!(over.osc52_clipboard, Some(false));
}
#[test]
fn override_builder_chain() {
let over = CapabilityOverride::new()
.color_depth(Some(ColorDepth::TrueColor))
.unicode_box_drawing(Some(false))
.mouse_sgr(Some(false));
assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
assert_eq!(over.unicode_box_drawing, Some(false));
assert_eq!(over.mouse_sgr, Some(false));
assert!(over.sync_output.is_none());
}
#[test]
fn apply_to_overrides_caps() {
let base = TerminalCapabilities::dumb();
let over = CapabilityOverride::new()
.color_depth(Some(ColorDepth::TrueColor))
.unicode_box_drawing(Some(true));
let result = over.apply_to(base);
assert_eq!(result.color_depth, ColorDepth::TrueColor);
assert!(result.unicode_box_drawing);
assert!(!result.mouse_sgr);
}
#[test]
fn apply_to_none_keeps_original() {
let base = TerminalCapabilities::modern();
let over = CapabilityOverride::new();
let result = over.apply_to(base);
assert_eq!(result.color_depth, base.color_depth);
assert_eq!(result.mouse_sgr, base.mouse_sgr);
}
#[test]
fn push_pop_override() {
clear_all_overrides();
assert!(!has_active_overrides());
assert_eq!(override_depth(), 0);
{
let _guard = push_override(CapabilityOverride::dumb());
assert!(has_active_overrides());
assert_eq!(override_depth(), 1);
}
assert!(!has_active_overrides());
assert_eq!(override_depth(), 0);
}
#[test]
fn nested_overrides() {
clear_all_overrides();
{
let _outer = push_override(
CapabilityOverride::new()
.color_depth(Some(ColorDepth::TrueColor))
.mouse_sgr(Some(true)),
);
assert_eq!(override_depth(), 1);
{
let _inner =
push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
assert_eq!(override_depth(), 2);
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(caps.mouse_sgr); }
assert_eq!(override_depth(), 1);
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
}
assert_eq!(override_depth(), 0);
}
#[test]
fn with_capability_override_scope() {
clear_all_overrides();
let result = with_capability_override(CapabilityOverride::modern(), || {
assert!(has_active_overrides());
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
caps.supports_true_color()
});
assert!(result);
assert!(!has_active_overrides());
}
#[test]
fn with_capability_override_nested() {
clear_all_overrides();
with_capability_override(
CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)),
|| {
with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(!caps.mouse_sgr);
});
},
);
}
#[test]
fn with_overrides_method() {
clear_all_overrides();
with_capability_override(CapabilityOverride::dumb(), || {
let caps = TerminalCapabilities::with_overrides();
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.unicode_box_drawing);
assert!(!caps.unicode_emoji);
assert!(!caps.double_width);
});
}
#[test]
fn clear_all_overrides_works() {
let _g1 = push_override(CapabilityOverride::dumb());
let _g2 = push_override(CapabilityOverride::modern());
assert_eq!(override_depth(), 2);
clear_all_overrides();
assert_eq!(override_depth(), 0);
}
#[test]
fn default_override_is_empty() {
let over = CapabilityOverride::default();
assert!(over.is_empty());
}
#[test]
fn is_empty_false_for_single_override() {
let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
assert!(!over.is_empty());
}
#[test]
fn dumb_disables_all_fields() {
let over = CapabilityOverride::dumb();
assert_eq!(over.unicode_box_drawing, Some(false));
assert_eq!(over.unicode_emoji, Some(false));
assert_eq!(over.double_width, Some(false));
assert_eq!(over.osc8_hyperlinks, Some(false));
assert_eq!(over.scroll_region, Some(false));
assert_eq!(over.kitty_keyboard, Some(false));
assert_eq!(over.focus_events, Some(false));
assert_eq!(over.bracketed_paste, Some(false));
assert_eq!(over.osc52_clipboard, Some(false));
assert_eq!(over.in_tmux, Some(false));
assert_eq!(over.in_screen, Some(false));
assert_eq!(over.in_zellij, Some(false));
}
#[test]
fn modern_enables_features_disables_mux() {
let over = CapabilityOverride::modern();
assert_eq!(over.unicode_box_drawing, Some(true));
assert_eq!(over.unicode_emoji, Some(true));
assert_eq!(over.double_width, Some(true));
assert_eq!(over.osc8_hyperlinks, Some(true));
assert_eq!(over.scroll_region, Some(true));
assert_eq!(over.focus_events, Some(true));
assert_eq!(over.bracketed_paste, Some(true));
assert_eq!(over.osc52_clipboard, Some(true));
assert_eq!(over.in_screen, Some(false));
assert_eq!(over.in_zellij, Some(false));
}
#[test]
fn tmux_preserves_color_depth_and_sets_bracketed_paste() {
let over = CapabilityOverride::tmux();
assert_eq!(over.color_depth, None);
assert_eq!(over.bracketed_paste, Some(true));
assert_eq!(over.mouse_sgr, Some(true));
assert_eq!(over.scroll_region, Some(true));
assert_eq!(over.kitty_keyboard, Some(false));
}
#[test]
fn builder_all_optional_features() {
let over = CapabilityOverride::new()
.unicode_emoji(Some(true))
.double_width(Some(false))
.in_screen(Some(true))
.in_zellij(Some(true))
.osc8_hyperlinks(Some(true))
.osc52_clipboard(Some(false))
.scroll_region(Some(true))
.focus_events(Some(true))
.bracketed_paste(Some(false))
.kitty_keyboard(Some(true));
assert_eq!(over.unicode_emoji, Some(true));
assert_eq!(over.double_width, Some(false));
assert_eq!(over.in_screen, Some(true));
assert_eq!(over.in_zellij, Some(true));
assert_eq!(over.osc8_hyperlinks, Some(true));
assert_eq!(over.osc52_clipboard, Some(false));
assert_eq!(over.scroll_region, Some(true));
assert_eq!(over.focus_events, Some(true));
assert_eq!(over.bracketed_paste, Some(false));
assert_eq!(over.kitty_keyboard, Some(true));
}
#[test]
fn apply_to_covers_all_mux_flags() {
let base = TerminalCapabilities::dumb();
let over = CapabilityOverride::new()
.in_tmux(Some(true))
.in_screen(Some(true))
.in_zellij(Some(true))
.in_wezterm_mux(Some(true));
let result = over.apply_to(base);
assert!(result.in_tmux);
assert!(result.in_screen);
assert!(result.in_zellij);
assert!(result.in_wezterm_mux);
}
#[test]
fn modern_override_simulates_non_mux_even_under_wezterm() {
let mut base = TerminalCapabilities::modern();
base.in_wezterm_mux = true;
let result = CapabilityOverride::modern().apply_to(base);
assert!(!result.in_wezterm_mux);
assert!(!result.in_any_mux());
assert!(result.use_sync_output());
assert!(result.use_scroll_region());
assert!(result.use_hyperlinks());
assert!(result.use_clipboard());
}
#[test]
fn dumb_override_clears_wezterm_mux() {
let mut base = TerminalCapabilities::modern();
base.in_wezterm_mux = true;
let result = CapabilityOverride::dumb().apply_to(base);
assert!(!result.in_wezterm_mux);
}
#[test]
fn is_empty_false_for_in_wezterm_mux() {
assert!(
!CapabilityOverride::new()
.in_wezterm_mux(Some(true))
.is_empty()
);
}
#[test]
fn apply_to_covers_input_features() {
let base = TerminalCapabilities::dumb();
let over = CapabilityOverride::new()
.kitty_keyboard(Some(true))
.focus_events(Some(true))
.bracketed_paste(Some(true))
.osc52_clipboard(Some(true));
let result = over.apply_to(base);
assert!(result.kitty_keyboard);
assert!(result.focus_events);
assert!(result.bracketed_paste);
assert!(result.osc52_clipboard);
}
#[test]
fn current_capabilities_with_base_composes_stack() {
clear_all_overrides();
let base = TerminalCapabilities::dumb();
let _g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
let caps = current_capabilities_with_base(base);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.mouse_sgr);
clear_all_overrides();
}
#[test]
fn override_clone() {
let over = CapabilityOverride::new()
.color_depth(Some(ColorDepth::TrueColor))
.in_tmux(Some(false));
let cloned = over.clone();
assert_eq!(over.color_depth, cloned.color_depth);
assert_eq!(over.in_tmux, cloned.in_tmux);
}
#[test]
fn is_empty_false_for_color_depth() {
assert!(
!CapabilityOverride::new()
.color_depth(Some(ColorDepth::Ansi256))
.is_empty()
);
}
#[test]
fn is_empty_false_for_unicode_box_drawing() {
assert!(
!CapabilityOverride::new()
.unicode_box_drawing(Some(false))
.is_empty()
);
}
#[test]
fn is_empty_false_for_unicode_emoji() {
assert!(
!CapabilityOverride::new()
.unicode_emoji(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_double_width() {
assert!(
!CapabilityOverride::new()
.double_width(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_sync_output() {
assert!(
!CapabilityOverride::new()
.sync_output(Some(false))
.is_empty()
);
}
#[test]
fn is_empty_false_for_osc8_hyperlinks() {
assert!(
!CapabilityOverride::new()
.osc8_hyperlinks(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_scroll_region() {
assert!(
!CapabilityOverride::new()
.scroll_region(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_in_tmux() {
assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
}
#[test]
fn is_empty_false_for_in_screen() {
assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
}
#[test]
fn is_empty_false_for_in_zellij() {
assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
}
#[test]
fn is_empty_false_for_kitty_keyboard() {
assert!(
!CapabilityOverride::new()
.kitty_keyboard(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_focus_events() {
assert!(
!CapabilityOverride::new()
.focus_events(Some(false))
.is_empty()
);
}
#[test]
fn is_empty_false_for_bracketed_paste() {
assert!(
!CapabilityOverride::new()
.bracketed_paste(Some(true))
.is_empty()
);
}
#[test]
fn is_empty_false_for_mouse_sgr() {
assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
}
#[test]
fn is_empty_false_for_osc52_clipboard() {
assert!(
!CapabilityOverride::new()
.osc52_clipboard(Some(false))
.is_empty()
);
}
#[test]
fn apply_to_covers_unicode_emoji() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.unicode_emoji(Some(true))
.apply_to(base);
assert!(result.unicode_emoji);
}
#[test]
fn apply_to_covers_double_width() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.double_width(Some(true))
.apply_to(base);
assert!(result.double_width);
}
#[test]
fn apply_to_covers_sync_output() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.sync_output(Some(true))
.apply_to(base);
assert!(result.sync_output);
}
#[test]
fn apply_to_covers_osc8_hyperlinks() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.osc8_hyperlinks(Some(true))
.apply_to(base);
assert!(result.osc8_hyperlinks);
}
#[test]
fn apply_to_covers_scroll_region() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.scroll_region(Some(true))
.apply_to(base);
assert!(result.scroll_region);
}
#[test]
fn apply_to_covers_mouse_sgr() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::new()
.mouse_sgr(Some(true))
.apply_to(base);
assert!(result.mouse_sgr);
}
#[test]
fn dumb_override_disables_all_on_modern_base() {
let base = TerminalCapabilities::modern();
let result = CapabilityOverride::dumb().apply_to(base);
assert_eq!(result.color_depth, ColorDepth::Mono);
assert!(!result.unicode_box_drawing);
assert!(!result.unicode_emoji);
assert!(!result.double_width);
assert!(!result.sync_output);
assert!(!result.osc8_hyperlinks);
assert!(!result.scroll_region);
assert!(!result.in_tmux);
assert!(!result.in_screen);
assert!(!result.in_zellij);
assert!(!result.kitty_keyboard);
assert!(!result.focus_events);
assert!(!result.bracketed_paste);
assert!(!result.mouse_sgr);
assert!(!result.osc52_clipboard);
}
#[test]
fn modern_override_enables_features_on_dumb_base() {
let base = TerminalCapabilities::dumb();
let result = CapabilityOverride::modern().apply_to(base);
assert_eq!(result.color_depth, ColorDepth::TrueColor);
assert!(result.unicode_box_drawing);
assert!(result.unicode_emoji);
assert!(result.double_width);
assert!(result.sync_output);
assert!(result.osc8_hyperlinks);
assert!(result.scroll_region);
assert!(!result.in_tmux);
assert!(!result.in_screen);
assert!(!result.in_zellij);
assert!(result.kitty_keyboard);
assert!(result.focus_events);
assert!(result.bracketed_paste);
assert!(result.mouse_sgr);
assert!(result.osc52_clipboard);
}
#[test]
fn tmux_leaves_depth_and_unrelated_fields_unset() {
let over = CapabilityOverride::tmux();
assert_eq!(over.color_depth, None);
assert!(over.unicode_box_drawing.is_none());
assert!(over.unicode_emoji.is_none());
assert!(over.double_width.is_none());
}
#[test]
fn builder_in_tmux_individually() {
let over = CapabilityOverride::new().in_tmux(Some(true));
assert_eq!(over.in_tmux, Some(true));
assert!(over.color_depth.is_none()); }
#[test]
fn builder_sync_output_individually() {
let over = CapabilityOverride::new().sync_output(Some(false));
assert_eq!(over.sync_output, Some(false));
assert!(over.color_depth.is_none());
}
#[test]
fn builder_overwrite_field_to_none() {
let over = CapabilityOverride::new()
.color_depth(Some(ColorDepth::TrueColor))
.color_depth(None);
assert!(over.color_depth.is_none());
assert!(over.is_empty());
}
#[test]
fn builder_overwrite_dumb_field_to_none() {
let over = CapabilityOverride::dumb().color_depth(None);
assert!(over.color_depth.is_none());
assert!(!over.is_empty()); }
#[test]
fn guard_drop_after_clear_all_is_noop() {
clear_all_overrides();
let guard = push_override(CapabilityOverride::dumb());
assert_eq!(override_depth(), 1);
clear_all_overrides();
assert_eq!(override_depth(), 0);
drop(guard);
assert_eq!(override_depth(), 0);
}
#[test]
fn multiple_guards_drop_after_clear_all() {
clear_all_overrides();
let g1 = push_override(CapabilityOverride::dumb());
let g2 = push_override(CapabilityOverride::modern());
assert_eq!(override_depth(), 2);
clear_all_overrides();
assert_eq!(override_depth(), 0);
drop(g2);
drop(g1);
assert_eq!(override_depth(), 0);
}
#[test]
fn three_level_nesting_innermost_wins() {
clear_all_overrides();
let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
let _l2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
assert_eq!(override_depth(), 3);
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
clear_all_overrides();
}
#[test]
fn three_level_nesting_partial_overrides() {
clear_all_overrides();
let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Ansi256)));
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::Ansi256); assert!(caps.mouse_sgr); assert!(!caps.sync_output);
clear_all_overrides();
}
#[test]
fn with_overrides_from_applies_stack() {
clear_all_overrides();
let base = TerminalCapabilities::dumb();
let _g = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
let caps = base.with_overrides_from(base);
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
clear_all_overrides();
}
#[test]
fn with_overrides_from_without_active_overrides() {
clear_all_overrides();
let base = TerminalCapabilities::modern();
let caps = base.with_overrides_from(base);
assert_eq!(caps.color_depth, base.color_depth);
assert_eq!(caps.mouse_sgr, base.mouse_sgr);
}
#[test]
fn with_capability_override_cleans_up_on_panic() {
clear_all_overrides();
let result = std::panic::catch_unwind(|| {
with_capability_override(CapabilityOverride::dumb(), || {
assert!(has_active_overrides());
panic!("deliberate panic");
});
});
assert!(result.is_err());
assert!(!has_active_overrides());
assert_eq!(override_depth(), 0);
}
#[test]
fn debug_format_contains_field_names() {
let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
let dbg = format!("{over:?}");
assert!(dbg.contains("color_depth"));
assert!(dbg.contains("TrueColor"));
}
#[test]
fn debug_format_empty_override() {
let over = CapabilityOverride::new();
let dbg = format!("{over:?}");
assert!(dbg.contains("CapabilityOverride"));
assert!(dbg.contains("None"));
}
#[test]
fn clear_all_then_push_resumes_normally() {
clear_all_overrides();
let _g1 = push_override(CapabilityOverride::dumb());
clear_all_overrides();
assert_eq!(override_depth(), 0);
let _g2 = push_override(CapabilityOverride::modern());
assert_eq!(override_depth(), 1);
assert!(has_active_overrides());
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
clear_all_overrides();
}
#[test]
fn current_capabilities_uses_detect_as_base() {
clear_all_overrides();
let _g = push_override(CapabilityOverride::dumb());
let caps = current_capabilities();
assert_eq!(caps.color_depth, ColorDepth::Mono);
assert!(!caps.mouse_sgr);
clear_all_overrides();
}
#[test]
fn with_overrides_integrates_full_stack() {
clear_all_overrides();
let _g = push_override(CapabilityOverride::modern());
let caps = TerminalCapabilities::with_overrides();
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
assert!(caps.kitty_keyboard);
assert!(!caps.in_tmux);
clear_all_overrides();
}
#[test]
fn second_guard_dropped_first_still_active() {
clear_all_overrides();
let g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
let g2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
drop(g2);
assert_eq!(override_depth(), 1);
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
assert_eq!(caps.color_depth, ColorDepth::TrueColor);
drop(g1);
assert_eq!(override_depth(), 0);
}
#[test]
fn with_capability_override_returns_string() {
clear_all_overrides();
let val = with_capability_override(CapabilityOverride::dumb(), || {
String::from("computed value")
});
assert_eq!(val, "computed value");
}
#[test]
fn with_capability_override_returns_tuple() {
clear_all_overrides();
let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
(caps.supports_true_color(), caps.mouse_sgr)
});
assert!(a);
assert!(b);
}
#[test]
fn apply_to_disables_on_modern_base() {
let base = TerminalCapabilities::modern();
let result = CapabilityOverride::new()
.color_depth(Some(ColorDepth::Ansi256))
.kitty_keyboard(Some(false))
.apply_to(base);
assert_eq!(result.color_depth, ColorDepth::Ansi256);
assert!(!result.kitty_keyboard);
assert!(result.supports_256_colors());
assert!(result.unicode_box_drawing);
}
#[test]
fn current_capabilities_with_base_no_overrides_returns_base() {
clear_all_overrides();
let base = TerminalCapabilities::modern();
let caps = current_capabilities_with_base(base);
assert_eq!(caps.color_depth, base.color_depth);
assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
assert_eq!(caps.unicode_emoji, base.unicode_emoji);
assert_eq!(caps.double_width, base.double_width);
assert_eq!(caps.sync_output, base.sync_output);
assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
assert_eq!(caps.scroll_region, base.scroll_region);
assert_eq!(caps.in_tmux, base.in_tmux);
assert_eq!(caps.in_screen, base.in_screen);
assert_eq!(caps.in_zellij, base.in_zellij);
assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
assert_eq!(caps.focus_events, base.focus_events);
assert_eq!(caps.bracketed_paste, base.bracketed_paste);
assert_eq!(caps.mouse_sgr, base.mouse_sgr);
assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
}
}