#!/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: # - services/ThemeProfileModel.js carries the accent table with its `gnome` # member (Theme.qml exposes it; the theme-profile system owns it) # - 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)" accents="$repo_dir/config/dot/quickshell/services/ThemeProfileModel.js" 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 "$accents" "$script" "$portals"; do [[ -r "$path" ]] || fail "missing $path" done # ── The accent table ───────────────────────────────────────────────────────── mapping="$(python3 - "$accents" <<'PYTHON' import re, sys source = open(sys.argv[1]).read() table = re.search(r"var CURATED = \{(.*?)\n\};", source, re.S) if not table: raise SystemExit("curated accent table not found") # Each accent is a multi-line record now, so the name opens a block and the # member may be several lines below it. for entry in re.finditer(r"\n ([a-z]+): \{(.*?)\n \}", table.group(1), re.S): member = re.search(r'gnome: "([a-z]+)"', entry.group(2)) print(entry.group(1) + "\t" + (member.group(1) if member else "")) PYTHON )" || fail 'could not read the curated accent table from ThemeProfileModel.js' [[ -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 ─────────────────────────────── # The mapping used to be a second copy of this table, written out as a case # statement in panama-theme-apps. It now lives in config/palette.json beside # each accent's colours, and scripts/panama-palette reads it -- so the shell # and QML resolve the same member from the same file rather than from two # lists somebody has to remember to edit together. # # palette-contract checks palette.json against the QML table value by value; # this checks that the shell actually resolves through it, which is the half # that would let an accent be correct in Panama's own surfaces and wrong # everywhere else. helper="$repo_dir/config/dot/quickshell/scripts/panama-palette" [[ -r "$helper" ]] || fail 'scripts/panama-palette is missing, so nothing maps accents for the shell' grep -q 'accent_gnome()' "$helper" \ || fail 'panama-palette does not resolve a GNOME accent member' grep -q 'accent_gnome' "$script" \ || fail 'panama-theme-apps no longer asks the palette for the GNOME accent member' grep -q 'gnome_accent() {' "$script" \ && fail 'panama-theme-apps has grown its own copy of the mapping again' while IFS=$'\t' read -r name member; do [[ -n "$name" ]] || continue actual="$(bash -c "source '$helper'; accent_gnome '$name'")" [[ "$actual" == "$member" ]] \ || fail "Theme.qml maps \"$name\" to \"$member\" but the shell resolves \"$actual\"" done <<<"$mapping" # An accent the table knows but the palette does not would silently degrade to # blue, which is the failure this pair of files exists to prevent. palette="$repo_dir/config/dot/quickshell/config/palette.json" while IFS=$'\t' read -r name member; do [[ -n "$name" ]] || continue jq -e --arg n "$name" '.accents | has($n)' "$palette" >/dev/null \ || fail "the palette has no \"$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")"