DECSET 1049 — 备用屏幕缓冲
切换到独立屏幕缓冲区(vim/less 启动时使用)。
字节形式
涵盖所有常见的字符串字面量写法,方便正反查找。
\x1b[?1049h (enter) \x1b[?1049l (leave)\033[?1049h / \033[?1049l\e[?1049h / \e[?1049lESC [ ? 1 0 4 9 h / l1b 5b 3f 31 30 34 39 68 / 6c说明
DEC 私有模式 1049 是「进入全屏应用」的标准序列。进入(\x1b[?1049h)时,终端原子完成三件事:保存光标位置 + 属性(等同 DECSC \x1b7)、切换到全新的 alt 屏幕缓冲(无回滚)、清空该 alt 缓冲。离开(\x1b[?1049l)时,原 primary 缓冲被重绘且光标恢复 —— 这正是你 q 退出 vim / less / htop 后命令行提示符回到原位置、alt 缓冲内容被丢弃的原因。早期模式 47(\x1b[?47h)和 1047(\x1b[?1047h)行为类似,但缺少光标保存 / 恢复(47)或缺少进入时清屏(1047)—— 现代 TUI 应始终使用 1049,除非目标模拟器早于它出现(xterm patch #251,2010 年)。规范见 xterm-ctlseqs 的「Private Modes」章节。
alt-screen 是终端编程中最易碎的模式对,因为「TUI 清理契约」全靠对应的 \x1b[?1049l 真的运行。三类活跃陷阱集中在此。(1)换行陷阱:离开后光标常落在错的行 —— DECSET 1049 恢复保存的光标,但你程序在 alt-screen 内最后一次可见输出通常没以换行结尾,恢复后光标比预期高一行(见 alt-screen-newline);在 \x1b[?1049l 前发 \r\n 或 DECRC(\x1b8)即可。(2)alt-screen 内 CRLF 陷阱:单独的 \n(LF,0x0A)是「下移一行」但不带回车 —— 绕过 ncurses 的直接写程序若在状态行间只用 \n,会出现阶梯式输出(第 2 行从第 1 行结尾处开始)。应发 \r\n(或开启 IXON / 行规程的熟模式)—— 见 crlf-under-alt-screen。(3)更广义的崩溃后清理家族:若 TUI 进入时还发了 \x1b[?25l(隐藏光标)、\x1b[?1h\x1b=(smkx —— 应用键盘模式)、\x1b[?2004h(括号粘贴)或 \x1b[?1000h–\x1b[?1006h(鼠标跟踪),每一项都需要在退出 alt-screen 的同一清理块里反向关掉 —— 否则用户崩溃后的 shell 会出现光标不可见 / 方向键输出 OA / 粘贴带 \x1b[200~ 标记。见 stuck-cursor-hidden、stuck-app-mode、stuck-bracketed-paste。覆盖说明:cmd.exe(不走 ConPTY 的旧式 conhost)标 'partial' —— 早期 Win10 conhost 完全丢弃 1049;Win10 1709+ 走 ConPTY 时支持。
关闭用 \x1b[?1049l —— 且离开路径必须从每一个退出点可达,包括 SIGINT、SIGTERM、SIGQUIT、panic、segfault。Go:defer fmt.Print("\x1b[?1049l\x1b[?25h") 加 signal.Notify 处理器。Python:atexit.register(...) 加 signal.signal(signal.SIGINT, ...)。Rust crossterm:在守卫结构体的 Drop 实现里 LeaveAlternateScreen,让 unwind 触发清理。优先用成熟 TUI 库(crossterm、blessed、textual、bubbletea、ratatui)而非手撸 —— 它们集中化了清理契约且已经把上面这些坑都修过。几乎总是和 1049 同行的相关私有模式:cursor-visibility(\x1b[?25l/\x1b[?25h)—— 重绘时隐藏光标;dec-bracketed-paste(\x1b[?2004h)—— 区分粘贴与键入;cursor-save-restore(\x1b7/\x1b8)—— alt 缓冲内更细的位置簿记。
规范出处: xterm-ctlseqs (Private modes)
示例
printf '\033[?1049h'; sleep 2; printf '\033[?1049l'import sys, time\nsys.stdout.write('\x1b[?1049h'); sys.stdout.flush(); time.sleep(2); sys.stdout.write('\x1b[?1049l')fmt.Print("\x1b[?1049h"); time.Sleep(2*time.Second); fmt.Print("\x1b[?1049l")process.stdout.write('\x1b[?1049h'); setTimeout(() => process.stdout.write('\x1b[?1049l'), 2000)printf("\x1b[?1049h"); sleep(2); printf("\x1b[?1049l");在哪里用到
实际会发出该序列的工具——把抽象字节锚定到你已经用过的命令上。
- vim, neovim, nano启动时进入备用屏幕,编辑会话不污染滚动历史
- less, more, man进入分页器时切换备用屏幕,按 q 退出后还原到之前的终端状态
- htop, top, btop, glances
- tmux, GNU screen
- mc (Midnight Commander), ranger
常见问题
针对这条序列,开发者真正会去搜索的问题的简短回答。
- 为什么打开
less或vim后滚动条历史不见了? - 它们切换到了备用屏幕缓冲(
\x1b[?1049h)——一个独立的、没有滚动历史的页面,进入时被清空、退出时被丢弃。当程序发出\x1b[?1049l时原屏幕内容就回来了。这是设计行为:备用屏幕是为不想污染用户历史记录的全屏 TUI 准备的。 - 做备用屏幕该用
?1049还是?1047? - 新代码几乎都用
?1049。它在进入时原子地保存光标 + 清屏 + 切换备用屏幕,退出时恢复光标 + 退出——一次往返、无需自己做保存/恢复。?1047是更旧的 xterm 写法,只切换缓冲不保存/清屏,光标的 DECSC / DECRC 要自己管。新代码应该选?1049,除非你确实想要旧语义。 - 为什么退出备用屏幕后光标在错的行?
\x1b[?1049l会把光标恢复到进入备用屏幕之前所在的列 / 行,然后从那一行开始打下个字节——所以如果你的 shell 提示符指望从新一行开头开始,就会盖掉原有内容。退出后立刻发一个明确的\r\n(或经过 CRLF 转换后的\n)把光标落到新行。参见alt-screen-newline误区条目。
终端支持
- xterm
- 支持
- Linux console (fbcon)
- 支持
- macOS Terminal.app
- 支持
- iTerm2
- 支持
- Windows Terminal
- 支持
- cmd.exe / ConPTY
- 部分
- kitty
- 支持
- alacritty
- 支持
- WezTerm
- 支持
- Ghostty
- 支持
- GNOME Terminal
- 支持
- Konsole
- 支持
- tmux
- 支持
- GNU screen
- 支持
| xterm | Linux console (fbcon) | macOS Terminal.app | iTerm2 | Windows Terminal | cmd.exe / ConPTY | kitty | alacritty | WezTerm | Ghostty | GNOME Terminal | Konsole | tmux | GNU screen |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 支持 | 支持 | 支持 | 支持 | 支持 | 部分 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |