Files

888 lines
44 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
# Every token render_vicinae reads, in both schemes. The renderer runs under
# `set -u` in a subshell, so a token missing from one side is not a slightly
# duller launcher theme -- it aborts that render and the file is left as it was.
# The gate has to cover what the renderer actually reads, or it passes and the
# render fails anyway.
both_schemes_ok=true
for token in bg bgDark bgHighlight bgPanel bgPopover fg fgDim fgMuted gutter red \
accentAlt green magenta orange pink yellow cyan; 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}'