OSC 10 / 11 / 12 query — Detect default fg / bg / cursor color (dark vs light)
Ask the terminal for its current default foreground (10) / background (11) / cursor (12) color — the canonical way to auto-pick a dark or light theme.
Byte forms
Every common string-literal form so you can paste-and-search either direction.
\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 07Description
The OSC-color *query* form — substitute `?` for the color value in OSC 10 / 11 / 12 (defined under `osc-set-fg-bg` and `osc-cursor-color` respectively) and the terminal replies on stdin with the same OSC opcode and an XParseColor-shaped color: `\x1b]<Ps>;rgb:RRRR/GGGG/BBBB\x07` (the 16-bit-per-channel form is the canonical reply, even when the terminal stores 8-bit — pad each component by repeating: `#1e1e2e` → `rgb:1e1e/1e1e/2e2e`). The terminator matches whatever you sent — `BEL` (0x07) in → `BEL` out, `ST` (`\x1b\\`) in → `ST` out. **Dark / light auto-detection** — this is the standard handshake vim's `'background'` autodetect, neovim's `bg=` probe, [atuin](https://github.com/atuinsh/atuin)'s theme picker, and `bat --color=auto` all use to choose a colorscheme without the user setting `COLORFGBG`. Algorithm: 1. Save terminal mode, switch stdin to raw (no echo, no ICANON, VMIN=0, VTIME=1). 2. Write `\x1b]11;?\x07`. 3. Read until `BEL` (or `ST`) on stdin, parse out the three 16-bit hex components. 4. Compute perceived luminance — `Y = 0.299·R + 0.587·G + 0.114·B` (Rec. 601) — and compare against `0.5 · 0xFFFF`. Below ⇒ dark theme, above ⇒ light. Some implementations use the WCAG `(R+R+B+G+G+G) / 6` shortcut for speed. 5. Restore terminal mode. If no reply arrives within ~100 ms, fall back to the `$COLORFGBG` / `$TERM_PROGRAM` heuristic. **Why not just check `$COLORFGBG`** — only rxvt and a handful of derivatives set it; iTerm2 / Ghostty / WezTerm / Kitty / Windows Terminal don't. The OSC 11 query is the portable answer. **Caveat — OSC 12 cursor query** is the most spottily implemented of the three: xterm / iTerm2 / Kitty / WezTerm / Ghostty reply correctly; Linux console / cmd.exe ignore; macOS Terminal silently swallows; older gnome-terminal returns the *fg* color due to a long-standing parser bug. Treat the cursor-color reply as best-effort and fall back to the fg color when absent.
Spec citation: xterm-ctlseqs (OSC 10 / 11 / 12 query)
Parameters
| Ps | Color slot: 10 = default foreground, 11 = default background, 12 = text cursor color. |
| ? | Query sentinel — replaces the color value to request the current setting instead of changing it. |
Examples
# 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}Terminal support
- xterm
- yes
- Linux console (fbcon)
- no
- macOS Terminal.app
- partial
- iTerm2
- yes
- Windows Terminal
- yes
- cmd.exe / ConPTY
- no
- kitty
- yes
- alacritty
- yes
- WezTerm
- yes
- Ghostty
- yes
- GNOME Terminal
- partial
- Konsole
- yes
- tmux
- no
- GNU screen
- no
| xterm | Linux console (fbcon) | macOS Terminal.app | iTerm2 | Windows Terminal | cmd.exe / ConPTY | kitty | alacritty | WezTerm | Ghostty | GNOME Terminal | Konsole | tmux | GNU screen |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| yes | no | partial | yes | yes | no | yes | yes | yes | yes | partial | yes | no | no |