diff --git a/bin/omarchy-theme-set-templates b/bin/omarchy-theme-set-templates index a7266490..ab89869a 100755 --- a/bin/omarchy-theme-set-templates +++ b/bin/omarchy-theme-set-templates @@ -59,6 +59,120 @@ mix_color() { ' } +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 +} + add_template_value() { local key="$1" local value="$2" @@ -118,6 +232,42 @@ add_mix_values() { 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") + ;; + *) + 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)[[:space:]]+[^}]+[[:space:]]*\}\}' "$tpl" 2>/dev/null || true) + done +} + # Only generate dynamic templates for themes with a colors.toml definition if [[ -f $COLORS_FILE ]]; then sed_script=$(mktemp) @@ -136,6 +286,7 @@ if [[ -f $COLORS_FILE ]]; then done <"$COLORS_FILE" 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 diff --git a/default/themed/hyprland.lua.tpl b/default/themed/hyprland.lua.tpl index 5c9e2101..1a2d6d1a 100644 --- a/default/themed/hyprland.lua.tpl +++ b/default/themed/hyprland.lua.tpl @@ -1,15 +1,18 @@ -local active_border_color = "rgb({{ accent_strip }})" +local active_border_color = {{ hypr_gradient hyprland_active_border accent }} +local inactive_border_color = {{ hypr_gradient hyprland_inactive_border rgba(595959aa) }} hl.config({ general = { col = { active_border = active_border_color, + inactive_border = inactive_border_color, }, }, group = { col = { border_active = active_border_color, + border_inactive = inactive_border_color, }, }, }) diff --git a/default/themed/shell.toml.tpl b/default/themed/shell.toml.tpl index b3d219da..30f84338 100644 --- a/default/themed/shell.toml.tpl +++ b/default/themed/shell.toml.tpl @@ -126,8 +126,10 @@ border-alpha = 1.0 background = "{{ background }}" background-alpha = 1.0 text = "{{ foreground }}" -# Conventionally matches the Hyprland active-window border -border = "{{ accent }}" +# Conventionally matches the Hyprland active-window border. If a theme +# defines a gradient active border, the shell uses its first stop because +# QML Rectangle borders are flat colors. +border = "{{ gradient_start hyprland_active_border accent }}" border-alpha = 1.0 countdown = "{{ accent }}" diff --git a/test/cli b/test/cli index 0c875615..10755405 100755 --- a/test/cli +++ b/test/cli @@ -216,6 +216,7 @@ mkdir -p "$NEXT_THEME" "$CURRENT_THEME" "$USER_THEMED" cat >"$NEXT_THEME/colors.toml" <<'EOF' accent = "#336699" +hyprland_active_border = "rgba(010203ee) rgba(040506ee) 45deg" cursor = "#ffffff" foreground = "#ffffff" background = "#000000" @@ -245,12 +246,25 @@ strip={{ mix_strip background foreground 50% }} rgb={{ mix_rgb background foreground 50% }} EOF +cat >"$USER_THEMED/gradient-test.txt.tpl" <<'EOF' +hypr={{ hypr_gradient hyprland_active_border accent }} +shell={{ gradient_start hyprland_active_border accent }} +fallback-hypr={{ hypr_gradient missing accent }} +fallback-shell={{ gradient_start missing accent }} +EOF + OMARCHY_PATH="$ROOT" HOME="$PI_TMPDIR" "$ROOT/bin/omarchy-theme-set-templates" grep -qx 'mix=#808080' "$NEXT_THEME/mix-test.txt" || fail "theme template mix helper works" grep -qx 'strip=808080' "$NEXT_THEME/mix-test.txt" || fail "theme template mix_strip helper works" grep -qx 'rgb=128,128,128' "$NEXT_THEME/mix-test.txt" || fail "theme template mix_rgb helper works" pass "theme template color mix helpers work" +grep -qx 'hypr={ colors = { "rgba(010203ee)", "rgba(040506ee)" }, angle = 45 }' "$NEXT_THEME/gradient-test.txt" || fail "theme template hypr_gradient helper works" +grep -qx 'shell=#010203' "$NEXT_THEME/gradient-test.txt" || fail "theme template gradient_start helper works" +grep -qx 'fallback-hypr="#336699"' "$NEXT_THEME/gradient-test.txt" || fail "theme template hypr_gradient fallback works" +grep -qx 'fallback-shell=#336699' "$NEXT_THEME/gradient-test.txt" || fail "theme template gradient_start fallback works" +pass "theme template gradient helpers work" + jq -e '.name == "omarchy-system" and .vars.panel == "#0f0f0f" and .vars.border == "#4d4d4d" and .colors.accent == "accent"' "$NEXT_THEME/pi.json" >/dev/null pass "pi theme template is generated from standard themed templates" cp "$NEXT_THEME/pi.json" "$CURRENT_THEME/pi.json" diff --git a/themes/hackerman/colors.toml b/themes/hackerman/colors.toml index 517ff366..63a33957 100644 --- a/themes/hackerman/colors.toml +++ b/themes/hackerman/colors.toml @@ -1,4 +1,5 @@ accent = "#82FB9C" +hyprland_active_border = "rgba(26a269ee) rgba(2ec27eee) 45deg" cursor = "#ddf7ff" foreground = "#ddf7ff" background = "#0B0C16" diff --git a/themes/last-horizon/colors.toml b/themes/last-horizon/colors.toml index 08e6145a..7ec8cc19 100644 --- a/themes/last-horizon/colors.toml +++ b/themes/last-horizon/colors.toml @@ -1,5 +1,7 @@ # Accent and UI colors accent = "#b59790" +hyprland_active_border = "rgba(8a8588ee) rgba(e2dddcee)" +hyprland_inactive_border = "rgba(584e51aa)" active_border_color = "#d6d3de" active_tab_background = "#a5a0b6" diff --git a/themes/last-horizon/shell.toml b/themes/last-horizon/shell.toml index d0bea2a1..d8adae2a 100644 --- a/themes/last-horizon/shell.toml +++ b/themes/last-horizon/shell.toml @@ -1,5 +1,5 @@ # last-horizon preserves the historical behavior of matching notification -# borders to the theme's Hyprland active-window border ($activeBorderColor -# in hyprland.conf), rather than the accent. +# borders to the theme's Hyprland active-window border (hyprland_active_border +# in colors.toml), rather than the accent. [notifications] border = "#8a8588" diff --git a/themes/solitude/colors.toml b/themes/solitude/colors.toml index c7e26a1b..a81c4ec0 100644 --- a/themes/solitude/colors.toml +++ b/themes/solitude/colors.toml @@ -1,5 +1,7 @@ # Accent and UI colors accent = "#798186" +hyprland_active_border = "rgba(798186ee) rgba(caccccee)" +hyprland_inactive_border = "rgb(1e1e1e)" active_border_color = "#a8adb0" active_tab_background = "#798186" diff --git a/themes/solitude/shell.toml b/themes/solitude/shell.toml index 5f2e138b..ed5d4afc 100644 --- a/themes/solitude/shell.toml +++ b/themes/solitude/shell.toml @@ -1,5 +1,5 @@ # solitude preserves the historical behavior of matching notification -# borders to the theme's Hyprland active-window border ($activeBorderColor -# in hyprland.conf), rather than the accent. +# borders to the theme's Hyprland active-window border (hyprland_active_border +# in colors.toml), rather than the accent. [notifications] border = "#798186" diff --git a/themes/tokyo-night/colors.toml b/themes/tokyo-night/colors.toml index fe51dd04..e7448486 100644 --- a/themes/tokyo-night/colors.toml +++ b/themes/tokyo-night/colors.toml @@ -1,4 +1,5 @@ accent = "#7aa2f7" +hyprland_active_border = "rgba(33ccffee) rgba(00ff99ee) 45deg" cursor = "#c0caf5" foreground = "#a9b1d6" background = "#1a1b26"