Files
omarchy/bin/omarchy-theme-set-templates
T
David Heinemeier HanssonandClaude Opus 5.5 a98e55eb91 Pick window transparency from Appearance > Window Transparency, and a Glassy bar background (#14991)
* Pick window transparency from Appearance > Window Transparency

Transparent, the default, keeps today's windows. Opaque turns the windows
Omarchy makes transparent solid. Glassy is the frosted glass Starship
(#14726): its blur behind every see-through window and shell surface, and a
see-through, blurred background in the theme's terminal files, since foot
turns compositor blur down and a Hyprland opacity rule fades text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Leave window transparency to Appearance when animations are off

Turning animations off no longer makes windows opaque. VM installs start
with opaque windows instead, and a migration keeps them opaque for anyone
who already has animations off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add a Glassy bar background, under Menu Bar > Bar Background

Menu Bar > Bar Background picks Opaque Bar, Glassy Bar or Clear Bar, and
Indicators becomes Bar Indicators to match. Glassy keeps the theme
background faintly and Hyprland blurs what is behind it, with the same blur
as Glassy windows.

omarchy bar background opaque|glassy|none|cycle sets it and prints it when
given nothing. omarchy bar transparent true|false|toggle stays for
compatibility, as none and opaque. Double-clicking the bar cycles through
the three backgrounds the same way, from clear to glassy to opaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stop when a saved corner choice can't be removed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Make btop follow glassy windows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:21:00 +09:00

455 lines
12 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"
# Theme switching waits on this script before the transition starts, so it
# avoids a process per value: helpers return through REPLY rather than $(...),
# and one awk pass computes the mixes and renders every template.
declare -A THEME_COLORS
MIX_TOKEN_PATTERN='\{\{[[:space:]]*mix(_strip|_rgb)?[[:space:]]+[A-Za-z0-9_]+[[:space:]]+[A-Za-z0-9_]+[[:space:]]+[0-9]+([.][0-9]+)?%?[[:space:]]*\}\}'
GRADIENT_TOKEN_PATTERN='\{\{[[:space:]]*(hypr_gradient|gradient_start|shell_gradient)[[:space:]]+[^}]+[[:space:]]*\}\}'
# Convert hex color to decimal RGB (e.g., "#1e1e2e" -> "30,30,46")
hex_to_rgb() {
local hex="${1#\#}"
printf -v REPLY "%d,%d,%d" "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
REPLY="$value"
}
resolve_theme_ref() {
local ref="$1"
local fallback="${2:-}"
if [[ -n ${THEME_COLORS[$ref]+_} ]]; then
REPLY="${THEME_COLORS[$ref]}"
elif [[ -n $fallback && -n ${THEME_COLORS[$fallback]+_} ]]; then
REPLY="${THEME_COLORS[$fallback]}"
elif [[ -n $fallback ]]; then
REPLY="$fallback"
else
REPLY="$ref"
fi
}
resolve_gradient_color() {
trim "$1"
if [[ -n ${THEME_COLORS[$REPLY]+_} ]]; then
REPLY="${THEME_COLORS[$REPLY]}"
fi
}
parse_gradient() {
local spec="$1"
local part
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
resolve_gradient_color "$part"
GRADIENT_COLORS+=("$REPLY")
fi
done
}
color_to_shell_hex() {
local color r g b
resolve_gradient_color "$1"
color="$REPLY"
if [[ $color =~ ^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$ ]]; then
REPLY="#${color:1:6}"
elif [[ $color =~ ^[Rr][Gg][Bb][Aa]?\(([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?\)$ ]]; then
REPLY="#${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 -v REPLY "#%02x%02x%02x" "$r" "$g" "$b"
elif [[ $color =~ ^0x[0-9A-Fa-f]{8}$ ]]; then
REPLY="#${color:4:6}"
else
REPLY="$color"
fi
}
hypr_gradient_value() {
local spec index value
resolve_theme_ref "$1" "${2:-}"
spec="$REPLY"
parse_gradient "$spec"
if (( ${#GRADIENT_COLORS[@]} == 0 )); then
REPLY="\"$spec\""
elif (( ${#GRADIENT_COLORS[@]} == 1 )); then
REPLY="\"${GRADIENT_COLORS[0]}\""
else
value='{ colors = {'
for index in "${!GRADIENT_COLORS[@]}"; do
(( index > 0 )) && value+=','
value+=" \"${GRADIENT_COLORS[$index]}\""
done
value+=' }'
[[ -n $GRADIENT_ANGLE ]] && value+=", angle = $GRADIENT_ANGLE"
REPLY="$value }"
fi
}
gradient_start_value() {
local spec
resolve_theme_ref "$1" "${2:-}"
spec="$REPLY"
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 value=""
resolve_theme_ref "$1" "${2:-}"
spec="$REPLY"
parse_gradient "$spec"
if (( ${#GRADIENT_COLORS[@]} == 0 )); then
REPLY="$spec"
return
fi
for index in "${!GRADIENT_COLORS[@]}"; do
(( index > 0 )) && value+=" "
value+="${GRADIENT_COLORS[$index]}"
done
[[ -n $GRADIENT_ANGLE ]] && value+=" ${GRADIENT_ANGLE}deg"
REPLY="$value"
}
# The value table holds token, unit separator, value lines for the renderer to
# substitute. Tokens may contain tabs, so a tab cannot delimit them.
add_template_value() {
local key="$1"
local value="$2"
printf '{{ %s }}\037%s\n{{ %s_strip }}\037%s\n' "$key" "$value" "$key" "${value#\#}" >>"$value_table"
if [[ $value =~ ^#[0-9A-Fa-f]{6}$ ]]; then
hex_to_rgb "$value"
printf '{{ %s_rgb }}\037%s\n' "$key" "$REPLY" >>"$value_table"
fi
}
# A mix of two hex colors goes to the renderer as mix, token, function, start,
# end and amount, split by unit separators, since awk computes it.
add_mix_value() {
local token="$1"
local content fn start_key end_key amount start end
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
[[ $fn == "mix" || $fn == "mix_strip" || $fn == "mix_rgb" ]] || return
printf 'mix\037%s\037%s\037%s\037%s\037%s\n' "$token" "$fn" "$start" "$end" "$amount" >>"$mix_table"
}
add_gradient_function_value() {
local token="$1"
local content fn key fallback
content="${token#\{\{}"
content="${content%\}\}}"
read -r fn key fallback <<<"$content"
case "$fn" in
hypr_gradient)
hypr_gradient_value "$key" "$fallback"
;;
gradient_start)
gradient_start_value "$key" "$fallback"
;;
shell_gradient)
shell_gradient_value "$key" "$fallback"
;;
*)
return
;;
esac
printf '%s\037%s\n' "$token" "$REPLY" >>"$value_table"
}
# Scan every template once for function tokens, instead of a grep per template
# per function family.
add_function_values() {
local token
local -A seen=()
(( ${#template_files[@]} > 0 )) || return
while IFS= read -r token; do
[[ -n ${seen[$token]:-} ]] && continue
seen[$token]=1
if [[ $token =~ ^$MIX_TOKEN_PATTERN$ ]]; then
add_mix_value "$token"
else
add_gradient_function_value "$token"
fi
done < <(grep -hEo "$MIX_TOKEN_PATTERN|$GRADIENT_TOKEN_PATTERN" "${template_files[@]}" 2>/dev/null || true)
}
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
}
# Substitute every template in one awk process. Each {{ ... }} span is looked
# up whole, as the per-template sed script used to match it. A mix is rounded
# with awk's floating point exactly as before, so no generated color moves.
render_templates() {
local tpl filename
local -a pairs=()
# Process user templates first, then built-in templates (user overrides
# built-in). Don't overwrite configs the theme already ships.
for tpl in "${template_files[@]}"; do
filename=${tpl##*/}
filename=${filename%.tpl}
if [[ ! -f $NEXT_THEME_DIR/$filename && -z ${rendered[$filename]:-} ]]; then
rendered[$filename]=1
pairs+=("$tpl" "$NEXT_THEME_DIR/$filename")
fi
done
(( ${#pairs[@]} > 0 )) || return
awk -v value_table="$value_table" -v mix_table="$mix_table" '
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))
}
function mix_color(start, end, amount, start_r, start_g, start_b, end_r, end_g, end_b, red, green, blue) {
start = substr(start, 2)
end = substr(end, 2)
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)
return sprintf("#%02x%02x%02x", red, green, blue)
}
function render(line, out, open, span, token) {
out = ""
while ((open = index(line, "{{")) > 0) {
span = index(substr(line, open + 2), "}}")
if (span == 0) break
token = substr(line, open, span + 3)
if (token in values) {
out = out substr(line, 1, open - 1) values[token]
line = substr(line, open + length(token))
} else {
out = out substr(line, 1, open)
line = substr(line, open + 1)
}
}
return out line
}
BEGIN {
while ((getline row < value_table) > 0) {
sep = index(row, "\037")
values[substr(row, 1, sep - 1)] = substr(row, sep + 1)
}
close(value_table)
while ((getline row < mix_table) > 0) {
split(row, field, "\037")
value = mix_color(field[4], field[5], field[6])
if (field[3] == "mix_strip") {
value = substr(value, 2)
} else if (field[3] == "mix_rgb") {
value = sprintf("%d,%d,%d", hex_pair_to_int(substr(value, 2), 1), hex_pair_to_int(substr(value, 2), 3), hex_pair_to_int(substr(value, 2), 5))
}
values[field[2]] = value
}
close(mix_table)
for (i = 1; i < ARGC; i += 2) {
template = ARGV[i]
output = ARGV[i + 1]
# RT holds the terminator each line was read with, so a template that
# ends without a newline renders without one, as sed left it.
while ((getline line < template) > 0) printf "%s%s", render(line), RT > output
close(template)
close(output)
# A template with no lines still leaves its output file behind.
printf "" >> output
close(output)
}
exit
}
' "${pairs[@]}"
}
# Only generate dynamic templates for themes with a colors.toml definition
if [[ -f $COLORS_FILE ]]; then
value_table=$(mktemp)
mix_table=$(mktemp)
declare -A rendered=()
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_function_values
render_templates
rm "$value_table" "$mix_table"
fi
apply_shell_section_overrides
omarchy-theme-set-glass "$NEXT_THEME_DIR"