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[?<Ps>;<Pm>$y (private) \x1b[<Ps>;<Pm>$y (ANSI)\033[?2004;1$y\e[?2004;1$yESC [ ? Ps ; Pm $ y1b 5b 3f ... 24 79Description
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
| Ps | Echoes the mode number from the originating DECRQM query. Match the reply's Ps to your request to disambiguate when multiple probes are in flight. |
| Pm | 0 unrecognised, 1 set, 2 reset, 3 permanently set (hardwired on), 4 permanently reset (hardwired off). |
Examples
# 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\"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))// 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}// 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}/* 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
| xterm | Linux console (fbcon) | macOS Terminal.app | iTerm2 | Windows Terminal | cmd.exe / ConPTY | kitty | alacritty | WezTerm | Ghostty | GNOME Terminal | Konsole | tmux | GNU screen |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| yes | partial | partial | yes | yes | partial | yes | partial | yes | yes | partial | yes | yes | yes |