跳到主要内容
ansicode

终端键位映射 —— 箭头键、修饰键组合、kitty CSI u 协议

按下 Up 箭头时 \x1b[A 意味着什么?为什么 Ctrl+Up 发出 \x1b[1;5A?为什么单独的 ESC 键是最难可靠检测的按键?本页面覆盖输入侧 ANSI 词汇 —— 终端为每次按键发回给应用的字节 —— 涵盖 DECCKM 光标键模式、xterm 的 1;mod 修饰键约定、F1–F4 的 SS3 与 F5+ CSI tilde 之分、三种并存的 Home/End 编码、消除所有旧歧义的现代 kitty CSI u 协议,以及每个 TUI 用于区分单独 ESC 与序列开头的 20–50ms 超时模式。

输出与输入

输出与输入 —— 词汇相同、方向相反

ansicode 的逐序列页面记录的是**输出**侧:应用写出去用于渲染颜色、移动光标、设置窗口标题的字节。本页面覆盖**输入**侧:终端就每次按键回发给应用的字节。两个方向共用同一套 CSI / SS3 / OSC 词汇 —— `\x1b[` 是 CSI、`\x1bO` 是 SS3 —— 但解析器与生产者的角色互换。终端拥有键盘转换表(通过 `infocmp $TERM` 的 `kcuu1` 上箭头、`kf1` F1、`khome` Home 等能力配置),应用是接收方。有趣的组合:箭头键依 DECCKM 模式有两种形式,修饰键组合用一种各模拟器不一致的 CSI 参数约定,单独的 ESC 键与任意序列开头字节相同,kitty 键盘协议则是消除全部歧义的现代尝试。

`cat -v` 把 ESC 显示为 `^[`,逐字符显示真实字节。

# Inspect what your terminal actually emits for a key.
# Run this, then press the key once, then press Enter to exit.
cat -v
# Example output for Up arrow under tmux:
# ^[[A
# Example output for Ctrl+Up under xterm:
# ^[[1;5A
箭头键(DECCKM)

箭头键 —— CSI \e[A 与 SS3 \eOA 由 DECCKM 切换

光标键根据 DECCKM(DEC Cursor Keys Mode,CSI ? 1 h 启用、CSI ? 1 l 关闭)发出两种字节序列之一。在**普通**模式(默认)下,Up/Down/Right/Left = `\x1b[A` / `\x1b[B` / `\x1b[C` / `\x1b[D`(CSI)。**应用**模式下同一按键 = `\x1bOA` / `\x1bOB` / `\x1bOC` / `\x1bOD`(SS3 —— escape + 大写 O)。vim 与 emacs 通常进入时设置 DECCKM 应用模式、退出时关闭;bash readline 通过 inputrc 中 `\eOA`(或 `\e[A`)绑定同时识别两种形式。**注意**:DECCKM 独立于 DECKPAM/DECKPNM(小键盘应用/数字模式 —— 见 /sequence/deckpam-deckpnm);箭头键由 DECCKM 控制而**非** DECKPAM。混淆二者的应用在小键盘模式翻转时会错解箭头。

在编写 readline 风格输入层时,两种形式都应当绑定。

# Toggle DECCKM and observe Up arrow.
printf '\e[?1h'   # application cursor keys → Up = \eOA
printf '\e[?1l'   # normal cursor keys      → Up = \e[A

# Bash inputrc example — bind both forms:
# "\e[A": previous-history
# "\eOA": previous-history
修饰键

修饰键约定 —— \e[1;<mod><letter>,mod = 1 + 位掩码

按住修饰键(Shift、Alt、Ctrl、Meta)配合光标键或功能键时,xterm 系终端发出相同的终止字节,但在 CSI 中插入一个修饰参数:修饰过的 Up 是 `\x1b[1;<mod>A`,修饰过的 F1 是 `\x1b[1;<mod>P`,依此类推。`mod` 值为 `1 + (Shift?1:0) + (Alt?2:0) + (Ctrl?4:0) + (Meta?8:0)`,所以 2=Shift、3=Alt、4=Shift+Alt、5=Ctrl、6=Shift+Ctrl、7=Alt+Ctrl、8=Shift+Alt+Ctrl。示例:`\x1b[1;2A`(Shift+Up)、`\x1b[1;5A`(Ctrl+Up)、`\x1b[1;6A`(Shift+Ctrl+Up)、`\x1b[15;5~`(Ctrl+F5)。较旧的 Linux console 与部分 VT100 模拟器完全忽略修饰参数。XTMODKEYS / XTQMODKEYS(见 /sequence/xtmodkeys 与 /sequence/xtqmodkeys)允许应用设置或读取 modifyCursorKeys / modifyFunctionKeys 运行时模式,控制是否发出修饰参数。

记住 1-2-4-8 位掩码一次即可;适用于每个修饰过的箭头与 F 键。

# Modifier byte = 1 + Shift + 2·Alt + 4·Ctrl + 8·Meta
# Shift+Up        \e[1;2A
# Alt+Up          \e[1;3A
# Shift+Alt+Up    \e[1;4A
# Ctrl+Up         \e[1;5A
# Shift+Ctrl+Up   \e[1;6A
# Alt+Ctrl+Up     \e[1;7A
# Shift+Alt+Ctrl  \e[1;8A
# Same shape for F-keys: Ctrl+F5 = \e[15;5~
功能键 F1–F12

功能键 —— F1–F4 是 SS3,F5+ 是 CSI tilde

功能键编码是输入侧最碎片化的部分。xterm 系约定:F1 = `\x1bOP`、F2 = `\x1bOQ`、F3 = `\x1bOR`、F4 = `\x1bOS`(全部 SS3 —— escape + 大写 O + 字母,与应用模式箭头同形)。F5 及以上切换到 CSI tilde:F5 = `\x1b[15~`、F6 = `\x1b[17~`、F7 = `\x1b[18~`、F8 = `\x1b[19~`、F9 = `\x1b[20~`、F10 = `\x1b[21~`、F11 = `\x1b[23~`、F12 = `\x1b[24~`。16 与 22 的空缺是历史原因 —— DEC VT220 把这些数字留给了 xterm 未暴露的键(Help、Do)。Linux console 又不同:F1-F5 = `\x1b[[A` 至 `\x1b[[E`(非标准的双方括号形式),F6-F12 才接上 CSI-tilde 序列。务必查 `infocmp` 或采用 kitty 键盘协议,不要硬编码。

F1–F4 不遵循 F 键的总模式;F5 起则遵循修饰键约定。

# Modified F-keys reuse the modifier convention.
# Shift+F5         \e[15;2~
# Ctrl+F5          \e[15;5~
# Alt+F12          \e[24;3~
# Shift+Ctrl+F11   \e[23;6~

# Linux console F1-F5 are the outlier: \e[[A through \e[[E
Home / End / PgUp / PgDn / 退格

特殊键 —— Home/End 三种编码并存,Backspace = 0x7F 或 0x08

Home/End 是碎片化最严重的特殊键。**xterm** 发 `\x1b[H`(Home)与 `\x1b[F`(End)—— 终止字节与不带参的光标定位相同。**vt220** 发 `\x1b[1~`(Home)与 `\x1b[4~`(End)。**rxvt** 发 `\x1b[7~`(Home)与 `\x1b[8~`(End)。PgUp = `\x1b[5~`、PgDn = `\x1b[6~` 全局一致。Insert = `\x1b[2~`、Delete = `\x1b[3~`。**退格键**更糟 —— xterm 与 Linux console 发 DEL(`0x7F`,terminfo 名为 `kbs`),而 Windows console 与许多 telnet 主机发 BS(`\x08`)。只读 `^?`(0x7F)忽略 0x08 的工具在 Windows 上失效;规范修复是在输入层同时绑定两者。修饰形式与 F 键约定相同:`\x1b[H` Home、`\x1b[1;5H` Ctrl+Home、`\x1b[1;2F` Shift+End。

三种 Home、三种 End、两种退格的绑定让 readline 在各终端中保持可移植。

# Bind every Home/End encoding readline might see.
# In ~/.inputrc:
"\e[H":  beginning-of-line
"\e[1~": beginning-of-line
"\e[7~": beginning-of-line
"\e[F":  end-of-line
"\e[4~": end-of-line
"\e[8~": end-of-line
# Backspace defensively:
"\C-?":  backward-delete-char  # DEL  (0x7F)
"\C-h":  backward-delete-char  # BS   (0x08)
Kitty 键盘协议(CSI u)

Kitty 键盘协议 —— CSI u,消除每个按键的歧义

kitty 键盘协议(有时按其终止字节称作「CSI u」)以统一形式替代所有旧编码:任意键 = `\x1b[<codepoint>;<mod>u`,其中 `codepoint` 是未修饰键的 Unicode 码点(如 ESC = 27、Enter = 13、Space = 32、'a' = 97),`mod` 是与 xterm 系相同的 1+位掩码。应用以 `\x1b[>{flags}u`(push)启用,以 `\x1b[<u`(pop)恢复。标志位:1 = 消歧(默认 —— 区分 ESC 与序列开头、Ctrl+I 与 Tab、Ctrl+M 与 Enter)、2 = 报告事件类型(按下/重复/释放)、4 = 报告替代键(受布局影响的键的 shift 形式与基本形式)、8 = 所有键以转义码上报(不再有纯 ASCII)、16 = 附带相关文本。采纳情况:kitty(起源)、foot、WezTerm、ghostty、Konsole 24.02+、neovide;尚未支持的有 xterm、gnome-terminal、alacritty、Windows Terminal。通过 XTGETTCAP 查询(terminfo cap `kkbds`)或 DECRQM 探测检测支持,否则退回到旧编码。

务必在 SIGINT/SIGTERM 处理器中成对 push/pop —— 应用中途崩溃会让用户 shell 卡在 CSI-u 模式。

# Opt into kitty keyboard, run app, restore on exit.
printf '\e[>1u'       # push: disambiguate flag
your-app
printf '\e[<u'        # pop: restore previous flag set

# Sample shapes the app then sees:
# 'a'           \e[97u
# Ctrl+a        \e[97;5u
# Shift+Enter   \e[13;2u
# Lone ESC      \e[27u   (NOT mistakable for a sequence start)
# F1            \e[57364u  (function keys also unified)
可靠地读键

可靠读键 —— 单独 ESC 与序列开头的歧义、20–50ms 超时

输入侧最难的 bug:单独按 ESC(用户想取消 vim 插入模式)的起始字节与每个 CSI / SS3 序列完全一致。终端**不会**发送分隔符。经典修复是计时器 —— 若 ESC 之后的字节在 20–50ms 内到达,视为序列开头;否则视为单独的 ESC。大多数 TUI 库(ncurses、blessed、crossterm、termion、tcell、ratatui、bubbletea)通过 `ESCDELAY` 环境变量替你处理(ncurses 默认 1000ms —— 对现代手感来说太长;用户常设为 ESCDELAY=25)。各语言的原始模式原语:bash `read -rsn1`(一字节、不回显)配合 `read -t 0.05`;Python `termios.tcsetattr` + `select.select(timeout=0.05)`;Rust `crossterm::event::poll(Duration::from_millis(50))`;Go `tcell.Screen.PollEvent`。kitty 键盘协议的标志位 1(消歧)通过对单独 ESC 发送 `\e[27u` 直接消除该问题 —— 无歧义、无超时 —— 这是每个现代 TUI 都在抢着采纳它的实际原因。

50ms 足以接住任何真实序列的剩余字节,也短到用户感到响应敏捷。

# Bash readline-style escape disambiguation in pure bash.
IFS= read -rsn1 c
if [ "$c" = $'\e' ]; then
  # Could be start of a sequence — wait up to 50ms for more bytes.
  IFS= read -rsn1 -t 0.05 c2 && rest="$c2" || rest=""
  if [ -z "$rest" ]; then
    echo "Lone ESC pressed"
  else
    echo "Sequence start: \\e$rest..."
  fi
fi

参见

/sequence/deckpam-deckpnm —— 小键盘应用与数字模式(**不**控制箭头)。/sequence/dec-bracketed-paste —— \e[?2004h 在粘贴输入两端加标记。/sequence/xtmodkeys —— 控制是否发出修饰键参数的运行时开关。/terminfo —— 每个键名 kbs / khome / kcuu1 / kf1–kf12 的 terminfo 能力对照。