Files
Panama/config/dot/quickshell/scripts/panama-theme-apps
T
Gabriel Brown cb7c09d208 Give the desktop real themes, video wallpapers, and honest titlebars
Appearance now opens on Themes: light and dark side by side, each
remembering its own choice, over galleries of ten shipped themes —
Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and
Everforest in both modes. A theme is a complete palette: the catalog
lives in themes.json, Theme.qml reads every color token from the
active record, and one render pipeline carries it to kitty, tmux,
btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme
editor builds new ones from four wells — wheel, hex, or eyedropper —
with derived surfaces, a saturation slider, debounced fine-tune, and
effects that save with the theme. Custom edits finally keep GNOME's
accent, kitty's border, and hyprlock in sync.

Wallpapers can be video: mpvpaper per output, hardware-decoded, muted
and looped, supervised and respawned. Panama owns the pausing — games,
battery, and a bar pill for right now — because the compositor
rebuilds full-screen blur for every frame a video wallpaper draws.
The lock screen gets a still frame.

Titlebars stop lying. GNOME apps get close-only on your chosen side,
the maximize and double-click settings are gone, the Settings window
obeys the same rules, and its titlebar can be turned off entirely.
Typography becomes five labeled dropdowns instead of a wall of
samples.

Contracts updated and written throughout (165 now); per the redesign
workflow none were executed — the full sweep runs once at the end.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:39:04 -04:00

882 lines
43 KiB
Bash
Executable File

