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
This commit is contained in:
Gabriel Brown
2026-08-23 23:39:04 -04:00
parent 7578348db1
commit cb7c09d208
68 changed files with 6115 additions and 1138 deletions
+718 -115
View File
@@ -1,38 +1,52 @@
#!/usr/bin/env bash
# Propagates the color scheme to applications that do not read the desktop
# 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: 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.
# 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.
#
# Terminals are the notable exception -- they predate the standard and carry
# their own palettes. kitty is handled here.
# 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:
#
# 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.
# 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 scheme change appears to do
# already open change now. Without the second, a theme 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.
# 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, from the one file that holds them.
# The accents and the theme resolver, from the one file that holds them.
# shellcheck source=./panama-palette
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/panama-palette"
script_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
source "$script_dir/panama-palette"
scheme="${1:-dark}"
case "$scheme" in
@@ -50,87 +64,153 @@ case "$accent" in
*) 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.
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
# hyprlock wants "R, G, B" decimal, not hex.
# ── 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_)"
accent_border_hex="$(accent_hex "$accent" "$scheme")"
# 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.
#
# 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"
# 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"
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
if render_hyprlock "$scheme" "$lock_template" "$lock_dir/hyprlock.conf" "$accent"; 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"
# 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 [[ -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"
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 command -v tmux >/dev/null 2>&1 && tmux has-session 2>/dev/null; then
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
@@ -141,31 +221,98 @@ if [[ -r "$tmux_theme" ]]; then
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.
# 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="${XDG_CONFIG_HOME:-$HOME/.config}/btop/btop.conf"
btop_theme="tokyonight-moon"
[[ "$scheme" == "light" ]] && btop_theme="tokyonight-day"
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 [[ -w "$btop_conf" ]]; then
if sed -i "s|^color_theme *=.*|color_theme = \"$btop_theme\"|" "$btop_conf" 2>/dev/null; then
status_btop="written"
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 ──────────────────────────────────────────────────────────────────────
# ── 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
@@ -176,7 +323,7 @@ fi
status_gtk="skipped"
for gtk_version in 3.0 4.0; do
gtk_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-$gtk_version"
gtk_dir="$config_home/gtk-$gtk_version"
template="$gtk_dir/settings.ini.template"
[[ -r "$template" ]] || continue
@@ -192,6 +339,138 @@ for gtk_version in 3.0 4.0; do
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
@@ -199,10 +478,9 @@ done
#
# 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.
# 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
@@ -222,41 +500,350 @@ if command -v gsettings >/dev/null 2>&1; then
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"
# ── 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 [[ -r "$theme_file" ]]; then
if [[ -d "$kitty_dir" ]] && [[ "$palette_ok" == true ]] && [[ "$ansi_ok" == true ]]; then
# Written atomically: kitty may read this while a new window is starting.
# The accent override is appended after the copied theme rather than
# edited into the tracked theme file: kitty applies settings top to
# bottom, so a later active_border_color line wins, and this file is
# itself generated -- the tracked per-scheme theme stays scheme-only.
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
&& # Anything else on this machine that wants to know. Runs after every generator
# above, so a hook sees the finished state rather than a half-applied one, and
# a broken hook cannot cost you a theme change -- panama-hook reports and steps
# over. See bin/panama-hook.
"${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-hook" theme-set "$scheme" "$accent" || true
printf 'active_border_color #%s\n' "$accent_border_hex" >>"$kitty_dir/current-theme.conf.tmp" \
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
status_kitty="written"
if render_kitty >"$kitty_theme.tmp" 2>/dev/null; then
status_kitty="$(replace_if_changed "$kitty_theme" "$kitty_theme.tmp")"
else
rm -f "$kitty_dir/current-theme.conf.tmp"
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 command -v kitty >/dev/null 2>&1 && command -v ss >/dev/null 2>&1; then
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 "$theme_file" >/dev/null 2>&1 \
&& kitty @ --to "unix:@${socket#@}" set-colors --override "active_border_color=#$accent_border_hex" >/dev/null 2>&1; then
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)
@@ -264,15 +851,31 @@ printf 'active_border_color #%s\n' "$accent_border_hex" >>"$kitty_dir/current-th
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" \
'{scheme: $scheme, kitty: $kitty, gtk: $gtk, btop: $btop, tmux: $tmux, hyprlock: $hyprlock, adwaita: $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}'