# Dark and Light Mode Detection (?2031)

> Have the terminal tell the program whether its colors are dark or light, and again whenever they change.

- **Sequence:** `CSI ? 2031 h/l`
- **Specification:** [Dark and Light Mode detection, Contour VT extensions](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)

A program that picks its own colors wants to know whether it is drawing on
a dark background or a light one, and to hear about it when the desktop
switches from one to the other, or the user changes the terminal's profile.
`CSI ? 996 n` asks, and the terminal answers with a private Device Status
Report, `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light. With
`CSI ? 2031 h` set, the terminal sends the same report on its own whenever
its colors change between the two; `CSI ? 2031 l` stops the reports.

It is neither xterm's nor DEC's. Its definition is
[Dark and Light Mode detection](https://github.com/contour-terminal/contour/blob/ab57ab3288c3dca5ff727311cb3f1401bac30549/docs/vt-extensions/color-palette-update-notifications.md),
kept by the Contour terminal's authors with their other VT extensions, read
at commit ab57ab3. It is short. The report is sent when the terminal's
palette changes because the user changed the profile, directly or by
changing the operating system's theme, and not otherwise. It does not say
that setting the mode sends a report, nor how a terminal decides that its
colors are dark, and terminals differ on both:

- **Ghostty** answers from its configuration's theme state, which follows
  the operating system's appearance when the configuration gives a light
  and a dark theme, and sends a report when that changes or the
  configuration is reloaded.
- **kitty** calls its colors dark when its default background is
  ([`is_dark`](https://github.com/kovidgoyal/kitty/blob/v0.49.2/kitty/window.py#L1690-L1705)),
  and sends a report when its colors change other than by an escape code.
- **foot** answers from which of its two themes is active, `[colors-dark]`
  or `[colors-light]`, and sends a report when it switches theme
  ([`csi.c`](https://codeberg.org/dnkl/foot/src/tag/1.28.0/csi.c#L1584-L1600),
  [`term_theme_switch_to_dark`](https://codeberg.org/dnkl/foot/src/tag/1.28.0/terminal.c#L4782-L4845)).
- **VTE** calls its colors dark when its default background is no lighter
  than its default foreground, and sends a report when the program hosting
  it changes either of them, but not when an escape code does
  ([`set_color`](https://github.com/GNOME/vte/blob/0.84.1/src/vte.cc#L2300-L2303),
  [`is_color_palette_dark`](https://github.com/GNOME/vte/blob/0.84.1/src/vte.cc#L2655-L2687)).

None of them sends a report when the mode is set, so a program that wants
the state at the start asks with `CSI ? 996 n` as well as setting the mode.
A program finds out whether a terminal has it by asking with DECRQM: an
answer of `0`, *not recognized*, or no answer at all, means it does not.

A default xterm has no mode `2031`. It answers DECRQM for it with `0`, as it
does for any mode it does not know
([`do_dec_rqm`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/misc.c#L5855-L5869)),
and does not answer `CSI ? 996 n`, which falls through to the empty default
of its DSR dispatch
([`CASE_DSR`](https://github.com/ThomasDickey/xterm-snapshots/blob/xterm-411a/charproc.c#L4599-L4746)).
The cases follow xterm.

libghostty-vt has the mode, `report_color_scheme` in `modes.zig`, off to
begin with and reset by [RIS](https://control-codes.page/esc/ris/index.html.md), and answers DECRQM for it with `1`
or `2`, so the first case is a known difference. Whether the colors are dark
is the host's to say: `CSI ? 996 n` is answered only through a callback the
host sets with `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME`, and is dropped without
one. This site's wrapper sets none, so libghostty-vt here answers the query
no more than xterm does. Sending a report when the colors change is the
host's job too, as Ghostty does from its own configuration.

## Support

| Terminal | Version | Support | Notes |
| --- | --- | --- | --- |
| VT220 | [manual](https://vt100.net/docs/vt220-rm/), 2nd ed., 1984 | No | not in the manual |
| VT510 | [manual](https://vt100.net/docs/vt510-rm/), 1st ed., 1993 | No | not in the manual; not in [Table 5-6](https://vt100.net/docs/vt510-rm/DECRPM.html) |
| xterm | patch 412, [xterm-411a](https://github.com/ThomasDickey/xterm-snapshots/tree/xterm-411a) | No | not recognized; DECRQM reports `0`, and `CSI ? 996 n` is not answered |
| libghostty-vt | [0f171f6](https://github.com/ghostty-org/ghostty/tree/0f171f650a1cbbd6df4a2573b6b0c7de57cc6d90) | Yes | COLORSCHEME-1: implements the mode; the query is answered through a callback the host sets, and reports are the host's to send |
| Konsole | 26.11.70, [a24c3d71](https://invent.kde.org/utilities/konsole/-/tree/a24c3d71be3684f24d030aa2dbe9bd723d985233) | No | no mode `2031` and no `CSI ? 996 n` |
| kitty | 0.49.2, its documentation | Yes | dark when the default background is ([`window.py`](https://github.com/kovidgoyal/kitty/blob/v0.49.2/kitty/window.py#L1690-L1705), [`screen.c`](https://github.com/kovidgoyal/kitty/blob/v0.49.2/kitty/screen.c#L3257-L3258)); since 0.38.1 |
| foot | 1.28.0 | Yes | dark when the `[colors-dark]` theme is active ([`csi.c`](https://codeberg.org/dnkl/foot/src/tag/1.28.0/csi.c#L568-L569)); since 1.23.0 |
| VTE | 0.84.1, [source](https://gitlab.gnome.org/GNOME/vte/-/tree/0.84.1) | Yes | dark when the default background is no lighter than the foreground ([`vte.cc`](https://github.com/GNOME/vte/blob/0.84.1/src/vte.cc#L2655-L2687)); since 0.82.0 |

## Validation

### COLORSCHEME-1: A default xterm does not recognize it

*A known difference: libghostty-vt does not pass this case.*

Input, one step per line:

```text
\e[?2031$p
\e[?2031h
\e[?2031$p
```

Expected screen:

```text
|____|
reply \e[?2031;0$y\e[?2031;0$y
```

### COLORSCHEME-2: Setting it sends nothing

Input, one step per line:

```text
\e[?2031h
```

Expected screen:

```text
|____|
reply none
```

### COLORSCHEME-3: A default xterm does not answer the query

Input, one step per line:

```text
\e[?996n
```

Expected screen:

```text
|____|
reply none
```

### COLORSCHEME-4: The screen does not change

Input, one step per line:

```text
A
\e[?2031h
\e[?996n
\e[?2031l
B
```

Expected screen:

```text
|AB__|
cursor 1,3
```

---

This is the Markdown version of <https://control-codes.page/modes/color-scheme/>. On that page every validation case runs live in libghostty-vt, the terminal emulation core of Ghostty, compiled to WebAssembly.

The validation cases are written in the notation described in <https://control-codes.page/notation/index.html.md>.
