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