Skip to main content

ftui_core/
capability_override.rs

1#![forbid(unsafe_code)]
2
3//! Runtime capability override injection for testing (bd-k4lj.3).
4//!
5//! This module provides a thread-local override mechanism for terminal
6//! capabilities, enabling tests to simulate various terminal environments
7//! without modifying global state.
8//!
9//! # Overview
10//!
11//! - **Thread-local**: Overrides are scoped to the current thread, ensuring
12//!   test isolation in parallel test runs.
13//! - **Stackable**: Multiple overrides can be nested, with inner overrides
14//!   taking precedence.
15//! - **RAII-based**: Overrides are automatically removed when the guard is
16//!   dropped, even on panic.
17//!
18//! # Invariants
19//!
20//! 1. **Thread isolation**: Overrides on one thread never affect another.
21//! 2. **Stack ordering**: Later pushes override earlier ones; pops restore
22//!    the previous state.
23//! 3. **Cleanup guarantee**: Guards implement Drop to ensure cleanup even
24//!    on panic or early return.
25//! 4. **No runtime cost when unused**: If no overrides are active, capability
26//!    resolution has minimal overhead (just checking the thread-local stack).
27//!
28//! # Failure Modes
29//!
30//! | Mode | Condition | Behavior |
31//! |------|-----------|----------|
32//! | Guard leaked | Guard moved without dropping | Override persists until thread exit |
33//! | Stack underflow | Bug in guard management | Panics (debug) or no-op (release) |
34//! | Thread exit | Thread terminates with active overrides | TLS destructor cleans up |
35//!
36//! # Example
37//!
38//! ```
39//! use ftui_core::capability_override::{with_capability_override, CapabilityOverride};
40//! use ftui_core::terminal_capabilities::{ColorDepth, TerminalCapabilities};
41//!
42//! // Simulate a dumb terminal
43//! let override_cfg = CapabilityOverride::new()
44//!     .color_depth(Some(ColorDepth::Mono))
45//!     .mouse_sgr(Some(false));
46//!
47//! with_capability_override(override_cfg, || {
48//!     let caps = TerminalCapabilities::with_overrides();
49//!     assert_eq!(caps.color_depth, ColorDepth::Mono);
50//!     assert!(!caps.mouse_sgr);
51//! });
52//! ```
53
54use crate::terminal_capabilities::{ColorDepth, TerminalCapabilities};
55use std::cell::RefCell;
56
57// ============================================================================
58// Capability Override
59// ============================================================================
60
61/// Override specification for terminal capabilities.
62///
63/// Boolean fields use `Option<bool>`:
64/// - `Some(true)` - Force capability ON
65/// - `Some(false)` - Force capability OFF
66/// - `None` - Don't override (use base or previous override)
67///
68/// `color_depth` uses `Option<ColorDepth>` with the same `None` inheritance
69/// semantics.
70#[derive(Debug, Clone, Default)]
71pub struct CapabilityOverride {
72    // Color
73    pub color_depth: Option<ColorDepth>,
74
75    // Glyph support
76    pub unicode_box_drawing: Option<bool>,
77    pub unicode_emoji: Option<bool>,
78    pub double_width: Option<bool>,
79
80    // Advanced features
81    pub sync_output: Option<bool>,
82    pub osc8_hyperlinks: Option<bool>,
83    pub scroll_region: Option<bool>,
84
85    // Multiplexer flags
86    pub in_tmux: Option<bool>,
87    pub in_screen: Option<bool>,
88    pub in_zellij: Option<bool>,
89    pub in_wezterm_mux: Option<bool>,
90
91    // Input features
92    pub kitty_keyboard: Option<bool>,
93    pub focus_events: Option<bool>,
94    pub bracketed_paste: Option<bool>,
95    pub mouse_sgr: Option<bool>,
96
97    // Optional features
98    pub osc52_clipboard: Option<bool>,
99}
100
101impl CapabilityOverride {
102    /// Create a new empty override (no fields overridden).
103    #[must_use]
104    pub const fn new() -> Self {
105        Self {
106            color_depth: None,
107            unicode_box_drawing: None,
108            unicode_emoji: None,
109            double_width: None,
110            sync_output: None,
111            osc8_hyperlinks: None,
112            scroll_region: None,
113            in_tmux: None,
114            in_screen: None,
115            in_zellij: None,
116            in_wezterm_mux: None,
117            kitty_keyboard: None,
118            focus_events: None,
119            bracketed_paste: None,
120            mouse_sgr: None,
121            osc52_clipboard: None,
122        }
123    }
124
125    /// Create an override that disables all capabilities (dumb terminal).
126    #[must_use]
127    pub const fn dumb() -> Self {
128        Self {
129            color_depth: Some(ColorDepth::Mono),
130            unicode_box_drawing: Some(false),
131            unicode_emoji: Some(false),
132            double_width: Some(false),
133            sync_output: Some(false),
134            osc8_hyperlinks: Some(false),
135            scroll_region: Some(false),
136            in_tmux: Some(false),
137            in_screen: Some(false),
138            in_zellij: Some(false),
139            in_wezterm_mux: Some(false),
140            kitty_keyboard: Some(false),
141            focus_events: Some(false),
142            bracketed_paste: Some(false),
143            mouse_sgr: Some(false),
144            osc52_clipboard: Some(false),
145        }
146    }
147
148    /// Create an override that enables all capabilities (modern terminal).
149    #[must_use]
150    pub const fn modern() -> Self {
151        Self {
152            color_depth: Some(ColorDepth::TrueColor),
153            unicode_box_drawing: Some(true),
154            unicode_emoji: Some(true),
155            double_width: Some(true),
156            sync_output: Some(true),
157            osc8_hyperlinks: Some(true),
158            scroll_region: Some(true),
159            in_tmux: Some(false),
160            in_screen: Some(false),
161            in_zellij: Some(false),
162            in_wezterm_mux: Some(false),
163            kitty_keyboard: Some(true),
164            focus_events: Some(true),
165            bracketed_paste: Some(true),
166            mouse_sgr: Some(true),
167            osc52_clipboard: Some(true),
168        }
169    }
170
171    /// Create an override that simulates running inside tmux.
172    ///
173    /// The transport overlay preserves the base terminal's color depth. Use
174    /// [`TerminalCapabilities::tmux`](crate::terminal_capabilities::TerminalCapabilities::tmux)
175    /// when a concrete 256-color tmux profile is required.
176    #[must_use]
177    pub const fn tmux() -> Self {
178        Self {
179            color_depth: None,
180            unicode_box_drawing: None,
181            unicode_emoji: None,
182            double_width: None,
183            sync_output: Some(false),
184            osc8_hyperlinks: Some(false),
185            scroll_region: Some(true),
186            in_tmux: Some(true),
187            in_screen: Some(false),
188            in_zellij: Some(false),
189            in_wezterm_mux: Some(false),
190            kitty_keyboard: Some(false),
191            focus_events: Some(false),
192            bracketed_paste: Some(true),
193            mouse_sgr: Some(true),
194            osc52_clipboard: Some(false),
195        }
196    }
197
198    // ── Builder Methods ────────────────────────────────────────────────
199
200    /// Override maximum color fidelity.
201    #[must_use]
202    pub const fn color_depth(mut self, value: Option<ColorDepth>) -> Self {
203        self.color_depth = value;
204        self
205    }
206
207    /// Override Unicode box drawing support.
208    #[must_use]
209    pub const fn unicode_box_drawing(mut self, value: Option<bool>) -> Self {
210        self.unicode_box_drawing = value;
211        self
212    }
213
214    /// Override emoji glyph support.
215    #[must_use]
216    pub const fn unicode_emoji(mut self, value: Option<bool>) -> Self {
217        self.unicode_emoji = value;
218        self
219    }
220
221    /// Override double-width glyph support.
222    #[must_use]
223    pub const fn double_width(mut self, value: Option<bool>) -> Self {
224        self.double_width = value;
225        self
226    }
227
228    /// Override synchronized output support.
229    #[must_use]
230    pub const fn sync_output(mut self, value: Option<bool>) -> Self {
231        self.sync_output = value;
232        self
233    }
234
235    /// Override OSC 8 hyperlinks support.
236    #[must_use]
237    pub const fn osc8_hyperlinks(mut self, value: Option<bool>) -> Self {
238        self.osc8_hyperlinks = value;
239        self
240    }
241
242    /// Override scroll region support.
243    #[must_use]
244    pub const fn scroll_region(mut self, value: Option<bool>) -> Self {
245        self.scroll_region = value;
246        self
247    }
248
249    /// Override tmux detection.
250    #[must_use]
251    pub const fn in_tmux(mut self, value: Option<bool>) -> Self {
252        self.in_tmux = value;
253        self
254    }
255
256    /// Override GNU screen detection.
257    #[must_use]
258    pub const fn in_screen(mut self, value: Option<bool>) -> Self {
259        self.in_screen = value;
260        self
261    }
262
263    /// Override Zellij detection.
264    #[must_use]
265    pub const fn in_zellij(mut self, value: Option<bool>) -> Self {
266        self.in_zellij = value;
267        self
268    }
269
270    /// Override WezTerm mux detection.
271    #[must_use]
272    pub const fn in_wezterm_mux(mut self, value: Option<bool>) -> Self {
273        self.in_wezterm_mux = value;
274        self
275    }
276
277    /// Override Kitty keyboard protocol support.
278    #[must_use]
279    pub const fn kitty_keyboard(mut self, value: Option<bool>) -> Self {
280        self.kitty_keyboard = value;
281        self
282    }
283
284    /// Override focus events support.
285    #[must_use]
286    pub const fn focus_events(mut self, value: Option<bool>) -> Self {
287        self.focus_events = value;
288        self
289    }
290
291    /// Override bracketed paste mode support.
292    #[must_use]
293    pub const fn bracketed_paste(mut self, value: Option<bool>) -> Self {
294        self.bracketed_paste = value;
295        self
296    }
297
298    /// Override SGR mouse protocol support.
299    #[must_use]
300    pub const fn mouse_sgr(mut self, value: Option<bool>) -> Self {
301        self.mouse_sgr = value;
302        self
303    }
304
305    /// Override OSC 52 clipboard support.
306    #[must_use]
307    pub const fn osc52_clipboard(mut self, value: Option<bool>) -> Self {
308        self.osc52_clipboard = value;
309        self
310    }
311
312    /// Check if any capability is overridden.
313    #[must_use]
314    pub const fn is_empty(&self) -> bool {
315        self.color_depth.is_none()
316            && self.unicode_box_drawing.is_none()
317            && self.unicode_emoji.is_none()
318            && self.double_width.is_none()
319            && self.sync_output.is_none()
320            && self.osc8_hyperlinks.is_none()
321            && self.scroll_region.is_none()
322            && self.in_tmux.is_none()
323            && self.in_screen.is_none()
324            && self.in_zellij.is_none()
325            && self.in_wezterm_mux.is_none()
326            && self.kitty_keyboard.is_none()
327            && self.focus_events.is_none()
328            && self.bracketed_paste.is_none()
329            && self.mouse_sgr.is_none()
330            && self.osc52_clipboard.is_none()
331    }
332
333    /// Apply this override on top of base capabilities.
334    #[must_use]
335    pub fn apply_to(&self, mut caps: TerminalCapabilities) -> TerminalCapabilities {
336        if let Some(v) = self.color_depth {
337            caps.color_depth = v;
338        }
339        if let Some(v) = self.unicode_box_drawing {
340            caps.unicode_box_drawing = v;
341        }
342        if let Some(v) = self.unicode_emoji {
343            caps.unicode_emoji = v;
344        }
345        if let Some(v) = self.double_width {
346            caps.double_width = v;
347        }
348        if let Some(v) = self.sync_output {
349            caps.sync_output = v;
350        }
351        if let Some(v) = self.osc8_hyperlinks {
352            caps.osc8_hyperlinks = v;
353        }
354        if let Some(v) = self.scroll_region {
355            caps.scroll_region = v;
356        }
357        if let Some(v) = self.in_tmux {
358            caps.in_tmux = v;
359        }
360        if let Some(v) = self.in_screen {
361            caps.in_screen = v;
362        }
363        if let Some(v) = self.in_zellij {
364            caps.in_zellij = v;
365        }
366        if let Some(v) = self.in_wezterm_mux {
367            caps.in_wezterm_mux = v;
368        }
369        if let Some(v) = self.kitty_keyboard {
370            caps.kitty_keyboard = v;
371        }
372        if let Some(v) = self.focus_events {
373            caps.focus_events = v;
374        }
375        if let Some(v) = self.bracketed_paste {
376            caps.bracketed_paste = v;
377        }
378        if let Some(v) = self.mouse_sgr {
379            caps.mouse_sgr = v;
380        }
381        if let Some(v) = self.osc52_clipboard {
382            caps.osc52_clipboard = v;
383        }
384        caps
385    }
386}
387
388// ============================================================================
389// Thread-Local Override Stack
390// ============================================================================
391
392thread_local! {
393    /// Stack of active capability overrides for this thread.
394    static OVERRIDE_STACK: RefCell<Vec<CapabilityOverride>> = const { RefCell::new(Vec::new()) };
395}
396
397/// RAII guard that removes an override when dropped.
398///
399/// Do not leak this guard - it must be dropped to restore the previous state.
400#[must_use]
401pub struct OverrideGuard {
402    /// Marker to prevent Send/Sync (thread-local data)
403    _marker: std::marker::PhantomData<*const ()>,
404}
405
406impl Drop for OverrideGuard {
407    fn drop(&mut self) {
408        // Silently ignore if stack is empty - this can happen if clear_all_overrides()
409        // was called while guards were still active. This is documented behavior.
410        OVERRIDE_STACK.with(|stack| {
411            stack.borrow_mut().pop();
412        });
413    }
414}
415
416/// Push an override onto the thread-local stack.
417///
418/// Returns a guard that will pop the override when dropped.
419///
420/// # Example
421///
422/// ```
423/// use ftui_core::capability_override::{push_override, CapabilityOverride};
424///
425/// let _guard = push_override(CapabilityOverride::dumb());
426/// // Override is active here
427/// // Automatically removed when _guard is dropped
428/// ```
429#[must_use = "the override is removed when the guard is dropped"]
430pub fn push_override(over: CapabilityOverride) -> OverrideGuard {
431    OVERRIDE_STACK.with(|stack| {
432        stack.borrow_mut().push(over);
433    });
434    OverrideGuard {
435        _marker: std::marker::PhantomData,
436    }
437}
438
439/// Execute a closure with a capability override active.
440///
441/// The override is automatically removed when the closure returns,
442/// even if it panics.
443///
444/// # Example
445///
446/// ```
447/// use ftui_core::capability_override::{with_capability_override, CapabilityOverride};
448/// use ftui_core::terminal_capabilities::{ColorDepth, TerminalCapabilities};
449///
450/// with_capability_override(CapabilityOverride::dumb(), || {
451///     let caps = TerminalCapabilities::with_overrides();
452///     assert_eq!(caps.color_depth, ColorDepth::Mono);
453/// });
454/// ```
455pub fn with_capability_override<F, R>(over: CapabilityOverride, f: F) -> R
456where
457    F: FnOnce() -> R,
458{
459    let _guard = push_override(over);
460    f()
461}
462
463/// Get the current effective capabilities with all overrides applied.
464///
465/// This starts with `TerminalCapabilities::detect()` and applies each
466/// override in the stack from bottom to top.
467#[must_use]
468pub fn current_capabilities() -> TerminalCapabilities {
469    let base = TerminalCapabilities::detect();
470    current_capabilities_with_base(base)
471}
472
473/// Get effective capabilities starting from a specified base.
474#[must_use]
475pub fn current_capabilities_with_base(base: TerminalCapabilities) -> TerminalCapabilities {
476    OVERRIDE_STACK.with(|stack| {
477        let stack = stack.borrow();
478        stack.iter().fold(base, |caps, over| over.apply_to(caps))
479    })
480}
481
482/// Check if any overrides are currently active on this thread.
483#[must_use]
484pub fn has_active_overrides() -> bool {
485    OVERRIDE_STACK.with(|stack| !stack.borrow().is_empty())
486}
487
488/// Get the number of active overrides on this thread.
489#[must_use]
490pub fn override_depth() -> usize {
491    OVERRIDE_STACK.with(|stack| stack.borrow().len())
492}
493
494/// Clear all overrides on this thread.
495///
496/// **Warning**: This bypasses RAII guards and should only be used for
497/// cleanup in test harnesses, not in production code.
498pub fn clear_all_overrides() {
499    OVERRIDE_STACK.with(|stack| {
500        stack.borrow_mut().clear();
501    });
502}
503
504// ============================================================================
505// Extension to TerminalCapabilities
506// ============================================================================
507
508/// Parse an operator policy switch: `1`/`true`/`on`/`yes` enable,
509/// `0`/`false`/`off`/`no` disable, anything else (including unset) is `None`.
510fn policy_switch(value: Option<String>) -> Option<bool> {
511    let value = value?;
512    match value.trim().to_ascii_lowercase().as_str() {
513        "1" | "true" | "on" | "yes" => Some(true),
514        "0" | "false" | "off" | "no" => Some(false),
515        _ => None,
516    }
517}
518
519/// Which operator policy switches were present and what they forced.
520///
521/// Returned by [`apply_env_policy_overrides`] so the runtime can record the
522/// override in its capability decision ledger (`capability_decision`
523/// evidence rows) instead of silently mutating the detected capabilities.
524#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
525pub struct PolicyOverrides {
526    /// `FTUI_SYNC_OUTPUT` forced synchronized output on (`Some(true)`) or
527    /// off (`Some(false)`); `None` when the switch was absent or unparsable.
528    pub sync_output: Option<bool>,
529    /// `FTUI_SCROLL_REGION`, same encoding.
530    pub scroll_region: Option<bool>,
531}
532
533impl PolicyOverrides {
534    /// `true` when no switch was applied.
535    #[must_use]
536    pub const fn is_empty(&self) -> bool {
537        self.sync_output.is_none() && self.scroll_region.is_none()
538    }
539}
540
541/// Apply the operator environment switches that override detection policy.
542///
543/// - `FTUI_SYNC_OUTPUT=1|0` forces synchronized-output (DEC 2026) brackets on
544///   or off. `1` also lifts the conservative WezTerm-identity gate, because an
545///   explicit operator statement outranks an identity heuristic; real
546///   multiplexer evidence (tmux/screen/zellij) is never overridden.
547/// - `FTUI_SCROLL_REGION=1|0` does the same for the inline scroll-region
548///   (DECSTBM) strategy.
549///
550/// These exist so a user on a terminal the allowlist does not know (or one
551/// where a probe cannot run) can opt in without rebuilding, and so a flaky
552/// terminal can be opted out without editing code.
553///
554/// Returns which switches were applied so callers can log the decision.
555pub fn apply_env_policy_overrides_with<F>(
556    caps: &mut TerminalCapabilities,
557    get_env: F,
558) -> PolicyOverrides
559where
560    F: Fn(&str) -> Option<String>,
561{
562    let overrides = PolicyOverrides {
563        sync_output: policy_switch(get_env("FTUI_SYNC_OUTPUT")),
564        scroll_region: policy_switch(get_env("FTUI_SCROLL_REGION")),
565    };
566    if let Some(enabled) = overrides.sync_output {
567        caps.sync_output = enabled;
568        if enabled {
569            caps.in_wezterm_mux = false;
570        }
571    }
572    if let Some(enabled) = overrides.scroll_region {
573        caps.scroll_region = enabled;
574        if enabled {
575            caps.in_wezterm_mux = false;
576        }
577    }
578    overrides
579}
580
581/// Apply the operator environment switches from the process environment.
582///
583/// Returns which switches were applied so callers can log the decision.
584pub fn apply_env_policy_overrides(caps: &mut TerminalCapabilities) -> PolicyOverrides {
585    apply_env_policy_overrides_with(caps, |key| std::env::var(key).ok())
586}
587
588impl TerminalCapabilities {
589    /// Detect capabilities and apply any active thread-local overrides,
590    /// then the operator environment switches (`FTUI_SYNC_OUTPUT`,
591    /// `FTUI_SCROLL_REGION`; see [`apply_env_policy_overrides`]).
592    ///
593    /// This is the recommended way to get capabilities in code that may
594    /// be running under test with overrides.
595    #[must_use]
596    pub fn with_overrides() -> Self {
597        let mut caps = current_capabilities();
598        let _applied = apply_env_policy_overrides(&mut caps);
599        caps
600    }
601
602    /// Apply overrides to these capabilities.
603    #[must_use]
604    pub fn with_overrides_from(self, base: Self) -> Self {
605        current_capabilities_with_base(base)
606    }
607}
608
609// ============================================================================
610// Tests
611// ============================================================================
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    fn env_from<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
618        move |key| {
619            pairs
620                .iter()
621                .find(|(k, _)| *k == key)
622                .map(|(_, v)| (*v).to_string())
623        }
624    }
625
626    #[test]
627    fn env_policy_switch_parses_common_spellings() {
628        for on in ["1", "true", "ON", " yes "] {
629            assert_eq!(policy_switch(Some(on.to_string())), Some(true), "{on:?}");
630        }
631        for off in ["0", "false", "Off", "no"] {
632            assert_eq!(policy_switch(Some(off.to_string())), Some(false), "{off:?}");
633        }
634        assert_eq!(policy_switch(Some("maybe".to_string())), None);
635        assert_eq!(policy_switch(None), None);
636    }
637
638    #[test]
639    fn env_policy_override_forces_sync_output_on_and_lifts_wezterm_gate() {
640        let mut caps = TerminalCapabilities::xterm_256color();
641        caps.in_wezterm_mux = true;
642        assert!(!caps.use_sync_output());
643        apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
644        assert!(caps.sync_output);
645        assert!(
646            !caps.in_wezterm_mux,
647            "explicit opt-in outranks the identity gate"
648        );
649        assert!(caps.use_sync_output());
650    }
651
652    #[test]
653    fn env_policy_override_never_lifts_real_multiplexer_evidence() {
654        let mut caps = TerminalCapabilities::modern();
655        caps.in_tmux = true;
656        apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "1")]));
657        assert!(caps.sync_output);
658        assert!(caps.in_tmux);
659        assert!(!caps.use_sync_output(), "tmux policy still wins");
660    }
661
662    #[test]
663    fn env_policy_override_forces_off_and_ignores_unset_or_junk() {
664        let mut caps = TerminalCapabilities::modern();
665        apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "0")]));
666        assert!(!caps.sync_output);
667        assert!(caps.scroll_region, "unrelated switch untouched");
668
669        let mut caps = TerminalCapabilities::modern();
670        apply_env_policy_overrides_with(
671            &mut caps,
672            env_from(&[("FTUI_SYNC_OUTPUT", "banana"), ("FTUI_SCROLL_REGION", "0")]),
673        );
674        assert!(caps.sync_output, "junk value ignored");
675        assert!(!caps.scroll_region);
676        assert!(!caps.use_scroll_region());
677    }
678
679    #[test]
680    fn env_policy_override_reports_which_switches_applied() {
681        let mut caps = TerminalCapabilities::modern();
682        let none = apply_env_policy_overrides_with(&mut caps, env_from(&[]));
683        assert!(none.is_empty());
684        assert_eq!(none, PolicyOverrides::default());
685
686        let mut caps = TerminalCapabilities::modern();
687        let applied = apply_env_policy_overrides_with(
688            &mut caps,
689            env_from(&[("FTUI_SYNC_OUTPUT", "off"), ("FTUI_SCROLL_REGION", "yes")]),
690        );
691        assert_eq!(applied.sync_output, Some(false));
692        assert_eq!(applied.scroll_region, Some(true));
693        assert!(!applied.is_empty());
694        assert!(!caps.sync_output);
695        assert!(caps.scroll_region);
696
697        let mut caps = TerminalCapabilities::modern();
698        let junk =
699            apply_env_policy_overrides_with(&mut caps, env_from(&[("FTUI_SYNC_OUTPUT", "maybe")]));
700        assert!(junk.is_empty(), "unparsable switch reports nothing applied");
701    }
702
703    #[test]
704    fn override_new_is_empty() {
705        let over = CapabilityOverride::new();
706        assert!(over.is_empty());
707    }
708
709    #[test]
710    fn override_dumb_disables_all() {
711        let over = CapabilityOverride::dumb();
712        assert!(!over.is_empty());
713        assert_eq!(over.color_depth, Some(ColorDepth::Mono));
714        assert_eq!(over.sync_output, Some(false));
715        assert_eq!(over.mouse_sgr, Some(false));
716    }
717
718    #[test]
719    fn override_modern_enables_all() {
720        let over = CapabilityOverride::modern();
721        assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
722        assert_eq!(over.sync_output, Some(true));
723        assert_eq!(over.kitty_keyboard, Some(true));
724        // But mux flags are false
725        assert_eq!(over.in_tmux, Some(false));
726    }
727
728    #[test]
729    fn override_tmux_sets_mux() {
730        let over = CapabilityOverride::tmux();
731        assert_eq!(over.in_tmux, Some(true));
732        assert_eq!(over.sync_output, Some(false));
733        assert_eq!(over.osc52_clipboard, Some(false));
734    }
735
736    #[test]
737    fn override_builder_chain() {
738        let over = CapabilityOverride::new()
739            .color_depth(Some(ColorDepth::TrueColor))
740            .unicode_box_drawing(Some(false))
741            .mouse_sgr(Some(false));
742
743        assert_eq!(over.color_depth, Some(ColorDepth::TrueColor));
744        assert_eq!(over.unicode_box_drawing, Some(false));
745        assert_eq!(over.mouse_sgr, Some(false));
746        assert!(over.sync_output.is_none());
747    }
748
749    #[test]
750    fn apply_to_overrides_caps() {
751        let base = TerminalCapabilities::dumb();
752        let over = CapabilityOverride::new()
753            .color_depth(Some(ColorDepth::TrueColor))
754            .unicode_box_drawing(Some(true));
755
756        let result = over.apply_to(base);
757        assert_eq!(result.color_depth, ColorDepth::TrueColor);
758        assert!(result.unicode_box_drawing);
759        // Unchanged fields remain from base
760        assert!(!result.mouse_sgr);
761    }
762
763    #[test]
764    fn apply_to_none_keeps_original() {
765        let base = TerminalCapabilities::modern();
766        let over = CapabilityOverride::new(); // All None
767
768        let result = over.apply_to(base);
769        assert_eq!(result.color_depth, base.color_depth);
770        assert_eq!(result.mouse_sgr, base.mouse_sgr);
771    }
772
773    #[test]
774    fn push_pop_override() {
775        clear_all_overrides();
776        assert!(!has_active_overrides());
777        assert_eq!(override_depth(), 0);
778
779        {
780            let _guard = push_override(CapabilityOverride::dumb());
781            assert!(has_active_overrides());
782            assert_eq!(override_depth(), 1);
783        }
784
785        assert!(!has_active_overrides());
786        assert_eq!(override_depth(), 0);
787    }
788
789    #[test]
790    fn nested_overrides() {
791        clear_all_overrides();
792
793        {
794            let _outer = push_override(
795                CapabilityOverride::new()
796                    .color_depth(Some(ColorDepth::TrueColor))
797                    .mouse_sgr(Some(true)),
798            );
799            assert_eq!(override_depth(), 1);
800
801            {
802                let _inner =
803                    push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
804                assert_eq!(override_depth(), 2);
805
806                // Inner override takes precedence
807                let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
808                assert_eq!(caps.color_depth, ColorDepth::Mono);
809                assert!(caps.mouse_sgr); // Outer: true
810            }
811
812            // Inner dropped, outer still active
813            assert_eq!(override_depth(), 1);
814            let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
815            assert_eq!(caps.color_depth, ColorDepth::TrueColor);
816        }
817
818        assert_eq!(override_depth(), 0);
819    }
820
821    #[test]
822    fn with_capability_override_scope() {
823        clear_all_overrides();
824
825        let result = with_capability_override(CapabilityOverride::modern(), || {
826            assert!(has_active_overrides());
827            let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
828            caps.supports_true_color()
829        });
830
831        assert!(result);
832        assert!(!has_active_overrides());
833    }
834
835    #[test]
836    fn with_capability_override_nested() {
837        clear_all_overrides();
838
839        with_capability_override(
840            CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)),
841            || {
842                with_capability_override(CapabilityOverride::new().mouse_sgr(Some(false)), || {
843                    let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
844                    assert_eq!(caps.color_depth, ColorDepth::TrueColor);
845                    assert!(!caps.mouse_sgr);
846                });
847            },
848        );
849    }
850
851    #[test]
852    fn with_overrides_method() {
853        clear_all_overrides();
854
855        with_capability_override(CapabilityOverride::dumb(), || {
856            let caps = TerminalCapabilities::with_overrides();
857            assert_eq!(caps.color_depth, ColorDepth::Mono);
858            assert!(!caps.unicode_box_drawing);
859            assert!(!caps.unicode_emoji);
860            assert!(!caps.double_width);
861        });
862    }
863
864    #[test]
865    fn clear_all_overrides_works() {
866        let _g1 = push_override(CapabilityOverride::dumb());
867        let _g2 = push_override(CapabilityOverride::modern());
868        assert_eq!(override_depth(), 2);
869
870        clear_all_overrides();
871        assert_eq!(override_depth(), 0);
872    }
873
874    #[test]
875    fn default_override_is_empty() {
876        let over = CapabilityOverride::default();
877        assert!(over.is_empty());
878    }
879
880    #[test]
881    fn is_empty_false_for_single_override() {
882        let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
883        assert!(!over.is_empty());
884    }
885
886    #[test]
887    fn dumb_disables_all_fields() {
888        let over = CapabilityOverride::dumb();
889        assert_eq!(over.unicode_box_drawing, Some(false));
890        assert_eq!(over.unicode_emoji, Some(false));
891        assert_eq!(over.double_width, Some(false));
892        assert_eq!(over.osc8_hyperlinks, Some(false));
893        assert_eq!(over.scroll_region, Some(false));
894        assert_eq!(over.kitty_keyboard, Some(false));
895        assert_eq!(over.focus_events, Some(false));
896        assert_eq!(over.bracketed_paste, Some(false));
897        assert_eq!(over.osc52_clipboard, Some(false));
898        assert_eq!(over.in_tmux, Some(false));
899        assert_eq!(over.in_screen, Some(false));
900        assert_eq!(over.in_zellij, Some(false));
901    }
902
903    #[test]
904    fn modern_enables_features_disables_mux() {
905        let over = CapabilityOverride::modern();
906        assert_eq!(over.unicode_box_drawing, Some(true));
907        assert_eq!(over.unicode_emoji, Some(true));
908        assert_eq!(over.double_width, Some(true));
909        assert_eq!(over.osc8_hyperlinks, Some(true));
910        assert_eq!(over.scroll_region, Some(true));
911        assert_eq!(over.focus_events, Some(true));
912        assert_eq!(over.bracketed_paste, Some(true));
913        assert_eq!(over.osc52_clipboard, Some(true));
914        assert_eq!(over.in_screen, Some(false));
915        assert_eq!(over.in_zellij, Some(false));
916    }
917
918    #[test]
919    fn tmux_preserves_color_depth_and_sets_bracketed_paste() {
920        let over = CapabilityOverride::tmux();
921        assert_eq!(over.color_depth, None);
922        assert_eq!(over.bracketed_paste, Some(true));
923        assert_eq!(over.mouse_sgr, Some(true));
924        assert_eq!(over.scroll_region, Some(true));
925        assert_eq!(over.kitty_keyboard, Some(false));
926    }
927
928    #[test]
929    fn builder_all_optional_features() {
930        let over = CapabilityOverride::new()
931            .unicode_emoji(Some(true))
932            .double_width(Some(false))
933            .in_screen(Some(true))
934            .in_zellij(Some(true))
935            .osc8_hyperlinks(Some(true))
936            .osc52_clipboard(Some(false))
937            .scroll_region(Some(true))
938            .focus_events(Some(true))
939            .bracketed_paste(Some(false))
940            .kitty_keyboard(Some(true));
941
942        assert_eq!(over.unicode_emoji, Some(true));
943        assert_eq!(over.double_width, Some(false));
944        assert_eq!(over.in_screen, Some(true));
945        assert_eq!(over.in_zellij, Some(true));
946        assert_eq!(over.osc8_hyperlinks, Some(true));
947        assert_eq!(over.osc52_clipboard, Some(false));
948        assert_eq!(over.scroll_region, Some(true));
949        assert_eq!(over.focus_events, Some(true));
950        assert_eq!(over.bracketed_paste, Some(false));
951        assert_eq!(over.kitty_keyboard, Some(true));
952    }
953
954    #[test]
955    fn apply_to_covers_all_mux_flags() {
956        let base = TerminalCapabilities::dumb();
957        let over = CapabilityOverride::new()
958            .in_tmux(Some(true))
959            .in_screen(Some(true))
960            .in_zellij(Some(true))
961            .in_wezterm_mux(Some(true));
962        let result = over.apply_to(base);
963        assert!(result.in_tmux);
964        assert!(result.in_screen);
965        assert!(result.in_zellij);
966        assert!(result.in_wezterm_mux);
967    }
968
969    #[test]
970    fn modern_override_simulates_non_mux_even_under_wezterm() {
971        // Regression: CapabilityOverride had no in_wezterm_mux field, so
972        // modern() could not neutralize a WezTerm host — in_any_mux() stayed
973        // true and use_sync_output()/use_scroll_region()/use_clipboard()
974        // returned false, making override-based tests host-dependent.
975        let mut base = TerminalCapabilities::modern();
976        base.in_wezterm_mux = true; // simulate detection on a WezTerm host
977
978        let result = CapabilityOverride::modern().apply_to(base);
979        assert!(!result.in_wezterm_mux);
980        assert!(!result.in_any_mux());
981        assert!(result.use_sync_output());
982        assert!(result.use_scroll_region());
983        assert!(result.use_hyperlinks());
984        assert!(result.use_clipboard());
985    }
986
987    #[test]
988    fn dumb_override_clears_wezterm_mux() {
989        let mut base = TerminalCapabilities::modern();
990        base.in_wezterm_mux = true;
991        let result = CapabilityOverride::dumb().apply_to(base);
992        assert!(!result.in_wezterm_mux);
993    }
994
995    #[test]
996    fn is_empty_false_for_in_wezterm_mux() {
997        assert!(
998            !CapabilityOverride::new()
999                .in_wezterm_mux(Some(true))
1000                .is_empty()
1001        );
1002    }
1003
1004    #[test]
1005    fn apply_to_covers_input_features() {
1006        let base = TerminalCapabilities::dumb();
1007        let over = CapabilityOverride::new()
1008            .kitty_keyboard(Some(true))
1009            .focus_events(Some(true))
1010            .bracketed_paste(Some(true))
1011            .osc52_clipboard(Some(true));
1012        let result = over.apply_to(base);
1013        assert!(result.kitty_keyboard);
1014        assert!(result.focus_events);
1015        assert!(result.bracketed_paste);
1016        assert!(result.osc52_clipboard);
1017    }
1018
1019    #[test]
1020    fn current_capabilities_with_base_composes_stack() {
1021        clear_all_overrides();
1022        let base = TerminalCapabilities::dumb();
1023
1024        let _g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1025        let _g2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1026
1027        let caps = current_capabilities_with_base(base);
1028        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1029        assert!(caps.mouse_sgr);
1030
1031        clear_all_overrides();
1032    }
1033
1034    #[test]
1035    fn override_clone() {
1036        let over = CapabilityOverride::new()
1037            .color_depth(Some(ColorDepth::TrueColor))
1038            .in_tmux(Some(false));
1039        let cloned = over.clone();
1040        assert_eq!(over.color_depth, cloned.color_depth);
1041        assert_eq!(over.in_tmux, cloned.in_tmux);
1042    }
1043
1044    // ── is_empty per-field ────────────────────────────────────────────
1045
1046    #[test]
1047    fn is_empty_false_for_color_depth() {
1048        assert!(
1049            !CapabilityOverride::new()
1050                .color_depth(Some(ColorDepth::Ansi256))
1051                .is_empty()
1052        );
1053    }
1054
1055    #[test]
1056    fn is_empty_false_for_unicode_box_drawing() {
1057        assert!(
1058            !CapabilityOverride::new()
1059                .unicode_box_drawing(Some(false))
1060                .is_empty()
1061        );
1062    }
1063
1064    #[test]
1065    fn is_empty_false_for_unicode_emoji() {
1066        assert!(
1067            !CapabilityOverride::new()
1068                .unicode_emoji(Some(true))
1069                .is_empty()
1070        );
1071    }
1072
1073    #[test]
1074    fn is_empty_false_for_double_width() {
1075        assert!(
1076            !CapabilityOverride::new()
1077                .double_width(Some(true))
1078                .is_empty()
1079        );
1080    }
1081
1082    #[test]
1083    fn is_empty_false_for_sync_output() {
1084        assert!(
1085            !CapabilityOverride::new()
1086                .sync_output(Some(false))
1087                .is_empty()
1088        );
1089    }
1090
1091    #[test]
1092    fn is_empty_false_for_osc8_hyperlinks() {
1093        assert!(
1094            !CapabilityOverride::new()
1095                .osc8_hyperlinks(Some(true))
1096                .is_empty()
1097        );
1098    }
1099
1100    #[test]
1101    fn is_empty_false_for_scroll_region() {
1102        assert!(
1103            !CapabilityOverride::new()
1104                .scroll_region(Some(true))
1105                .is_empty()
1106        );
1107    }
1108
1109    #[test]
1110    fn is_empty_false_for_in_tmux() {
1111        assert!(!CapabilityOverride::new().in_tmux(Some(true)).is_empty());
1112    }
1113
1114    #[test]
1115    fn is_empty_false_for_in_screen() {
1116        assert!(!CapabilityOverride::new().in_screen(Some(true)).is_empty());
1117    }
1118
1119    #[test]
1120    fn is_empty_false_for_in_zellij() {
1121        assert!(!CapabilityOverride::new().in_zellij(Some(true)).is_empty());
1122    }
1123
1124    #[test]
1125    fn is_empty_false_for_kitty_keyboard() {
1126        assert!(
1127            !CapabilityOverride::new()
1128                .kitty_keyboard(Some(true))
1129                .is_empty()
1130        );
1131    }
1132
1133    #[test]
1134    fn is_empty_false_for_focus_events() {
1135        assert!(
1136            !CapabilityOverride::new()
1137                .focus_events(Some(false))
1138                .is_empty()
1139        );
1140    }
1141
1142    #[test]
1143    fn is_empty_false_for_bracketed_paste() {
1144        assert!(
1145            !CapabilityOverride::new()
1146                .bracketed_paste(Some(true))
1147                .is_empty()
1148        );
1149    }
1150
1151    #[test]
1152    fn is_empty_false_for_mouse_sgr() {
1153        assert!(!CapabilityOverride::new().mouse_sgr(Some(true)).is_empty());
1154    }
1155
1156    #[test]
1157    fn is_empty_false_for_osc52_clipboard() {
1158        assert!(
1159            !CapabilityOverride::new()
1160                .osc52_clipboard(Some(false))
1161                .is_empty()
1162        );
1163    }
1164
1165    // ── apply_to remaining fields ─────────────────────────────────────
1166
1167    #[test]
1168    fn apply_to_covers_unicode_emoji() {
1169        let base = TerminalCapabilities::dumb();
1170        let result = CapabilityOverride::new()
1171            .unicode_emoji(Some(true))
1172            .apply_to(base);
1173        assert!(result.unicode_emoji);
1174    }
1175
1176    #[test]
1177    fn apply_to_covers_double_width() {
1178        let base = TerminalCapabilities::dumb();
1179        let result = CapabilityOverride::new()
1180            .double_width(Some(true))
1181            .apply_to(base);
1182        assert!(result.double_width);
1183    }
1184
1185    #[test]
1186    fn apply_to_covers_sync_output() {
1187        let base = TerminalCapabilities::dumb();
1188        let result = CapabilityOverride::new()
1189            .sync_output(Some(true))
1190            .apply_to(base);
1191        assert!(result.sync_output);
1192    }
1193
1194    #[test]
1195    fn apply_to_covers_osc8_hyperlinks() {
1196        let base = TerminalCapabilities::dumb();
1197        let result = CapabilityOverride::new()
1198            .osc8_hyperlinks(Some(true))
1199            .apply_to(base);
1200        assert!(result.osc8_hyperlinks);
1201    }
1202
1203    #[test]
1204    fn apply_to_covers_scroll_region() {
1205        let base = TerminalCapabilities::dumb();
1206        let result = CapabilityOverride::new()
1207            .scroll_region(Some(true))
1208            .apply_to(base);
1209        assert!(result.scroll_region);
1210    }
1211
1212    #[test]
1213    fn apply_to_covers_mouse_sgr() {
1214        let base = TerminalCapabilities::dumb();
1215        let result = CapabilityOverride::new()
1216            .mouse_sgr(Some(true))
1217            .apply_to(base);
1218        assert!(result.mouse_sgr);
1219    }
1220
1221    // ── apply_to with presets ─────────────────────────────────────────
1222
1223    #[test]
1224    fn dumb_override_disables_all_on_modern_base() {
1225        let base = TerminalCapabilities::modern();
1226        let result = CapabilityOverride::dumb().apply_to(base);
1227        assert_eq!(result.color_depth, ColorDepth::Mono);
1228        assert!(!result.unicode_box_drawing);
1229        assert!(!result.unicode_emoji);
1230        assert!(!result.double_width);
1231        assert!(!result.sync_output);
1232        assert!(!result.osc8_hyperlinks);
1233        assert!(!result.scroll_region);
1234        assert!(!result.in_tmux);
1235        assert!(!result.in_screen);
1236        assert!(!result.in_zellij);
1237        assert!(!result.kitty_keyboard);
1238        assert!(!result.focus_events);
1239        assert!(!result.bracketed_paste);
1240        assert!(!result.mouse_sgr);
1241        assert!(!result.osc52_clipboard);
1242    }
1243
1244    #[test]
1245    fn modern_override_enables_features_on_dumb_base() {
1246        let base = TerminalCapabilities::dumb();
1247        let result = CapabilityOverride::modern().apply_to(base);
1248        assert_eq!(result.color_depth, ColorDepth::TrueColor);
1249        assert!(result.unicode_box_drawing);
1250        assert!(result.unicode_emoji);
1251        assert!(result.double_width);
1252        assert!(result.sync_output);
1253        assert!(result.osc8_hyperlinks);
1254        assert!(result.scroll_region);
1255        // mux flags disabled by modern preset
1256        assert!(!result.in_tmux);
1257        assert!(!result.in_screen);
1258        assert!(!result.in_zellij);
1259        assert!(result.kitty_keyboard);
1260        assert!(result.focus_events);
1261        assert!(result.bracketed_paste);
1262        assert!(result.mouse_sgr);
1263        assert!(result.osc52_clipboard);
1264    }
1265
1266    // ── tmux None fields ──────────────────────────────────────────────
1267
1268    #[test]
1269    fn tmux_leaves_depth_and_unrelated_fields_unset() {
1270        let over = CapabilityOverride::tmux();
1271        assert_eq!(over.color_depth, None);
1272        assert!(over.unicode_box_drawing.is_none());
1273        assert!(over.unicode_emoji.is_none());
1274        assert!(over.double_width.is_none());
1275    }
1276
1277    // ── builder remaining methods ─────────────────────────────────────
1278
1279    #[test]
1280    fn builder_in_tmux_individually() {
1281        let over = CapabilityOverride::new().in_tmux(Some(true));
1282        assert_eq!(over.in_tmux, Some(true));
1283        assert!(over.color_depth.is_none()); // other fields unchanged
1284    }
1285
1286    #[test]
1287    fn builder_sync_output_individually() {
1288        let over = CapabilityOverride::new().sync_output(Some(false));
1289        assert_eq!(over.sync_output, Some(false));
1290        assert!(over.color_depth.is_none());
1291    }
1292
1293    // ── builder overwrite to None ─────────────────────────────────────
1294
1295    #[test]
1296    fn builder_overwrite_field_to_none() {
1297        let over = CapabilityOverride::new()
1298            .color_depth(Some(ColorDepth::TrueColor))
1299            .color_depth(None);
1300        assert!(over.color_depth.is_none());
1301        assert!(over.is_empty());
1302    }
1303
1304    #[test]
1305    fn builder_overwrite_dumb_field_to_none() {
1306        let over = CapabilityOverride::dumb().color_depth(None);
1307        assert!(over.color_depth.is_none());
1308        assert!(!over.is_empty()); // other fields still set
1309    }
1310
1311    // ── guard drop after clear_all is safe ────────────────────────────
1312
1313    #[test]
1314    fn guard_drop_after_clear_all_is_noop() {
1315        clear_all_overrides();
1316
1317        let guard = push_override(CapabilityOverride::dumb());
1318        assert_eq!(override_depth(), 1);
1319
1320        clear_all_overrides();
1321        assert_eq!(override_depth(), 0);
1322
1323        // Drop guard after clear - should be silent noop (pop on empty)
1324        drop(guard);
1325        assert_eq!(override_depth(), 0);
1326    }
1327
1328    #[test]
1329    fn multiple_guards_drop_after_clear_all() {
1330        clear_all_overrides();
1331
1332        let g1 = push_override(CapabilityOverride::dumb());
1333        let g2 = push_override(CapabilityOverride::modern());
1334        assert_eq!(override_depth(), 2);
1335
1336        clear_all_overrides();
1337        assert_eq!(override_depth(), 0);
1338
1339        // Both guards drop on empty stack
1340        drop(g2);
1341        drop(g1);
1342        assert_eq!(override_depth(), 0);
1343    }
1344
1345    // ── 3-level deep nesting ──────────────────────────────────────────
1346
1347    #[test]
1348    fn three_level_nesting_innermost_wins() {
1349        clear_all_overrides();
1350
1351        let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1352        let _l2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1353        let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1354
1355        assert_eq!(override_depth(), 3);
1356        let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1357        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1358
1359        clear_all_overrides();
1360    }
1361
1362    #[test]
1363    fn three_level_nesting_partial_overrides() {
1364        clear_all_overrides();
1365
1366        let _l1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1367        let _l2 = push_override(CapabilityOverride::new().mouse_sgr(Some(true)));
1368        let _l3 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Ansi256)));
1369
1370        let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1371        assert_eq!(caps.color_depth, ColorDepth::Ansi256); // l3 wins
1372        assert!(caps.mouse_sgr); // l2
1373        assert!(!caps.sync_output); // base dumb
1374
1375        clear_all_overrides();
1376    }
1377
1378    // ── with_overrides_from method ────────────────────────────────────
1379
1380    #[test]
1381    fn with_overrides_from_applies_stack() {
1382        clear_all_overrides();
1383
1384        let base = TerminalCapabilities::dumb();
1385        let _g = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1386
1387        // with_overrides_from uses current_capabilities_with_base
1388        let caps = base.with_overrides_from(base);
1389        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1390
1391        clear_all_overrides();
1392    }
1393
1394    #[test]
1395    fn with_overrides_from_without_active_overrides() {
1396        clear_all_overrides();
1397
1398        let base = TerminalCapabilities::modern();
1399        let caps = base.with_overrides_from(base);
1400        // No overrides active, should equal base
1401        assert_eq!(caps.color_depth, base.color_depth);
1402        assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1403    }
1404
1405    // ── with_capability_override panic cleanup ────────────────────────
1406
1407    #[test]
1408    fn with_capability_override_cleans_up_on_panic() {
1409        clear_all_overrides();
1410
1411        let result = std::panic::catch_unwind(|| {
1412            with_capability_override(CapabilityOverride::dumb(), || {
1413                assert!(has_active_overrides());
1414                panic!("deliberate panic");
1415            });
1416        });
1417
1418        assert!(result.is_err());
1419        // Guard should have been dropped during unwind
1420        assert!(!has_active_overrides());
1421        assert_eq!(override_depth(), 0);
1422    }
1423
1424    // ── Debug formatting ──────────────────────────────────────────────
1425
1426    #[test]
1427    fn debug_format_contains_field_names() {
1428        let over = CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor));
1429        let dbg = format!("{over:?}");
1430        assert!(dbg.contains("color_depth"));
1431        assert!(dbg.contains("TrueColor"));
1432    }
1433
1434    #[test]
1435    fn debug_format_empty_override() {
1436        let over = CapabilityOverride::new();
1437        let dbg = format!("{over:?}");
1438        assert!(dbg.contains("CapabilityOverride"));
1439        assert!(dbg.contains("None"));
1440    }
1441
1442    // ── clear_all then push resumes ───────────────────────────────────
1443
1444    #[test]
1445    fn clear_all_then_push_resumes_normally() {
1446        clear_all_overrides();
1447
1448        let _g1 = push_override(CapabilityOverride::dumb());
1449        clear_all_overrides();
1450        assert_eq!(override_depth(), 0);
1451
1452        // Push again should work normally
1453        let _g2 = push_override(CapabilityOverride::modern());
1454        assert_eq!(override_depth(), 1);
1455        assert!(has_active_overrides());
1456
1457        let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1458        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1459
1460        clear_all_overrides();
1461    }
1462
1463    // ── current_capabilities with override ────────────────────────────
1464
1465    #[test]
1466    fn current_capabilities_uses_detect_as_base() {
1467        clear_all_overrides();
1468
1469        // Force a known state via dumb override
1470        let _g = push_override(CapabilityOverride::dumb());
1471        let caps = current_capabilities();
1472        assert_eq!(caps.color_depth, ColorDepth::Mono);
1473        assert!(!caps.mouse_sgr);
1474
1475        clear_all_overrides();
1476    }
1477
1478    // ── with_overrides method ─────────────────────────────────────────
1479
1480    #[test]
1481    fn with_overrides_integrates_full_stack() {
1482        clear_all_overrides();
1483
1484        let _g = push_override(CapabilityOverride::modern());
1485        let caps = TerminalCapabilities::with_overrides();
1486        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1487        assert!(caps.kitty_keyboard);
1488        assert!(!caps.in_tmux); // modern disables mux
1489
1490        clear_all_overrides();
1491    }
1492
1493    // ── multiple guards drop ordering ─────────────────────────────────
1494
1495    #[test]
1496    fn second_guard_dropped_first_still_active() {
1497        clear_all_overrides();
1498
1499        let g1 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::TrueColor)));
1500        let g2 = push_override(CapabilityOverride::new().color_depth(Some(ColorDepth::Mono)));
1501
1502        // Drop g2 first (LIFO order)
1503        drop(g2);
1504        assert_eq!(override_depth(), 1);
1505        let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1506        assert_eq!(caps.color_depth, ColorDepth::TrueColor);
1507
1508        drop(g1);
1509        assert_eq!(override_depth(), 0);
1510    }
1511
1512    // ── with_capability_override return value propagation ─────────────
1513
1514    #[test]
1515    fn with_capability_override_returns_string() {
1516        clear_all_overrides();
1517
1518        let val = with_capability_override(CapabilityOverride::dumb(), || {
1519            String::from("computed value")
1520        });
1521        assert_eq!(val, "computed value");
1522    }
1523
1524    #[test]
1525    fn with_capability_override_returns_tuple() {
1526        clear_all_overrides();
1527
1528        let (a, b) = with_capability_override(CapabilityOverride::modern(), || {
1529            let caps = current_capabilities_with_base(TerminalCapabilities::dumb());
1530            (caps.supports_true_color(), caps.mouse_sgr)
1531        });
1532        assert!(a);
1533        assert!(b);
1534    }
1535
1536    // ── apply_to flips true to false ──────────────────────────────────
1537
1538    #[test]
1539    fn apply_to_disables_on_modern_base() {
1540        let base = TerminalCapabilities::modern();
1541        let result = CapabilityOverride::new()
1542            .color_depth(Some(ColorDepth::Ansi256))
1543            .kitty_keyboard(Some(false))
1544            .apply_to(base);
1545        assert_eq!(result.color_depth, ColorDepth::Ansi256);
1546        assert!(!result.kitty_keyboard);
1547        // Others still modern
1548        assert!(result.supports_256_colors());
1549        assert!(result.unicode_box_drawing);
1550    }
1551
1552    // ── empty override stack returns base unchanged ───────────────────
1553
1554    #[test]
1555    fn current_capabilities_with_base_no_overrides_returns_base() {
1556        clear_all_overrides();
1557
1558        let base = TerminalCapabilities::modern();
1559        let caps = current_capabilities_with_base(base);
1560        assert_eq!(caps.color_depth, base.color_depth);
1561        assert_eq!(caps.unicode_box_drawing, base.unicode_box_drawing);
1562        assert_eq!(caps.unicode_emoji, base.unicode_emoji);
1563        assert_eq!(caps.double_width, base.double_width);
1564        assert_eq!(caps.sync_output, base.sync_output);
1565        assert_eq!(caps.osc8_hyperlinks, base.osc8_hyperlinks);
1566        assert_eq!(caps.scroll_region, base.scroll_region);
1567        assert_eq!(caps.in_tmux, base.in_tmux);
1568        assert_eq!(caps.in_screen, base.in_screen);
1569        assert_eq!(caps.in_zellij, base.in_zellij);
1570        assert_eq!(caps.kitty_keyboard, base.kitty_keyboard);
1571        assert_eq!(caps.focus_events, base.focus_events);
1572        assert_eq!(caps.bracketed_paste, base.bracketed_paste);
1573        assert_eq!(caps.mouse_sgr, base.mouse_sgr);
1574        assert_eq!(caps.osc52_clipboard, base.osc52_clipboard);
1575    }
1576}