theming: live-retheme GTK3 apps via a named theme per wallpaper

GTK3 parses ~/.config/gtk-3.0/gtk.css once per process behind a static
guard and never re-reads it, and it outranks the theme at PRIORITY_USER —
so colours there pin an app to whatever was live when it started. Install
a Fred-<wallpaper> theme instead and switch gtk-theme over gsettings,
which xdg-desktop-portal-gtk forwards to running apps.

One name per wallpaper because gtk_css_provider_get_named memoises
providers in a static hash table keyed by name.

GTK4/libadwaita ignores gtk-theme-name, so it keeps the user CSS and
still needs a restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-19 16:23:40 +01:00
parent f1a0a70244
commit e9f27b8255

View file

@ -6,10 +6,10 @@
# `theme-apply` drops that wallpaper's rendered config into place and nudges # `theme-apply` drops that wallpaper's rendered config into place and nudges
# every app that can reload without restarting. # every app that can reload without restarting.
# #
# What follows a switch immediately: quickshell, hyprland, vesktop, folder # What follows a switch immediately: quickshell, hyprland, ghostty (D-Bus
# icons, and ghostty (reloaded over its D-Bus action). GTK apps only re-read # reload-config), GTK3 apps such as nemo (each wallpaper installs its own
# their stylesheet at startup, so nemo etc. change on next launch; so do btop, # named GTK theme, switched over gsettings) and the folder icon tint. On next
# zen and spotify-player. # launch only: GTK4/libadwaita apps, btop, zen, spotify-player.
# #
# stylix stays the source of truth for build-time defaults and for every # stylix stays the source of truth for build-time defaults and for every
# target not listed here — walls/wallpaper.png remains its image. # target not listed here — walls/wallpaper.png remains its image.
@ -337,11 +337,60 @@ let
done done
''; '';
# A real, installed GTK3 theme per wallpaper — the only way a RUNNING GTK3
# app ever changes colour.
#
# ~/.config/gtk-3.0/gtk.css cannot do it. gtksettings.c loads that file
# behind `if (G_UNLIKELY (!css_provider))` on a static, so it is parsed once
# per process and never re-read; and it sits at PRIORITY_USER, above the
# theme, so leaving a stale copy there would also outrank whatever the theme
# supplies. The named-theme provider, by contrast, is reloaded whenever
# gtk-theme-name changes — that signal reaches apps here through
# xdg-desktop-portal-gtk.
#
# Hence one theme NAME per wallpaper rather than a single "Fred" that gets
# rewritten: gtk_css_provider_get_named memoises providers in a static hash
# table keyed by name, so reusing the name would hand back the CSS parsed
# the first time. Distinct names also make switching back to an earlier
# wallpaper a cache hit with the right colours.
gtkThemeName = f: "Fred-${wallSlug f}";
gtkTheme = f: s: pkgs.runCommand "gtk-theme-${wallSlug f}" { } ''
mkdir -p $out/gtk-3.0
# GTK resolves the theme's relative url(assets/...) against the theme dir,
# not the imported file, so the artwork has to be reachable from here.
ln -s ${pkgs.adw-gtk3}/share/themes/adw-gtk3/gtk-3.0/assets $out/gtk-3.0/assets
# Both variants: gtk-application-prefer-dark-theme is on, so GTK asks for
# gtk-dark.css, and _gtk_css_provider_load_named falls back to gtk.css
# only if the variant is missing entirely.
for v in gtk.css gtk-dark.css; do
{
echo "@import url(\"file://${pkgs.adw-gtk3}/share/themes/adw-gtk3/gtk-3.0/$v\");"
cat ${gtkCss s}
} > $out/gtk-3.0/$v
done
cat > $out/index.theme <<'EOF'
[Desktop Entry]
Type=X-GNOME-Metatheme
Name=${gtkThemeName f}
Comment=adw-gtk3 tinted to ${wallStem f}
Encoding=UTF-8
[X-GNOME-Metatheme]
GtkTheme=${gtkThemeName f}
EOF
grep -q '@import url' $out/gtk-3.0/gtk-dark.css
grep -q '@define-color window_bg_color' $out/gtk-3.0/gtk-dark.css
'';
# ── One theme directory per wallpaper ────────────────────────────────── # ── One theme directory per wallpaper ──────────────────────────────────
themeFor = f: let s = schemeOf f; in pkgs.runCommand "theme-${wallSlug f}" { } '' themeFor = f: let s = schemeOf f; in pkgs.runCommand "theme-${wallSlug f}" { } ''
mkdir -p $out mkdir -p $out
cp ${wallPalette f}/palette.json $out/palette.json cp ${wallPalette f}/palette.json $out/palette.json
cp ${gtkCss s} $out/gtk3.css cp ${gtkCss s} $out/gtk3.css
echo -n "${gtkThemeName f}" > $out/gtk-theme-name
cp ${gtk4Css s} $out/gtk4.css cp ${gtk4Css s} $out/gtk4.css
cp ${ghosttyConf s} $out/ghostty.conf cp ${ghosttyConf s} $out/ghostty.conf
cp ${btopTheme s} $out/btop.theme cp ${btopTheme s} $out/btop.theme
@ -403,7 +452,14 @@ let
# Ghostty and btop are pointed straight at the state symlink instead # Ghostty and btop are pointed straight at the state symlink instead
# (see the home-manager block below) — one less write to go wrong, and no # (see the home-manager block below) — one less write to go wrong, and no
# way for a stale copy to survive a switch. # way for a stale copy to survive a switch.
install -Dm644 "$theme/gtk3.css" "$HOME/.config/gtk-3.0/gtk.css" # NOTE: no ~/.config/gtk-3.0/gtk.css. GTK3's colours come from the
# installed Fred-<wallpaper> theme instead (see gtkTheme above); a copy
# here would sit at PRIORITY_USER, outranking the theme, and GTK3 parses
# it once per process — so it would pin every GTK3 app to whichever
# wallpaper was live when the app started.
#
# GTK4 still needs it: libadwaita ignores gtk-theme-name, so user CSS is
# the only way in, and those apps do change only on restart.
install -Dm644 "$theme/gtk4.css" "$HOME/.config/gtk-4.0/gtk.css" install -Dm644 "$theme/gtk4.css" "$HOME/.config/gtk-4.0/gtk.css"
# Vesktop gets the same CSS twice, on purpose. # Vesktop gets the same CSS twice, on purpose.
# #
@ -463,22 +519,13 @@ let
fi fi
fi fi
# GTK reads ~/.config/gtk-{3,4}.0/gtk.css once at startup and does not # Point GTK3 at this wallpaper's theme. Every wallpaper has its own theme
# watch it, so running apps only re-read on a theme-name change. Toggling # name, so this is always a real change: gsettings emits, the portal
# the gsettings key away and back is the one nudge that reaches them, and # forwards it, and running GTK3 apps reload the named theme — no restart,
# only lands if something is bridging gsettings to GTK (the portal). Nemo # no away-and-back dance.
# picks the new colours up on next launch regardless. gtktheme=$(cat "$theme/gtk-theme-name")
gsettings=${pkgs.glib.bin}/bin/gsettings ${pkgs.glib.bin}/bin/gsettings set org.gnome.desktop.interface gtk-theme "$gtktheme" \
iface=org.gnome.desktop.interface || echo "gsettings set gtk-theme $gtktheme failed"
if cur=$($gsettings get $iface gtk-theme 2>/dev/null); then
cur=''${cur//\'/}
# The detour has to be a DIFFERENT name or gsettings writes the same
# value, emits no change signal, and nothing reloads.
away=Adwaita
[ "$cur" = "$away" ] && away=Adwaita-dark
$gsettings set $iface gtk-theme "$away" 2>/dev/null || true
$gsettings set $iface gtk-theme "$cur" 2>/dev/null || true
fi
''; '';
in in
{ {
@ -527,8 +574,18 @@ in
# every ANSI-coloured program in a ghostty window follows a switch live. # every ANSI-coloured program in a ghostty window follows a switch live.
stylix.targets.fish.enable = false; stylix.targets.fish.enable = false;
# Everything stylix's gtk target did apart from writing gtk.css. # Every wallpaper's GTK3 theme, installed up front. theme-apply only
gtk.theme = { package = pkgs.adw-gtk3; name = "adw-gtk3"; }; # flips gtk-theme between them, so they all have to exist on disk —
# GTK silently falls back to Adwaita for a name it cannot find.
home.file = lib.listToAttrs (map (f: lib.nameValuePair
".local/share/themes/${gtkThemeName f}"
{ source = gtkTheme f (schemeOf f); })
wallFiles);
# Everything stylix's gtk target did apart from writing gtk.css. The
# name here is only the pre-first-switch fallback and what lands in
# settings.ini; the live value comes from gsettings via the portal.
gtk.theme = { package = pkgs.adw-gtk3; name = gtkThemeName (lib.head wallFiles); };
# Explicitly null, not merely unset: home-manager defaults gtk4.theme to # Explicitly null, not merely unset: home-manager defaults gtk4.theme to
# gtk.theme and then writes gtk-4.0/gtk.css to @import the theme, which # gtk.theme and then writes gtk-4.0/gtk.css to @import the theme, which
# is a file theme-apply owns. gtk4Css reproduces that import itself. # is a file theme-apply owns. gtk4Css reproduces that import itself.