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
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:
| Variable | Contribution |
|---|---|
TERM=dumb or unset (without WT_SESSION) | Forces every capability to false. |
COLORTERM=truecolor | 24bit | Truecolor 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 kitty | Kitty profile + keyboard protocol. |
WT_SESSION | Windows Terminal identity (TERM often absent). |
TMUX, STY, ZELLIJ_SESSION_ID, WEZTERM_* | Multiplexer flags; disable sync output, scroll region, focus events, kitty keyboard. |
NO_COLOR | Force 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.
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-odds | probability | interpretation |
|---|---|---|
| −4.6 | ~1 % | Very unlikely |
| −2.2 | ~10 % | Unlikely |
| 0.0 | 50 % | 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.
with .
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. Theuse_sync_output()policy method returnsfalsein any mux.scroll_region(DECSTBM). Behavior varies wildly across tmux versions; some leak the region across panes. Theuse_scroll_region()policy method returnsfalsein 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
- Screen modes — how
sync_outputandscroll_regionselect inline strategies. - Terminal session — which session options are sanitized against these flags.
- Synchronized output — DEC 2026 emission.
- Bayesian capability detection — the full ledger design with proofs.
- Model trait — how the runtime consumes the resulting profile.