#13604

Mode 2031 reports do not work unless both a dark and a light theme are configured

Openjcollieopened 2mo ago

Update:

I managed to find the source of the issue.

With an isolated Ghostty (debug build) instance launched using:

ghostty  --config-default-files=false --window-theme=dark

Ghostty's GTK log correctly reports:

debug(gtk_ghostty_application): style manager changed scheme=.dark

However, querying the current color scheme with CSI ? 996 n returns:

CSI ? 997 ; 2 n

which indicates light mode.

I was able to narrow this down further by configuring an actual conditional theme pair:

theme = light:Rose Pine Dawn,dark:Rose Pine

With that configuration, the same query correctly returns:

CSI ? 997 ; 1 n

The issue appears to be in Config.changeConditionalState. The function only rebuilds the configuration when the changed state key is present in _conditional_set. A single, non-conditional theme such as:

theme = Kitty Default

does not add .theme to that set. As a result, changing the application color scheme from the default .light state to .dark is treated as irrelevant and the function returns null.

The surface and GTK runtime therefore know that the color scheme is dark, but the configuration copied into Termio retains its default _conditional_state.theme = .light. Since the mode-2031 response reads the theme from the Termio configuration, it reports light.

The following minimal patch treats the theme state as relevant even when the configured theme itself is not conditional, and updates the existing test accordingly:

diff --git a/src/config/Config.zig b/src/config/Config.zig
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -4416,7 +4416,7 @@
 
             // Conditional set contains the keys that this config uses. So we
             // only continue if we use this key.
