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
295 lines
13 KiB
Bash
Executable File
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}'
|