Two of Section F. Hooks are the pressure valve. "Can Panama also do X when the theme changes" is now a five-line file in ~/.config/panama/hooks rather than a fork, a feature request, or a patch somebody rebases forever. Each name takes a single file and a .d directory so several things can react without fighting over one, and a broken hook is reported and stepped over: somebody's script must never cost a theme change, an upgrade or a login. Wired at theme-set, post-upgrade and post-migrate. This is the thirty-line version of the plugin host the upstream ledger defers, and it has no API to keep stable beyond "we will run your script and tell you what happened". Testing it caught a real bug the reading would not have: run_one captured the script path but never shifted it off, so every hook got its own filename as $1 and the real arguments arrived one place late. A hook reading $1 as the colour scheme got a path. The crash watcher notices when a program dumps core and says so. Under GNOME, ABRT does this; here nothing did, and applications died silently, which is most of how "Linux is flaky" gets earned. Once per program per session is the entire design, not a nicety. This machine's portal backend crashes between eleven and sixty times a day, and a notification per crash would be one every few minutes for something nobody can act on. The first is news; the fortieth is why people turn notifications off. The health page keeps the running count. It waits for the notification server before reporting, because the crash most worth hearing about is the one that took the shell with it, and it names the executable rather than the kernel's comm field, which truncates at fifteen characters. Verified against real segfaults.
279 lines
13 KiB
Bash
Executable File
279 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. 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 \
|
|
&& # Anything else on this machine that wants to know. Runs after every generator
|
|
# above, so a hook sees the finished state rather than a half-applied one, and
|
|
# a broken hook cannot cost you a theme change -- panama-hook reports and steps
|
|
# over. See bin/panama-hook.
|
|
"${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-hook" theme-set "$scheme" "$accent" || true
|
|
|
|
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}'
|