Skip to main content
ansicode
DEC private modes

DEC private modes — alt screen, cursor, mouse, focus

DEC private mode set / reset. The `ESC [ ? n h` and `ESC [ ? n l` family — alternate screen buffer, cursor visibility, bracketed paste, focus events, mouse tracking, application keypad, line wrap. Originally DEC VT-series extensions, adopted universally by xterm-family terminals.

17 sequences

Mode cookbook — building a TUI from six DEC switches

DEC private modes are how every full-screen TUI works — vim, less, tmux, htop, fzf all live inside this dialect. Six stops, top to bottom: the ?-prefix syntax that separates private modes from ECMA-48 standard ones; the alt-screen bedrock; cursor visibility while you repaint; bracketed paste so you can tell typing from a Cmd-V; mouse tracking with the SGR extended encoding; and focus events + synchronized update for flicker-free live UIs.

  1. 1. The ? prefix — DECSET vs SM

    Every code in this family carries a leading ? inside the parameter — \x1b[?25h shows the cursor, \x1b[?25l hides it. The ? is what makes the sequence a **DEC private mode** (DECSET / DECRST) rather than an ECMA-48 standard mode (SM / RM, no ?). Using the same number without ? either does nothing or hits a completely unrelated mode — \x1b[4h enables insert mode (IRM), nothing like ?4 which would be DECSCLM smooth-scroll. Final byte is h for **set / on** and l for **reset / off**. Multiple modes can be combined in one sequence with ; separators: \x1b[?25;1049h hides the cursor + enters the alt-screen in one go.

  2. 2. Alt-screen — \x1b[?1049h / l

    Switching to the alt-screen is the bedrock TUI move — vim, less, tmux, htop all emit \x1b[?1049h on launch and \x1b[?1049l on exit. The 1049 variant does three things at once: saves the cursor position, switches to a separate screen buffer, and clears that buffer. Exit (l) restores the cursor and switches back, so the user's shell scrollback is exactly as they left it — nothing the TUI drew leaks into the scrollback log. The older ?1047 only switches buffers (no cursor save) and the original ?47 doesn't even clear the alt buffer — pre-2000 emulators only had ?47; modern TUIs should always emit ?1049. Forgetting the close half (l) on a crash leaves the user staring at half a vim screen with no escape — install SIGINT / SIGTERM handlers that emit it before exit.

  3. 3. Cursor visibility & blink — ?25, ?12

    Hide the cursor with \x1b[?25l before repainting a screen, show it again with \x1b[?25h at the end — otherwise the cursor jumps across the frame as you write and looks like a glitch. The blink attribute is a separate switch: \x1b[?12h turns blink on, \x1b[?12l turns it off, **independent** of ?25. So a hidden-and-non-blinking cursor is \x1b[?25l\x1b[?12l; a visible-but-steady cursor for a code editor is \x1b[?25h\x1b[?12l. DECTCEM (?25) is universally supported across every modern emulator; the ?12 blink toggle is honoured by xterm, kitty, alacritty, wezterm, iTerm2, and ghostty but ignored by Windows Terminal pre-1.20 and the Linux console. For cursor *shape* (block / underline / bar), use DECSCUSR \x1b[N q — that's a CSI primitive, not a DEC private mode.

  4. 4. Bracketed paste — \x1b[?2004h

    Without bracketed paste your shell or editor has no way to tell cat | sudo rm -rf / typed character-by-character from the same string arriving as a single Cmd-V — they look identical on the wire. Send \x1b[?2004h and the terminal starts wrapping pasted text in markers: \x1b[200~ before, \x1b[201~ after. Now your input loop knows: anything between those markers came from the system clipboard, so don't auto-indent it, don't execute on \n mid-paste, don't interpret Ctrl-C inside. Bash 4.4+, zsh, fish, vim, neovim, emacs, and readline-based REPLs all opt in by default. The classic trap is forgetting to send \x1b[?2004l on exit — if your TUI crashes with bracketed-paste left on, the user's shell starts showing literal ^[[200~ in front of every paste they make until they run reset(1).

  5. 5. Mouse tracking — ?1000 + ?1006

    Enable click events with \x1b[?1000h (the X10 / VT200 format) — the terminal then reports each press as \x1b[Mbxy where b, x, y are single bytes (x + 32, y + 32). The catch: single-byte coords mean column 224+ rolls over and your TUI thinks the user clicked at column 1. **Always pair ?1000h with ?1006h** to switch into the SGR-extended encoding — reports become \x1b[<b;x;yM (press) / \x1b[<b;x;ym (release) with decimal integers, no cap. Add ?1002h to also receive drag events while a button is held; ?1003h for *all* motion (heavy — every pixel of movement emits an event). Disable each with the matching l. On exit, emit \x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l so the user's terminal is clean — otherwise every click in their shell prints escape-code spam.

  6. 6. Focus events & sync update — ?1004, ?2026

    Two live-UI conveniences. \x1b[?1004h enables focus reporting — the terminal sends \x1b[I when its window gains keyboard focus and \x1b[O when it loses focus. Useful for pausing a live tail or clock redraw while the user is in another window, then resuming on return — htop, fzf, tig, and lazygit all do this. \x1b[?2026h is the newer synchronized-update mode (kitty / iTerm2 / wezterm / ghostty / foot / VTE-based emulators since ~2022): everything written between ?2026h and ?2026l is buffered then committed to the screen atomically — no half-painted frames, no flicker on full repaints. The dance is ?2026h → emit cursor moves + every cell update for one frame → ?2026l. Emulators that don't recognise the mode silently ignore it (writes still arrive sequentially, just without the atomic guarantee), so it's safe to emit unconditionally — no capability detection required.

All sequences in this family

Browse other families