From e9f27b825515c5c639ebd3eb93ffe3df605e6da1 Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 19 Aug 2026 16:23:40 +0100 Subject: [PATCH] theming: live-retheme GTK3 apps via a named theme per wallpaper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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- 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 --- settings/theming.nix | 103 +++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 23 deletions(-) diff --git a/settings/theming.nix b/settings/theming.nix index 7cc2e4e..4ba2192 100644 --- a/settings/theming.nix +++ b/settings/theming.nix @@ -6,10 +6,10 @@ # `theme-apply` drops that wallpaper's rendered config into place and nudges # every app that can reload without restarting. # -# What follows a switch immediately: quickshell, hyprland, vesktop, folder -# icons, and ghostty (reloaded over its D-Bus action). GTK apps only re-read -# their stylesheet at startup, so nemo etc. change on next launch; so do btop, -# zen and spotify-player. +# What follows a switch immediately: quickshell, hyprland, ghostty (D-Bus +# reload-config), GTK3 apps such as nemo (each wallpaper installs its own +# named GTK theme, switched over gsettings) and the folder icon tint. On next +# launch only: GTK4/libadwaita apps, btop, zen, spotify-player. # # stylix stays the source of truth for build-time defaults and for every # target not listed here — walls/wallpaper.png remains its image. @@ -337,11 +337,60 @@ let 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 ────────────────────────────────── themeFor = f: let s = schemeOf f; in pkgs.runCommand "theme-${wallSlug f}" { } '' mkdir -p $out cp ${wallPalette f}/palette.json $out/palette.json cp ${gtkCss s} $out/gtk3.css + echo -n "${gtkThemeName f}" > $out/gtk-theme-name cp ${gtk4Css s} $out/gtk4.css cp ${ghosttyConf s} $out/ghostty.conf cp ${btopTheme s} $out/btop.theme @@ -403,7 +452,14 @@ let # 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 # 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- 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" # Vesktop gets the same CSS twice, on purpose. # @@ -463,22 +519,13 @@ let fi fi - # GTK reads ~/.config/gtk-{3,4}.0/gtk.css once at startup and does not - # watch it, so running apps only re-read on a theme-name change. Toggling - # the gsettings key away and back is the one nudge that reaches them, and - # only lands if something is bridging gsettings to GTK (the portal). Nemo - # picks the new colours up on next launch regardless. - gsettings=${pkgs.glib.bin}/bin/gsettings - iface=org.gnome.desktop.interface - 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 + # Point GTK3 at this wallpaper's theme. Every wallpaper has its own theme + # name, so this is always a real change: gsettings emits, the portal + # forwards it, and running GTK3 apps reload the named theme — no restart, + # no away-and-back dance. + gtktheme=$(cat "$theme/gtk-theme-name") + ${pkgs.glib.bin}/bin/gsettings set org.gnome.desktop.interface gtk-theme "$gtktheme" \ + || echo "gsettings set gtk-theme $gtktheme failed" ''; in { @@ -527,8 +574,18 @@ in # every ANSI-coloured program in a ghostty window follows a switch live. stylix.targets.fish.enable = false; - # Everything stylix's gtk target did apart from writing gtk.css. - gtk.theme = { package = pkgs.adw-gtk3; name = "adw-gtk3"; }; + # Every wallpaper's GTK3 theme, installed up front. theme-apply only + # 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 # 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.