diff --git a/.gitignore b/.gitignore index 860119d..ea3e2dc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ __pycache__/ # Generated from the colour scheme; machine state, not configuration. /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 diff --git a/config/dot/gtk-3.0/settings.ini b/config/dot/gtk-3.0/settings.ini.template similarity index 60% rename from config/dot/gtk-3.0/settings.ini rename to config/dot/gtk-3.0/settings.ini.template index 02eb639..e4ebce2 100644 --- a/config/dot/gtk-3.0/settings.ini +++ b/config/dot/gtk-3.0/settings.ini.template @@ -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] # 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 # themes that aren't installed (Tahoe-Dark, WhiteSur-cursors) without anything # appearing broken. -gtk-theme-name=adw-gtk3-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 -gtk-application-prefer-dark-theme=1 +gtk-application-prefer-dark-theme=@PREFER_DARK@ gtk-toolbar-style=GTK_TOOLBAR_ICONS gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR diff --git a/config/dot/gtk-4.0/settings.ini b/config/dot/gtk-4.0/settings.ini deleted file mode 100644 index bb748bb..0000000 --- a/config/dot/gtk-4.0/settings.ini +++ /dev/null @@ -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 diff --git a/config/dot/gtk-4.0/settings.ini.template b/config/dot/gtk-4.0/settings.ini.template new file mode 100644 index 0000000..973a818 --- /dev/null +++ b/config/dot/gtk-4.0/settings.ini.template @@ -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 diff --git a/config/dot/quickshell/scripts/panama-theme-apps b/config/dot/quickshell/scripts/panama-theme-apps index 8b2708e..a33ff31 100755 --- a/config/dot/quickshell/scripts/panama-theme-apps +++ b/config/dot/quickshell/scripts/panama-theme-apps @@ -11,6 +11,11 @@ # Terminals are the notable exception -- they predate the standard and carry # 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 # # 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 ;; 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" theme_file="$kitty_dir/themes/tokyonight-moon.conf" [[ "$scheme" == "light" ]] && theme_file="$kitty_dir/themes/tokyonight-day.conf" diff --git a/config/dot/quickshell/services/ColorScheme.qml b/config/dot/quickshell/services/ColorScheme.qml index 4f09838..2de1c7b 100644 --- a/config/dot/quickshell/services/ColorScheme.qml +++ b/config/dot/quickshell/services/ColorScheme.qml @@ -78,9 +78,21 @@ Singleton { root.lastError = ""; 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. - const gtkTheme = root.dark ? "Adwaita-dark" : "Adwaita"; + + // adw-gtk3, not Adwaita. This is the bug that made dark mode look + // 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 = [ ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme], diff --git a/setup/scripts/link-dotfiles b/setup/scripts/link-dotfiles index b3e0fde..352480b 100755 --- a/setup/scripts/link-dotfiles +++ b/setup/scripts/link-dotfiles @@ -70,6 +70,34 @@ for dir in "${dirs[@]}"; do log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir" 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 # 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 diff --git a/tests/quickshell/gtk-theme-contract.sh b/tests/quickshell/gtk-theme-contract.sh new file mode 100755 index 0000000..07e0b26 --- /dev/null +++ b/tests/quickshell/gtk-theme-contract.sh @@ -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'