跳到主要内容
ansicode
DEC 私有模式

DEC 私有模式 —— 备用屏幕、光标、鼠标、焦点

DEC 私有模式 set / reset。`ESC [ ? n h` 与 `ESC [ ? n l` 家族 —— 备用屏幕缓冲、光标可见性、括号粘贴、焦点事件、鼠标追踪、应用小键盘、自动换行等。最初是 DEC VT 系列的扩展,已被 xterm 系全部终端普遍采纳。

17 条序列

模式食谱 —— 六个 DEC 开关搭出一个 TUI

DEC 私有模式是每一个全屏 TUI 的底层机制 —— vim、less、tmux、htop、fzf 全都跑在这套方言里。下面六步:先认 ? 前缀语法(私有模式与 ECMA-48 标准模式靠它分开);再是 alt-screen 这块基石;接着是重绘期间的光标显隐;之后是括号粘贴让你能区分键入与 Cmd-V;然后用 SGR 扩展编码做鼠标跟踪;最后用焦点事件 + 同步更新做不闪屏的实时 UI。

  1. 1. ? 前缀 —— DECSET 与 SM 的分水岭

    这一族每个码都在参数前面带一个 ? —— \x1b[?25h 显示光标,\x1b[?25l 隐藏。? 是把序列归为 **DEC 私有模式**(DECSET / DECRST)而不是 ECMA-48 公有模式(SM / RM,不带 ?)的关键。同一个数字不带 ? 要么完全无效,要么撞上完全无关的模式 —— \x1b[4h 打开插入模式(IRM),跟 ?4(DECSCLM 平滑滚动)八竿子打不到一起。结尾字节 h 表示**打开**,l 表示**关闭**。多个模式可以用 ; 合并:\x1b[?25;1049h 一条同时隐藏光标 + 切到 alt-screen。

  2. 2. 备用屏 —— \x1b[?1049h / l

    切换到备用屏是 TUI 最经典的一招 —— vim、less、tmux、htop 启动时都发 \x1b[?1049h,退出时发 \x1b[?1049l。1049 这个变种一次做三件事:保存光标位置、切到独立屏幕缓冲区、清空那个缓冲。退出(l)会恢复光标 + 切回原主屏,所以用户原来的 shell 滚动历史保持原样 —— TUI 画的东西完全不会泄漏到滚动日志里。老的 ?1047 只切缓冲不保存光标,最原始的 ?47 甚至不清空备用缓冲 —— 2000 年前的终端只有 ?47,现代 TUI 一律用 ?1049。崩溃时漏发结尾的 l 会让用户卡在半截 vim 画面里出不来 —— 务必装上 SIGINT / SIGTERM 处理器,在退出前补发一次。

  3. 3. 光标显隐与闪烁 —— ?25?12

    重绘屏幕之前先用 \x1b[?25l 隐藏光标,画完再用 \x1b[?25h 显示 —— 否则光标会跟着写入位置满屏跳,看起来像闪屏 bug。闪烁属性是独立的另一个开关:\x1b[?12h 开启闪烁、\x1b[?12l 关闭,跟 ?25 **互不影响**。所以「隐藏 + 不闪烁」就是 \x1b[?25l\x1b[?12l;代码编辑器常要的「可见但稳定不闪」是 \x1b[?25h\x1b[?12l。DECTCEM(?25)在所有现代终端通用;?12 闪烁开关 xterm、kitty、alacritty、wezterm、iTerm2、ghostty 都支持,但 Windows Terminal 1.20 之前的版本和 Linux console 都忽略。要改光标 *形状*(方块 / 下划线 / 竖线)用 DECSCUSR \x1b[N q —— 那是 CSI 原语,不是 DEC 私有模式。

  4. 4. 括号粘贴 —— \x1b[?2004h

    没有括号粘贴的话,你的 shell 或编辑器根本分不清 cat | sudo rm -rf / 是用户一个字一个字键入的,还是一整段 Cmd-V 粘进来的 —— 字节流上两者一模一样。发 \x1b[?2004h 之后终端开始给粘贴内容包标记:前面加 \x1b[200~,后面加 \x1b[201~。这样你的输入循环就能识别:夹在两个标记之间的全是剪贴板来的,别自动缩进、别遇到 \n 就执行、里面的 Ctrl-C 也别解读。Bash 4.4+、zsh、fish、vim、neovim、emacs、所有基于 readline 的 REPL 默认都启用。最容易踩的坑是退出时忘记发 \x1b[?2004l —— 如果你的 TUI 崩溃时把括号粘贴留在打开状态,用户在 shell 里每次粘贴前都会先看到字面的 ^[[200~,得敲 reset(1) 才救得回来。

  5. 5. 鼠标跟踪 —— ?1000 + ?1006

    \x1b[?1000h 打开点击事件(X10 / VT200 格式)—— 终端把每次按下报作 \x1b[Mbxy,其中 bxy 是单字节(值为 x + 32y + 32)。坑在这里:单字节坐标意味着第 224 列以后就会回卷,你的 TUI 会以为用户点了第 1 列。**?1000h 一定要配 ?1006h** 切到 SGR 扩展编码 —— 上报变成 \x1b[<b;x;yM(按下)/ \x1b[<b;x;ym(释放),用十进制整数,没有上限。再加 ?1002h 可同时接收按住时的拖拽事件;?1003h 接收 *所有* 移动事件(开销大 —— 每移动一像素都触发)。各项关闭用对应的 l。退出时务必发 \x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1006l 把用户终端恢复原状,否则用户在 shell 里每次点击都会看到一堆字面 escape 码。

  6. 6. 焦点事件与同步更新 —— ?1004?2026

    两个让 live UI 更稳的便利码。\x1b[?1004h 打开焦点上报 —— 窗口获得键盘焦点时终端发 \x1b[I,失去焦点时发 \x1b[O。可以在用户切去别的窗口时暂停 live tail 或时钟刷新,回来时再恢复 —— htop、fzf、tig、lazygit 都这么做。\x1b[?2026h 是更新的同步更新模式(kitty / iTerm2 / wezterm / ghostty / foot / 2022 年以来基于 VTE 的终端都支持):?2026h?2026l 之间写入的内容全部先在终端侧缓冲,然后一次原子提交到屏幕 —— 不会出现画到一半的帧,全屏重绘也不闪。流程是 ?2026h → 发出本帧所有光标移动与单元格更新 → ?2026l。不识别这个模式的终端会静默忽略(写入仍然顺序到达,只是没了原子保证),所以可以无条件发出,不必先做能力探测。

本家族的全部序列

浏览其他家族