Files
omarchycn/bin/omarchy-theme-set-templates
T
David Heinemeier HanssonandClaude Fable 5 3bd6e72211 Share one theme color resolver across consumers
Five scripts each hand-rolled colors.toml parsing plus their own
alias/fallback cascade, and the tables had drifted: theme-osc grew a
divergent color8/color9 chain, tmux and gnome carried private mode
detection, and the preview tool duplicated the lot. The differences
never mattered for shipped themes but were a standing bug factory for
sparse user themes.

omarchy-theme-color is now the single resolver. Its cascade is a
verbatim port of the canonical theme-set-templates logic (semantic
keys, legacy colorN aliases, derived shades, selection/cursor
derivation, mode precedence), exposed as --all, --raw, or single-key
lookup with fallback.

Generated output is byte-identical across all 21 shipped themes on
every surface: 17 template files, OSC byte stream, tmux and gsettings
command logs, and previews. Sparse legacy themes now resolve the same
palette everywhere instead of five slightly different ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:06:31 -07:00

405 lines
9.7 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Generate themed config files from Omarchy templates
# omarchy:hidden=true
TEMPLATES_DIR="$OMARCHY_PATH/default/themed"
USER_TEMPLATES_DIR="$HOME/.config/omarchy/themed"
NEXT_THEME_DIR="$HOME/.local/state/omarchy/current/next-theme"
COLORS_FILE="$NEXT_THEME_DIR/colors.toml"
declare -A THEME_COLORS
# Convert hex color to decimal RGB (e.g., "#1e1e2e" -> "30,30,46")
hex_to_rgb() {
local hex="${1#\#}"
printf "%d,%d,%d" "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}
# Mix two hex colors. Amount may be a fraction (0.30) or percentage (30%).
mix_color() {
local start="${1#\#}"
local end="${2#\#}"
local amount="$3"
awk -v start="$start" -v end="$end" -v amount="$amount" '
function hex_value(char) {
return index("0123456789abcdef", tolower(char)) - 1
}
function hex_pair_to_int(hex, idx) {
return hex_value(substr(hex, idx, 1)) * 16 + hex_value(substr(hex, idx + 1, 1))
}
BEGIN {
if (amount ~ /%$/) {
sub(/%$/, "", amount)
amount = amount / 100
} else {
amount += 0
if (amount > 1) amount = amount / 100
}
if (amount < 0) amount = 0
if (amount > 1) amount = 1
start_r = hex_pair_to_int(start, 1)
start_g = hex_pair_to_int(start, 3)
start_b = hex_pair_to_int(start, 5)
end_r = hex_pair_to_int(end, 1)
end_g = hex_pair_to_int(end, 3)
end_b = hex_pair_to_int(end, 5)
red = int(start_r * (1 - amount) + end_r * amount + 0.5)
green = int(start_g * (1 - amount) + end_g * amount + 0.5)
blue = int(start_b * (1 - amount) + end_b * amount + 0.5)
printf "#%02x%02x%02x\n", red, green, blue
}
'
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf "%s" "$value"
}
resolve_theme_ref() {
local ref="$1"
local fallback="${2:-}"
if [[ -n ${THEME_COLORS[$ref]+_} ]]; then
printf "%s" "${THEME_COLORS[$ref]}"
elif [[ -n $fallback && -n ${THEME_COLORS[$fallback]+_} ]]; then
printf "%s" "${THEME_COLORS[$fallback]}"
elif [[ -n $fallback ]]; then
printf "%s" "$fallback"
else
printf "%s" "$ref"
fi
}
resolve_gradient_color() {
local color
color=$(trim "$1")
if [[ -n ${THEME_COLORS[$color]+_} ]]; then
color="${THEME_COLORS[$color]}"
fi
printf "%s" "$color"
}
parse_gradient() {
local spec="$1"
local part color
local -a parts
GRADIENT_COLORS=()
GRADIENT_ANGLE=""
read -ra parts <<<"$spec"
for part in "${parts[@]}"; do
[[ -n $part ]] || continue
if [[ $part =~ ^-?[0-9]+([.][0-9]+)?deg$ ]]; then
GRADIENT_ANGLE="${part%deg}"
else
color=$(resolve_gradient_color "$part")
GRADIENT_COLORS+=("$color")
fi
done
}
color_to_shell_hex() {
local color r g b
color=$(resolve_gradient_color "$1")
if [[ $color =~ ^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$ ]]; then
printf "#%s" "${color:1:6}"
elif [[ $color =~ ^[Rr][Gg][Bb][Aa]?\(([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?\)$ ]]; then
printf "#%s" "${BASH_REMATCH[1]}"
elif [[ $color =~ ^[Rr][Gg][Bb][Aa]?\(([0-9]+),([0-9]+),([0-9]+)(,[0-9.]+)?\)$ ]]; then
r=${BASH_REMATCH[1]}
g=${BASH_REMATCH[2]}
b=${BASH_REMATCH[3]}
(( r > 255 )) && r=255
(( g > 255 )) && g=255
(( b > 255 )) && b=255
printf "#%02x%02x%02x" "$r" "$g" "$b"
elif [[ $color =~ ^0x[0-9A-Fa-f]{8}$ ]]; then
printf "#%s" "${color:4:6}"
else
printf "%s" "$color"
fi
}
hypr_gradient_value() {
local spec color index
spec=$(resolve_theme_ref "$1" "${2:-}")
parse_gradient "$spec"
if (( ${#GRADIENT_COLORS[@]} == 0 )); then
printf '"%s"' "$spec"
elif (( ${#GRADIENT_COLORS[@]} == 1 )); then
printf '"%s"' "${GRADIENT_COLORS[0]}"
else
printf '{ colors = {'
for index in "${!GRADIENT_COLORS[@]}"; do
(( index > 0 )) && printf ','
printf ' "%s"' "${GRADIENT_COLORS[$index]}"
done
printf ' }'
[[ -n $GRADIENT_ANGLE ]] && printf ', angle = %s' "$GRADIENT_ANGLE"
printf ' }'
fi
}
gradient_start_value() {
local spec
spec=$(resolve_theme_ref "$1" "${2:-}")
parse_gradient "$spec"
if (( ${#GRADIENT_COLORS[@]} == 0 )); then
color_to_shell_hex "$spec"
else
color_to_shell_hex "${GRADIENT_COLORS[0]}"
fi
}
shell_gradient_value() {
local spec index
spec=$(resolve_theme_ref "$1" "${2:-}")
parse_gradient "$spec"
if (( ${#GRADIENT_COLORS[@]} == 0 )); then
printf "%s" "$spec"
return
fi
for index in "${!GRADIENT_COLORS[@]}"; do
(( index > 0 )) && printf " "
printf "%s" "${GRADIENT_COLORS[$index]}"
done
[[ -n $GRADIENT_ANGLE ]] && printf " %sdeg" "$GRADIENT_ANGLE"
}
add_template_value() {
local key="$1"
local value="$2"
local rgb
printf 's|{{ %s }}|%s|g\n' "$key" "$value" >>"$sed_script"
printf 's|{{ %s_strip }}|%s|g\n' "$key" "${value#\#}" >>"$sed_script"
if [[ $value =~ ^#[0-9A-Fa-f]{6}$ ]]; then
rgb=$(hex_to_rgb "$value")
printf 's|{{ %s_rgb }}|%s|g\n' "$key" "$rgb" >>"$sed_script"
fi
}
add_mix_value() {
local token="$1"
local content fn start_key end_key amount start end value
content="${token#\{\{}"
content="${content%\}\}}"
read -r fn start_key end_key amount <<<"$content"
start="${THEME_COLORS[$start_key]:-}"
end="${THEME_COLORS[$end_key]:-}"
[[ $start =~ ^#[0-9A-Fa-f]{6}$ && $end =~ ^#[0-9A-Fa-f]{6}$ ]] || return
value=$(mix_color "$start" "$end" "$amount")
case "$fn" in
mix)
;;
mix_strip)
value="${value#\#}"
;;
mix_rgb)
value=$(hex_to_rgb "$value")
;;
*)
return
;;
esac
printf 's|%s|%s|g\n' "$token" "$value" >>"$sed_script"
}
add_mix_values() {
local tpl token
local -A seen=()
for tpl in "${template_files[@]}"; do
while IFS= read -r token; do
[[ -n ${seen[$token]:-} ]] && continue
seen[$token]=1
add_mix_value "$token"
done < <(grep -hEo '\{\{[[:space:]]*mix(_strip|_rgb)?[[:space:]]+[A-Za-z0-9_]+[[:space:]]+[A-Za-z0-9_]+[[:space:]]+[0-9]+([.][0-9]+)?%?[[:space:]]*\}\}' "$tpl" 2>/dev/null || true)
done
}
add_gradient_function_value() {
local token="$1"
local content fn key fallback value
content="${token#\{\{}"
content="${content%\}\}}"
read -r fn key fallback <<<"$content"
case "$fn" in
hypr_gradient)
value=$(hypr_gradient_value "$key" "$fallback")
;;
gradient_start)
value=$(gradient_start_value "$key" "$fallback")
;;
shell_gradient)
value=$(shell_gradient_value "$key" "$fallback")
;;
*)
return
;;
esac
printf 's|%s|%s|g\n' "$token" "$value" >>"$sed_script"
}
add_gradient_function_values() {
local tpl token
local -A seen=()
for tpl in "${template_files[@]}"; do
while IFS= read -r token; do
[[ -n ${seen[$token]:-} ]] && continue
seen[$token]=1
add_gradient_function_value "$token"
done < <(grep -hEo '\{\{[[:space:]]*(hypr_gradient|gradient_start|shell_gradient)[[:space:]]+[^}]+[[:space:]]*\}\}' "$tpl" 2>/dev/null || true)
done
}
strip_shell_section_header() {
local section="$1"
local file="$2"
awk -v section="$section" '
BEGIN { skipping_header = 0 }
/^[[:space:]]*\[[^]]+\][[:space:]]*($|#)/ {
if ($0 ~ "^[[:space:]]*\\[" section "\\][[:space:]]*($|#)") {
skipping_header = 1
next
}
}
{ print }
' "$file"
}
apply_shell_section_override() {
local override="$1"
local section tmp body
[[ -f $NEXT_THEME_DIR/shell.toml ]] || return
[[ -f $override ]] || return
section=$(basename "$override")
section="${section#shell.}"
section="${section%.toml}"
[[ $section =~ ^[A-Za-z0-9_-]+$ ]] || return
tmp=$(mktemp)
body=$(mktemp)
strip_shell_section_header "$section" "$override" >"$body"
awk -v section="$section" -v body="$body" '
function emit_override() {
if (emitted) return
print "[" section "]"
while ((getline line < body) > 0) print line
close(body)
emitted = 1
}
/^[[:space:]]*\[[^]]+\][[:space:]]*($|#)/ {
if (in_section) {
emit_override()
in_section = 0
}
if ($0 ~ "^[[:space:]]*\\[" section "\\][[:space:]]*($|#)") {
in_section = 1
next
}
}
!in_section { print }
END {
if (in_section || !emitted) {
if (NR > 0) print ""
emit_override()
}
}
' "$NEXT_THEME_DIR/shell.toml" >"$tmp"
mv "$tmp" "$NEXT_THEME_DIR/shell.toml"
rm "$body"
}
apply_shell_section_overrides() {
local override
shopt -s nullglob
for override in "$NEXT_THEME_DIR"/shell.*.toml; do
[[ $(basename "$override") != "shell.toml" ]] || continue
apply_shell_section_override "$override"
done
}
# Only generate dynamic templates for themes with a colors.toml definition
if [[ -f $COLORS_FILE ]]; then
sed_script=$(mktemp)
shopt -s nullglob
template_files=("$USER_TEMPLATES_DIR"/*.tpl "$TEMPLATES_DIR"/*.tpl)
# Parsing and alias/fallback resolution (legacy colorN names, derived
# shades, mode/theme_type) is shared with every other colors.toml consumer.
while IFS=$'\t' read -r key value; do
THEME_COLORS[$key]="$value"
done < <(omarchy-theme-color --file "$COLORS_FILE" --all)
for key in "${!THEME_COLORS[@]}"; do
add_template_value "$key" "${THEME_COLORS[$key]}"
done
add_mix_values
add_gradient_function_values
# Process user templates first, then built-in templates (user overrides built-in)
for tpl in "${template_files[@]}"; do
filename=$(basename "$tpl" .tpl)
output_path="$NEXT_THEME_DIR/$filename"
# Don't overwrite configs already exists in the output directory (copied from theme specific folder)
if [[ ! -f $output_path ]]; then
sed -f "$sed_script" "$tpl" >"$output_path"
fi
done
rm "$sed_script"
fi
apply_shell_section_overrides