OSC 10 / 11 / 12 查询 — 探测默认前景 / 背景 / 光标色(深色对浅色)
向终端查询当前默认前景(10)/ 背景(11)/ 光标(12)色 —— 自动选择深色或浅色主题的标准做法。
字节形式
涵盖所有常见的字符串字面量写法,方便正反查找。
\x1b]10;?\x07 (fg) \x1b]11;?\x07 (bg) \x1b]12;?\x07 (cursor)\033]10;?\007 / \033]11;?\007 / \033]12;?\007\e]10;?\a / \e]11;?\a / \e]12;?\aESC ] 10 ; ? BEL / ESC ] 11 ; ? BEL / ESC ] 12 ; ? BEL1b 5d 31 30 3b 3f 07 / 1b 5d 31 31 3b 3f 07 / 1b 5d 31 32 3b 3f 07说明
OSC 颜色的*查询*形式 —— 把 OSC 10 / 11 / 12(分别参见 `osc-set-fg-bg` 与 `osc-cursor-color`)的颜色值改为 `?`,终端会在标准输入回复同一 OSC opcode 与一个 XParseColor 形状的颜色:`\x1b]<Ps>;rgb:RRRR/GGGG/BBBB\x07`(每通道 16 位的形式是规范回复,即使终端内部存 8 位也会重复扩展:`#1e1e2e` → `rgb:1e1e/1e1e/2e2e`)。终止符与发送时一致 —— 送 `BEL`(0x07)则回 `BEL`,送 `ST`(`\x1b\\`)则回 `ST`。 **深色 / 浅色自动检测** —— 这是 vim 的 `'background'` 自动检测、neovim 的 `bg=` 探测、[atuin](https://github.com/atuinsh/atuin) 的主题选择、`bat --color=auto` 等工具用来在用户未设 `COLORFGBG` 时选择配色方案的标准握手。算法: 1. 保存终端模式,切到 raw(无回显、无 ICANON、VMIN=0、VTIME=1)。 2. 发送 `\x1b]11;?\x07`。 3. 在标准输入读到 `BEL`(或 `ST`),解析三段 16 位十六进制。 4. 计算感知亮度 —— `Y = 0.299·R + 0.587·G + 0.114·B`(Rec. 601)—— 与 `0.5 · 0xFFFF` 比较。低于 ⇒ 深色主题,高于 ⇒ 浅色。一些实现用 WCAG 的 `(R+R+B+G+G+G) / 6` 快捷式提速。 5. 恢复终端模式。若约 100 ms 内无回复,回退到 `$COLORFGBG` / `$TERM_PROGRAM` 启发式。 **为何不直接看 `$COLORFGBG`** —— 只有 rxvt 与少数衍生终端会设;iTerm2 / Ghostty / WezTerm / Kitty / Windows Terminal 都不设。OSC 11 查询才是可移植答案。 **注意 —— OSC 12 光标查询**在三者中实现最不齐:xterm / iTerm2 / Kitty / WezTerm / Ghostty 正确回复;Linux console / cmd.exe 忽略;macOS Terminal 静默吞掉;老版 gnome-terminal 因长期存在的解析器 bug 返回的是*前景*色。把光标色回复当作尽力而为,缺失时回退到前景色。
规范出处: xterm-ctlseqs (OSC 10 / 11 / 12 query)
参数
| Ps | 颜色槽位:10 = 默认前景、11 = 默认背景、12 = 文本光标颜色。 |
| ? | 查询占位符 —— 替换颜色值,请求当前设置而非修改它。 |
示例
# Probe background brightness, exit 0 = dark, 1 = light.\nold=$(stty -g); stty -echo raw min 0 time 1\nprintf '\033]11;?\007' > /dev/tty\nIFS= read -r -d $'\\a' reply < /dev/tty\nstty "$old"\nhex=${reply##*rgb:}\nr=$((16#${hex%%/*})); g=${hex#*/}; g=$((16#${g%%/*})); b=$((16#${hex##*/}))\ny=$(( (299*r + 587*g + 114*b) / 1000 ))\n[ "$y" -lt 32767 ] && echo dark || echo lightimport sys, termios, tty, select, re\nfd = sys.stdin.fileno(); old = termios.tcgetattr(fd); tty.setraw(fd)\ntry:\n sys.stdout.write('\x1b]11;?\x07'); sys.stdout.flush()\n r, _, _ = select.select([fd], [], [], 0.2)\n if not r:\n print('no reply — falling back'); sys.exit(2)\n buf = b''\n while not buf.endswith(b'\\x07'):\n buf += sys.stdin.buffer.read1(64)\n m = re.search(rb'rgb:([0-9a-f]+)/([0-9a-f]+)/([0-9a-f]+)', buf, re.I)\n R, G, B = (int(x, 16) for x in m.groups())\n Y = (299*R + 587*G + 114*B) // 1000\n print('dark' if Y < 32767 else 'light')\nfinally:\n termios.tcsetattr(fd, termios.TCSADRAIN, old)// Detect dark/light by querying OSC 11 (background).\nimport (\n \"bufio\"; \"fmt\"; \"os\"; \"regexp\"; \"strconv\"\n \"golang.org/x/term\"\n)\nfunc isDark() bool {\n fd := int(os.Stdin.Fd())\n st, _ := term.MakeRaw(fd); defer term.Restore(fd, st)\n fmt.Print(\"\\x1b]11;?\\x07\")\n rd := bufio.NewReader(os.Stdin)\n line, _ := rd.ReadString(0x07)\n m := regexp.MustCompile(`rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)`).FindStringSubmatch(line)\n r, _ := strconv.ParseInt(m[1], 16, 32)\n g, _ := strconv.ParseInt(m[2], 16, 32)\n b, _ := strconv.ParseInt(m[3], 16, 32)\n return (299*r + 587*g + 114*b)/1000 < 32767\n}// Node — assumes raw stdin and a TTY.\nprocess.stdin.setRawMode(true); process.stdin.resume();\nprocess.stdout.write('\\x1b]11;?\\x07');\nlet buf = '';\nprocess.stdin.on('data', chunk => {\n buf += chunk.toString();\n if (!buf.endsWith('\\x07')) return;\n process.stdin.setRawMode(false); process.stdin.pause();\n const [, r, g, b] = buf.match(/rgb:([0-9a-f]+)\\/([0-9a-f]+)\\/([0-9a-f]+)/i);\n const Y = (299 * parseInt(r, 16) + 587 * parseInt(g, 16) + 114 * parseInt(b, 16)) / 1000;\n console.log(Y < 0x7fff ? 'dark' : 'light');\n});/* Minimal OSC 11 query — error handling omitted. */\n#include <stdio.h>\n#include <termios.h>\n#include <unistd.h>\nint main(void) {\n struct termios old, raw; tcgetattr(0, &old); raw = old;\n raw.c_lflag &= ~(ICANON|ECHO); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 2;\n tcsetattr(0, TCSANOW, &raw);\n printf(\"\\x1b]11;?\\x07\"); fflush(stdout);\n char buf[128]; int n = read(0, buf, sizeof buf - 1);\n tcsetattr(0, TCSANOW, &old);\n if (n > 0) { buf[n] = 0; printf(\"reply: %s\\n\", buf); }\n return 0;\n}终端支持
- 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 支持 | 不支持 | 部分 | 支持 | 支持 | 不支持 | 支持 | 支持 | 支持 | 支持 | 部分 | 支持 | 不支持 | 不支持 |