Files
dotfiles/script/omarchy-theme-color
T
gitadmin 9c353f257b feat(themes): add color derivation and themed app templates
Add script/omarchy-theme-color and expand omarchy-theme-set-templates to
derive a full palette from each theme's colors.toml, then render new
themed templates (shell, pi, vscode theme, gum, neovim, claude, hermes,
t3code) from it.

Adds backgrounds and palette updates for existing themes plus new themes
(last-horizon, lupine, solitude, tidewater-*, miasma, ...).
2026-09-24 21:25:12 -04:00

305 lines
10 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Resolve semantic colors from an Omarchy theme colors.toml
# omarchy:args=[--file <colors.toml>] (--all | --raw | <key> [fallback])
# omarchy:hidden=true
# Shared colors.toml parser/resolver. The alias/fallback cascade mirrors what
# omarchy-theme-set-templates bakes into the generated configs at theme-set
# time, so every consumer (templates, OSC sequences, tmux, GNOME, previews)
# resolves the exact same palette.
#
# --all print every resolved key<TAB>value pair (sorted by key)
# --raw print only the key<TAB>value pairs defined in the file
# <key> [fallback] print one resolved value; the fallback is tried as
# another palette key first, then used verbatim
#
# Theme mode precedence: `mode` key, legacy `theme_type` key, a light.mode
# file beside the colors.toml, background luminance auto-detect, dark.
COLORS_FILE="$HOME/.local/state/omarchy/current/theme/colors.toml"
OUTPUT=""
QUERY_KEY=""
QUERY_FALLBACK=""
usage() {
echo "Usage: omarchy-theme-color [--file <colors.toml>] (--all | --raw | <key> [fallback])"
}
while (( $# > 0 )); do
case "$1" in
--file)
COLORS_FILE="${2:-}"
shift 2
;;
--all)
OUTPUT="all"
shift
;;
--raw)
OUTPUT="raw"
shift
;;
-h | --help)
usage
exit 0
;;
*)
if [[ -z $QUERY_KEY ]]; then
QUERY_KEY="$1"
elif [[ -z $QUERY_FALLBACK ]]; then
QUERY_FALLBACK="$1"
else
usage >&2
exit 1
fi
shift
;;
esac
done
if [[ -z $OUTPUT && -z $QUERY_KEY ]] || [[ -n $OUTPUT && -n $QUERY_KEY ]]; then
usage >&2
exit 1
fi
COLORS_DIR=$(dirname "$COLORS_FILE")
declare -A THEME_COLORS
# 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
}
'
}
alias_theme_color() {
local key="$1"
local fallback="$2"
[[ ${THEME_COLORS[$key]} ]] || THEME_COLORS[$key]="${THEME_COLORS[$fallback]}"
}
resolve_theme_mode() {
local bg_hex lum
[[ ${THEME_COLORS[mode]} ]] || THEME_COLORS[mode]="${THEME_COLORS[theme_type]}"
[[ ${THEME_COLORS[mode]} ]] && return
if [[ -f $COLORS_DIR/light.mode ]]; then
THEME_COLORS[mode]="light"
elif [[ ${THEME_COLORS[background]} =~ ^#[0-9A-Fa-f]{6}$ ]]; then
bg_hex="${THEME_COLORS[background]#\#}"
lum=$(( $(printf "%d" "0x${bg_hex:0:2}") + $(printf "%d" "0x${bg_hex:2:2}") + $(printf "%d" "0x${bg_hex:4:2}") ))
(( lum > 382 )) && THEME_COLORS[mode]="light" || THEME_COLORS[mode]="dark"
else
THEME_COLORS[mode]="dark"
fi
}
parse_colors_file() {
local key value
[[ -f $COLORS_FILE ]] || return 0
while IFS='=' read -r key value; do
key="${key//[\"\' ]/}" # strip quotes and spaces from key
[[ $key && $key != \#* ]] || continue # skip empty lines and comments
if [[ $value == *[\"\']* ]]; then
value="${value#*[\"\']}"
value="${value%%[\"\']*}" # extract value between quotes (ignores inline comments)
else
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}" # trim unquoted values
fi
# Values reach consumers as sed replacement text, so the charset excludes
# the delimiter, backslash, and & while still covering everything a real
# palette holds: hex, rgb()/rgba() lists, gradient angles like -45deg,
# decimals, and bare words. Rejections are announced so a third-party theme
# doesn't lose a key silently and leave a raw {{ placeholder }} behind.
if [[ ! $key =~ ^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-]+$ ]]; then
printf 'omarchy-theme-color: skipping key with unsupported characters\n' >&2
continue
fi
if [[ ! $value =~ ^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#(),._+/%\ -]*$ ]]; then
printf 'omarchy-theme-color: skipping %s: unsupported characters in value\n' "$key" >&2
continue
fi
THEME_COLORS[$key]="$value"
[[ $OUTPUT == "raw" ]] && printf '%s\t%s\n' "$key" "$value"
done <"$COLORS_FILE"
return 0
}
resolve_theme_colors() {
local key
# Accept the complete legacy short-name palette before applying ANSI
# fallbacks or deriving shades. Canonical names take precedence when a theme
# defines both forms.
declare -A legacy_palette_alias=(
[background]=bg
[dark_background]=dark_bg
[darker_background]=darker_bg
[lighter_background]=lighter_bg
[foreground]=fg
[dark_foreground]=dark_fg
[light_foreground]=light_fg
[bright_foreground]=bright_fg
)
for key in "${!legacy_palette_alias[@]}"; do
alias_theme_color "$key" "${legacy_palette_alias[$key]}"
done
# Themes generated before the semantic palette may only define ANSI names.
[[ ${THEME_COLORS[background]} ]] || THEME_COLORS[background]="${THEME_COLORS[color0]}"
[[ ${THEME_COLORS[foreground]} ]] || THEME_COLORS[foreground]="${THEME_COLORS[color7]}"
[[ ${THEME_COLORS[background]} ]] && THEME_COLORS[color0]="${THEME_COLORS[background]}"
[[ ${THEME_COLORS[foreground]} ]] && THEME_COLORS[color7]="${THEME_COLORS[foreground]}"
# Legacy compatibility: map ANSI color0..color15 to semantic names.
declare -A legacy_alias=(
[red]=color1
[green]=color2
[yellow]=color3
[blue]=color4
[magenta]=color5
[cyan]=color6
[bright_red]=color9
[bright_green]=color10
[bright_yellow]=color11
[bright_blue]=color12
[bright_magenta]=color13
[bright_cyan]=color14
)
for key in "${!legacy_alias[@]}"; do
alias_theme_color "$key" "${legacy_alias[$key]}"
done
alias_theme_color magenta purple
alias_theme_color bright_magenta bright_purple
[[ ${THEME_COLORS[light_foreground]} ]] || THEME_COLORS[light_foreground]="${THEME_COLORS[color7]:-${THEME_COLORS[foreground]}}"
[[ ${THEME_COLORS[bright_foreground]} ]] || THEME_COLORS[bright_foreground]="${THEME_COLORS[color15]:-${THEME_COLORS[foreground]}}"
THEME_COLORS[cursor]="${THEME_COLORS[bright_foreground]}"
[[ ${THEME_COLORS[lighter_background]} ]] || THEME_COLORS[lighter_background]="${THEME_COLORS[color0]:-${THEME_COLORS[background]}}"
[[ ${THEME_COLORS[dark_foreground]} ]] || THEME_COLORS[dark_foreground]="${THEME_COLORS[color8]:-${THEME_COLORS[foreground]}}"
[[ ${THEME_COLORS[muted]} ]] || THEME_COLORS[muted]="${THEME_COLORS[color8]:-${THEME_COLORS[dark_foreground]}}"
[[ ${THEME_COLORS[selection]} ]] || THEME_COLORS[selection]="${THEME_COLORS[selection_background]:-${THEME_COLORS[color8]:-${THEME_COLORS[color0]:-${THEME_COLORS[background]}}}}"
[[ ${THEME_COLORS[selection_background]} ]] || THEME_COLORS[selection_background]="${THEME_COLORS[selection]}"
[[ ${THEME_COLORS[selection_foreground]} ]] || THEME_COLORS[selection_foreground]="${THEME_COLORS[bright_foreground]}"
[[ ${THEME_COLORS[orange]} ]] || THEME_COLORS[orange]="${THEME_COLORS[yellow]}"
[[ ${THEME_COLORS[brown]} ]] || THEME_COLORS[brown]=$(mix_color "${THEME_COLORS[orange]}" "#000000" 50%)
# Auto-derive shades from base accents when not defined and not aliased from colorN
[[ ${THEME_COLORS[dark_background]} ]] || THEME_COLORS[dark_background]=$(mix_color "${THEME_COLORS[background]}" "#000000" 25%)
[[ ${THEME_COLORS[darker_background]} ]] || THEME_COLORS[darker_background]=$(mix_color "${THEME_COLORS[background]}" "#000000" 50%)
[[ ${THEME_COLORS[bright_red]} ]] || THEME_COLORS[bright_red]=$(mix_color "${THEME_COLORS[red]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_yellow]} ]] || THEME_COLORS[bright_yellow]=$(mix_color "${THEME_COLORS[yellow]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_green]} ]] || THEME_COLORS[bright_green]=$(mix_color "${THEME_COLORS[green]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_cyan]} ]] || THEME_COLORS[bright_cyan]=$(mix_color "${THEME_COLORS[cyan]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_blue]} ]] || THEME_COLORS[bright_blue]=$(mix_color "${THEME_COLORS[blue]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_magenta]} ]] || THEME_COLORS[bright_magenta]=$(mix_color "${THEME_COLORS[magenta]}" "#ffffff" 20%)
alias_theme_color purple magenta
alias_theme_color bright_purple bright_magenta
# Keep semantic themes compatible with consumers that still reference the
# legacy ANSI names directly.
declare -A ansi_alias=(
[color0]=background
[color1]=red
[color2]=green
[color3]=yellow
[color4]=blue
[color5]=magenta
[color6]=cyan
[color7]=foreground
[color8]=muted
[color9]=bright_red
[color10]=bright_green
[color11]=bright_yellow
[color12]=bright_blue
[color13]=bright_magenta
[color14]=bright_cyan
[color15]=bright_foreground
)
for key in "${!ansi_alias[@]}"; do
alias_theme_color "$key" "${ansi_alias[$key]}"
done
# Keep canonical themes compatible with old user templates and consumers
# that still query the short palette names directly.
for key in "${!legacy_palette_alias[@]}"; do
if [[ ${THEME_COLORS[$key]} ]]; then
THEME_COLORS["${legacy_palette_alias[$key]}"]="${THEME_COLORS[$key]}"
fi
done
resolve_theme_mode
THEME_COLORS[theme_type]="${THEME_COLORS[mode]}"
}
parse_colors_file
if [[ $OUTPUT == "raw" ]]; then
exit 0
fi
resolve_theme_colors
if [[ $OUTPUT == "all" ]]; then
while IFS= read -r key; do
printf '%s\t%s\n' "$key" "${THEME_COLORS[$key]}"
done < <(printf '%s\n' "${!THEME_COLORS[@]}" | LC_ALL=C sort)
exit 0
fi
value="${THEME_COLORS[$QUERY_KEY]:-}"
if [[ -z $value && -n $QUERY_FALLBACK ]]; then
value="${THEME_COLORS[$QUERY_FALLBACK]:-$QUERY_FALLBACK}"
fi
[[ -n $value ]] || exit 1
printf '%s\n' "$value"