#!/usr/bin/env bash # Propagates the colour 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 # # 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. set -euo pipefail scheme="${1:-dark}" case "$scheme" in dark|light) ;; *) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;; esac # ── 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 # colours 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 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. if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \ && mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then status_kitty="written" 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; 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" \ '{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux}'