-            if (self._conditional_set.contains(key) and !deepEqual(
+            if ((key == .theme or self._conditional_set.contains(key)) and !deepEqual(
                 @TypeOf(@field(self._conditional_state, field.name)),
                 @field(self._conditional_state, field.name),
                 @field(new, field.name),
@@ -10587,7 +10587,7 @@ test "changed" {
     try testing.expect(!source.changed(&dest, .@"font-size"));
 }
 
-test "changeConditionalState ignores irrelevant changes" {
+test "changeConditionalState preserves non-conditional theme state" {
     const testing = std.testing;
     const alloc = testing.allocator;
 
@@ -10600,9 +10600,12 @@ test "changeConditionalState ignores irrelevant changes" {
         try cfg.loadIter(alloc, &it);
         try cfg.finalize();
 
-        try testing.expect(try cfg.changeConditionalState(
-            .{ .theme = .dark },
-        ) == null);
+        var cfg2 = (try cfg.changeConditionalState(.{
+            .theme = .dark,
+        })).?;
+        defer cfg2.deinit();
+
+        try testing.expectEqual(.dark, cfg2._conditional_state.theme);
     }
 }

I changed the existing changeConditionalState test because it explicitly asserted the behavior responsible for the bug: a light-to-dark state change with a non-conditional theme was expected to return null. That assumption is no longer valid because the theme state is observable through mode 2031 even when it does not change any configured values. The revised test therefore verifies that the new configuration preserves _conditional_state.theme = .dark.

The drawback is that this replaces the test coverage for the original optimization, where genuinely irrelevant conditional-state changes should still return null. If there is a better way to implement a new test for this, I would defer to that instead.

This was reproduced against commit 6687d60.

Originally posted by @UnsaltedScholar in https://github.com/ghostty-org/ghostty/discussions/13448#discussioncomment-17889061

Activity

5
harrykwokdev2mo ago

Confirming that this also affects macOS (not only GTK).

Environment:

  • Ghostty 1.3.1 stable
  • macOS 26.6.2 (25G83), system appearance: Dark
  • Default Ghostty config (no user overrides): background = #282c34, window-theme = auto
  • pi 0.84.2 as the downstream application

In a fresh Ghostty window, I queried both mechanisms directly:

CSI ? 996 n  -> CSI ? 997 ; 2 n                         # light
OSC 11       -> ESC ] 11 ; rgb:2828/2c2c/3434 BEL      # #282c34, dark

This causes pi's automatic light/dark theme setting to select light, because it correctly gives the explicit color-scheme report precedence over the OSC 11 background response.

So the same stale conditional_state.theme = .light behavior is reproducible on macOS with the current stable Ghostty release. PR #13605 appears to address the underlying issue.

harrykwokdev2mo ago

Confirming that this also affects macOS (not only GTK).

Environment:

  • Ghostty 1.3.1 stable
  • macOS 26.6.2 (25G83), system appearance: Dark
  • Default Ghostty config (no user overrides): background = #282c34, window-theme = auto
  • pi 0.84.2 as the downstream application

In a fresh Ghostty window, I queried both mechanisms directly:

CSI ? 996 n  -> CSI ? 997 ; 2 n                         # light
OSC 11       -> ESC ] 11 ; rgb:2828/2c2c/3434 BEL      # #282c34, dark

This causes pi's automatic light/dark theme setting to select light, because it correctly gives the explicit color-scheme report precedence over the OSC 11 background response.

So the same stale conditional_state.theme = .light behavior is reproducible on macOS with the current stable Ghostty release. PR #13605 appears to address the underlying issue.

NizarAlKabbani2mo ago

Confirming on Linux/GTK, with one refinement to the workaround.

Environment

  • Ghostty 1.3.1-3.fc44 (Fedora 44, Terra), GTK, Wayland
  • KDE Plasma. The portal reports dark: org.freedesktop.appearance color-scheme reads 1 (prefer-dark), and GTK is at gtk-application-prefer-dark-theme=true
  • Colours set directly in the config (background = #0b0e0b, foreground, palette), with no theme = line at all

CSI ? 996 n returns CSI ? 997 ; 2 n (light) while OSC 11 returns the dark background, as in the macOS report above. Unchanged by --window-theme=light|dark|system, by --background=000000 or --background=ffffff, by GTK_THEME=Adwaita:dark, and by ADW_DEBUG_COLOR_SCHEME=prefer-dark. All re-run with --gtk-single-instance=false, so the flags are not being swallowed by an already-running instance.

The refinement: the conditional-theme-pair workaround only works if the two themes differ.

configreply
theme = light:embermoss,dark:embermossCSI ? 997 ; 2 n (still light)
theme = light:embermoss-light,dark:embermossCSI ? 997 ; 1 n (correct)

Both reproduced twice on the same build in the same session, with the two theme files differing only in background and foreground. Naming one theme on both sides is apparently not enough to get .theme into _conditional_set, which matters for anyone reaching for the workaround who only has one theme they actually want: they have to invent a second one that differs.

Downstream effect, in case it is useful for weighing #13605: yazi 26.8 chooses between its dark and light flavours on exactly this report. A flavour configured only for dark therefore vanishes inside Ghostty, while staying correct in Konsole, which does not implement the query and falls back to OSC 11, and inside tmux, which answers 997;1.

NizarAlKabbani2mo ago

Confirming on Linux/GTK, with one refinement to the workaround.

Environment

  • Ghostty 1.3.1-3.fc44 (Fedora 44, Terra), GTK, Wayland
  • KDE Plasma. The portal reports dark: org.freedesktop.appearance color-scheme reads 1 (prefer-dark), and GTK is at gtk-application-prefer-dark-theme=true
  • Colours set directly in the config (background = #0b0e0b, foreground, palette), with no theme = line at all

CSI ? 996 n returns CSI ? 997 ; 2 n (light) while OSC 11 returns the dark background, as in the macOS report above. Unchanged by --window-theme=light|dark|system, by --background=000000 or --background=ffffff, by GTK_THEME=Adwaita:dark, and by ADW_DEBUG_COLOR_SCHEME=prefer-dark. All re-run with --gtk-single-instance=false, so the flags are not being swallowed by an already-running instance.

The refinement: the conditional-theme-pair workaround only works if the two themes differ.

configreply
theme = light:embermoss,dark:embermossCSI ? 997 ; 2 n (still light)
theme = light:embermoss-light,dark:embermossCSI ? 997 ; 1 n (correct)

Both reproduced twice on the same build in the same session, with the two theme files differing only in background and foreground. Naming one theme on both sides is apparently not enough to get .theme into _conditional_set, which matters for anyone reaching for the workaround who only has one theme they actually want: they have to invent a second one that differs.

Downstream effect, in case it is useful for weighing #13605: yazi 26.8 chooses between its dark and light flavours on exactly this report. A flavour configured only for dark therefore vanishes inside Ghostty, while staying correct in Konsole, which does not implement the query and falls back to OSC 11, and inside tmux, which answers 997;1.

pluiedev2mo ago

Just for the record, the fact this is an issue means that we have a reliable way to reproduce it already. We therefore don't really need other reproductions and you should shift your attention to testing the associated PR instead: #13605