Skip to Content
ftui-coreTerminal capabilities

Terminal Capabilities

TerminalCapabilities is a compact feature profile — a #[derive(Copy, Clone, PartialEq, Eq)] record of booleans and enums describing what the terminal in front of the user is actually able to do. It answers questions like “is it safe to emit CSI ?2026 h here?” and “should I downgrade truecolor to 256-color?” long before any escape sequence hits stdout.

The struct is trivially cheap to pass around and to cache; detection runs once at startup and the result flows through the runtime to every subsystem that needs it (the presenter, the inline mode selector, the style pipeline). Re-detecting mid-session is not forbidden, but the capability is monotone-conservative: once detected as absent, it stays absent.

Detection is a two-stage pipeline: a fast environment scan produces a prior; optional active probes (DA1, DA2, DECRPM, OSC 11) update a per-capability Bayesian evidence ledger expressed in log-odds. The final boolean flag is produced by thresholding the posterior probability.

Motivation

Terminals lie. A $TERM of xterm-256color means the user’s system has that terminfo entry; it does not mean the process is running in xterm or even a faithful clone. $COLORTERM=truecolor is honest in WezTerm, aspirational in tmux-inside-mosh-inside-ssh, and absent in every terminal that actually supports truecolor and forgot to set it. TERM_PROGRAM is set only by iTerm, Apple Terminal, WezTerm, and a handful of others.

The sane response is evidence accumulation, not single-signal dispatch. Each environment variable contributes a log-Bayes-factor; each optional probe response contributes another; the posterior is what we believe. Log-odds makes the arithmetic associative (just additions) and makes the fail-open default (no evidence → log_odds = 0.0 → 50% → disabled by a > 0 threshold) principled rather than arbitrary.

The profile at a glance

