From 99f452c99949774c522af7dcfb39d764964cb4ec Mon Sep 17 00:00:00 2001 From: euletheia <154215350+euletheia@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:31:39 +0000 Subject: [PATCH] feat(Themes): add support for GTK3 dynamic theming (#2523) * feat(Themes): add support for GTK3 dynamic theming Instead of overriding named colors of all GTK3 themes via the config directory file, only override named colors for each variant of the adw-gtk3 theme (light and dark), and let GTK3 apps change themes depending on the value of org.gnome.desktop.interface color-scheme. * fix(Themes): apply requested changes - remove gtk3 override if it's a symlink - remove call to gtk.sh with old arg 'assets-only' - append key to dconf path to respect dconf write signature * refactor(Themes): separate 'include'/'apply' logic for GTK3 dynamic theming * revert(Themes): reset timer between color-scheme refresh - rename DMS_ENABLE_GTK4_REFRESH to DMS_ENABLE_GTK_REFRESH - revert a3fba8 * feat(Themes): add 'remove' mode to gtk.sh Allow to remove all inclusions and overrides for GTK. * fix(Themes): backup adw-gtk3 original css in 'apply' (not 'include') * style(Themes): rename argument 'include' to 'patch' for clarity --- core/internal/matugen/matugen.go | 18 +- quickshell/Common/Theme.qml | 46 ++- quickshell/Services/PortalService.qml | 2 +- quickshell/matugen/templates/gtk-colors.css | 4 + .../matugen/templates/gtk-light-colors.css | 4 + quickshell/scripts/gtk.sh | 307 +++++++++++++----- 6 files changed, 283 insertions(+), 98 deletions(-) diff --git a/core/internal/matugen/matugen.go b/core/internal/matugen/matugen.go index e263ba486..e1c798a88 100644 --- a/core/internal/matugen/matugen.go +++ b/core/internal/matugen/matugen.go @@ -386,8 +386,8 @@ func buildOnce(opts *Options) (bool, error) { default: syncAccentColor(primaryDark) } - refreshGTK(opts.Mode) - refreshGTK4() + refreshGTKTheme(opts.Mode) + refreshGTKColorScheme() } if isDMSKDEColorSchemeActive(opts.ConfigDir) { @@ -911,7 +911,7 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo } func isDMSGTKActive(configDir string) bool { - gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css") + gtkCSS := filepath.Join(configDir, "gtk-4.0", "gtk.css") info, err := os.Lstat(gtkCSS) if err != nil { @@ -968,7 +968,7 @@ func applyKDEColorScheme(mode ColorMode) { } } -func refreshGTK(mode ColorMode) { +func refreshGTKTheme(mode ColorMode) { if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil { log.Warnf("Failed to reset gtk-theme: %v", err) } @@ -993,8 +993,8 @@ func expectColorSchemeEcho(scheme string) { // apps reload ~/.config/gtk-4.0 CSS (a gtk-theme flip does not). But apps // following the portal color-scheme (Chromium) can drop the restore signal // mid-repaint and latch the wrong mode, so this is opt-in. -func refreshGTK4() { - if os.Getenv("DMS_ENABLE_GTK4_REFRESH") != "1" { +func refreshGTKColorScheme() { + if os.Getenv("DMS_ENABLE_GTK_REFRESH") != "1" { return } output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme") @@ -1012,13 +1012,13 @@ func refreshGTK4() { expectColorSchemeEcho(toggle) if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil { - log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err) + log.Warnf("Failed to toggle color-scheme for GTK refresh: %v", err) return } - time.Sleep(400 * time.Millisecond) + time.Sleep(50 * time.Millisecond) expectColorSchemeEcho(current) if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil { - log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err) + log.Warnf("Failed to restore color-scheme for GTK refresh: %v", err) } } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 55fbfce93..eccd3fc84 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -144,7 +144,7 @@ Singleton { Quickshell.execDetached(["mkdir", "-p", stateDir]); // shellDir may be an embedded-UI extraction, which is read-only and // unexecutable (dankgo shellapp/shellfs makeReadOnly chmods 0444) - Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "", shellDir, "assets-only"]); + Quickshell.execDetached(["bash", shellDir + "/scripts/gtk.sh", configDir, "", "", shellDir]); Proc.runCommand("matugenCheck", ["sh", "-c", "command -v matugen"], (output, code) => { matugenAvailable = (code === 0) && !envDisableMatugen; @@ -1926,6 +1926,41 @@ Singleton { return colors; } + function refreshGtkTheme() { + const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode); + const theme = isLight ? "adw-gtk3" : "adw-gtk3-dark"; + const schema = "org.gnome.desktop.interface"; + const key = "gtk-theme"; + + const makeCmd = (tool, schema, val) => { + if (tool === "gsettings") { + return `gsettings set ${schema} ${key} '' && gsettings set ${schema} ${key} ${val}`; + } else { + const dconfPath = `/${schema.replace(/\./g, "/")}`; + return `dconf write ${dconfPath}/${key} "''" && dconf write ${dconfPath}/${key} "'${val}'"`; + } + }; + + Proc.runCommand("gtkRefresher", ["sh", "-c", makeCmd("gsettings", schema, theme)], (output, exitCode) => { + if (exitCode !== 0) { + Proc.runCommand("gtkRefreshFallback", ["sh", "-c", makeCmd("dconf", schema, theme)], (output, exitCode) => { + if (exitCode !== 0) { + log.warn("Failed to refresh gtk-theme"); + } + }); + } + }); + } + + function patchGtk3colors() { + const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode); + Proc.runCommand("gtk3Patcher", ["bash", shellDir + "/scripts/gtk.sh", configDir, "patch", isLight, shellDir], (output, exitCode) => { + if (exitCode !== 0) { + log.warn(`Failed to patch GTK3 colors: ${output}`); + } + }); + } + function applyGtkColors() { if (!matugenAvailable) { if (typeof ToastService !== "undefined") { @@ -1935,7 +1970,7 @@ Singleton { } const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"; - Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, isLight, shellDir], (output, exitCode) => { + Proc.runCommand("gtkApplier", ["bash", shellDir + "/scripts/gtk.sh", configDir, "apply", isLight, shellDir], (output, exitCode) => { if (exitCode === 0) { if (typeof ToastService !== "undefined" && typeof NiriService !== "undefined" && !NiriService.matugenSuppression) { ToastService.showInfo(I18n.tr("GTK colors applied successfully")); @@ -2131,8 +2166,13 @@ Singleton { root.matugenCompleted(currentMode, "error"); } - if (!pendingThemeRequest) + if (!pendingThemeRequest) { + if (SettingsData.matugenTemplateGtk) { + patchGtk3colors(); + refreshGtkTheme(); + } return; + } const req = pendingThemeRequest; pendingThemeRequest = null; diff --git a/quickshell/Services/PortalService.qml b/quickshell/Services/PortalService.qml index 82c7bc82c..60783405a 100644 --- a/quickshell/Services/PortalService.qml +++ b/quickshell/Services/PortalService.qml @@ -117,7 +117,7 @@ Singleton { Timer { id: colorSchemeSettleTimer - interval: 1000 + interval: 750 onTriggered: { if (!root.canSyncColorScheme()) return; diff --git a/quickshell/matugen/templates/gtk-colors.css b/quickshell/matugen/templates/gtk-colors.css index 2d181aa47..e620755d6 100644 --- a/quickshell/matugen/templates/gtk-colors.css +++ b/quickshell/matugen/templates/gtk-colors.css @@ -1,3 +1,5 @@ +/* BEGIN DMS OVERRIDE */ + /* * GTK Colors * Generated with Matugen @@ -46,3 +48,5 @@ a color between surface and surface container so I think just giving this surfac @define-color theme_unfocused_base_color @window_bg_color; @define-color theme_unfocused_selected_bg_color @accent_bg_color; @define-color theme_unfocused_selected_fg_color @accent_fg_color; + +/* END DMS OVERRIDE */ diff --git a/quickshell/matugen/templates/gtk-light-colors.css b/quickshell/matugen/templates/gtk-light-colors.css index 644762f20..9428d46a9 100644 --- a/quickshell/matugen/templates/gtk-light-colors.css +++ b/quickshell/matugen/templates/gtk-light-colors.css @@ -1,3 +1,5 @@ +/* BEGIN DMS OVERRIDE */ + /* * GTK Colors * Generated with Matugen @@ -7820,3 +7822,5 @@ window.background:not(.solid-csd):not(.csd) > notebook > stack:not(:only-child) @define-color sidebar_fg_color @window_fg_color; @define-color sidebar_border_color @window_bg_color; @define-color sidebar_backdrop_color @window_bg_color; + +/* END DMS OVERRIDE */ diff --git a/quickshell/scripts/gtk.sh b/quickshell/scripts/gtk.sh index 0c1e5a4c0..cb5ab705c 100755 --- a/quickshell/scripts/gtk.sh +++ b/quickshell/scripts/gtk.sh @@ -1,114 +1,251 @@ #!/usr/bin/env bash CONFIG_DIR="$1" -SHELL_DIR="$3" +# apply: setup symlinks and config dirs +# patch: refresh overrides in adw-gtk3 +# remove: remove all overrides +MODE="${2:-apply}" +IS_LIGHT="${3:-light}" if [ -z "$CONFIG_DIR" ]; then - echo "Usage: $0 [is_light] [shell_dir]" >&2 - exit 1 + echo "Usage: $0 [apply|patch|remove] [is_light] [shell_dir]" >&2 + exit 1 fi -# The light template references adw-gtk3 image assets relative to gtk.css -# (check/radio glyphs, slider knobs); without them checked boxes render as -# solid blocks. -link_gtk3_assets() { - local gtk3_dir="$1" - local assets_link="$gtk3_dir/assets" +get_adw_gtk3_dir() { + local variant="$1" + local name="" + [ "$variant" == "dark" ] && name="-$variant" - if [ -e "$assets_link" ] && [ ! -L "$assets_link" ]; then - echo "Leaving user-managed $assets_link in place" - return - fi + local candidates=( + "$HOME/.local/share/themes/adw-gtk3${name}/gtk-3.0" + "$HOME/.themes/adw-gtk3${name}/gtk-3.0" + "/usr/share/themes/adw-gtk3${name}/gtk-3.0" + "/usr/local/share/themes/adw-gtk3${name}/gtk-3.0" + ) + local target="" + for c in "${candidates[@]}"; do + if [ -d "$c" ]; then + target="$c" + break + fi + done + echo "$target" +} - local candidates=( - "$HOME/.local/share/themes/adw-gtk3/gtk-3.0/assets" - "$HOME/.themes/adw-gtk3/gtk-3.0/assets" - "/usr/share/themes/adw-gtk3/gtk-3.0/assets" - "/usr/local/share/themes/adw-gtk3/gtk-3.0/assets" - ) - local target="" - for c in "${candidates[@]}"; do - if [ -d "$c" ]; then - target="$c" - break - fi - done - if [ -z "$target" ] && [ -n "$SHELL_DIR" ] && [ -d "$SHELL_DIR/matugen/gtk3-assets" ]; then - target="$SHELL_DIR/matugen/gtk3-assets" - fi - if [ -z "$target" ]; then - return - fi +remove_gtk3_patch() { + local theme_dir="$1" + local css_variant="$2" + [ "$css_variant" != "-dark" ] && css_variant="" + sed -i '/\/\* BEGIN DMS OVERRIDE \*\//,/\/\* END DMS OVERRIDE \*\//d' "${theme_dir}/gtk${css_variant}.css" + return $? +} - ln -sfn "$target" "$assets_link" - echo "Linked GTK3 assets: $assets_link -> $target" +remove_gtk3_colors() { + local config_dir="$1" + + local gtk3_dir="$config_dir/gtk-3.0" + + # remove global override + if [ ! -f "${gtk3_dir}/dank-colors.css" ]; then + echo "Nothing to remove at '${gtk3_dir}'" + else + if rm "${gtk3_dir}/dank-colors.css"; then + echo "Removed GTK3 override form '${gtk3_dir}'" + else + echo "Failed to removed GTK3 override from '${gtk3_dir}'" + fi + fi + + # remove adw-gtk3 inclusions + for variant in light dark; do + local adw_gtk3_dir && adw_gtk3_dir=$(get_adw_gtk3_dir "$variant") + + if [ -z "$adw_gtk3_dir" ]; then + echo "Error: No version of adw-gtk3 ${variant} was found" >&2 + exit 1 + fi + + for css_variant in light dark; do + if remove_gtk3_patch "$adw_gtk3_dir" "$css_variant"; then + echo "Removed GTK colors patch from '${adw_gtk3_dir}' in '$css_variant' stylesheet" + else + echo "Failed to remove GTK colors patch from '${adw_gtk3_dir}' in '$css_variant' stylesheet" >&2 + fi + done + done +} + +do_patch() { + local theme_dir="$1" + local variant="$2" + local css_variant="" + [ "$variant" = "dark" ] && css_variant="-${variant}" + if { + remove_gtk3_patch "$theme_dir" "$css_variant" + cat "${gtk3_dir}/dank-colors.css" >>"${theme_dir}/gtk${css_variant}.css" + }; then + echo "Successfully patched '$theme_dir/gtk${css_variant}.css' with GTK '$variant' colors" + else + echo "Error: failed to patch '$theme_dir/gtk${css_variant}.css' with GTK '$variant' colors" >&2 + exit 1 + fi +} + +patch_gtk3_colors() { + local config_dir="$1" + local is_light="$2" + + # Include generated colors for current variant + local gtk3_dir="$config_dir/gtk-3.0" + local variant="light" + [ "$is_light" = "false" ] && variant="dark" + local adw_gtk3_dir && adw_gtk3_dir=$(get_adw_gtk3_dir "$variant") + + if [ -z "$adw_gtk3_dir" ]; then + echo "Warning: No version of adw-gtk3 ${variant} was found" >&2 + exit 1 + fi + + if [[ "$adw_gtk3_dir" =~ ^/usr ]]; then + echo "Warning: No user version of adw-gtk3 ${variant} was found." >&2 + exit 1 + fi + + if [ ! -f "${gtk3_dir}/dank-colors.css" ]; then + echo "Error: GTK3 dank-colors.css not found at '${gtk3_dir}'" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi + + # NOTE : for adw-gtk3-dark gtk.css and gtk-dark.css are the same file + if [ "$variant" = "dark" ]; then + do_patch "$adw_gtk3_dir" "dark" + do_patch "$adw_gtk3_dir" "light" + do_patch "$(get_adw_gtk3_dir "light")" "dark" + else + do_patch "$adw_gtk3_dir" "light" + fi } apply_gtk3_colors() { - local config_dir="$1" + local config_dir="$1" - local gtk3_dir="$config_dir/gtk-3.0" - local dank_colors="$gtk3_dir/dank-colors.css" - local gtk_css="$gtk3_dir/gtk.css" + local gtk3_dir="$config_dir/gtk-3.0" + local gtk3_override="$gtk3_dir/gtk.css" + # If no adw-gtk3 or only system wide, use global override + local adw_gtk3 && adw_gtk3="$(get_adw_gtk3_dir)" + if [[ "$adw_gtk3" =~ ^/usr ]] || [[ -z "$adw_gtk3" ]]; then + echo "Warning: No user version of adw-gtk3 found" >&2 + echo "Falling back on global css override" >&2 + local dank_colors="$gtk3_dir/dank-colors.css" - if [ ! -f "$dank_colors" ]; then - echo "Error: dank-colors.css not found at $dank_colors" >&2 - echo "Run matugen first to generate theme files" >&2 - exit 1 - fi + if [ ! -f "$dank_colors" ]; then + echo "Error: dank-colors.css not found at $dank_colors" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi - if [ -L "$gtk_css" ]; then - rm "$gtk_css" - elif [ -f "$gtk_css" ]; then - mv "$gtk_css" "$gtk_css.backup.$(date +%s)" - echo "Backed up existing gtk.css" - fi + if [ -L "$gtk3_override" ]; then + rm "$gtk3_override" + elif [ -f "$gtk3_override" ]; then + mv "$gtk3_override" "$gtk3_override.backup.$(date +%s)" + echo "Backed up existing gtk.css" + fi - ln -s "dank-colors.css" "$gtk_css" - echo "Created symlink: $gtk_css -> dank-colors.css" + ln -s "dank-colors.css" "$gtk3_override" + echo "Created symlink: $gtk3_override -> dank-colors.css" + + link_gtk3_assets "$gtk3_dir" + + return + fi + + # Else ensure there's no global override + if [ -L "$gtk3_override" ]; then + rm "$gtk3_override" + elif [ -f "$gtk3_override" ]; then + mv "$gtk3_override" "$gtk3_override.backup.$(date +%s)" + echo "Backed up and removed existing gtk.css" + fi + + # Backup adw-gtk3 stylesheets + for variant in light dark; do + local adw_gtk3_dir && adw_gtk3_dir="$(get_adw_gtk3_dir "$variant")" + cp "$adw_gtk3_dir/gtk-3.0/gtk.css" "$adw_gtk3_dir/gtk-3.0/gtk.css.backup.$(date +%s)" + cp "$adw_gtk3_dir/gtk-3.0/gtk-dark.css" "$adw_gtk3_dir/gtk-3.0/gtk-dark.css.backup.$(date +%s)" + done +} + +remove_gtk4_colors() { + local config_dir="$1" + + local gtk4_dir="$config_dir/gtk-4.0" + local dank_colors="$gtk4_dir/dank-colors.css" + local gtk_css="$gtk4_dir/gtk.css" + + if [ ! -f "$dank_colors" ]; then + echo "Nothing to remove in '$gtk4_dir'" + return + fi + + rm "$dank_colors" + echo "Removed 'dank-colors.css' from '$gtk4_dir'" + + local gtk4_import="@import url(\"dank-colors.css\");" + if [ -f "$gtk_css" ] && grep -q '^@import url.*dank-colors\.css.*);$' "$gtk_css"; then + sed -i "/$gtk4_import/d" "$gtk_css" + echo "Removed gtk4 import in '$gtk_css'" + fi - link_gtk3_assets "$gtk3_dir" } apply_gtk4_colors() { - local config_dir="$1" + local config_dir="$1" - local gtk4_dir="$config_dir/gtk-4.0" - local dank_colors="$gtk4_dir/dank-colors.css" - local gtk_css="$gtk4_dir/gtk.css" - local gtk4_import="@import url(\"dank-colors.css\");" + local gtk4_dir="$config_dir/gtk-4.0" + local dank_colors="$gtk4_dir/dank-colors.css" + local gtk_css="$gtk4_dir/gtk.css" + local gtk4_import="@import url(\"dank-colors.css\");" - if [ ! -f "$dank_colors" ]; then - echo "Error: GTK4 dank-colors.css not found at $dank_colors" >&2 - echo "Run matugen first to generate theme files" >&2 - exit 1 - fi + if [ ! -f "$dank_colors" ]; then + echo "Error: GTK4 dank-colors.css not found at $dank_colors" >&2 + echo "Run matugen first to generate theme files" >&2 + exit 1 + fi - if [ -f "$gtk_css" ] && grep -q '^@import url.*dank-colors\.css.*);$' "$gtk_css"; then - echo "GTK4 import already exists" - return - fi + if [ -f "$gtk_css" ] && grep -q '^@import url.*dank-colors\.css.*);$' "$gtk_css"; then + echo "GTK4 import already exists" + return + fi - if [ -f "$gtk_css" ] && [ -s "$gtk_css" ]; then - sed -i "1i\\$gtk4_import" "$gtk_css" - else - echo "$gtk4_import" >"$gtk_css" - fi - echo "Updated GTK4 CSS import" + if [ -f "$gtk_css" ] && [ -s "$gtk_css" ]; then + sed -i "1i\\$gtk4_import" "$gtk_css" + else + echo "$gtk4_import" >"$gtk_css" + fi + echo "Updated GTK4 CSS import" } -# Repair pass for shells whose gtk.css was linked before asset handling -# existed; only acts when DMS already manages gtk.css. -if [ "$4" = "assets-only" ]; then - if [ -L "$CONFIG_DIR/gtk-3.0/gtk.css" ]; then - link_gtk3_assets "$CONFIG_DIR/gtk-3.0" - fi - exit 0 -fi +case "$MODE" in + patch) + patch_gtk3_colors "$CONFIG_DIR" "$IS_LIGHT" + echo "GTK3 colors patched successfully" + ;; + remove) + remove_gtk3_colors "$CONFIG_DIR" + remove_gtk4_colors "$CONFIG_DIR" + ;; + apply) + mkdir -p "$CONFIG_DIR/gtk-3.0" "$CONFIG_DIR/gtk-4.0" -mkdir -p "$CONFIG_DIR/gtk-3.0" "$CONFIG_DIR/gtk-4.0" + apply_gtk3_colors "$CONFIG_DIR" + apply_gtk4_colors "$CONFIG_DIR" -apply_gtk3_colors "$CONFIG_DIR" -apply_gtk4_colors "$CONFIG_DIR" - -echo "GTK colors applied successfully" + echo "GTK colors applied successfully" + ;; + *) + echo "Usage: $0 [apply|patch|remove] [is_light] [shell_dir]" >&2 + exit 1 + ;; +esac