Files
omarchy/test/shell.d/hermes-theme-test.sh
T
Spencer Bull 6baae0f556 Make Hermes follow the Omarchy theme as a skin
Hermes Desktop installed under Install > AI kept its own palette while every other agent app retinted with the theme. Hermes' skin is its one theme unit for the desktop app, the TUI and the CLI, and its gateway watches the active skin file and broadcasts changes to every surface, so Omarchy publishes a skin named omarchy from a template on every theme switch and nothing Omarchy-specific goes upstream.

Activation goes through hermes config set, which writes the active profile's config and touches the skin so a running gateway repaints at once, and it only replaces Hermes' default skin so a choice made in Hermes stays. A theme switch runs that activation too when the desktop package is present and Hermes is still on its default, so a hand-over the installer missed is finished by the next switch; once the config names the skin a switch never starts Hermes. The desktop adopts a skin from a change broadcast rather than from the config it finds at connect time, and its first launch builds the runtime over minutes, so the installer starts --wait as a transient user unit that outlives the install terminal, activates once the runtime marker appears, republishes after the gateway is up, and reports to the journal. A migration hands the skin to existing Hermes Desktop installs through --activate, which also renders the skin for a theme applied before the template existed.

The generated file is validated before it is published, because Hermes parses it as YAML: only the name, a plain description and #rrggbb colours pass, so an unresolved palette key or a cloned theme's own hermes.yaml leaves the previous skin in place.

🤖 Generated by Fable 5.1 in Claude Code. Reviewed by Fable 5.1 code-review at high.
2026-09-05 23:05:10 -05:00

268 lines
12 KiB
Bash

