summaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-17 11:07:32 +0100
committerIgor Soarez <igor@soarez.org>2026-10-17 11:07:32 +0100
commit95056eacdff97a710129bbb29e707ab83f5faa72 (patch)
treeef8fe50947e506859bc0190bd777c50f2f666453 /bin
parentc0771c839df57a627ceb21bd398752ba2b1ed3f1 (diff)
theme-gen: derive tmux and ghostty colours for any theme
`theme --set <mode> <name>` needs a tmux conf and a ghostty colour file that alacritty-format themes do not provide, so such a name fell back to the generic dark/light tmux colours and left ghostty unchanged, exiting 0. bin/theme-gen reads a theme in alacritty .toml format (by alacritty-theme checkout name or path) or Ghostty's bundled `palette = N=#hex` format, and writes config/tmux/colors/<name>.conf and config/ghostty/colors/<name>, plus config/alacritty/colors/<name>.toml for a ghostty-format source. Generated with it: sonokai, solarized_osaka_night, kanagawabones and dracula_plus_high_contrast, with nvim stand-ins for the three that have no nvim port installed. Parsed keys go through eval, so only plain section.key names and hex values are accepted: a theme file could otherwise run commands. From dot request 20261017-110255-work-laptop-48c22c.
Diffstat (limited to 'bin')
-rwxr-xr-xbin/theme-gen223
1 files changed, 223 insertions, 0 deletions
diff --git a/bin/theme-gen b/bin/theme-gen
new file mode 100755
index 0000000..6000e5e
--- /dev/null
+++ b/bin/theme-gen
@@ -0,0 +1,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}"