bin/theme-gen (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
#!/bin/bash
# Generate tmux, ghostty and (from ghostty-format sources) alacritty color
# files, so bin/theme can use any theme with `theme --set <mode> <name>`.
#
# 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/<name>.conf
# $XDG_CONFIG_HOME/ghostty/colors/<name>
# $XDG_CONFIG_HOME/alacritty/colors/<name>.toml (ghostty-format sources
# only: an alacritty-format
# source is used in place)
#
# Usage:
# theme-gen <name> # theme from the alacritty-theme checkout
# theme-gen /path/to/theme.toml # or a ghostty theme file
# theme-gen -n <slug> <path> # generate as <slug>, not the basename
# theme-gen -f <name> # overwrite existing generated files
#
# Nothing is activated: run `theme --set <mode> <name>` 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
# <section>.<key>=<hex> 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 <mode> ${name}"
|