#!/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}'