They were written down five times: ThemeProfileModel.js for QML, looks.lua for the compositor, and again in panama-theme-apps and panama-lock. The GNOME accent-name mapping was a sixth list. Adding a ninth accent meant editing all of them, and the file most likely to be missed was the lock screen, which fails silently -- the machine locks in last season's colour and nothing says why. panama-theme-apps admitted it in a comment: "there is no shared source between QML and a shell script". config/palette.json is that source now. looks.lua reads it through a new prefs.readJson, which uses the same never-raise parser the settings store uses, so an unreadable palette costs the accent colours and never the compositor config. The two shell generators read it through scripts/panama-palette, which also carries the hex-to-rgb conversion hyprlock needs and the GNOME member lookup. QML keeps its table, because a .js module imported into QML cannot read a file. That is still a copy, so the palette contract compares the two value by value -- every accent, every field -- and fails on any disagreement. Verified by planting a wrong hex and watching it name the exact field. The adwaita contract used to check the shell's own copy of the GNOME mapping. It now checks that the shell resolves through the palette, and fails if that copy ever grows back.
273 lines
12 KiB
Bash
Executable File
273 lines
12 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. The lookup lives in scripts/panama-palette,
|
|
# which reads config/palette.json -- the one place the eight accents are
|
|
# written down for everything outside QML.
|
|
|
|
set -euo pipefail
|
|
|
|
# The accents, from the one file that holds them.
|
|
# shellcheck source=./panama-palette
|
|
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/panama-palette"
|
|
|
|
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.
|
|
|
|
# hyprlock wants "R, G, B" decimal, not hex.
|
|
|
|
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. Which one each Panama accent maps to is
|
|
# recorded in config/palette.json alongside its colours, for the same reason
|
|
# accent lookup once was: there was no shared source between QML and shell.
|
|
# There is now -- config/palette.json -- and palette-contract fails when the
|
|
# QML table and it disagree.
|
|
#
|
|
# 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.
|
|
|
|
status_adwaita="skipped"
|
|
if command -v gsettings >/dev/null 2>&1; then
|
|
adwaita_accent="$(accent_gnome "$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}'
|