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