Fix dark mode: the GTK theme it asked for does not exist
Reported symptom: Electron applications and a Chromium-based browser, all set to follow the system, went light when the desktop went light and never came back. Nothing reported an error, and the portal was serving the correct value the whole time. Cause: ColorScheme set gtk-theme to "Adwaita-dark" for dark and "Adwaita" for light. Neither is installed on Fedora 44 -- only adw-gtk3 and adw-gtk3-dark are. GTK responds to an unknown theme name by falling back to its built-in default, which is LIGHT. So asking for light worked by accident, asking for dark silently produced light, and anything that takes its cue from the GTK theme rather than the portal stayed light no matter what org.freedesktop.appearance said. Verified: the portal emits correctly in both directions, so this was never the portal's fault. Second cause, the mirror of the first: gtk-3.0/settings.ini and gtk-4.0/settings.ini were pinned to adw-gtk3-dark and prefer-dark=1 and never regenerated. Under GNOME that file is ignored because gnome-settings-daemon publishes over XSETTINGS; under Hyprland nothing does, so for GTK3 it is authoritative -- and it contradicted the scheme in light mode. They are now generated from a template on every switch, gitignored as machine state, and seeded by link-dotfiles, the same shape kitty's current-theme.conf already uses. These directories are symlinked into the repository, so writing the live file directly would dirty the working tree on every theme switch. The failure was silent by construction, so it gets a test rather than a comment: gtk-theme-contract asserts every theme name Panama sets is actually installed, and that the generated GTK config agrees with the scheme in both directions. Verified it catches both original bugs. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -16,3 +16,7 @@ __pycache__/
|
|||||||
|
|
||||||
# Generated from the colour scheme; machine state, not configuration.
|
# Generated from the colour scheme; machine state, not configuration.
|
||||||
/config/dot/kitty/current-theme.conf
|
/config/dot/kitty/current-theme.conf
|
||||||
|
|
||||||
|
# Generated from the colour scheme; machine state, not configuration.
|
||||||
|
/config/dot/gtk-3.0/settings.ini
|
||||||
|
/config/dot/gtk-4.0/settings.ini
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
|
#
|
||||||
|
# The theme name and dark preference below follow Panama's colour
|
||||||
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
# settings daemon, so for GTK3 it is authoritative -- which is why it
|
||||||
|
# has to change with the scheme rather than being pinned to dark.
|
||||||
|
#
|
||||||
[Settings]
|
[Settings]
|
||||||
# These values must match `gsettings get org.gnome.desktop.interface ...`.
|
# These values must match `gsettings get org.gnome.desktop.interface ...`.
|
||||||
#
|
#
|
||||||
@@ -6,12 +15,12 @@
|
|||||||
# this file becomes authoritative for GTK3 — which is why it previously named
|
# this file becomes authoritative for GTK3 — which is why it previously named
|
||||||
# themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything
|
# themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything
|
||||||
# appearing broken.
|
# appearing broken.
|
||||||
gtk-theme-name=adw-gtk3-dark
|
gtk-theme-name=@GTK_THEME@
|
||||||
gtk-icon-theme-name=Adwaita
|
gtk-icon-theme-name=Adwaita
|
||||||
gtk-font-name=Adwaita Sans 11
|
gtk-font-name=Adwaita Sans 11
|
||||||
gtk-cursor-theme-name=oreo_blue_cursors
|
gtk-cursor-theme-name=oreo_blue_cursors
|
||||||
gtk-cursor-theme-size=24
|
gtk-cursor-theme-size=24
|
||||||
gtk-application-prefer-dark-theme=1
|
gtk-application-prefer-dark-theme=@PREFER_DARK@
|
||||||
|
|
||||||
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
gtk-toolbar-style=GTK_TOOLBAR_ICONS
|
||||||
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
|
gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[Settings]
|
|
||||||
# libadwaita apps normally take their dark preference from the
|
|
||||||
# org.freedesktop.appearance portal (served by xdg-desktop-portal-gtk, which
|
|
||||||
# reads gsettings). This file is the fallback for plain GTK4 apps and for the
|
|
||||||
# window before the portal answers.
|
|
||||||
gtk-application-prefer-dark-theme=1
|
|
||||||
gtk-theme-name=adw-gtk3-dark
|
|
||||||
gtk-icon-theme-name=Adwaita
|
|
||||||
gtk-font-name=Adwaita Sans 11
|
|
||||||
gtk-cursor-theme-name=oreo_blue_cursors
|
|
||||||
gtk-cursor-theme-size=24
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# GENERATED FILE -- edit settings.ini.template instead.
|
||||||
|
#
|
||||||
|
# The theme name and dark preference below follow Panama's colour
|
||||||
|
# scheme, so this file is regenerated on every switch and is not
|
||||||
|
# committed. Under GNOME, gnome-settings-daemon publishes these over
|
||||||
|
# XSETTINGS and this file is ignored; under Hyprland there is no
|
||||||
|
# settings daemon, so for GTK3 it is authoritative -- which is why it
|
||||||
|
# has to change with the scheme rather than being pinned to dark.
|
||||||
|
#
|
||||||
|
[Settings]
|
||||||
|
# libadwaita apps normally take their dark preference from the
|
||||||
|
# org.freedesktop.appearance portal (served by xdg-desktop-portal-gtk, which
|
||||||
|
# reads gsettings). This file is the fallback for plain GTK4 apps and for the
|
||||||
|
# window before the portal answers.
|
||||||
|
gtk-application-prefer-dark-theme=@PREFER_DARK@
|
||||||
|
gtk-theme-name=@GTK_THEME@
|
||||||
|
gtk-icon-theme-name=Adwaita
|
||||||
|
gtk-font-name=Adwaita Sans 11
|
||||||
|
gtk-cursor-theme-name=oreo_blue_cursors
|
||||||
|
gtk-cursor-theme-size=24
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
# Terminals are the notable exception -- they predate the standard and carry
|
# Terminals are the notable exception -- they predate the standard and carry
|
||||||
# their own palettes. kitty is handled here.
|
# their own palettes. kitty is handled here.
|
||||||
#
|
#
|
||||||
|
# GTK3 is the other one. Under GNOME, gnome-settings-daemon publishes the theme
|
||||||
|
# over XSETTINGS; under Hyprland nothing does, so ~/.config/gtk-3.0/settings.ini
|
||||||
|
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
||||||
|
# scheme in light mode, so it is generated from a template here instead.
|
||||||
|
#
|
||||||
# panama-theme-apps dark|light
|
# panama-theme-apps dark|light
|
||||||
#
|
#
|
||||||
# kitty gets it twice: the generated include file so terminals opened later
|
# kitty gets it twice: the generated include file so terminals opened later
|
||||||
@@ -26,6 +31,36 @@ case "$scheme" in
|
|||||||
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
|
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# ── GTK ──────────────────────────────────────────────────────────────────────
|
||||||
|
# adw-gtk3, not Adwaita: no Adwaita GTK theme is installed on Fedora 44, and
|
||||||
|
# naming a theme that does not exist makes GTK fall back to its light default --
|
||||||
|
# which made dark mode silently produce light windows.
|
||||||
|
if [[ "$scheme" == "light" ]]; then
|
||||||
|
gtk_theme="adw-gtk3"
|
||||||
|
prefer_dark=0
|
||||||
|
else
|
||||||
|
gtk_theme="adw-gtk3-dark"
|
||||||
|
prefer_dark=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
status_gtk="skipped"
|
||||||
|
for gtk_version in 3.0 4.0; do
|
||||||
|
gtk_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-$gtk_version"
|
||||||
|
template="$gtk_dir/settings.ini.template"
|
||||||
|
[[ -r "$template" ]] || continue
|
||||||
|
|
||||||
|
# Written atomically: a GTK application starting mid-write would otherwise
|
||||||
|
# read a truncated file and fall back to defaults.
|
||||||
|
if sed -e "s/@GTK_THEME@/$gtk_theme/" -e "s/@PREFER_DARK@/$prefer_dark/" "$template" \
|
||||||
|
>"$gtk_dir/settings.ini.tmp" 2>/dev/null \
|
||||||
|
&& mv "$gtk_dir/settings.ini.tmp" "$gtk_dir/settings.ini" 2>/dev/null; then
|
||||||
|
status_gtk="written"
|
||||||
|
else
|
||||||
|
rm -f "$gtk_dir/settings.ini.tmp"
|
||||||
|
status_gtk="failed"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
kitty_dir="${XDG_CONFIG_HOME:-$HOME/.config}/kitty"
|
kitty_dir="${XDG_CONFIG_HOME:-$HOME/.config}/kitty"
|
||||||
theme_file="$kitty_dir/themes/tokyonight-moon.conf"
|
theme_file="$kitty_dir/themes/tokyonight-moon.conf"
|
||||||
[[ "$scheme" == "light" ]] && theme_file="$kitty_dir/themes/tokyonight-day.conf"
|
[[ "$scheme" == "light" ]] && theme_file="$kitty_dir/themes/tokyonight-day.conf"
|
||||||
|
|||||||
@@ -78,9 +78,21 @@ Singleton {
|
|||||||
root.lastError = "";
|
root.lastError = "";
|
||||||
|
|
||||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
||||||
// Adwaita's light and dark are the same theme; only the preference and
|
|
||||||
// the -dark suffix differ, so applications that honour either agree.
|
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
||||||
const gtkTheme = root.dark ? "Adwaita-dark" : "Adwaita";
|
// broken while light mode looked fine:
|
||||||
|
//
|
||||||
|
// Neither "Adwaita" nor "Adwaita-dark" is an installed theme on Fedora
|
||||||
|
// 44 -- only adw-gtk3 and adw-gtk3-dark are. Naming a theme that does
|
||||||
|
// not exist makes GTK fall back to its built-in default, which is
|
||||||
|
// LIGHT. So asking for light accidentally worked, asking for dark
|
||||||
|
// silently produced light, and applications that take their cue from
|
||||||
|
// the GTK theme rather than the portal -- Chromium and Electron, when
|
||||||
|
// built against GTK -- stayed light no matter what the portal said.
|
||||||
|
//
|
||||||
|
// gtk-theme-contract asserts these names are actually installed,
|
||||||
|
// because the failure mode is silent in exactly this way.
|
||||||
|
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
||||||
|
|
||||||
const commands = [
|
const commands = [
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
||||||
|
|||||||
@@ -70,6 +70,34 @@ for dir in "${dirs[@]}"; do
|
|||||||
log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir"
|
log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# GTK3 has no include mechanism, so its settings.ini is generated whole from a
|
||||||
|
# template rather than layered. Without this, a fresh checkout has a template
|
||||||
|
# and no settings.ini, and GTK3 applications fall back to their built-in theme.
|
||||||
|
# panama-theme-apps rewrites both files on every scheme change after this.
|
||||||
|
for gtk_version in 3.0 4.0; do
|
||||||
|
gtk_template="$PANAMA_DOT/gtk-$gtk_version/settings.ini.template"
|
||||||
|
gtk_settings="$PANAMA_DOT/gtk-$gtk_version/settings.ini"
|
||||||
|
[ -r "$gtk_template" ] || continue
|
||||||
|
if [ -e "$gtk_settings" ]; then
|
||||||
|
log "Keeping existing GTK settings at $gtk_settings"
|
||||||
|
else
|
||||||
|
gtk_scheme="dark"
|
||||||
|
gtk_prefs="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json"
|
||||||
|
if [ -r "$gtk_prefs" ]; then
|
||||||
|
gtk_stored="$(jq -r '.colorScheme // "dark"' "$gtk_prefs" 2>/dev/null || echo dark)"
|
||||||
|
[ "$gtk_stored" = "light" ] && gtk_scheme="light"
|
||||||
|
fi
|
||||||
|
if [ "$gtk_scheme" = "light" ]; then
|
||||||
|
gtk_name="adw-gtk3"; gtk_dark=0
|
||||||
|
else
|
||||||
|
gtk_name="adw-gtk3-dark"; gtk_dark=1
|
||||||
|
fi
|
||||||
|
sed -e "s/@GTK_THEME@/$gtk_name/" -e "s/@PREFER_DARK@/$gtk_dark/" \
|
||||||
|
"$gtk_template" > "$gtk_settings"
|
||||||
|
log "Generated GTK $gtk_version settings ($gtk_name) → $gtk_settings"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# kitty.conf ends with `include current-theme.conf`, and that file is generated
|
# kitty.conf ends with `include current-theme.conf`, and that file is generated
|
||||||
# from the desktop colour scheme rather than committed -- it is machine state.
|
# from the desktop colour scheme rather than committed -- it is machine state.
|
||||||
# A fresh checkout therefore has no such file, and kitty starts by complaining
|
# A fresh checkout therefore has no such file, and kitty starts by complaining
|
||||||
|
|||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Every GTK theme name Panama sets must be a theme that is actually installed.
|
||||||
|
#
|
||||||
|
# This exists because of a bug that was invisible for weeks. ColorScheme set
|
||||||
|
# gtk-theme to "Adwaita-dark" for dark and "Adwaita" for light. Neither is
|
||||||
|
# installed on Fedora 44 -- only adw-gtk3 and adw-gtk3-dark are -- and GTK
|
||||||
|
# responds to an unknown theme name by silently falling back to its built-in
|
||||||
|
# default, which is LIGHT.
|
||||||
|
#
|
||||||
|
# So light mode appeared to work, dark mode produced light windows, and nothing
|
||||||
|
# anywhere reported an error. Applications that take their cue from the GTK
|
||||||
|
# theme rather than the portal -- Chromium and Electron among them -- were stuck
|
||||||
|
# light with no way to diagnose it from inside the application.
|
||||||
|
#
|
||||||
|
# The failure is silent by construction, so it needs a test rather than a
|
||||||
|
# comment. Checks the compositor-facing setting and the generated GTK config
|
||||||
|
# agree, and that both name something real.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
color_scheme="$repo_dir/config/dot/quickshell/services/ColorScheme.qml"
|
||||||
|
theme_apps="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf 'gtk theme contract: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_installed() {
|
||||||
|
local name="$1" dir
|
||||||
|
for dir in /usr/share/themes "$HOME/.themes" "$HOME/.local/share/themes"; do
|
||||||
|
[[ -d "$dir/$name/gtk-3.0" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── The names ColorScheme sets must exist ────────────────────────────────────
|
||||||
|
names="$(grep -oE 'root\.dark \? "[a-zA-Z0-9-]+" : "[a-zA-Z0-9-]+"' "$color_scheme" \
|
||||||
|
| grep -oE '"[a-zA-Z0-9-]+"' | tr -d '"' | grep -E '^adw-|^Adwaita' | sort -u)"
|
||||||
|
[[ -n "$names" ]] || fail 'could not find the GTK theme names in ColorScheme.qml -- this contract is not reading it correctly'
|
||||||
|
|
||||||
|
while read -r name; do
|
||||||
|
[[ -n "$name" ]] || continue
|
||||||
|
theme_installed "$name" \
|
||||||
|
|| fail "ColorScheme sets gtk-theme to \"$name\", which is not installed. GTK falls back to its light default when a theme is missing, so this produces light windows in dark mode with no error anywhere."
|
||||||
|
done <<<"$names"
|
||||||
|
|
||||||
|
# ── The generated GTK config must agree, in both directions ──────────────────
|
||||||
|
# Generated into a fixture rather than the live config, so running this cannot
|
||||||
|
# retheme the desktop it is running on.
|
||||||
|
fixture="$(mktemp -d /tmp/panama-gtk-theme.XXXXXX)"
|
||||||
|
trap 'rm -rf "$fixture"' EXIT
|
||||||
|
|
||||||
|
for version in 3.0 4.0; do
|
||||||
|
mkdir -p "$fixture/gtk-$version"
|
||||||
|
cp "$repo_dir/config/dot/gtk-$version/settings.ini.template" "$fixture/gtk-$version/" \
|
||||||
|
|| fail "gtk-$version has no settings.ini.template -- the generated file would never be produced"
|
||||||
|
done
|
||||||
|
|
||||||
|
for scheme in dark light; do
|
||||||
|
XDG_CONFIG_HOME="$fixture" "$theme_apps" "$scheme" >/dev/null 2>&1
|
||||||
|
|
||||||
|
for version in 3.0 4.0; do
|
||||||
|
generated="$fixture/gtk-$version/settings.ini"
|
||||||
|
[[ -r "$generated" ]] || fail "gtk-$version settings.ini was not generated for $scheme"
|
||||||
|
|
||||||
|
grep -q '@GTK_THEME@\|@PREFER_DARK@' "$generated" \
|
||||||
|
&& fail "gtk-$version settings.ini still contains an unsubstituted placeholder for $scheme"
|
||||||
|
|
||||||
|
theme="$(sed -n 's/^gtk-theme-name=//p' "$generated")"
|
||||||
|
prefer="$(sed -n 's/^gtk-application-prefer-dark-theme=//p' "$generated")"
|
||||||
|
|
||||||
|
theme_installed "$theme" \
|
||||||
|
|| fail "gtk-$version settings.ini names \"$theme\" for $scheme, which is not installed"
|
||||||
|
|
||||||
|
if [[ "$scheme" == "dark" ]]; then
|
||||||
|
[[ "$prefer" == "1" ]] || fail "gtk-$version asks for prefer-dark=$prefer in dark mode"
|
||||||
|
[[ "$theme" == *dark* ]] || fail "gtk-$version uses \"$theme\" in dark mode, which is not a dark theme"
|
||||||
|
else
|
||||||
|
[[ "$prefer" == "0" ]] || fail "gtk-$version asks for prefer-dark=$prefer in light mode"
|
||||||
|
[[ "$theme" != *dark* ]] || fail "gtk-$version uses \"$theme\" in light mode, which is a dark theme"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
printf 'gtk theme contract: PASS\n'
|
||||||
Reference in New Issue
Block a user