crates/ftui-core/src/terminal_capabilities.rs
pub struct TerminalCapabilities { profile: TerminalProfile, // Detected | Kitty | Xterm256 | ... // Color pub true_color: bool, // 24-bit RGB pub colors_256: bool, // 256-color palette // Glyphs pub unicode_box_drawing: bool, pub unicode_emoji: bool, pub double_width: bool, // CJK / emoji width = 2 // Advanced features pub sync_output: bool, // DEC 2026 pub osc8_hyperlinks: bool, // OSC 8 links pub scroll_region: bool, // DECSTBM // Multiplexer flags pub in_tmux: bool, pub in_screen: bool, pub in_zellij: bool, pub in_wezterm_mux: bool, // Input pub kitty_keyboard: bool, pub focus_events: bool, pub bracketed_paste: bool, pub mouse_sgr: bool, // Optional pub osc52_clipboard: bool, }

The profile discriminant lets callers branch on named presets (TerminalProfile::Kitty, ::Tmux, …) without re-reading every boolean. TerminalCapabilities::detect() returns a fully populated instance.

Detection pipeline

┌────────────────────────────────────┐ │ env vars: TERM, COLORTERM, │ │ TERM_PROGRAM, TMUX, STY, ZELLIJ, │ │ WT_SESSION, WEZTERM_*, KITTY_*, │ │ NO_COLOR │ └────────────────┬───────────────────┘ │ prior log-odds ┌────────────────────────┐ │ CapabilityLedger × N │ one per capability └───────────┬────────────┘ ┌───────────────────────┼────────────────────────┐ ▼ ▼ ▼ DA1 / DA2 DECRPM (?2026p) OSC 11 bg color "what are you?" "is feature on?" "what's your bg?" │ │ │ ▼ ▼ ▼ log-odds update log-odds update log-odds update │ │ │ └───────────────────────┼────────────────────────┘ posterior P = logistic(Σ log_odds) bool flag = P > threshold TerminalCapabilities (final)

The ledger lives in crates/ftui-core/src/caps_probe.rs:L893-L998. Each piece of evidence is an EvidenceEntry { source, log_odds }; the posterior is the logistic of the summed log-odds.

Environment signals (prior)

detect() hashes the environment into a DetectInputs record, then assigns a prior:

VariableContribution
TERM=dumb or unset (without WT_SESSION)Forces every capability to false.
COLORTERM=truecolor | 24bitTruecolor prior +3.0 log-odds.
TERM=*256color*256-color prior +3.0.
TERM_PROGRAM ∈ (iTerm, WezTerm, Ghostty, Kitty, Alacritty, …)Broad prior boost: truecolor, emoji, hyperlinks.
KITTY_WINDOW_ID, TERM contains kittyKitty profile + keyboard protocol.
WT_SESSIONWindows Terminal identity (TERM often absent).
TMUX, STY, ZELLIJ_SESSION_ID, WEZTERM_*Multiplexer flags; disable sync output, scroll region, focus events, kitty keyboard.
NO_COLORForce true_color = false, colors_256 = false.

The “modern terminal” list (crates/ftui-core/src/terminal_capabilities.rs near MODERN_TERMINALS) catches the major names; unknown TERM_PROGRAM values default to conservative.

The Bayesian logit ledger

Log-odds arithmetic: if the prior is 0 (50%) and the environment says “COLORTERM=truecolor” (+3.0 log-odds, ~95% likelihood ratio), the posterior is logistic(3.0) ≈ 0.953. Adding a timeout from a DA1 probe (−0.4) drops this to logistic(2.6) ≈ 0.931. Two independent positive signals (env + DA2 identify kitty) compound to logistic(6.0) ≈ 0.998.

crates/ftui-core/src/caps_probe.rs
pub struct CapabilityLedger { pub capability: ProbeableCapability, total_log_odds: f64, entries: Vec<EvidenceEntry>, } impl CapabilityLedger { pub fn record(&mut self, source: EvidenceSource, log_odds: f64) { /* ... */ } pub fn probability(&self) -> f64 { logistic(self.total_log_odds) } pub fn is_supported(&self) -> bool { self.total_log_odds > 0.0 } pub fn confident_at(&self, threshold: f64) -> bool { self.probability() >= threshold } }

Confidence calibration cheatsheet:

log-oddsprobabilityinterpretation
−4.6~1 %Very unlikely
−2.2~10 %Unlikely
0.050 %No evidence
+2.2~90 %Likely
+4.6~99 %Very likely

Evidence weights live in the evidence_weights module: ENV_POSITIVE = +3.0, ENV_ABSENT = −0.4, and so on. Tuning any individual weight shifts the posterior monotonically for that capability — a useful property when we want to sharpen a detection without re-deriving the whole pipeline.

P(supportedevidence)=σ(ii),σ(x)=11+exP(\text{supported} \mid \text{evidence}) = \sigma\left(\sum_i \ell_i\right), \quad \sigma(x) = \frac{1}{1 + e^{-x}}

with i=lnP(datumiH)P(datumi¬H)\ell_i = \ln \frac{P(\text{datum}_i \mid H)}{P(\text{datum}_i \mid \neg H)}.

Multiplexer safety

Two capabilities are deliberately force-disabled inside any multiplexer — tmux, screen, zellij, wezterm-mux — regardless of the underlying terminal:

  • sync_output (DEC 2026). Bracket sequences pass through most muxes unmodified, which sounds fine, but inner sync on a flaky mux creates visible flashes when the mux re-emits partial frames. The use_sync_output() policy method returns false in any mux.
  • scroll_region (DECSTBM). Behavior varies wildly across tmux versions; some leak the region across panes. The use_scroll_region() policy method returns false in any mux.

See crates/ftui-core/src/terminal_capabilities.rs:L68-L90 for the policy comment and in_any_mux() for the predicate.

Usage

use ftui_core::terminal_capabilities::TerminalCapabilities; let caps = TerminalCapabilities::detect(); if caps.true_color { // Emit 24-bit RGB SGR sequences. } if caps.use_sync_output() { // Safe to wrap the frame in DEC 2026 brackets. }

For tests, prefer the explicit profile constructors (TerminalCapabilities::kitty(), ::xterm_256color(), ::dumb()) so the test does not depend on ambient environment.

Don’t mutate TerminalCapabilities after detection unless you know what you’re doing. Capability monotonicity is a session invariant: downstream code (the presenter’s state tracker, the inline-mode selector, the style cascade) caches decisions based on the flags at startup. Flipping a flag mid-run invites desynchronization. Use CapabilityOverride (under the same crate) if you genuinely need a scoped override for a debugging session.

Cross-references

Where next