#!/usr/bin/env bash
# Propagates the active THEME to applications that do not read the desktop
# portal.
#
# Most modern applications DO follow org.freedesktop.portal.Settings and need
# nothing from us for light-or-dark: 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.
#
# A scheme is not a theme, though. The portal can say "dark"; it cannot say
# "Catppuccin Mocha". Everything below is an application that carries its own
# palette, and every one of them used to be handed a copy of a Tokyo Night file
# -- which is why choosing any other theme restyled the shell and nothing else.
# They are now RENDERED from the active theme's palette instead:
#
# kitty current-theme.conf, plus a live `set-colors` over its socket
# tmux current-theme.conf, re-sourced into running servers
# btop a generated panama.theme, named by btop.conf's color_theme
# hyprlock the eight placeholders in hyprlock.conf.template
# GTK 3/4 the @define-color block in gtk.css, between the markers
# Vicinae panama-dark.toml / panama-light.toml in its data directory
# Firefox an Edge-Frfox override sheet, both schemes in one file
#
# panama-theme-apps dark|light [accent]
#
# The two arguments are the desktop's scheme and its nearest curated accent
# name. They remain the interface because that is what ColorScheme.qml knows
# and what GNOME's accent-color enum needs -- but the palette no longer comes
# from them. This script resolves the active theme itself, from settings.json
# and config/themes.json, through scripts/panama-palette. panama-lock resolves
# it exactly the same way, from the same function.
#
# 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 theme change appears to do
# nothing until you open a new window.
#
# The tokyonight-* files under kitty/themes, tmux/themes and btop/themes are
# still shipped -- a user may name one directly -- but nothing copies them any
# more.
set -euo pipefail
# The accents and the theme resolver, from the one file that holds them.
# shellcheck source=./panama-palette
script_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
source "$script_dir/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
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
# ── The theme ────────────────────────────────────────────────────────────────
# The active one for everything that renders a single palette, and BOTH schemes
# for Firefox -- its stylesheet carries light-dark() pairs so the browser flips
# on its own, without this script being re-run.
eval "$(theme_vars "$(resolve_theme "$scheme")")"
eval "$(theme_vars "$(resolve_theme dark)" dark_)"
eval "$(theme_vars "$(resolve_theme light)" light_)"
# The accent pair falls back to the curated accent named on the command line.
# A stored custom theme is allowed to carry an accent and no palette; one that
# carries neither is still a real selection and should not render an empty
# colour into a config file.
theme_accent="$(hex_or "${theme_accent:-}" "$(accent_hex "$accent" "$scheme")")"
theme_secondary="$(hex_or "${theme_secondary:-}" "$(accent_secondary_hex "$accent" "$scheme")")"
dark_theme_accent="$(hex_or "${dark_theme_accent:-}" "$(accent_hex "$accent" dark)")"
light_theme_accent="$(hex_or "${light_theme_accent:-}" "$(accent_hex "$accent" light)")"
# Whether there is enough of a palette to render from at all. An unreadable
# themes.json is the only way to get here, and the honest answer is then to
# leave every application's existing colours alone rather than to write half a
# theme over them.
palette_ok=true
for token in bg bgDark bgHighlight bgPanel bgPopover fg fgDim fgMuted gutter \
accentAlt cyan teal green yellow orange red redDeep magenta pink; do
name="pal_$token"
[[ -n "${!name:-}" ]] || palette_ok=false
done
ansi_ok=true
for token in black red green yellow blue magenta cyan white \
brightBlack brightRed brightGreen brightYellow brightBlue \
brightMagenta brightCyan brightWhite; do
name="ansi_$token"
[[ -n "${!name:-}" ]] || ansi_ok=false
done
both_schemes_ok=true
for token in bg bgDark bgHighlight bgPanel bgPopover fg fgDim fgMuted gutter red; do
for side in dark light; do
name="${side}_pal_$token"
[[ -n "${!name:-}" ]] || both_schemes_ok=false
done
done
# GTK and Firefox want rgba(), not hex, wherever a colour carries transparency.
rgba_of() {
printf 'rgba(%s, %s)' "$(hex_to_rgb "$1")" "$2"
}
# Text on a filled accent, in this theme's own grounds rather than in black and
# white: a pastel accent takes the theme's darkest surface, a saturated one
# takes its lightest text.
accent_fg="$(on_hex "$theme_accent" "${pal_bgDark:-}" "${pal_fg:-}")"
# Replace one file in place, but only when the result actually differs, and say
# which happened: "written", "unchanged", or "failed". Several of these targets
# are tracked files reached through the dotfile symlinks, and rewriting them
# with identical bytes on every accent sample would show up as a dirty working
# tree for no change at all.
#
# A word rather than an exit status because every caller is under `set -e`,
# where a helper that reports by returning 2 aborts the script instead.
replace_if_changed() {
local destination="$1" temporary="$2"
if [[ -e "$destination" ]] && cmp -s "$temporary" "$destination"; then
rm -f "$temporary"
printf 'unchanged'
return 0
fi
if mv "$temporary" "$destination" 2>/dev/null; then
printf 'written'
else
rm -f "$temporary"
printf 'failed'
fi
}
# ── 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.
#
# The substitutions themselves live in panama-palette's render_hyprlock: this
# script, panama-lock and setup/scripts/link-dotfiles all need them, and when
# each carried its own copy the lock screen was the one that silently kept the
# old colours.
lock_dir="$config_home/hypr"
lock_template="$lock_dir/hyprlock.conf.template"
status_hyprlock="skipped"
if [[ -r "$lock_template" ]]; then
if render_hyprlock "$scheme" "$lock_template" "$lock_dir/hyprlock.conf" "$accent"; then
status_hyprlock="written"
else
status_hyprlock="failed"
fi
fi
# ── tmux ─────────────────────────────────────────────────────────────────────
# 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="$config_home/tmux"
render_tmux() {
printf '# Generated by Panama from the "%s" theme. Do not edit.\n' "${theme_id:-custom}"
printf '#\n# tmux.conf sources this file; panama-theme-apps rewrites it on every theme\n'
printf '# change. The tokyonight-* files beside it are shipped for anyone who wants\n'
printf '# to source one directly, and are no longer copied here.\n\n'
printf 'set -g mode-style "fg=#%s,bg=#%s"\n\n' "$theme_accent" "$pal_gutter"
printf 'set -g message-style "fg=#%s,bg=#%s"\n' "$theme_accent" "$pal_gutter"
printf 'set -g message-command-style "fg=#%s,bg=#%s"\n\n' "$theme_accent" "$pal_gutter"
printf 'set -g pane-border-style "fg=#%s"\n' "$theme_accent"
printf 'set -g pane-active-border-style "fg=#%s"\n\n' "$theme_secondary"
printf 'set -g status-style "fg=#%s,bg=#%s"\n' "$theme_secondary" "$pal_gutter"
printf 'set -g status-bg "#%s"\n\n' "$pal_bg"
printf 'set -g status-left "#[fg=#%s,bg=#%s,bold] #S #[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]"\n' \
"$ansi_black" "$theme_secondary" "$ansi_black" "$theme_secondary"
printf 'set -g status-right "#[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]#[fg=#%s,bg=#%s] #{prefix_highlight} #[fg=#%s,bg=#%s]#[fg=#%s,bg=#%s] %%Y/%%m/%%d %%I:%%M %%p #[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]#[fg=#%s,bg=#%s,bold,italics] #h "\n\n' \
"$ansi_black" "$theme_accent" "$theme_secondary" "$pal_gutter" \
"$pal_gutter" "$pal_gutter" "$theme_secondary" "$pal_gutter" \
"$theme_accent" "$pal_gutter" "$ansi_black" "$theme_secondary"
printf 'setw -g window-status-format "#[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]#[fg=#%s,bg=#%s] #I #W #F #[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]"\n' \
"$ansi_black" "$pal_gutter" "$theme_accent" "$pal_gutter" "$ansi_black" "$pal_gutter"
printf 'setw -g window-status-current-format "#[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]#[fg=#%s,bg=#%s,bold] #I #W #F #[fg=#%s,bg=#%s,nobold,nounderscore,noitalics]"\n\n' \
"$ansi_black" "$pal_gutter" "$theme_secondary" "$pal_gutter" "$pal_gutter" "$pal_gutter"
printf 'setw -g window-status-separator ""\n'
}
status_tmux="skipped"
if [[ -d "$tmux_dir" ]] && [[ "$palette_ok" == true ]] && [[ "$ansi_ok" == true ]]; then
if render_tmux >"$tmux_dir/current-theme.conf.tmp" 2>/dev/null; then
status_tmux="$(replace_if_changed "$tmux_dir/current-theme.conf" "$tmux_dir/current-theme.conf.tmp")"
# Only if a server is actually running; `tmux source-file` would
# otherwise start one just to theme it.
if [[ "$status_tmux" != "failed" ]] \
&& 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 ─────────────────────────────────────────────────────────────────────
# The theme is generated into btop's own themes directory and btop.conf is
# pointed at it by name. 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.
#
# 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="$config_home/btop/btop.conf"
btop_theme_dir="$config_home/btop/themes"
render_btop() {
printf '# Generated by Panama from the "%s" theme. Do not edit.\n' "${theme_id:-custom}"
printf '#\n# The *_start/_mid/_end triples are gradients btop draws meters with: low,\n'
printf '# middle, and high. They run cool -> warm -> hot so a saturated resource is\n'
printf '# obvious at a glance without reading the number.\n\n'
printf 'theme[main_bg]="#%s"\n' "$pal_bg"
printf 'theme[main_fg]="#%s"\n' "$pal_fg"
printf 'theme[title]="#%s"\n' "$pal_fg"
printf 'theme[hi_fg]="#%s"\n' "$theme_accent"
printf 'theme[selected_bg]="#%s"\n' "$pal_gutter"
printf 'theme[selected_fg]="#%s"\n' "$theme_accent"
printf 'theme[inactive_fg]="#%s"\n' "$pal_fgMuted"
printf 'theme[proc_misc]="#%s"\n\n' "$pal_green"
# The four boxes share one hairline, a step brighter than the gutter so the
# frame reads as structure rather than as content.
local box; box="$(mix_hex "$pal_gutter" "$pal_fgDim" 30)"
printf 'theme[cpu_box]="#%s"\n' "$box"
printf 'theme[mem_box]="#%s"\n' "$box"
printf 'theme[net_box]="#%s"\n' "$box"
printf 'theme[proc_box]="#%s"\n' "$box"
printf 'theme[div_line]="#%s"\n\n' "$pal_gutter"
printf 'theme[temp_start]="#%s"\n' "$theme_accent"
printf 'theme[temp_mid]="#%s"\n' "$pal_yellow"
printf 'theme[temp_end]="#%s"\n\n' "$pal_red"
printf 'theme[cpu_start]="#%s"\n' "$theme_accent"
printf 'theme[cpu_mid]="#%s"\n' "$pal_magenta"
printf 'theme[cpu_end]="#%s"\n\n' "$pal_red"
printf 'theme[free_start]="#%s"\n' "$pal_gutter"
printf 'theme[free_mid]="#%s"\n' "$pal_accentAlt"
printf 'theme[free_end]="#%s"\n\n' "$pal_cyan"
printf 'theme[cached_start]="#%s"\n' "$pal_cyan"
printf 'theme[cached_mid]="#%s"\n' "$theme_accent"
printf 'theme[cached_end]="#%s"\n\n' "$pal_magenta"
printf 'theme[available_start]="#%s"\n' "$pal_yellow"
printf 'theme[available_mid]="#%s"\n' "$pal_orange"
printf 'theme[available_end]="#%s"\n\n' "$pal_red"
printf 'theme[used_start]="#%s"\n' "$pal_green"
printf 'theme[used_mid]="#%s"\n' "$pal_yellow"
printf 'theme[used_end]="#%s"\n\n' "$pal_red"
printf 'theme[download_start]="#%s"\n' "$pal_gutter"
printf 'theme[download_mid]="#%s"\n' "$theme_accent"
printf 'theme[download_end]="#%s"\n\n' "$pal_cyan"
printf 'theme[upload_start]="#%s"\n' "$pal_gutter"
printf 'theme[upload_mid]="#%s"\n' "$pal_magenta"
printf 'theme[upload_end]="#%s"\n' "$pal_pink"
}
status_btop="skipped"
if [[ "$palette_ok" == true ]] && mkdir -p "$btop_theme_dir" 2>/dev/null; then
if render_btop >"$btop_theme_dir/panama.theme.tmp" 2>/dev/null; then
status_btop="$(replace_if_changed "$btop_theme_dir/panama.theme" "$btop_theme_dir/panama.theme.tmp")"
else
rm -f "$btop_theme_dir/panama.theme.tmp"
status_btop="failed"
fi
if [[ "$status_btop" != "failed" && -w "$btop_conf" ]]; then
sed -i 's|^color_theme *=.*|color_theme = "panama"|' "$btop_conf" 2>/dev/null \
|| status_btop="failed"
fi
fi
# ── GTK settings.ini ─────────────────────────────────────────────────────────
# 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.
#
# GTK3 is the reason this file is generated at all. 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.
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="$config_home/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
# ── GTK gtk.css ──────────────────────────────────────────────────────────────
# settings.ini names a theme; these are the colours that theme is overridden
# with. libadwaita reads @define-color from the user stylesheet, which is how
# Nautilus, Papers and every other GTK application end up wearing the Panama
# palette rather than adw-gtk3's stock greys.
#
# Only the marked block is rewritten. Everything else in these files is the
# structural CSS of a vendored theme -- a thousand lines of rounding, padding
# and window controls that has nothing to do with colour, and that regenerating
# the file wholesale would destroy.
#
# All three files get the ACTIVE theme rather than one being pinned light and
# another dark. gtk-4.0/gtk.css was a byte-for-byte copy of the dark one, so a
# light desktop drew dark GTK4 windows; rendering the active theme into each is
# what actually fixes that, whichever of the two GTK decides to read.
gtk_css_block() {
local shade sidebar_line
shade="$(rgba_of "$pal_bgDark" 0.25)"
sidebar_line="$(rgba_of "$pal_gutter" 0.36)"
printf '/* Generated from the "%s" theme by panama-theme-apps. Edit the\n' "${theme_id:-custom}"
printf ' * theme, not this block -- the next theme change overwrites it. */\n'
printf '@define-color accent_bg_color #%s;\n' "$theme_accent"
printf '@define-color accent_color #%s;\n' "$pal_accentAlt"
printf '@define-color accent_fg_color #%s;\n' "$accent_fg"
printf '@define-color destructive_bg_color #%s;\n' "$pal_redDeep"
printf '@define-color destructive_color #%s;\n' "$pal_red"
printf '@define-color destructive_fg_color #%s;\n' "$(on_hex "$pal_redDeep" "$pal_bgDark" "$pal_fg")"
printf '@define-color success_bg_color #%s;\n' "$pal_green"
printf '@define-color success_color #%s;\n' "$pal_green"
printf '@define-color success_fg_color #%s;\n' "$(on_hex "$pal_green" "$pal_bgDark" "$pal_fg")"
printf '@define-color warning_bg_color #%s;\n' "$pal_yellow"
printf '@define-color warning_color #%s;\n' "$pal_yellow"
printf '@define-color warning_fg_color #%s;\n' "$(on_hex "$pal_yellow" "$pal_bgDark" "$pal_fg")"
printf '@define-color error_bg_color #%s;\n' "$pal_redDeep"
printf '@define-color error_color #%s;\n' "$pal_red"
printf '@define-color error_fg_color #%s;\n' "$(on_hex "$pal_redDeep" "$pal_bgDark" "$pal_fg")"
printf '@define-color window_bg_color #%s;\n' "$pal_bgPanel"
printf '@define-color window_fg_color #%s;\n' "$pal_fg"
printf '@define-color view_bg_color #%s;\n' "$pal_bg"
printf '@define-color view_fg_color #%s;\n' "$pal_fg"
printf '@define-color headerbar_bg_color #%s;\n' "$pal_bgPanel"
printf '@define-color headerbar_fg_color #%s;\n' "$pal_fg"
printf '@define-color headerbar_border_color #%s;\n' "$pal_gutter"
printf '@define-color headerbar_backdrop_color #%s;\n' "$pal_bgDark"
printf '@define-color headerbar_shade_color %s;\n' "$(rgba_of "$pal_bgDark" 0.08)"
printf '@define-color headerbar_darker_shade_color %s;\n' "$(rgba_of "$pal_bgDark" 0.9)"
printf '@define-color sidebar_bg_color #%s;\n' "$pal_bgDark"
printf '@define-color sidebar_fg_color #%s;\n' "$pal_fg"
printf '@define-color sidebar_backdrop_color #%s;\n' "$pal_bgDark"
printf '@define-color sidebar_shade_color %s;\n' "$(rgba_of "$pal_gutter" 0.08)"
printf '@define-color sidebar_border_color %s;\n' "$sidebar_line"
printf '@define-color secondary_sidebar_bg_color #%s;\n' "$pal_bgDark"
printf '@define-color secondary_sidebar_fg_color #%s;\n' "$pal_fg"
printf '@define-color secondary_sidebar_backdrop_color #%s;\n' "$pal_bgDark"
printf '@define-color secondary_sidebar_shade_color %s;\n' "$shade"
printf '@define-color secondary_sidebar_border_color %s;\n' "$sidebar_line"
printf '@define-color card_bg_color #%s;\n' "$pal_bgHighlight"
printf '@define-color card_fg_color #%s;\n' "$pal_fg"
printf '@define-color card_shade_color %s;\n' "$shade"
printf '@define-color dialog_bg_color #%s;\n' "$pal_bgPopover"
printf '@define-color dialog_fg_color #%s;\n' "$pal_fg"
printf '@define-color popover_bg_color #%s;\n' "$pal_bgPopover"
printf '@define-color popover_fg_color #%s;\n' "$pal_fg"
printf '@define-color popover_shade_color %s;\n' "$shade"
printf '@define-color thumbnail_bg_color #%s;\n' "$pal_bgHighlight"
printf '@define-color thumbnail_fg_color #%s;\n' "$pal_fg"
printf '@define-color shade_color %s;\n' "$shade"
printf '@define-color scrollbar_outline_color %s;\n' "$(rgba_of "$pal_bgDark" 0.5)"
}
# Substitutes the block between the markers, leaving every other byte alone.
# The block is handed over in a file rather than through `awk -v`, which
# processes backslash escapes in the value it is given.
write_gtk_block() {
local target="$1" block_file="$2" temporary="$1.tmp"
if ! grep -q 'PANAMA THEME BEGIN' "$target" 2>/dev/null \
|| ! grep -q 'PANAMA THEME END' "$target" 2>/dev/null; then
printf 'no marker'
return 0
fi
awk -v block="$block_file" '
index($0, "PANAMA THEME BEGIN") {
print
while ((getline line < block) > 0) print line
close(block)
skipping = 1
next
}
index($0, "PANAMA THEME END") { skipping = 0; print; next }
!skipping { print }
' "$target" >"$temporary" 2>/dev/null || true
if [[ ! -s "$temporary" ]]; then
rm -f "$temporary"
printf 'failed'
return 0
fi
replace_if_changed "$target" "$temporary"
}
status_gtkcss="skipped"
if [[ "$palette_ok" == true ]]; then
gtk_block_file="$(mktemp "${TMPDIR:-/tmp}/panama-gtk-block.XXXXXX")"
if gtk_css_block >"$gtk_block_file" 2>/dev/null; then
for gtk_css in "$config_home/gtk-3.0/gtk.css" \
"$config_home/gtk-4.0/gtk.css" \
"$config_home/gtk-4.0/gtk-dark.css"; do
[[ -w "$gtk_css" ]] || continue
gtk_css_result="$(write_gtk_block "$gtk_css" "$gtk_block_file")"
# The worst outcome across the three files is the one worth
# reporting: two written and one missing its markers is not
# "written".
case "$gtk_css_result" in
failed) status_gtkcss="failed" ;;
"no marker") [[ "$status_gtkcss" == "failed" ]] || status_gtkcss="no marker" ;;
unchanged) [[ "$status_gtkcss" == "skipped" ]] && status_gtkcss="unchanged" || true ;;
written) [[ "$status_gtkcss" == "skipped" || "$status_gtkcss" == "unchanged" ]] \
&& status_gtkcss="written" || true ;;
esac
done
else
status_gtkcss="failed"
fi
rm -f "$gtk_block_file"
fi
# ── 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, and this is the one
# place the command line's accent NAME is still the input rather than the
# theme's own hex: an enum cannot take a colour.
#
# 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
# ── Vicinae ──────────────────────────────────────────────────────────────────
# The launcher selects a theme per system appearance, so both schemes are
# rendered every time and vicinae.json names them permanently. They are written
# straight into Vicinae's data directory rather than into the repository's
# themes folder: that folder is symlinked into ~/.local/share/vicinae/themes,
# and a generated file inside it would be a machine's colours showing up as a
# change to the repository.
vicinae_dir="$data_home/vicinae/themes"
render_vicinae() {
(
set -euo pipefail
eval "$(theme_vars "$(resolve_theme "$1")")"
local variant="$1"
local core_accent core_secondary selection secondary_selection tile on_accent
core_accent="$(hex_or "${theme_accent:-}" "$(accent_hex "$2" "$1")")"
core_secondary="$(hex_or "${theme_secondary:-}" "$(accent_secondary_hex "$2" "$1")")"
on_accent="$(on_hex "$core_accent" "$pal_bgDark" "$pal_fg")"
# The two halves of the Prism selection, mixed into the highlight
# surface rather than named as tokens -- no palette key describes
# "a row that is selected".
selection="$(mix_hex "$pal_bgHighlight" "$core_accent" 30)"
secondary_selection="$(mix_hex "$pal_bgHighlight" "$core_secondary" 30)"
tile="$(mix_hex "$pal_bg" "$pal_bgHighlight" 50)"
printf '# Generated by Panama from the "%s" theme. Do not edit.\n' "${theme_id:-custom}"
printf '# panama-theme-apps rewrites this file on every theme change.\n\n'
printf '[meta]\nversion = 1\n'
printf 'name = "Panama %s"\n' "$variant"
printf 'description = "The active Panama theme, rendered for Vicinae."\n'
printf 'variant = "%s"\n\n' "$variant"
printf '[colors.core]\n'
printf 'background = "#%s"\n' "$pal_bg"
printf 'foreground = "#%s"\n' "$pal_fg"
printf 'secondary_background = "#%s"\n' "$pal_bgDark"
printf 'border = "#%s"\n' "$pal_gutter"
printf 'accent = "#%s"\n' "$core_accent"
printf 'accent_foreground = "#%s"\n\n' "$on_accent"
printf '[colors.accents]\n'
# The terminal blue, not accentAlt: on a light theme accentAlt is often
# the same cyan the next line already names.
printf 'blue = "#%s"\n' "$(hex_or "${ansi_blue:-}" "$pal_accentAlt")"
printf 'green = "#%s"\n' "$pal_green"
printf 'magenta = "#%s"\n' "$pal_magenta"
printf 'orange = "#%s"\n' "$pal_orange"
printf 'purple = "#%s"\n' "$pal_pink"
printf 'red = "#%s"\n' "$pal_red"
printf 'yellow = "#%s"\n' "$pal_yellow"
printf 'cyan = "#%s"\n\n' "$pal_cyan"
printf '[colors.main_window]\n'
printf 'border = "#%s"\n' "$pal_gutter"
printf 'footer = { background = "colors.core.secondary_background" }\n\n'
printf '[colors.settings_window]\n'
printf 'border = "#%s"\n\n' "$pal_gutter"
printf '[colors.shortcut]\n'
printf 'border = "colors.core.border"\n\n'
printf '[colors.text]\n'
printf 'default = "colors.core.foreground"\n'
printf 'muted = "#%s"\n' "$pal_fgDim"
printf 'danger = "#%s"\n' "$pal_red"
printf 'success = "#%s"\n' "$pal_green"
printf 'placeholder = "#%s"\n' "$pal_fgMuted"
printf 'selection = { background = "#%s", foreground = "#%s" }\n\n' "$core_accent" "$on_accent"
printf '[colors.text.links]\n'
printf 'default = "#%s"\n' "$core_accent"
printf 'visited = "#%s"\n\n' "$pal_magenta"
printf '[colors.input]\n'
printf 'border = "#%s"\n' "$pal_gutter"
printf 'border_focus = "#%s"\n' "$core_accent"
printf 'border_error = "#%s"\n\n' "$pal_red"
printf '[colors.button.primary]\n'
printf 'background = "#%s"\n' "$pal_bgHighlight"
printf 'foreground = "#%s"\n' "$pal_fg"
printf 'hover = { background = "#%s" }\n' "$pal_gutter"
printf 'focus = { outline = "colors.core.accent" }\n\n'
printf '[colors.list.item.hover]\n'
printf 'foreground = "#%s"\n' "$pal_fg"
printf 'secondary_foreground = "#%s"\n\n' "$pal_fgDim"
printf '[colors.list.item.selection]\n'
printf 'background = "#%s"\n' "$selection"
printf 'foreground = "#%s"\n' "$pal_fg"
printf 'secondary_background = "#%s"\n' "$secondary_selection"
printf 'secondary_foreground = "#%s"\n\n' "$pal_fg"
printf '[colors.grid.item]\n'
printf 'background = "#%s"\n' "$tile"
printf 'hover = { outline = "#%s" }\n' "$core_accent"
printf 'selection = { outline = "#%s" }\n\n' "$core_secondary"
printf '[colors.scrollbars]\n'
printf 'background = "#%s"\n\n' "$pal_gutter"
printf '[colors.loading]\n'
printf 'bar = "#%s"\n' "$core_accent"
printf 'spinner = "#%s"\n' "$pal_fg"
)
}
status_vicinae="skipped"
if [[ "$both_schemes_ok" == true ]] && mkdir -p "$vicinae_dir" 2>/dev/null; then
status_vicinae="written"
for vicinae_scheme in dark light; do
target="$vicinae_dir/panama-$vicinae_scheme.toml"
if render_vicinae "$vicinae_scheme" "$accent" >"$target.tmp" 2>/dev/null; then
[[ "$(replace_if_changed "$target" "$target.tmp")" == "failed" ]] \
&& status_vicinae="failed" || true
else
rm -f "$target.tmp"
status_vicinae="failed"
fi
done
fi
# ── Firefox ──────────────────────────────────────────────────────────────────
# Edge-Frfox, the vendored chrome theme, takes its colours from GTK system
# colours on Linux -- AccentColor, -moz-dialog, Field. Those follow the portal,
# which knows light from dark and nothing about which theme is on. So Firefox
# was the one surface that stayed grey-blue whatever Panama was wearing.
#
# This file is generated beside custom.css and imported by it, rather than being
# custom.css itself: custom.css is the seam a person edits, and regenerating it
# on every theme change would eat their overrides. It carries BOTH schemes as
# light-dark() pairs, so Firefox flips with the portal without this script
# running again -- and so a private window, which forces its own dark scheme,
# is themed too.
#
# Chrome only. about: pages are styled from userContent.css, which is an
# upstream file and does not import this one.
firefox_chrome="${PANAMA_FIREFOX_CHROME:-$(cd "$script_dir/../../../.." 2>/dev/null && pwd)/config/firefox/chrome}"
render_firefox() {
# light-dark() pairs, built from the two resolved palettes. Firefox picks
# the half that matches the chrome's own color-scheme.
pair() {
local light="light_pal_$1" dark="dark_pal_$1"
printf 'light-dark(#%s, #%s)' "${!light}" "${!dark}"
}
local accents on_light on_dark pressed
accents="light-dark(#$light_theme_accent, #$dark_theme_accent)"
on_light="$(on_hex "$light_theme_accent" "$light_pal_bgDark" "$light_pal_fg")"
on_dark="$(on_hex "$dark_theme_accent" "$dark_pal_bgDark" "$dark_pal_fg")"
# A pressed button has to read as a step past hover, and no palette token
# sits between the gutter and the text -- so it is mixed.
pressed="light-dark(#$(mix_hex "$light_pal_gutter" "$light_pal_fg" 15), #$(mix_hex "$dark_pal_gutter" "$dark_pal_fg" 15))"
printf '/* Generated by panama-theme-apps. Do not edit -- custom.css imports this,\n'
printf ' * and the next theme change overwrites it. Local Firefox tweaks belong in\n'
printf ' * custom.css, which is never regenerated.\n'
printf ' *\n'
printf ' * Both schemes are here as light-dark() pairs so the browser follows the\n'
printf ' * portal on its own. :root:root out-specifies Edge-Frfox colors.css, which\n'
printf ' * uses :root:not([lwtheme]) -- a plain :root would lose the tie. */\n\n'
printf ':root:root:not([lwtheme]),\n'
printf ':root:root[privatebrowsingmode="temporary"] {\n'
printf ' /* Toolbars */\n'
printf ' --lwt-accent-color: %s !important;\n' "$(pair bg)"
printf ' --lwt-accent-color-inactive: %s !important;\n' "$(pair bgDark)"
printf ' --lwt-text-color: %s !important;\n' "$(pair fg)"
printf ' --lwt-text-color-inactive: %s !important;\n' "$(pair fgDim)"
printf ' --toolbar-bgcolor: %s !important;\n' "$(pair bgPanel)"
printf ' --toolbar-non-lwt-bgcolor: var(--toolbar-bgcolor) !important;\n'
printf ' --toolbar-color: %s !important;\n' "$(pair fg)"
printf ' --toolbarbutton-icon-fill: var(--toolbar-color) !important;\n'
printf ' --toolbarbutton-icon-fill-attention: %s !important;\n' "$accents"
printf ' --chrome-content-separator-color: %s !important;\n' "$(pair gutter)"
printf ' --newtab-background-color: %s !important;\n' "$(pair bg)"
printf ' /* Tabs */\n'
printf ' --tab-selected-bgcolor: var(--toolbar-bgcolor) !important;\n'
printf ' --tab-selected-textcolor: %s !important;\n' "$(pair fg)"
printf ' --tab-selected-textcolor-inactive: %s !important;\n' "$(pair fgDim)"
printf ' --tab-attention-icon-color: %s !important;\n' "$accents"
printf ' /* URL bar */\n'
printf ' --toolbar-field-background-color: %s !important;\n' "$(pair bgHighlight)"
printf ' --toolbar-field-color: %s !important;\n' "$(pair fg)"
printf ' --toolbar-field-border-color: transparent !important;\n'
printf ' --toolbar-field-focus-background-color: %s !important;\n' "$(pair bgHighlight)"
printf ' --toolbar-field-focus-color: %s !important;\n' "$(pair fg)"
printf ' --toolbar-field-focus-border-color: %s !important;\n' "$accents"
printf ' --urlbar-box-bgcolor: %s !important;\n' "$(pair bgPopover)"
printf ' --urlbar-box-hover-bgcolor: %s !important;\n' "$(pair bgHighlight)"
printf ' --urlbar-box-active-bgcolor: %s !important;\n' "$(pair gutter)"
printf ' --urlbar-box-focus-bgcolor: var(--urlbar-box-hover-bgcolor) !important;\n'
printf ' --link-color: %s !important;\n' "$accents"
printf ' --uc-urlbarView-accent-color: %s !important;\n' "$accents"
printf ' --urlbarView-highlight-background: %s !important;\n' "$(pair bgHighlight)"
printf ' --urlbarView-highlight-color: %s !important;\n' "$(pair fg)"
printf ' --urlbarView-hover-background: %s !important;\n' "$(pair gutter)"
printf ' --urlbarView-separator-color: %s !important;\n' "$(pair gutter)"
printf ' /* Menus and panels */\n'
printf ' --arrowpanel-background: %s !important;\n' "$(pair bgPopover)"
printf ' --arrowpanel-color: %s !important;\n' "$(pair fg)"
printf ' --arrowpanel-border-color: %s !important;\n' "$(pair gutter)"
printf ' --panel-separator-color: %s !important;\n' "$(pair gutter)"
printf ' --panel-description-color: %s !important;\n' "$(pair fgDim)"
printf ' --panel-disabled-color: %s !important;\n' "$(pair fgMuted)"
printf ' --panel-item-hover-bgcolor: %s !important;\n' "$(pair bgHighlight)"
printf ' --panel-item-active-bgcolor: %s !important;\n' "$(pair gutter)"
printf ' /* Buttons */\n'
printf ' --button-primary-bgcolor: %s !important;\n' "$accents"
printf ' --button-primary-color: light-dark(#%s, #%s) !important;\n' "$on_light" "$on_dark"
printf ' --button-bgcolor: %s !important;\n' "$(pair bgHighlight)"
printf ' --button-color: %s !important;\n' "$(pair fg)"
printf ' --button-hover-bgcolor: %s !important;\n' "$(pair gutter)"
printf ' --button-active-bgcolor: %s !important;\n' "$pressed"
printf ' /* Inputs */\n'
printf ' --focus-outline-color: %s !important;\n' "$accents"
printf ' --input-bgcolor: %s !important;\n' "$(pair bgPopover)"
printf ' --input-color: %s !important;\n' "$(pair fg)"
printf ' --input-border-color: %s !important;\n' "$(pair gutter)"
printf ' --error-text-color: %s !important;\n' "$(pair red)"
printf ' --input-error-border-color: %s !important;\n' "$(pair red)"
printf ' /* Sidebar */\n'
printf ' --sidebar-background-color: %s !important;\n' "$(pair bgPanel)"
printf ' --sidebar-text-color: %s !important;\n' "$(pair fg)"
printf ' --sidebar-border-color: %s !important;\n' "$(pair gutter)"
# Chrome-level dialogs -- the bookmark editor, the print sheet. Edge-Frfox
# points these at the panel colours; the in-content names are what the
# dialog itself reads.
printf '\n &[dialogroot] {\n'
printf ' --in-content-page-background: %s !important;\n' "$(pair bgPopover)"
printf ' --in-content-page-color: %s !important;\n' "$(pair fg)"
printf ' --in-content-accent-color: %s !important;\n' "$accents"
printf ' --in-content-primary-button-background: %s !important;\n' "$accents"
printf ' --in-content-primary-button-text-color: light-dark(#%s, #%s) !important;\n' \
"$on_light" "$on_dark"
printf ' --in-content-button-background: %s !important;\n' "$(pair bgHighlight)"
printf ' --in-content-button-text-color: %s !important;\n' "$(pair fg)"
printf ' --in-content-box-background: %s !important;\n' "$(pair bg)"
printf ' --in-content-box-border-color: %s !important;\n' "$(pair gutter)"
printf ' }\n'
printf '}\n'
}
status_firefox="skipped"
if [[ "$both_schemes_ok" == true && -d "$firefox_chrome" && -w "$firefox_chrome" ]]; then
firefox_target="$firefox_chrome/panama-theme.css"
if render_firefox >"$firefox_target.tmp" 2>/dev/null; then
status_firefox="$(replace_if_changed "$firefox_target" "$firefox_target.tmp")"
else
rm -f "$firefox_target.tmp"
status_firefox="failed"
fi
fi
# ── kitty ────────────────────────────────────────────────────────────────────
# kitty.conf ends with `include current-theme.conf`; that file is machine state
# and is rendered here rather than copied from a per-scheme file, so any theme
# reaches the terminal rather than only the two Tokyo Nights.
kitty_dir="$config_home/kitty"
kitty_theme="$kitty_dir/current-theme.conf"
render_kitty() {
printf '# Generated by Panama from the "%s" theme. Do not edit.\n' "${theme_id:-custom}"
printf '#\n# kitty.conf includes this file; panama-theme-apps rewrites it on every theme\n'
printf '# change and pushes the same colours to running terminals over the kitty\n'
printf '# control socket. The tokyonight-* files beside it are shipped for anyone who\n'
printf '# wants to include one directly, and are no longer copied here.\n\n'
printf 'foreground #%s\n' "$pal_fg"
printf 'background #%s\n' "$pal_bg"
# The classic terminal inversion: selected text wears the foreground as its
# ground. Derived rather than named -- no palette token is "selection".
printf 'selection_foreground #%s\n' "$pal_bg"
printf 'selection_background #%s\n' "$pal_fg"
printf 'cursor #%s\n' "$pal_fg"
printf 'cursor_text_color #%s\n' "$pal_bg"
printf 'url_color #%s\n\n' "$pal_teal"
# The focused border is the accent role; the unfocused one is the gutter,
# the palette's neutral contrast surface.
printf 'active_border_color #%s\n' "$theme_accent"
printf 'inactive_border_color #%s\n\n' "$pal_gutter"
printf 'tab_bar_background #%s\n' "$pal_bg"
printf 'active_tab_foreground #%s\n' "$pal_bgDark"
printf 'active_tab_background #%s\n' "$theme_accent"
printf 'inactive_tab_foreground #%s\n' "$pal_fgDim"
printf 'inactive_tab_background #%s\n\n' "$pal_bgHighlight"
printf 'color0 #%s\n' "$ansi_black"
printf 'color8 #%s\n' "$ansi_brightBlack"
printf 'color1 #%s\n' "$ansi_red"
printf 'color9 #%s\n' "$ansi_brightRed"
printf 'color2 #%s\n' "$ansi_green"
printf 'color10 #%s\n' "$ansi_brightGreen"
printf 'color3 #%s\n' "$ansi_yellow"
printf 'color11 #%s\n' "$ansi_brightYellow"
printf 'color4 #%s\n' "$ansi_blue"
printf 'color12 #%s\n' "$ansi_brightBlue"
printf 'color5 #%s\n' "$ansi_magenta"
printf 'color13 #%s\n' "$ansi_brightMagenta"
printf 'color6 #%s\n' "$ansi_cyan"
printf 'color14 #%s\n' "$ansi_brightCyan"
printf 'color7 #%s\n' "$ansi_white"
printf 'color15 #%s\n' "$ansi_brightWhite"
# 16 and 17 are the extended slots tokyonight uses for orange and a deep
# red; some tools (delta, bat) reference them.
printf 'color16 #%s\n' "$pal_orange"
printf 'color17 #%s\n' "$pal_redDeep"
}
status_kitty="skipped"
if [[ -d "$kitty_dir" ]] && [[ "$palette_ok" == true ]] && [[ "$ansi_ok" == true ]]; then
# Written atomically: kitty may read this while a new window is starting.
if render_kitty >"$kitty_theme.tmp" 2>/dev/null; then
status_kitty="$(replace_if_changed "$kitty_theme" "$kitty_theme.tmp")"
else
rm -f "$kitty_theme.tmp"
status_kitty="failed"
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 [[ "$status_kitty" != "failed" ]] \
&& 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 "$kitty_theme" >/dev/null 2>&1 \
&& kitty @ --to "unix:@${socket#@}" set-colors --override "active_border_color=#$theme_accent" >/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
# 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.
#
# It used to sit INSIDE the kitty branch's if-condition, which meant it fired
# in the middle of writing kitty's theme file and not at all on a machine
# without a kitty theme to write.
"${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-hook" theme-set "$scheme" "$accent" || true
# 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 theme "${theme_id:-}" \
--arg kitty "$status_kitty" \
--arg gtk "$status_gtk" \
--arg gtkcss "$status_gtkcss" \
--arg btop "$status_btop" \
--arg tmux "$status_tmux" \
--arg hyprlock "$status_hyprlock" \
--arg adwaita "$status_adwaita" \
--arg vicinae "$status_vicinae" \
--arg firefox "$status_firefox" \
'{scheme: $scheme, theme: $theme, kitty: $kitty, gtk: $gtk, gtkcss: $gtkcss,
btop: $btop, tmux: $tmux, hyprlock: $hyprlock, adwaita: $adwaita,
vicinae: $vicinae, firefox: $firefox}'