diff --git a/config/dot/quickshell/config/Theme.qml b/config/dot/quickshell/config/Theme.qml index c694ead..5ef3337 100644 --- a/config/dot/quickshell/config/Theme.qml +++ b/config/dot/quickshell/config/Theme.qml @@ -59,15 +59,22 @@ Singleton { // // Blue is the shipped Prism -- blue leading, orchid following -- and stays // the default. + // + // `gnome` is the nearest member of GNOME's own accent-color enum, which is + // a fixed list of nine we do not get to extend. It is what libadwaita + // applications -- Files, Papers, Loupe -- are told to use, so choosing an + // accent here recolors them too instead of leaving them in GNOME blue. + // Nearest by hue, not by name: "rose" maps to red rather than pink because + // it is the red role in this palette. readonly property var accents: ({ - "blue": { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue" }, - "orchid": { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid" }, - "teal": { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal" }, - "green": { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green" }, - "amber": { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber" }, - "orange": { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange" }, - "rose": { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose" }, - "slate": { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate" } + "blue": { dark: "#82aaff", darkSecondary: "#b172b0", light: "#2e7de9", lightSecondary: "#9854f1", label: "Prism blue", gnome: "blue" }, + "orchid": { dark: "#c099ff", darkSecondary: "#fca7ea", light: "#7847bd", lightSecondary: "#9854f1", label: "Orchid", gnome: "purple" }, + "teal": { dark: "#86e1fc", darkSecondary: "#82aaff", light: "#007197", lightSecondary: "#2e7de9", label: "Teal", gnome: "teal" }, + "green": { dark: "#c3e88d", darkSecondary: "#86e1fc", light: "#587539", lightSecondary: "#007197", label: "Green", gnome: "green" }, + "amber": { dark: "#ffc777", darkSecondary: "#ff966c", light: "#8c6c3e", lightSecondary: "#b15c00", label: "Amber", gnome: "yellow" }, + "orange": { dark: "#ff966c", darkSecondary: "#ff757f", light: "#b15c00", lightSecondary: "#c64343", label: "Orange", gnome: "orange" }, + "rose": { dark: "#ff757f", darkSecondary: "#c099ff", light: "#f52a65", lightSecondary: "#9854f1", label: "Rose", gnome: "red" }, + "slate": { dark: "#828bb8", darkSecondary: "#82aaff", light: "#6172b0", lightSecondary: "#2e7de9", label: "Slate", gnome: "slate" } }) // Falls back to blue for an unknown name, so a settings file written by a diff --git a/config/dot/quickshell/scripts/panama-theme-apps b/config/dot/quickshell/scripts/panama-theme-apps index 47e727f..b63037d 100755 --- a/config/dot/quickshell/scripts/panama-theme-apps +++ b/config/dot/quickshell/scripts/panama-theme-apps @@ -204,6 +204,46 @@ for gtk_version in 3.0 4.0; do fi done +# ── libadwaita accent ──────────────────────────────────────────────────────── +# Files, Papers, Loupe and every other libadwaita application read their accent +# from the Settings portal, not from anything Panama draws, so without this they +# render in GNOME blue no matter which accent the desktop is set to. +# +# GNOME's accent-color is a FIXED enum of nine names -- there is no hex here to +# match, only a nearest member to pick -- so this maps our eight to theirs. Kept +# in sync by hand with config/Theme.qml's `gnome` field, for the same reason +# accent_hex() above is: there is no shared source between QML and shell. +# +# The portal must route Settings to the gnome backend for this to reach +# anything; xdg-desktop-portal-gtk cannot serve accent-color at all. See +# config/dot/xdg-desktop-portal/hyprland-portals.conf. +gnome_accent() { + case "$1" in + orchid) printf 'purple' ;; + teal) printf 'teal' ;; + green) printf 'green' ;; + amber) printf 'yellow' ;; + orange) printf 'orange' ;; + rose) printf 'red' ;; + slate) printf 'slate' ;; + *) printf 'blue' ;; + esac +} + +status_adwaita="skipped" +if command -v gsettings >/dev/null 2>&1; then + adwaita_accent="$(gnome_accent "$accent")" + # Writing the same value emits no change signal, so applications already + # showing this accent are not asked to restyle for nothing. + if [[ "$(gsettings get org.gnome.desktop.interface accent-color 2>/dev/null)" == "'$adwaita_accent'" ]]; then + status_adwaita="unchanged" + elif gsettings set org.gnome.desktop.interface accent-color "$adwaita_accent" 2>/dev/null; then + status_adwaita="$adwaita_accent" + else + status_adwaita="failed" + fi +fi + 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" @@ -250,4 +290,5 @@ jq -cn \ --arg btop "$status_btop" \ --arg tmux "$status_tmux" \ --arg hyprlock "$status_hyprlock" \ - '{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux, hyprlock: $hyprlock}' + --arg adwaita "$status_adwaita" \ + '{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux, hyprlock: $hyprlock, adwaita: $adwaita}' diff --git a/config/dot/quickshell/services/ColorScheme.qml b/config/dot/quickshell/services/ColorScheme.qml index 962e352..bf95585 100644 --- a/config/dot/quickshell/services/ColorScheme.qml +++ b/config/dot/quickshell/services/ColorScheme.qml @@ -170,13 +170,18 @@ Singleton { `hl.config({ general = { col = { active_border = ${activeBorder} } } })`]); // Applications that predate org.freedesktop.appearance and carry - // their own palettes -- terminals, chiefly. Everything that reads - // the portal (GTK4, Qt6, Chromium, Electron) is already handled by - // the gsettings write above and needs nothing here. The accent is - // passed through too: kitty's border and the hyprlock fallback - // template are also the accent role, not just the scheme -- so - // this still has to run on an accent-only change, even though the - // scheme-relative work it also does is then redundant. + // their own palettes -- terminals, chiefly. Portal readers (GTK4, + // Qt6, Chromium, Electron) already have their SCHEME from the + // gsettings write above. + // + // Their ACCENT does not come from here: GNOME's accent-color is a + // fixed enum of nine names rather than a color, so the mapping from + // our eight lives in panama-theme-apps beside the other per- + // application translations. The accent is passed through for that, + // and because kitty's border and the hyprlock fallback template are + // the accent role too -- so this still has to run on an accent-only + // change, even though the scheme-relative work it also does is then + // redundant. commands.push([root.appThemePath, root.dark ? "dark" : "light", accentName]); } diff --git a/config/dot/xdg-desktop-portal/hyprland-portals.conf b/config/dot/xdg-desktop-portal/hyprland-portals.conf index c55f331..ed08545 100644 --- a/config/dot/xdg-desktop-portal/hyprland-portals.conf +++ b/config/dot/xdg-desktop-portal/hyprland-portals.conf @@ -7,7 +7,8 @@ # Why each line: # hyprland -> Screenshot, ScreenCast, GlobalShortcuts, InputCapture # (this is what makes OBS / Sunshine / Zoom screen sharing work) -# gtk -> FileChooser, Settings, Notification, Print, Access +# gtk -> FileChooser, Notification, Print, Access, and Settings as +# the fallback behind gnome (see below) # Settings is load-bearing: xdg-desktop-portal-gtk reads # gsettings' org.gnome.desktop.interface and serves # org.freedesktop.appearance, which is how libadwaita apps know @@ -25,7 +26,17 @@ # any Flatpak app's own "run/start on login" toggle silently # fails (Bitwarden logged this on every launch). # -# xdg-desktop-portal-gnome stays installed and is now used for one interface +# Settings -> gnome FIRST, gtk behind it. The frontend merges Settings +# backends in order, so this is additive rather than a swap. +# It has to be this way round because xdg-desktop-portal-gtk +# cannot serve org.freedesktop.appearance accent-color at all -- +# the string does not appear in the 1.15.3 binary -- so +# libadwaita applications rendered in GNOME blue no matter what +# accent this desktop was set to. The gnome backend serves it, +# and gtk staying listed means color-scheme still resolves if +# the gnome backend is ever unavailable. +# +# xdg-desktop-portal-gnome stays installed and is now used for two interfaces # here; it is still the default handler for the GNOME session. # ───────────────────────────────────────────────────────────────────────────── @@ -35,3 +46,4 @@ org.freedesktop.impl.portal.Screenshot=hyprland org.freedesktop.impl.portal.ScreenCast=hyprland org.freedesktop.impl.portal.Secret=gnome-keyring org.freedesktop.impl.portal.Background=gnome +org.freedesktop.impl.portal.Settings=gnome;gtk diff --git a/tests/quickshell/adwaita-accent-contract.sh b/tests/quickshell/adwaita-accent-contract.sh new file mode 100755 index 0000000..1192f90 --- /dev/null +++ b/tests/quickshell/adwaita-accent-contract.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +# Choosing an accent must recolor libadwaita applications too. +# +# Files, Papers and Loupe read their accent from the Settings portal, so an +# accent that never reaches GNOME's accent-color key leaves half the desktop in +# GNOME blue while the other half wears the chosen color. That is invisible from +# inside Panama's own surfaces, which look correct either way. +# +# Three things have to line up, and none of them share a source: +# - config/Theme.qml carries the accent table with its `gnome` member +# - scripts/panama-theme-apps maps the same names in shell +# - the portal routes Settings to a backend that can serve accent-color +# +# Read-only apart from a gsettings *read*; it never sets an accent. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +theme="$repo_dir/config/dot/quickshell/config/Theme.qml" +script="$repo_dir/config/dot/quickshell/scripts/panama-theme-apps" +portals="$repo_dir/config/dot/xdg-desktop-portal/hyprland-portals.conf" + +fail() { + printf 'adwaita accent contract: %s\n' "$1" >&2 + exit 1 +} + +for path in "$theme" "$script" "$portals"; do + [[ -r "$path" ]] || fail "missing $path" +done + +# ── The accent table ───────────────────────────────────────────────────────── +mapping="$(python3 - "$theme" <<'PYTHON' +import re, sys +source = open(sys.argv[1]).read() +table = re.search(r"readonly property var accents: \(\{(.*?)\n \}\)", source, re.S) +if not table: + raise SystemExit("accents table not found") +for line in table.group(1).splitlines(): + name = re.search(r'"([a-z]+)":', line) + if not name: + continue + member = re.search(r'gnome: "([a-z]+)"', line) + print(name.group(1) + "\t" + (member.group(1) if member else "")) +PYTHON +)" || fail 'could not read the accent table from Theme.qml' + +[[ -n "$mapping" ]] || fail 'the accent table is empty' + +while IFS=$'\t' read -r name member; do + [[ -n "$name" ]] || continue + [[ -n "$member" ]] \ + || fail "the \"$name\" accent has no gnome member, so libadwaita applications keep the previous accent when it is chosen" +done <<<"$mapping" + +# ── Every member must exist in GNOME's enum ────────────────────────────────── +# The enum is read from the system rather than hardcoded here: a hardcoded copy +# would still pass on a machine whose GNOME dropped or renamed a member, which +# is the case where gsettings silently refuses the write. +if command -v gsettings >/dev/null 2>&1; then + enum="$(gsettings range org.gnome.desktop.interface accent-color 2>/dev/null | tail -n +2 | tr -d "'")" + if [[ -n "$enum" ]]; then + while IFS=$'\t' read -r name member; do + [[ -n "$member" ]] || continue + grep -qx "$member" <<<"$enum" \ + || fail "the \"$name\" accent maps to \"$member\", which GNOME's accent-color enum does not publish" + done <<<"$mapping" + fi +fi + +# ── The shell helper must agree with the table ─────────────────────────────── +# panama-theme-apps carries its own copy of this mapping because a shell script +# cannot read a QML object. A copy that drifts is exactly how an accent ends up +# correct in Panama's own surfaces and wrong everywhere else. +shell_member() { + local name="$1" body + body="$(sed -n '/^gnome_accent() {/,/^}/p' "$script")" + [[ -n "$body" ]] || fail 'gnome_accent() is missing from panama-theme-apps' + local line + line="$(grep -E "^\s+${name}\)" <<<"$body" | head -1)" + if [[ -z "$line" ]]; then + # Not listed means it falls through to the default branch. + line="$(grep -E '^\s+\*\)' <<<"$body" | head -1)" + fi + sed -E "s/.*printf '([a-z]+)'.*/\1/" <<<"$line" +} + +while IFS=$'\t' read -r name member; do + [[ -n "$name" ]] || continue + actual="$(shell_member "$name")" + [[ "$actual" == "$member" ]] \ + || fail "Theme.qml maps \"$name\" to \"$member\" but panama-theme-apps maps it to \"$actual\"" +done <<<"$mapping" + +# The helper also validates the accent name it is handed; an accent the table +# knows but the helper rejects silently degrades to blue. +accepted="$(grep -oE '^\s+blue\|[a-z|]+\)' "$script" | head -1 | tr -d ' )' )" +while IFS=$'\t' read -r name member; do + [[ -n "$name" ]] || continue + grep -qE "(^|\|)$name(\||$)" <<<"$accepted" \ + || fail "panama-theme-apps does not accept the \"$name\" accent, so choosing it falls back to blue" +done <<<"$mapping" + +# ── The portal has to be able to serve it ──────────────────────────────────── +# xdg-desktop-portal-gtk cannot serve accent-color, so routing Settings to gtk +# alone makes every mapping above inert -- correct on paper, invisible in use. +settings_line="$(grep -E '^org\.freedesktop\.impl\.portal\.Settings=' "$portals")" \ + || fail 'the portal config does not route Settings at all, so accent-color has no backend' +[[ "$settings_line" == *gnome* ]] \ + || fail "Settings is routed to \"${settings_line#*=}\", and only the gnome backend serves accent-color" +[[ "${settings_line#*=}" == gnome* ]] \ + || fail "the gnome backend must come first in \"${settings_line#*=}\" or gtk answers Settings and accent-color is never published" + +printf 'adwaita accent contract: PASS (%d accents mapped)\n' "$(grep -c . <<<"$mapping")"