#!/bin/bash
set -euo pipefail
# omarchy-theme-set-hermes writes a file another program parses and asks that
# program to switch to it while it is still on its default. Both are exercised
# here against a throwaway HOME with the Hermes readiness probe, the hermes
# command and the theme refresh stubbed, so a skin that stopped being
# validated, a write into a Hermes that was never set up, or an activation
# that trampled a chosen skin shows up in what landed on disk and what was run.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-install-hermes-cli" <<'SH'
#!/bin/bash
echo "check" >>"$OMARCHY_TEST_HERMES_CALLS"
[[ $1 == "--check" && ${OMARCHY_TEST_HERMES_READY:-0} == "1" ]]
SH
# A theme switch finishes the hand-over only for the desktop app Omarchy
# installed; --activate is asked for by name and does not look.
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-1} == "1" ]]
SH
cat >"$mock_bin/hermes" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == "config" && $2 == "get" ]]; then
printf '%s\n' "${OMARCHY_TEST_HERMES_SKIN:-default}"
fi
SH
# A refresh re-stages the current theme, which is where the skin gets rendered.
cat >"$mock_bin/omarchy-theme-refresh" <<'SH'
#!/bin/bash
echo "refresh" >>"$OMARCHY_TEST_HERMES_CALLS"
printf 'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n' \
>"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
SH
# --wait sleeps between its polls and once more after activating; the stub
# records the delays it was asked for and returns at once.
cat >"$mock_bin/sleep" <<'SH'
#!/bin/bash
printf 'sleep %s\n' "$1" >>"$OMARCHY_TEST_HERMES_CALLS"
SH
chmod +x "$mock_bin"/*
good_skin='name: omarchy
description: Omarchy system theme
colors:
background: "#1a1b26"
ui_text: "#a9b1d6"
ui_accent: "#7aa2f7"'
test_home="$test_tmp/home"
hermes_home="$test_home/.hermes"
skin="$hermes_home/skins/omarchy.yaml"
hermes_calls="$test_tmp/hermes-calls"
# Each case gets a fresh HOME so no file survives from the one before. The
# Hermes home is created the way provisioning does on every machine; only
# --set-up adds the config that says Hermes itself has run, on its default
# skin unless --on names another.
reset_home() {
local source="$good_skin"
rm -rf "$test_home"
mkdir -p "$test_home/.local/state/omarchy/current/theme" "$hermes_home/skills"
: >"$hermes_calls"
while (( $# > 0 )); do
case "$1" in
--set-up) printf 'display:\n skin: default\n' >"$hermes_home/config.yaml" ;;
--on) printf 'display:\n skin: %s\n' "$2" >"$hermes_home/config.yaml"; shift ;;
*) source="$1" ;;
esac
shift
done
printf '%s\n' "$source" >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
}
run_hook() {
OMARCHY_TEST_HERMES_READY="${OMARCHY_TEST_HERMES_READY:-0}" \
OMARCHY_TEST_HERMES_SKIN="${OMARCHY_TEST_HERMES_SKIN:-default}" \
OMARCHY_TEST_HERMES_CALLS="$hermes_calls" \
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
PATH="$mock_bin:$PATH" \
HOME="$test_home" \
HERMES_HOME='' \
"$ROOT/bin/omarchy-theme-set-hermes" "$@"
}
# -- publishing ---------------------------------------------------------------
reset_home
run_hook
[[ ! -e $hermes_home/skins ]] || fail "a Hermes home that only holds the Omarchy skill gets no skin"
[[ ! -s $hermes_calls ]] || fail "nothing is run for a Hermes that never ran" "$(cat "$hermes_calls")"
pass "a theme switch leaves a machine that never ran Hermes alone"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
run_hook 2>"$test_tmp/stderr"
diff -q "$test_home/.local/state/omarchy/current/theme/hermes.yaml" "$skin" >/dev/null ||
fail "the generated skin is published to ~/.hermes/skins/omarchy.yaml"
diff -q "$skin" "$hermes_home/profiles/work/skins/omarchy.yaml" >/dev/null ||
fail "an existing Hermes profile gets the skin too"
[[ $(ls "$hermes_home/skins") == "omarchy.yaml" ]] || fail "no temporary file is left beside the skin"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is asked nothing more" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch says nothing about Hermes" "$(cat "$test_tmp/stderr")"
pass "the skin is published to the Hermes home and every profile"
reset_home --set-up 'name: omarchy
description: Omarchy system theme
colors:
background: "{{ background }}"'
mkdir -p "$hermes_home/skins"
printf 'name: omarchy\ncolors:\n background: "#000000"\n' >"$skin"
run_hook 2>"$test_tmp/stderr"
grep -q '#000000' "$skin" || fail "an unresolved placeholder keeps the previous skin in place"
grep -q 'not a plain color palette' "$test_tmp/stderr" || fail "an unresolved placeholder is reported"
pass "a skin with unresolved colors is not published"
for bad in \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\nbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26\\"\\n ui_text: \\"#ffffff"' \
$'name: nord\ndescription: Nord\ncolors:\n background: "#2e3440"' \
$'description: Omarchy system theme\ncolors:\n background: "#1a1b26"' \
$'name: omarchy\ndescription: Nord: arctic palette\ncolors:\n background: "#2e3440"'; do
reset_home --set-up "$bad"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a skin that is not exactly a named palette of hex colors is not published" "$bad"
done
pass "a skin is held to the shape Hermes loads"
# -- a theme switch finishes a missed hand-over ---------------------------------
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes still on its default is switched by a theme switch" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch activates quietly" "$(cat "$test_tmp/stderr")"
pass "a theme switch activates the skin on a Hermes still on its default"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ -f $skin ]] || fail "the skin is published when it is already active"
[[ ! -s $hermes_calls ]] || fail "a Hermes already on the skin is not started" "$(cat "$hermes_calls")"
pass "a theme switch does not start a Hermes already on the skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook
[[ -f $skin ]] || fail "a Hermes installed some other way still gets the skin published"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not touch a Hermes Omarchy did not install as the app" "$(cat "$hermes_calls")"
pass "a theme switch activates only for the desktop app Omarchy installed"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DESKTOP_INSTALLED=0 run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate switches whichever Hermes it is asked about" "$(cat "$hermes_calls")"
pass "--activate does not ask which Hermes it is"
reset_home --on ares
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook 2>"$test_tmp/stderr"
! grep -q 'config set' "$hermes_calls" || fail "a theme switch never replaces a chosen skin" "$(cat "$hermes_calls")"
[[ ! -s $test_tmp/stderr ]] || fail "a chosen skin is left without comment on a theme switch" "$(cat "$test_tmp/stderr")"
pass "a theme switch leaves a skin the user chose in Hermes"
# -- activation ---------------------------------------------------------------
reset_home
run_hook --activate 2>"$test_tmp/stderr"
[[ ! -e $hermes_home/skins && ! -e $hermes_home/config.yaml ]] ||
fail "--activate writes nothing into a Hermes that has never run"
grep -q 'not set up yet' "$test_tmp/stderr" || fail "--activate says why nothing happened"
pass "--activate waits for Hermes to have been set up"
reset_home --set-up
run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "--activate publishes the skin when Hermes is not ready"
[[ $(cat "$hermes_calls") == "check" ]] || fail "a Hermes that is not ready is not run" "$(cat "$hermes_calls")"
grep -q 'hermes config set display.skin omarchy' "$test_tmp/stderr" || fail "an unready Hermes gets the command that finishes the job"
pass "--activate publishes but does not run a Hermes that is not ready"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>"$test_tmp/stderr"
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy' ]] ||
fail "a ready Hermes is asked for its skin and then to switch" "$(cat "$hermes_calls")"
grep -q 'on the Omarchy skin' "$test_tmp/stderr" || fail "--activate reports success"
pass "--activate goes through hermes config set when Hermes runs"
reset_home --on omarchy
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=omarchy run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "--activate goes through Hermes even when the config already names the skin" "$(cat "$hermes_calls")"
pass "--activate always asks Hermes to switch"
for chosen in omarchy ""; do
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN="$chosen" run_hook --activate 2>/dev/null
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "the default and the omarchy skin are both replaced" "skin='$chosen': $(cat "$hermes_calls")"
done
pass "--activate replaces Hermes' default skin"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook --activate 2>"$test_tmp/stderr"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
! grep -q 'config set' "$hermes_calls" || fail "a skin the user chose is not replaced" "$(cat "$hermes_calls")"
grep -q "'ares' skin" "$test_tmp/stderr" || fail "leaving a chosen skin is reported"
pass "--activate leaves a skin the user chose in Hermes"
# -- a skin the current theme has not rendered yet -------------------------------
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook
[[ ! -e $hermes_home/skins && ! -s $hermes_calls ]] ||
fail "a theme switch without a rendered skin publishes nothing" "$(cat "$hermes_calls")"
pass "a theme switch has nothing to do without a rendered skin"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
if run_hook --activate 2>"$test_tmp/stderr"; then
fail "--activate fails when no theme has been selected"
fi
grep -q 'Select an Omarchy theme' "$test_tmp/stderr" || fail "a missing theme is reported"
pass "--activate fails without a current theme to render the skin from"
reset_home --set-up
rm "$test_home/.local/state/omarchy/current/theme/hermes.yaml"
echo tokyo-night >"$test_home/.local/state/omarchy/current/theme.name"
OMARCHY_TEST_HERMES_READY=1 run_hook --activate 2>/dev/null
[[ $(head -1 "$hermes_calls") == "refresh" ]] || fail "a theme applied before the template existed is re-staged" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "the freshly rendered skin is published"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" || fail "the freshly rendered skin is activated" "$(cat "$hermes_calls")"
pass "--activate renders the skin for a theme that predates it"
# -- waiting for the desktop app's first launch ----------------------------------
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>/dev/null
[[ $(cat "$hermes_calls") == $'check\nconfig get display.skin\nconfig set display.skin omarchy\nsleep 60' ]] ||
fail "--wait activates as soon as the runtime marker is there, then republishes after the gateway is up" "$(cat "$hermes_calls")"
[[ -f $skin ]] || fail "--wait publishes the skin"
pass "--wait activates once the desktop app has built its runtime"
reset_home
OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>"$test_tmp/stderr"
[[ $(head -1 "$hermes_calls") == "sleep 10" && ! -e $hermes_home/skins ]] ||
fail "--wait polls for the runtime instead of running Hermes" "$(head -3 "$hermes_calls")"
[[ $(grep -c 'sleep 10' "$hermes_calls") == 180 ]] || fail "--wait gives up after 30 minutes" "$(grep -c 'sleep 10' "$hermes_calls")"
grep -q 'did not finish setting up' "$test_tmp/stderr" || fail "giving up is reported"
pass "--wait polls until the desktop app has built its runtime and gives up in time"