#!/bin/bash # Generate tmux, ghostty and (from ghostty-format sources) alacritty color # files, so bin/theme can use any theme with `theme --set `. # # Sources: the alacritty-theme checkout (~/src/alacritty/alacritty-theme/ # themes) by name, or any theme file by path -- alacritty-format .toml, or the # `palette = N=#hex` format Ghostty.app bundles its built-in themes in. # # Writes: # $XDG_CONFIG_HOME/tmux/colors/.conf # $XDG_CONFIG_HOME/ghostty/colors/ # $XDG_CONFIG_HOME/alacritty/colors/.toml (ghostty-format sources # only: an alacritty-format # source is used in place) # # Usage: # theme-gen # theme from the alacritty-theme checkout # theme-gen /path/to/theme.toml # or a ghostty theme file # theme-gen -n # generate as , not the basename # theme-gen -f # overwrite existing generated files # # Nothing is activated: run `theme --set ` afterwards. set -euo pipefail : ${HOME:="$(cd ~ && pwd)"} : ${XDG_CONFIG_HOME:=$HOME/.config} themes_dir="$HOME/src/alacritty/alacritty-theme/themes" ghostty_themes_dir="/Applications/Ghostty.app/Contents/Resources/ghostty/themes" usage() { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//' exit "${1:-0}" } force=0 while getopts :fn:h opt; do case "$opt" in f) force=1 ;; n) override_name="$OPTARG" ;; h) usage 0 ;; *) usage 1 ;; esac done shift $((OPTIND - 1)) if [[ $# -ne 1 ]]; then usage 1 fi src="$1" if [[ -f "$src" ]]; then name="$(basename "$src")" name="${name%.toml}" else name="$src" src="${themes_dir}/${name}.toml" fi if [[ -n "${override_name:-}" ]]; then name="$override_name" fi if [[ ! -f "$src" ]]; then echo "ERROR: no theme file at $src" >&2 echo "Themes available in $themes_dir:" >&2 ls "$themes_dir" >&2 2>/dev/null || true echo "Ghostty built-ins live in $ghostty_themes_dir" >&2 exit 1 fi # Ghostty bundles themes as `palette = N=#hex` plus background/foreground; # alacritty themes are .toml sections. Parse either into the same #
.= lines (no leading '#', lowercased) the writers below # consume. parse_colors() { if grep -qE '^[ \t]*palette[ \t]*=' "$src"; then awk ' function hexof(val) { if (match(val, /#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/)) return tolower(substr(val, RSTART + 1, RLENGTH - 1)) return "" } BEGIN { split("black red green yellow blue magenta cyan white", K, " ") } /^[ \t]*palette[ \t]*=/ { line = $0 sub(/^[ \t]*palette[ \t]*=[ \t]*/, "", line) split(line, kv, "=") idx = kv[1] + 0 hex = tolower(kv[2]); gsub(/[ \t]/, "", hex); sub(/^#/, "", hex) if (hex !~ /^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]$/) next if (idx < 0 || idx > 15) next print ((idx < 8) ? "normal" : "bright") "." K[(idx % 8) + 1] "=" hex next } /^[ \t]*background[ \t]*=/ { hex = hexof($0); if (hex != "") print "primary.background=" hex; next } /^[ \t]*foreground[ \t]*=/ { hex = hexof($0); if (hex != "") print "primary.foreground=" hex; next } ' "$src" else awk ' /^[ \t]*\[/ { section = $0 sub(/^[ \t]*\[/, "", section); sub(/\].*$/, "", section) next } /[ \t]*=/ { sect = section sub(/^colors\./, "", sect) if (sect != "primary" && sect != "normal" && sect != "bright") next eq = index($0, "=") key = substr($0, 1, eq - 1); gsub(/[ \t]/, "", key) val = substr($0, eq + 1) if (match(val, /"[^"]*"|'"'"'[^'"'"']*'"'"'/)) { v = substr(val, RSTART + 1, RLENGTH - 2) } else if (match(val, /#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]/)) { v = substr(val, RSTART, RLENGTH) } else next gsub(/[ \t]/, "", v) if (v !~ /^#?[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]$/) next sub(/^#/, "", v) print sect "." key "=" tolower(v) } ' "$src" fi } # Ghostty-format sources have no alacritty file of their own, so theme-gen # writes one; alacritty-format sources are used in place, so none is written. ghostty_source=0 if grep -qE '^[ \t]*palette[ \t]*=' "$src"; then ghostty_source=1 fi # no associative arrays: /bin/bash on macOS is 3.2. Keys are sanitized into # variable names like c_primary_background. c_key() { printf '%s' "$1" | tr '.' '_' } while IFS='=' read -r k v; do # keys come from the theme file and go through eval: accept only plain # section.key names and hex values, or a theme file could run commands [[ "$k" =~ ^[a-z]+\.[a-z_]+$ && "$v" =~ ^[0-9a-f]{6}$ ]] || continue eval "c_$(c_key "$k")='$v'" done < <(parse_colors) get() { local var="c_$(c_key "$1")" fallback="${2-}" if [[ -n "${!var:-}" ]]; then printf '%s' "${!var}" elif [[ -n "$fallback" ]]; then printf '%s' "$fallback" else echo "ERROR: theme is missing $1" >&2 exit 1 fi } bg="$(get primary.background)" fg="$(get primary.foreground)" tmux_out="$XDG_CONFIG_HOME/tmux/colors/${name}.conf" ghostty_out="$XDG_CONFIG_HOME/ghostty/colors/${name}" alacritty_out="$XDG_CONFIG_HOME/alacritty/colors/${name}.toml" for out in "$tmux_out" "$ghostty_out" $([[ "$ghostty_source" -eq 1 ]] && echo "$alacritty_out"); do if [[ -e "$out" && "$force" -ne 1 ]]; then echo "ERROR: $out already exists; use -f to overwrite" >&2 exit 1 fi done if [[ "$ghostty_source" -eq 1 ]]; then { echo "# ${name} (generated by theme-gen from ${src})" echo "[colors.primary]" echo "background = \"#${bg}\"" echo "foreground = \"#${fg}\"" echo echo "[colors.normal]" for key in black red green yellow blue magenta cyan white; do echo "${key} = \"#$(get "normal.${key}")\"" done echo echo "[colors.bright]" for key in black red green yellow blue magenta cyan white; do echo "${key} = \"#$(get "bright.${key}")\"" done } > "$alacritty_out" fi # Ghostty: primary + full 16-color palette. { echo "# ${name} color scheme (generated by theme-gen from ${src})" echo echo "background = #${bg}" echo "foreground = #${fg}" echo local_i=0 for section in normal bright; do for key in black red green yellow blue magenta cyan white; do echo "palette = ${local_i}=#$(get "${section}.${key}" "$(get "normal.${key}" "${fg}")")" local_i=$((local_i + 1)) done done } > "$ghostty_out" # tmux: status bar, messages, pane borders. tmux >= 3.2 takes hex directly. pane_border="$(get bright.black "$(get normal.black "${fg}")")" pane_active="$(get normal.blue "${fg}")" mkdir -p "$(dirname "$tmux_out")" { echo "# ${name} (generated by theme-gen from ${src})" echo "set -g status-style fg=#${fg},bg=default" echo "set -g message-style fg=#${bg},bg=#${fg},fill=#${fg}" echo "set -g pane-border-style fg=#${pane_border}" echo "set -g pane-active-border-style fg=#${pane_active}" echo "set -g window-status-current-style fg=#${fg}" } > "$tmux_out" echo "wrote $tmux_out" echo "wrote $ghostty_out" if [[ "$ghostty_source" -eq 1 ]]; then echo "wrote $alacritty_out" fi echo "activate with: theme --set ${name}"