Skip to main content
ansicode

DECRPM decoder — parsing the DECRQM reply (`CSI ? Ps ; Pm $ y`)

How to read the CSI ? Ps ; Pm $ y (and CSI Ps ; Pm $ y) reply that DECRQM returns — the Pm value tells you whether the mode is set / reset / always-set / always-reset / unrecognised.

Byte forms

Every common string-literal form so you can paste-and-search either direction.

\\x1b[\x1b[?<Ps>;<Pm>$y (private) \x1b[<Ps>;<Pm>$y (ANSI)
\\033[\033[?2004;1$y
\\e[\e[?2004;1$y
ESC [ESC [ ? Ps ; Pm $ y
hex1b 5b 3f ... 24 79

Description

DECRPM — Report Private Mode (private form) or Report Mode (ANSI form) — is the reply terminals emit in response to a DECRQM query (CSI ? Ps $ p for private modes, CSI Ps $ p for ANSI modes; see decrqm). The reply echoes the queried Ps and adds a Pm value whose small enum tells you exactly what the terminal knows about that mode.

Pm values: - 0 — *mode not recognised*. Treat the mode as unsupported. Don't keep probing this Ps; cache the result as 'unsupported' for the session. - 1 — *mode is currently set*. Equivalent to \x1b[?<Ps>h (or \x1b[<Ps>h for ANSI) having been the last operation on it. - 2 — *mode is currently reset*. Equivalent to \x1b[?<Ps>l (or \x1b[<Ps>l for ANSI) having been the last operation on it. - 3 — *mode is permanently set*. The terminal hardwires it on; sending \x1b[?<Ps>l is a no-op. - 4 — *mode is permanently reset*. The terminal hardwires it off; sending \x1b[?<Ps>h is a no-op.

The ? in the reply mirrors the ? in the request — querying a private mode (e.g. ?2004 bracketed paste, ?1000 mouse) yields \x1b[?2004;Pm$y, while querying an ANSI mode (e.g. 4 IRM, 20 LNM) yields \x1b[4;Pm$y *without* the ?. Consumers must match the request shape literally — a stream that interleaves private and ANSI probes will mis-route replies otherwise.

Recommended consumer pattern (Python-ish pseudocode): ``` def probe_mode(ps, private=True, timeout=0.1): prefix = '?' if private else '' sys.stdout.write(f'\x1b[{prefix}{ps}$p') sys.stdout.flush() reply = read_until_finalbyte('y', timeout) # CSI ... $ y m = re.match(rf'\x1b\[{re.escape(prefix)}(\d+);(\d+)\$y', reply or '') if not m or int(m.group(1)) != ps: return 'no-reply' return {0:'unknown', 1:'set', 2:'reset', 3:'permaset', 4:'permareset'}[int(m.group(2))] ```

What 'permanently set/reset' really means: terminals use Pm=3 / Pm=4 to signal 'this feature is on/off and you can't toggle it from escape codes'. Examples: - A terminal that ships with bracketed paste always-on (some users configure xterm this way via X resources) returns Pm=3 for ?2004. - Older Linux console returns Pm=4 for ?1000 (mouse tracking) since the kernel vt driver never implemented it. - ConPTY returns Pm=4 for many DEC private modes (e.g. ?9 X10 mouse, ?1015 urxvt mouse) because the conhost translation layer fixes those features off.

Treat Pm=3 / Pm=4 the same as Pm=1 / Pm=2 for read purposes — the mode IS in that state — but don't bother emitting an h/l toggle that would be silently ignored.

Vendor compliance: - xterm, kitty, iTerm2, WezTerm, Ghostty, Konsole: full spec — return all five Pm values appropriately. - Alacritty: implements Pm ∈ {0, 1, 2} only — never returns 3 or 4. Tools that branch on permaset/permareset must treat 'absent' as 'unknown but probably supported'. - Linux console (vt): replies to a small whitelist of common modes (?1, ?7, ?25); silent on everything else (no reply at all — be sure to time out). - Windows Terminal: full compliance ≥ 1.20. Earlier builds returned Pm=0 for some supported modes. - cmd.exe / ConPTY: limited — most probes return Pm=0 even for modes the terminal honours. - macOS Terminal: partial; some modes return Pm=0 instead of Pm=1/Pm=2 even when state-toggling works. Don't trust DECRQM on macOS Terminal for feature detection — use behavioural probes instead.

DECRQM + DECRPM vs DECRQSS (see dcs-decrqss): DECRQM is the *generic mode-state* query — yes/no/permaset/permareset/unknown. DECRQSS returns the actual *value* of a settable resource (SGR stack, DECSCUSR shape, DECSTBM rows/cols, DECSCL conformance level). Use DECRQM for 'is bracketed paste on?', DECRQSS for 'which cursor shape is currently active?'. A useful TUI startup probe usually fires both: DECRQM to discover capabilities + DECRQSS to snapshot resources for later restore.

Spec citation: xterm-ctlseqs (DECRPM, CSI ? Ps ; Pm $ y / CSI Ps ; Pm $ y)

Parameters

PsEchoes the mode number from the originating DECRQM query. Match the reply's Ps to your request to disambiguate when multiple probes are in flight.
Pm0 unrecognised, 1 set, 2 reset, 3 permanently set (hardwired on), 4 permanently reset (hardwired off).

Examples

bash
# Probe bracketed-paste; expect ?2004;1$y if on, ?2004;2$y if off.\nprintf '\\033[?2004$p'\nIFS= read -rs -d y -t 0.1 reply </dev/tty 2>/dev/null\necho \"DECRPM: ${reply#$'\\033'}y\"
python
import sys, re, termios, tty, select\n\ndef probe(ps, private=True, timeout=0.1):\n    prefix = '?' if private else ''\n    fd = sys.stdin.fileno(); old = termios.tcgetattr(fd)\n    try:\n        tty.setcbreak(fd)\n        sys.stdout.write(f'\\x1b[{prefix}{ps}$p'); sys.stdout.flush()\n        buf = ''\n        while True:\n            r,_,_ = select.select([fd], [], [], timeout)\n            if not r: break\n            buf += sys.stdin.read(1)\n            if buf.endswith('y'): break\n    finally:\n        termios.tcsetattr(fd, termios.TCSADRAIN, old)\n    m = re.match(rf'\\x1b\\[{re.escape(prefix)}(\\d+);(\\d+)\\$y', buf)\n    if not m or int(m.group(1)) != ps: return 'no-reply'\n    return ['unknown','set','reset','permaset','permareset'][int(m.group(2))]\n\nprint(probe(2004))
go
// After DECRQM probe, switch on the Pm enum.\nswitch pm {\ncase 0: cap[ps] = capUnknown      // not recognised\ncase 1: cap[ps] = capOn           // currently set\ncase 2: cap[ps] = capOff          // currently reset\ncase 3: cap[ps] = capHardwiredOn  // can't toggle off — skip restore\ncase 4: cap[ps] = capHardwiredOff // can't toggle on  — skip enable\n}
javascript
// Node parser — match DECRPM reply, return {ps, state}.\nfunction parseDECRPM(buf) {\n  const m = /\\x1b\\[(\\??)(\\d+);(\\d+)\\$y/.exec(buf);\n  if (!m) return null;\n  return {\n    private: m[1] === '?',\n    ps: Number(m[2]),\n    state: ['unknown','set','reset','permaset','permareset'][Number(m[3])] ?? 'invalid',\n  };\n}
c
/* Skeleton — emit DECRQM for ?1006 (SGR mouse), parse DECRPM Pm. */\nfprintf(stdout, \"\\x1b[?1006$p\"); fflush(stdout);\n/* read until 'y'; then sscanf buf, \"\\x1b[?%d;%d$y\", &ps, &pm) */\nint ps = 0, pm = 0;\n/* ...read loop... */\nif (ps == 1006) {\n    static const char *st[] = {\"unknown\",\"set\",\"reset\",\"permaset\",\"permareset\"};\n    printf(\"SGR mouse: %s\\n\", (pm>=0 && pm<=4) ? st[pm] : \"invalid\");\n}

Terminal support

xterm
yes
Linux console (fbcon)
partial
macOS Terminal.app
partial
iTerm2
yes
Windows Terminal
yes
cmd.exe / ConPTY
partial
kitty
yes
alacritty
partial
WezTerm
yes
Ghostty
yes
GNOME Terminal
partial
Konsole
yes
tmux
yes
GNU screen
yes

Related sequences