Files
Panama/config/dot/quickshell/scripts/panama-theme-apps
T
Gabriel Brown 91306ce810 Give libadwaita applications the desktop's accent
Files, Papers, Loupe and every other libadwaita application read their
accent from the Settings portal, so they rendered in GNOME blue no
matter which accent this desktop was set to -- correct on our own
surfaces, wrong on half the screen.

xdg-desktop-portal-gtk cannot serve org.freedesktop.appearance
accent-color at all; the string does not appear in the 1.15.3 binary.
The gnome backend serves it, so Settings now routes to gnome with gtk
still listed behind it -- the frontend merges Settings backends in
order, so color-scheme keeps resolving if the gnome backend is ever
unavailable.

GNOME's accent-color is a fixed enum of nine names rather than a color,
so each of our eight accents carries its nearest member. Nearest by hue
rather than by name: rose maps to red, because it is the red role in
this palette.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 09:31:50 -04:00

295 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
# Propagates the color scheme to applications that do not read the desktop
# portal.
#
# Most modern applications DO follow org.freedesktop.portal.Settings and need
# nothing from us: GTK4/libadwaita, Qt6, Chromium and Electron all read
# org.freedesktop.appearance color-scheme, which xdg-desktop-portal-gtk serves
# from gsettings. Panama sets that, so those follow automatically.
#
# Terminals are the notable exception -- they predate the standard and carry
# their own palettes. kitty is handled here.
#
# GTK3 is the other one. Under GNOME, gnome-settings-daemon publishes the theme
# over XSETTINGS; under Hyprland nothing does, so ~/.config/gtk-3.0/settings.ini
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
# scheme in light mode, so it is generated from a template here instead.
#
# panama-theme-apps dark|light [accent]
#
# kitty gets it twice: the generated include file so terminals opened later
# start correct, and a live `set-colors` over its control socket so terminals
# already open change now. Without the second, a scheme change appears to do
# nothing until you open a new window.
#
# kitty's active_border_color and hyprlock's focus ring are also the accent
# role, not just the scheme -- see accent_hex() below, which mirrors the same
# lookup hypr/looks.lua and scripts/panama-lock carry for the same reason.
set -euo pipefail
scheme="${1:-dark}"
case "$scheme" in
dark|light) ;;
*) printf 'usage: panama-theme-apps [dark|light] [accent]\n' >&2; exit 2 ;;
esac
# Falls back to blue for an unknown or omitted name, matching Theme.qml's own
# accentPair fallback -- so a caller that only ever knew about scheme (a stale
# ColorScheme.qml, a manual invocation) still gets a real accent instead of an
# empty string reaching sed.
accent="${2:-blue}"
case "$accent" in
blue|orchid|teal|green|amber|orange|rose|slate) ;;
*) accent=blue ;;
esac
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
# hand -- there is no shared source between QML and a shell script), primary
# hue per scheme only: both consumers below draw a flat color, not the
# two-stop gradient the window border does.
accent_hex() {
local name="$1" scheme="$2"
case "$name" in
orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;;
teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;;
green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;;
amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;;
orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;;
rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;;
slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;;
*) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;;
esac
}
# hyprlock wants "R, G, B" decimal, not hex.
hex_to_rgb() {
local hex="$1"
printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}
accent_border_hex="$(accent_hex "$accent" "$scheme")"
# ── hyprlock ─────────────────────────────────────────────────────────────────
# The lock screen. hyprlock is launched fresh on every lock (`pidof hyprlock ||
# hyprlock`), so it reads this file each time and needs no restart.
#
# It takes rgba(r, g, b, a) in DECIMAL, not hex, so the palette is expressed as
# "R, G, B" triples here rather than the hex used everywhere else. The two
# _HEX values are the exception: they sit inside Pango markup, where hyprlock
# wants ##rrggbb.
lock_dir="${XDG_CONFIG_HOME:-$HOME/.config}/hypr"
lock_template="$lock_dir/hyprlock.conf.template"
if [[ "$scheme" == "light" ]]; then
lock_fg="55, 96, 191" # #3760bf
lock_muted="97, 114, 176" # #6172b0
lock_error="245, 42, 101" # #f52a65
lock_bg="225, 226, 231" # #e1e2e7
lock_field="208, 213, 227" # #d0d5e3
lock_muted_hex="6172b0"
lock_error_hex="f52a65"
else
lock_fg="200, 211, 245" # #c8d3f5
lock_muted="130, 139, 184" # #828bb8
lock_error="255, 117, 127" # #ff757f
lock_bg="34, 36, 54" # #222436
lock_field="46, 47, 61" # #2e2f3d
lock_muted_hex="828bb8"
lock_error_hex="ff757f"
fi
# The focus ring is the accent role, not a scheme-relative one -- follows
# accentName the same way scripts/panama-lock's own generator does.
lock_accent="$(hex_to_rgb "$accent_border_hex")"
status_hyprlock="skipped"
if [[ -r "$lock_template" ]]; then
# Written atomically: a lock triggered mid-write would otherwise read a
# truncated config and fall back to hyprlock's own defaults, which is a
# bright gray screen with none of this desktop's identity.
if sed -e "s/@FG@/$lock_fg/g" \
-e "s/@MUTED@/$lock_muted/g" \
-e "s/@ACCENT@/$lock_accent/g" \
-e "s/@ERROR@/$lock_error/g" \
-e "s/@BG@/$lock_bg/g" \
-e "s/@FIELD@/$lock_field/g" \
-e "s/@MUTED_HEX@/$lock_muted_hex/g" \
-e "s/@ERROR_HEX@/$lock_error_hex/g" \
"$lock_template" >"$lock_dir/hyprlock.conf.tmp" 2>/dev/null \
&& mv "$lock_dir/hyprlock.conf.tmp" "$lock_dir/hyprlock.conf" 2>/dev/null; then
status_hyprlock="written"
else
rm -f "$lock_dir/hyprlock.conf.tmp"
status_hyprlock="failed"
fi
fi
# ── tmux ─────────────────────────────────────────────────────────────────────
# Generated like kitty's: tmux.conf sources current-theme.conf, and that file is
# machine state rather than configuration. Running servers are re-sourced so an
# open session changes now instead of at next launch -- tmux applies a
# source-file to every attached client immediately.
tmux_dir="${XDG_CONFIG_HOME:-$HOME/.config}/tmux"
tmux_theme="$tmux_dir/themes/tokyonight-moon.conf"
[[ "$scheme" == "light" ]] && tmux_theme="$tmux_dir/themes/tokyonight-day.conf"
status_tmux="skipped"
if [[ -r "$tmux_theme" ]]; then
if cp "$tmux_theme" "$tmux_dir/current-theme.conf.tmp" 2>/dev/null \
&& mv "$tmux_dir/current-theme.conf.tmp" "$tmux_dir/current-theme.conf" 2>/dev/null; then
status_tmux="written"
# Only if a server is actually running; `tmux source-file` would
# otherwise start one just to theme it.
if command -v tmux >/dev/null 2>&1 && tmux has-session 2>/dev/null; then
tmux source-file "$tmux_dir/current-theme.conf" 2>/dev/null \
&& status_tmux="applied to running sessions"
fi
else
rm -f "$tmux_dir/current-theme.conf.tmp"
status_tmux="failed"
fi
fi
# ── btop ─────────────────────────────────────────────────────────────────────
# Only the color_theme line is rewritten, in place. btop OWNS btop.conf -- it
# rewrites the whole file on exit -- so the config is not symlinked into Panama
# and not replaced wholesale here; just this one value is edited, and btop keeps
# it on the next write.
#
# btop reads its theme once at startup, so a running instance keeps the old
# colors until it is restarted. That is acceptable for a monitor you open when
# you want it, and forcing a restart would kill a process the user is watching.
btop_conf="${XDG_CONFIG_HOME:-$HOME/.config}/btop/btop.conf"
btop_theme="tokyonight-moon"
[[ "$scheme" == "light" ]] && btop_theme="tokyonight-day"
status_btop="skipped"
if [[ -w "$btop_conf" ]]; then
if sed -i "s|^color_theme *=.*|color_theme = \"$btop_theme\"|" "$btop_conf" 2>/dev/null; then
status_btop="written"
else
status_btop="failed"
fi
fi
# ── GTK ──────────────────────────────────────────────────────────────────────
# adw-gtk3, not Adwaita: no Adwaita GTK theme is installed on Fedora 44, and
# naming a theme that does not exist makes GTK fall back to its light default --
# which made dark mode silently produce light windows.
if [[ "$scheme" == "light" ]]; then
gtk_theme="adw-gtk3"
prefer_dark=0
else
gtk_theme="adw-gtk3-dark"
prefer_dark=1
fi
status_gtk="skipped"
for gtk_version in 3.0 4.0; do
gtk_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-$gtk_version"
template="$gtk_dir/settings.ini.template"
[[ -r "$template" ]] || continue
# Written atomically: a GTK application starting mid-write would otherwise
# read a truncated file and fall back to defaults.
if sed -e "s/@GTK_THEME@/$gtk_theme/" -e "s/@PREFER_DARK@/$prefer_dark/" "$template" \
>"$gtk_dir/settings.ini.tmp" 2>/dev/null \
&& mv "$gtk_dir/settings.ini.tmp" "$gtk_dir/settings.ini" 2>/dev/null; then
status_gtk="written"
else
rm -f "$gtk_dir/settings.ini.tmp"
status_gtk="failed"
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"
status_kitty="skipped"
if [[ -r "$theme_file" ]]; then
# Written atomically: kitty may read this while a new window is starting.
# The accent override is appended after the copied theme rather than
# edited into the tracked theme file: kitty applies settings top to
# bottom, so a later active_border_color line wins, and this file is
# itself generated -- the tracked per-scheme theme stays scheme-only.
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
&& printf 'active_border_color #%s\n' "$accent_border_hex" >>"$kitty_dir/current-theme.conf.tmp" \
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
status_kitty="written"
else
rm -f "$kitty_dir/current-theme.conf.tmp"
fi
# Live-apply to running terminals. kitty appends its PID to the socket name
# from listen_on, so there is one socket per instance -- "unix:@mykitty"
# alone reaches nothing, which is exactly how this looked like remote
# control being disabled when it was not.
if command -v kitty >/dev/null 2>&1 && command -v ss >/dev/null 2>&1; then
applied=0
while read -r socket; do
[[ -n "$socket" ]] || continue
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1 \
&& kitty @ --to "unix:@${socket#@}" set-colors --override "active_border_color=#$accent_border_hex" >/dev/null 2>&1; then
applied=$((applied + 1))
fi
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
(( applied > 0 )) && status_kitty="applied to $applied"
fi
fi
# Every target reports what actually happened. A helper that says only
# "kitty: applied" while silently skipping three other applications is how a
# half-applied theme goes unnoticed.
jq -cn \
--arg scheme "$scheme" \
--arg kitty "$status_kitty" \
--arg gtk "$status_gtk" \
--arg btop "$status_btop" \
--arg tmux "$status_tmux" \
--arg hyprlock "$status_hyprlock" \
--arg adwaita "$status_adwaita" \
'{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux, hyprlock: $hyprlock, adwaita: $adwaita}'