Appearance
Terminal Detection β
How applications discover what your terminal can do
Applications need to know terminal capabilities before using advanced features. But there's no reliable universal method. The ecosystem uses a mix of environment variables, query-response sequences, and databases β each with significant blind spots. Understanding these mechanisms explains why feature detection is hard and why terminfo.dev takes the approach it does.
The Problem β
When a TUI application starts, it faces a fundamental question: what can this terminal do? Can it render truecolor? Does it support the Kitty keyboard protocol? Will OSC 8 hyperlinks work, or will they spew garbage? The application needs answers before it writes its first escape sequence, because sending an unsupported sequence can corrupt the display or confuse the user.
There is no single reliable mechanism for answering these questions. The traditional approach β reading $TERM and looking up capabilities in the terminfo database β was designed for a world where terminals were physical hardware with fixed feature sets. In that world, knowing you had a VT100 told you everything you needed to know. Today, terminal emulators are software that gets updated monthly, adds features via configuration, and often supports capabilities that no database tracks.
The result is a patchwork of detection methods. Applications check environment variables, query the terminal with escape sequences, consult databases, and sometimes just guess. Each method has trade-offs between reliability, coverage, and speed. Most applications use several methods together, falling back from one to the next.
$TERM β
The $TERM environment variable is the oldest and most widely used detection mechanism. The terminal emulator sets it before launching the shell, and it identifies the terminal type β in theory. Applications pass this value to the terminfo database to look up capabilities: does this terminal support 256 colors? Does it have an alternate screen? What sequence moves the cursor?
The problem is that $TERM tells you how the terminal wants to be treated, not what it actually is. Many terminals still default to xterm-256color β but not all. Kitty ships xterm-kitty, Alacritty uses alacritty, WezTerm uses wezterm, and Ghostty tried using ghostty during its beta period (then reverted to xterm-ghostty after too many applications broke because they string-match on "xterm" and reject anything else). The ncurses tput utility, Python's curses module, and countless shell scripts assume $TERM starts with "xterm" or is in a small list of known values β which is exactly why so many terminals still fall back to xterm-256color.
So $TERM is a compatibility hint, not a capability oracle. It tells you the baseline the terminal is willing to be treated as β which says nothing about truecolor, nothing about Kitty keyboard protocol, nothing about synchronized output, nothing about hyperlinks. The variable that was designed to solve terminal detection has become part of the problem.
$TERM is a compatibility hint, not a capability oracle
Many terminals default to $TERM=xterm-256color, but others ship their own values: Kitty uses xterm-kitty, Alacritty uses alacritty, WezTerm uses wezterm. Either way, $TERM tells you how the terminal wants to be treated β not what it actually supports. Terminals that do ship custom values run into a different problem: remote servers often don't have the matching terminfo entry installed, so SSH sessions break. The "just use your own TERM value" approach doesn't scale when every server needs the terminfo entry pre-installed.
$COLORTERM β
$COLORTERM is a non-standard environment variable that indicates truecolor (24-bit color) support. When set to truecolor or 24bit, it tells applications they can use full RGB colors via SGR sequences like ESC[38;2;R;G;Bm. Most modern terminals set this variable, and libraries like chalk, colorette, and termcolor check it.
The variable emerged organically β no standard body defined it. The termstandard/colors community project documented the convention and encouraged terminal emulators to adopt it. Today it's the most reliable way to detect truecolor support, simply because there was broad enough adoption to make it useful.
But $COLORTERM is a one-trick pony. It covers exactly one question β "does this terminal support 24-bit color?" β and nothing else. It says nothing about underline styles, cursor shapes, clipboard access, graphics protocols, keyboard protocols, or any of the other features that distinguish modern terminals from each other. And because it's not standardized, there's no equivalent convention for other capabilities. Some terminals set additional custom environment variables (Kitty sets TERM_PROGRAM=kitty, WezTerm sets TERM_PROGRAM=WezTerm), but there's no universal convention.
terminfo/termcap β
The terminfo database (and its predecessor, termcap) is the traditional solution to terminal capability detection. It's a compiled database that maps terminal names (from $TERM) to capability strings. When an application calls tput colors or uses the ncurses library, it's querying terminfo. The database contains entries for cursor movement sequences, color support, screen clearing, line insertion, and hundreds of other capabilities. It's maintained by Thomas Dickey alongside ncurses and has been the backbone of terminal application development since the 1980s.
The limitation is coverage. terminfo's vocabulary was designed for the features of DEC VT terminals and early xterm. It has capability entries for basic colors, cursor movement, line editing, and screen modes β but no entries for Kitty keyboard protocol, OSC 8 hyperlinks, synchronized output (DEC mode 2026), semantic prompts (OSC 133), Sixel graphics, Kitty graphics, styled underlines (curly, dotted, dashed), OSC 52 clipboard access, or any of the other features that define modern terminal applications. These features are invisible to terminfo.
This isn't a fixable gap. Adding new capabilities to terminfo requires defining them in the database schema, updating the ncurses source, getting the change accepted upstream, waiting for distributions to pick up the new version, and then waiting for terminal emulators to ship matching entries. That pipeline takes years per capability. Modern terminal features ship in months. The result is that the database perpetually lags behind the terminals it describes, and the most interesting capabilities β the ones that differentiate terminals from each other β are the ones terminfo can't represent.
DA1 (Primary Device Attributes) β
DA1 is an escape sequence query: the application sends CSI c (or CSI 0 c) and the terminal responds with a list of capability flags. The response format is CSI ? Ps ; Ps ; ... c, where each Ps is a numeric code indicating a supported feature class. For example, a response of CSI ? 62 ; 1 ; 2 ; 6 ; 7 ; 8 ; 9 c says "I'm a VT220-class terminal that supports these attribute groups."
In practice, DA1 is more useful for identifying the terminal than for detecting specific features. The response format dates back to DEC hardware, and the numeric codes map to broad categories (132-column mode, printer port, Sixel graphics, national replacement character sets) rather than individual features. Modern terminals include DA1 responses for compatibility, but the values they report are inconsistent. Some terminals report capabilities they don't actually support; others omit capabilities they do support. The "VT level" number (62 = VT220, 64 = VT420, 65 = VT520) is aspirational at best.
DA1's real value in modern detection is as a sentinel. Because nearly every terminal responds to DA1, applications can send an unsupported query followed by a DA1 query. If the terminal doesn't understand the first query, it ignores it β but it still responds to DA1. By checking whether the first query got a response before the DA1 response arrives, the application can infer whether the feature is supported. This "query + DA1 fallback" pattern (used by terminal-colorsaurus and others) is one of the more reliable runtime detection techniques.
DECRPM (Mode Report) β
DECRPM β DEC Private Mode Report β is the best general-purpose mechanism for probing individual terminal features at runtime. The application sends CSI ? Pm $ p (where Pm is a private mode number), and the terminal responds with CSI ? Pm ; Ps $ y, where Ps indicates the mode status: 1 = set, 2 = reset, 0 = not recognized.
This is powerful because many modern features are controlled via DEC private modes. Bracketed paste mode (2004), mouse tracking modes (1000/1002/1003/1006), focus tracking (1004), alternate screen (1049), synchronized output (2026), and grapheme clustering (2027) all have mode numbers. By sending a DECRPM query for each mode, an application can determine at runtime whether the terminal supports it β and it gets back a definitive answer, not a heuristic.
The limitation is that not all terminals support DECRPM itself. Older terminals and some lightweight emulators ignore the query entirely, producing no response (which the application must handle with a timeout or a DA1 sentinel). Additionally, DECRPM only covers features that are mode-toggled β it can't detect capabilities like OSC 8 hyperlinks, Kitty graphics, or styled underlines that don't have a corresponding mode number. For those features, other query mechanisms (or direct behavioral probing) are needed. Despite these limitations, DECRPM is the closest thing to a universal feature-detection API that terminals offer.
XTVERSION β
XTVERSION is a query sequence (CSI > 0 q) that asks the terminal to report its name and version string. The terminal responds with DCS > | name(version) ST β for example, DCS > | Ghostty(1.1.0) ST or DCS > | tmux 3.5 ST. This gives the application the exact identity of the terminal, which can be mapped to a known feature set.
XTVERSION was introduced by xterm and has been adopted by most major terminals: Ghostty, Kitty, iTerm2, WezTerm, foot, contour, and tmux all respond. Terminal.app and some older emulators do not. When it works, it's the most precise identification mechanism available β the application knows exactly what terminal and version it's talking to, which means it can look up capabilities in a table rather than probing each one individually.
The trade-off is that XTVERSION requires the application to maintain a mapping from terminal name+version to capabilities. This is essentially recreating a terminfo-style database, but with finer granularity (per-version rather than per-terminal-type). Libraries like terminal-colorsaurus and detection routines in fish shell use XTVERSION as a first pass β if the terminal identifies itself, the application can skip the slower per-feature probing. When XTVERSION fails (no response), the application falls back to DECRPM and DA1.
Runtime Probing β
Runtime probing is the approach that terminfo.dev takes β and it's the most reliable method for determining what a terminal actually supports. Instead of trusting a database entry, an environment variable, or a self-reported identity, runtime probing sends the actual escape sequence and checks whether the terminal handles it correctly.
The simplest form of runtime probing uses cursor position: the application saves the cursor position, sends an escape sequence (for example, a wide emoji character), queries the cursor position again, and checks whether the cursor moved the expected distance. If the terminal correctly handled the emoji as two columns wide, the cursor will be at the right position. If not, the application knows the terminal doesn't support that feature. More sophisticated probes use DECRPM queries, OSC response parsing, and DA1 sentinels.
This is what Termless does with headless backends β it instantiates a terminal emulator in-process, writes escape sequences, and reads back the terminal state programmatically. It's also what the npx terminfo.dev CLI does with real terminals β it sends probes over the PTY and reads the responses. The results are ground truth: not what a database says should work, not what an environment variable claims, but what the terminal actually did when presented with the sequence. This behavioral approach is why terminfo.dev can track features that terminfo has no vocabulary for.
Why terminfo.dev probes directly instead of using terminfo
The terminfo database has no capability entries for most modern features β Kitty keyboard protocol, OSC 8 hyperlinks, styled underlines, synchronized output, semantic prompts, Sixel graphics, clipboard access, and dozens more are invisible to terminfo. Even for features it does track, the database reflects what a terminal should support based on its $TERM value, not what it actually does. Runtime probing gives ground truth: send the sequence, check the result. That's why every data point on this site comes from an actual probe, not a database lookup. See Why not terminfo? for the full rationale.
Comparing Detection Methods β
| Method | Reliability | Coverage | Speed | Requires Response | Works Over SSH |
|---|---|---|---|---|---|
| $TERM | Low β almost everything lies | Legacy features only (via terminfo) | Instant | No | Yes |
| $COLORTERM | Medium β widely adopted for color | Truecolor only | Instant | No | Depends on forwarding |
| terminfo | Medium β accurate for what it tracks | Legacy features only | Instant (cached) | No | Yes (if entry installed) |
| DA1 | Medium β useful as sentinel | Terminal class, not specific features | Fast (~ms) | Yes | Yes |
| DECRPM | High β definitive answer | Mode-toggled features only | Fast (~ms) | Yes | Yes |
| XTVERSION | High β exact identity | All features (via lookup table) | Fast (~ms) | Yes | May report mux instead |
| Runtime probe | Highest β ground truth | Any observable behavior | Slow (~100ms per probe) | Yes | Yes |
Secondary Environment Hints β
Beyond $TERM and $COLORTERM, many terminals set additional environment variables that reveal their identity. These aren't standardized β each terminal chooses its own β but collectively they cover most of the major emulators.
| Variable | Set By | Value |
|---|---|---|
TERM_PROGRAM | Most modern terminals | Terminal name: iTerm.app, WezTerm, ghostty, Apple_Terminal, tmux |
TERM_PROGRAM_VERSION | Most modern terminals | Version string (e.g., 3.5.2, 1.1.0) β useful for feature gating by version |
VTE_VERSION | VTE-based terminals (GNOME Terminal, Tilix, Terminator) | Encoded version number (e.g., 7200 = 0.72.0). Reliable for detecting the VTE family on Linux |
KITTY_WINDOW_ID | Kitty | Window identifier. Its presence confirms Kitty β the value itself is rarely useful |
WT_SESSION | Windows Terminal | Session GUID. Reliable way to detect Windows Terminal, even under WSL |
GHOSTTY_RESOURCES_DIR | Ghostty | Path to Ghostty's resource bundle. Confirms Ghostty is the host terminal |
ITERM_SESSION_ID | iTerm2 | Session identifier (e.g., w0t0p0:4A2B3C). Confirms iTerm2 β also encodes window/tab/pane |
These variables are fast to check (no round-trip to the terminal) and more specific than $TERM. A common pattern is to check TERM_PROGRAM first for a quick identification, then fall back to XTVERSION or DECRPM for terminals that don't set it.
These variables don't survive multiplexers
tmux, screen, and SSH sessions typically strip or override these variables. If your application needs to detect the outer terminal from inside tmux, environment variables won't help β you'll need escape-sequence queries like XTVERSION or DA1, which pass through the multiplexer to the real terminal.
Multiplexer and SSH Caveats β
Terminal detection gets significantly harder when multiplexers (tmux, screen, Zellij) or SSH sessions sit between the application and the real terminal. Each layer can distort the signals that detection mechanisms rely on.
tmux overrides $TERM. When tmux starts, it replaces the terminal's $TERM value with tmux-256color or screen-256color (depending on its default-terminal setting). The application sees tmux's terminal type, not the outer terminal's. This is correct behavior β tmux is the terminal from the application's perspective β but it means $TERM tells you nothing about the real terminal's capabilities. tmux may support fewer features than the outer terminal (for example, it only recently added support for styled underlines and still doesn't support Kitty graphics).
SSH strips environment variables. By default, sshd only accepts a small whitelist of environment variables from the client (typically LANG, LC_*, and TERM). Variables like TERM_PROGRAM, COLORTERM, KITTY_WINDOW_ID, and GHOSTTY_RESOURCES_DIR are not forwarded. You can configure SendEnv on the client and AcceptEnv on the server, but most users don't, and you can't count on it for general-purpose detection.
Nested sessions compound the problem. SSH into a remote machine that launches tmux, and now the application is two layers removed from the real terminal. $TERM is tmux-256color, TERM_PROGRAM is unset, and the only variables that survived are the ones tmux itself set. The application has no direct evidence of the outer terminal's identity.
Escape-sequence queries pass through β mostly. DA1, DECRPM, and XTVERSION queries travel through tmux and SSH to the outer terminal, and the responses travel back. This makes them more reliable than environment variables in nested scenarios. However, there are caveats: XTVERSION inside tmux returns tmux's identity (tmux 3.5), not the outer terminal's. tmux can also intercept and modify some responses. And some escape sequences that the outer terminal supports may be consumed by tmux rather than passed through to the application.
Detection strategy inside multiplexers
- Check
$TMUXor$STYto detect that you're inside a multiplexer - Use DECRPM to probe the multiplexer's capabilities (which may be a subset of the outer terminal's)
- If you need the outer terminal's identity, try
XTVERSIONpassthrough via tmux's\ePtmux;DCS wrapper β but be prepared for it to fail - Accept that inside a multiplexer, you're constrained to whatever the multiplexer exposes
Practical Detection Recipes β
Here are minimal, copy-pasteable examples for the most common detection tasks.
Bash: detect truecolor support β
bash
has_truecolor() {
case "${COLORTERM-}" in
truecolor|24bit) return 0 ;;
esac
# Fallback: check TERM for known truecolor terminals
case "$TERM" in
*-direct|*-truecolor) return 0 ;;
esac
return 1
}
if has_truecolor; then
printf '\e[38;2;255;100;0mTruecolor works\e[0m\n'
fiPython: query DA1 with timeout β
python
import sys, os, select, termios, tty
def query_da1(timeout=0.5):
"""Send DA1, return response string or None."""
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
os.write(sys.stdout.fileno(), b'\x1b[c') # DA1 query
if select.select([fd], [], [], timeout)[0]:
resp = b''
while select.select([fd], [], [], 0.05)[0]:
resp += os.read(fd, 256)
return resp.decode('ascii', errors='replace')
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
return NoneJavaScript (Node.js): detect terminal capabilities β
js
function detectCapabilities() {
const env = process.env
return {
truecolor: /^(truecolor|24bit)$/i.test(env.COLORTERM ?? ""),
term: env.TERM ?? "unknown",
program: env.TERM_PROGRAM ?? null,
version: env.TERM_PROGRAM_VERSION ?? null,
isTmux: "TMUX" in env,
isSSH: "SSH_TTY" in env || "SSH_CLIENT" in env,
isKitty: "KITTY_WINDOW_ID" in env,
is256color: /256color/.test(env.TERM ?? ""),
}
}For runtime probing in Node.js (DA1, DECRPM, XTVERSION), see the npx terminfo.dev detect command β it handles raw mode, timeouts, and response parsing.
What Developers Should Do β
For maximum compatibility, use a layered detection strategy. Start with the fast, zero-round-trip checks: $TERM and $COLORTERM give you a baseline. If $COLORTERM is truecolor or 24bit, you can safely use 24-bit RGB colors. If $TERM ends with -256color, you have 256-color support. These checks cost nothing and cover the most common questions.
For specific features, probe at runtime. Send a DECRPM query for modes you care about (synchronized output, bracketed paste, focus tracking) and check the response. If you need the terminal's identity, try XTVERSION first β if it responds, you know exactly what you're working with and can enable features accordingly. Use DA1 as a sentinel for queries that might not get a response.
Most importantly, degrade gracefully. Don't assume that xterm-256color means full xterm compatibility β it almost certainly doesn't. Don't assume that a missing DECRPM response means "not supported" β the terminal might not support DECRPM itself. Always have a fallback path: if truecolor isn't available, fall back to 256 colors; if styled underlines aren't supported, use a basic underline; if the Kitty keyboard protocol isn't available, use traditional key encoding. The terminal ecosystem is heterogeneous, and the applications that work best are the ones that adapt to whatever the terminal actually provides.