Merge pull request #10381 from spencerbull/hermes-system-theme

Make Hermes follow the Omarchy theme as a skin
This commit is contained in:
Spencer Bull authored and GitHub committed 2026-09-06 00:39:19 -05:00
commit 988da12cb0
11 files changed
+775 -3

No files matched your search

+6
View File
@@ -21,6 +21,12 @@ omarchy-install-hermes-cli || true
echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
# Only a running Hermes can be told which skin to show, and the first launch
# takes minutes; a unit outlives this terminal and reports to the journal.
echo "Matching Hermes to the current theme once it is set up..."
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
systemd-run --user --quiet --collect --unit=omarchy-hermes-theme omarchy-theme-set-hermes --wait
echo ""
echo "Hermes Desktop has been installed."
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
+3
View File
@@ -8,6 +8,9 @@ set -euo pipefail
omarchy-pkg-drop hermes-desktop
# The installer leaves a unit waiting to hand the app the Omarchy theme.
systemctl --user stop omarchy-hermes-theme.service 2>/dev/null || true
# The mise CLI is the app's predecessor, not the app itself: Hermes Desktop takes
# it over on install and runs its own runtime instead, so a copy still here is one
# the app never superseded -- an interrupted install, or the terminal CLI from
+1
View File
@@ -326,6 +326,7 @@ post_theme_commands=(
omarchy-theme-set-gnome
omarchy-theme-set-pi
omarchy-theme-set-claude
omarchy-theme-set-hermes
omarchy-theme-set-browser
omarchy-theme-set-vscode
omarchy-theme-set-obsidian
+233
View File
@@ -0,0 +1,233 @@
#!/bin/bash
# omarchy:summary=Sync the generated Omarchy theme to Hermes as a skin
# omarchy:args=[--activate] [--wait]
# omarchy:hidden=true
# A skin is Hermes' one theme unit for the desktop app, the TUI and the CLI;
# its gateway watches the active skin file and repaints every surface on change.
set -euo pipefail
HERMES_SOURCE_PATH="$HOME/.local/state/omarchy/current/theme/hermes.yaml"
HERMES_THEME_NAME_PATH="$HOME/.local/state/omarchy/current/theme.name"
HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}"
HERMES_CONFIG_PATH="$HERMES_HOME/config.yaml"
HERMES_SKIN_NAME="omarchy"
# The command the readiness probe vets, rather than whichever hermes is on PATH.
HERMES_COMMAND="$HOME/.local/bin/hermes"
# Written by the desktop app once the runtime its first launch provisions is in.
HERMES_BOOTSTRAP_MARKER="$HERMES_HOME/hermes-agent/.hermes-bootstrap-complete"
HERMES_ACTIVATE=0
HERMES_WAIT=0
HERMES_WAIT_LIMIT=$((30 * 60))
usage() {
echo "Usage: omarchy-theme-set-hermes [--activate] [--wait]"
}
for arg in "$@"; do
case "$arg" in
--activate)
HERMES_ACTIVATE=1
;;
--wait)
HERMES_ACTIVATE=1
HERMES_WAIT=1
;;
-h | --help)
usage
exit 0
;;
*)
usage >&2
exit 1
;;
esac
done
# A theme switch runs this beside a dozen other hooks; only --activate explains.
note() {
if (( HERMES_ACTIVATE == 1 )); then
echo "$*" >&2
fi
}
# A theme applied before the template existed has no skin rendered yet.
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
(( HERMES_ACTIVATE == 1 )) || exit 0
if [[ ! -s $HERMES_THEME_NAME_PATH ]]; then
echo "Hermes skin source missing: $HERMES_SOURCE_PATH" >&2
echo "Select an Omarchy theme first." >&2
exit 1
fi
omarchy-theme-refresh
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
echo "Hermes skin source missing after refreshing the theme: $HERMES_SOURCE_PATH" >&2
exit 1
fi
fi
# The first launch takes minutes and may be abandoned; the readiness probe
# would start Hermes to answer, so poll for the marker instead.
if (( HERMES_WAIT == 1 )); then
waited=0
until [[ -f $HERMES_BOOTSTRAP_MARKER && -f $HERMES_CONFIG_PATH ]]; do
if (( waited >= HERMES_WAIT_LIMIT )); then
echo "Hermes did not finish setting up within $((HERMES_WAIT_LIMIT / 60)) minutes; run omarchy-theme-set-hermes --activate once it has." >&2
exit 0
fi
sleep 10
waited=$((waited + 10))
done
fi
# Provisioning creates ~/.hermes on every machine for the Omarchy skill; the
# config is what Hermes writes once it has actually run.
if [[ ! -f $HERMES_CONFIG_PATH ]]; then
note "Hermes is not set up yet; launch it once, then run omarchy-theme-set-hermes --activate."
exit 0
fi
# Hermes parses the skin as YAML and hands its strings to every surface, so only
# the name, a plain description and #rrggbb colours may reach it, in the order
# YAML needs them. YAML breaks lines on bytes grep does not, so the bytes are
# counted first, NUL included.
skin_is_well_formed() {
local skin="$1"
[[ -f $skin ]] &&
(( $(LC_ALL=C tr -d ' -~\n' <"$skin" | wc -c) == 0 )) &&
awk -v name="name: $HERMES_SKIN_NAME" '
bad { next }
/^#/ || /^[[:space:]]*$/ { next }
!seen_name { if ($0 == name) seen_name = 1; else bad = 1; next }
!seen_colors {
if ($0 == "colors:") seen_colors = 1
else if (!seen_description && $0 ~ /^description: [A-Za-z0-9 ,.()-]{0,200}$/) seen_description = 1
else bad = 1
next
}
/^ [a-z_]{1,64}: "#[0-9a-fA-F]{6}"$/ { colors++; next }
{ bad = 1 }
END { exit (bad || !seen_colors || colors == 0) }
' "$skin"
}
# The check has to cover the bytes that get published, and the theme can change
# underneath between the two, so a private copy is taken and that is checked.
snapshot_dir=$(mktemp -d)
trap 'rm -rf "$snapshot_dir"' EXIT
HERMES_SNAPSHOT="$snapshot_dir/$HERMES_SKIN_NAME.yaml"
take_snapshot() {
cp "$HERMES_SOURCE_PATH" "$HERMES_SNAPSHOT" 2>/dev/null && skin_is_well_formed "$HERMES_SNAPSHOT"
}
if ! take_snapshot; then
echo "Skipping Hermes skin: $(basename "$HERMES_SOURCE_PATH") is not a plain color palette." >&2
exit 0
fi
# The gateway reads the file whole on an mtime change, so the write is atomic;
# -T so a directory at the skin's path is an error rather than a destination.
publish_skin() {
local skins_dir="$1"
local tmp
mkdir -p "$skins_dir" 2>/dev/null || return 1
tmp=$(mktemp "$skins_dir/$HERMES_SKIN_NAME.yaml.XXXXXX" 2>/dev/null) || return 1
if ! cp "$HERMES_SNAPSHOT" "$tmp" 2>/dev/null || ! mv -T "$tmp" "$skins_dir/$HERMES_SKIN_NAME.yaml" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
}
# A profile is a Hermes home of its own; existing ones get the skin, none are
# made, and one that cannot take it does not cost the others.
publish_skin_everywhere() {
local profile
publish_skin "$HERMES_HOME/skins" || {
echo "Could not publish the Hermes skin to $HERMES_HOME/skins." >&2
return 1
}
for profile in "$HERMES_HOME"/profiles/*/; do
[[ -d $profile ]] || continue
publish_skin "${profile%/}/skins" || note "Could not publish the skin to the Hermes profile $(basename "$profile")."
done
}
publish_skin_everywhere
# Hermes reads the config of the profile named in active_profile; the profile
# exists once its directory does, with or without a config of its own.
active_config_path() {
local profile
profile=$(cat "$HERMES_HOME/active_profile" 2>/dev/null || true)
profile=${profile,,}
if [[ -n $profile && $profile != "default" && -d $HERMES_HOME/profiles/$profile ]]; then
echo "$HERMES_HOME/profiles/$profile/config.yaml"
else
echo "$HERMES_CONFIG_PATH"
fi
}
# A theme switch finishes the hand-over only for the app Omarchy installed, and
# only Hermes' default is ever replaced, so a config plainly naming another skin
# ends it here without starting Hermes. Anything less plain is for Hermes to read.
if (( HERMES_ACTIVATE == 0 )); then
omarchy-pkg-present hermes-desktop || exit 0
skin_line=$(grep -m1 -x ' skin: .*' "$(active_config_path)" 2>/dev/null || true)
case "${skin_line# skin: }" in
"" | default | null | true | false | *[!A-Za-z0-9_-]*) ;;
*) exit 0 ;;
esac
fi
# Omarchy's cold stub installs Hermes when run, so ask the probe before running it.
if ! omarchy-install-hermes-cli --check 2>/dev/null; then
note "Hermes is not ready, so the Omarchy skin is published but not active."
note "Once Hermes runs, activate it with: hermes config set display.skin $HERMES_SKIN_NAME"
exit 0
fi
# Only Hermes' own default is replaced, so a skin chosen in Hermes stays; an
# answer that did not come is not a default.
if ! current_skin=$(timeout 15 "$HERMES_COMMAND" config get display.skin 2>/dev/null); then
note "Hermes did not say which skin it is on, so the Omarchy skin is published but not active."
exit 0
fi
if [[ -n $current_skin && $current_skin != "default" && $current_skin != "$HERMES_SKIN_NAME" ]]; then
note "Hermes is set to the '$current_skin' skin; leaving it. Switch with: hermes config set display.skin $HERMES_SKIN_NAME"
exit 0
fi
# Hermes' own writer: it updates the active profile's config and touches the
# skin file so a running gateway broadcasts the change. A refusal is cosmetic.
if ! timeout 30 "$HERMES_COMMAND" config set display.skin "$HERMES_SKIN_NAME" >/dev/null 2>&1; then
note "Hermes refused to switch skins, so the Omarchy skin is published but not active."
exit 0
fi
note "Hermes is on the Omarchy skin."
# The desktop applies a skin only from a broadcast, and a config written before
# the gateway seeded its watcher goes unannounced; a later write is announced.
# The theme may have changed underneath in the meantime, so it is checked again.
if (( HERMES_WAIT == 1 )); then
sleep 60
if take_snapshot; then
publish_skin_everywhere
fi
fi
+47
View File
@@ -0,0 +1,47 @@
name: omarchy
description: Omarchy system theme
colors:
background: "{{ background }}"
ui_text: "{{ foreground }}"
ui_primary: "{{ accent }}"
ui_accent: "{{ accent }}"
ui_border: "{{ muted }}"
ui_label: "{{ accent }}"
ui_ok: "{{ green }}"
ui_warn: "{{ yellow }}"
ui_error: "{{ red }}"
ui_tool: "{{ cyan }}"
ui_thinking: "{{ dark_foreground }}"
banner_border: "{{ muted }}"
banner_title: "{{ accent }}"
banner_accent: "{{ accent }}"
banner_dim: "{{ dark_foreground }}"
banner_text: "{{ foreground }}"
prompt: "{{ bright_foreground }}"
input_rule: "{{ muted }}"
response_border: "{{ accent }}"
shell_dollar: "{{ blue }}"
selection_bg: "{{ selection }}"
session_label: "{{ accent }}"
session_border: "{{ muted }}"
status_bar_bg: "{{ dark_background }}"
status_bar_text: "{{ foreground }}"
status_bar_strong: "{{ accent }}"
status_bar_dim: "{{ dark_foreground }}"
status_bar_good: "{{ green }}"
status_bar_warn: "{{ yellow }}"
status_bar_bad: "{{ red }}"
status_bar_critical: "{{ bright_red }}"
voice_status_bg: "{{ dark_background }}"
completion_menu_bg: "{{ lighter_background }}"
completion_menu_current_bg: "{{ selection }}"
completion_menu_meta_bg: "{{ lighter_background }}"
completion_menu_meta_current_bg: "{{ selection }}"
diff_added: "{{ mix background green 15% }}"
diff_removed: "{{ mix background red 15% }}"
diff_added_word: "{{ green }}"
diff_removed_word: "{{ red }}"
syntax_string: "{{ green }}"
syntax_number: "{{ yellow }}"
syntax_keyword: "{{ magenta }}"
syntax_comment: "{{ muted }}"
+2 -2
View File
@@ -26,7 +26,7 @@ Pick your default agent with `omarchy default agent <name>` or under _Setup > De
Once you've chosen, `Super + Shift + Ctrl + A` launches the default agent in a dedicated terminal window (or brings up the picker if you haven't chosen yet). You can also launch it straight into a task with `omarchy agent prompt "Review this project"`. Agents launched this way run unattended in their respective don't-stop-to-ask modes, so be ready for them to actually do things! And since agents refuse to remember trust for your home directory, launches from `$HOME` start in `~/Work` instead.
There are terminal shortcuts too: `a` runs the default agent inline in the current terminal, while `c`, `cx`, and `cy` start OpenCode, Claude Code, and Codex directly (again in their auto-approving modes). Theme changes sync to the agents as well: Claude Code, Pi, and OpenCode all follow along when you switch the Omarchy theme.
There are terminal shortcuts too: `a` runs the default agent inline in the current terminal, while `c`, `cx`, and `cy` start OpenCode, Claude Code, and Codex directly (again in their auto-approving modes). Theme changes sync to the agents as well: Claude Code, Pi, OpenCode, and Hermes (once Hermes Desktop is installed) all follow along when you switch the Omarchy theme.
### The agents panel
@@ -46,7 +46,7 @@ Crashes can also be silenced one program at a time, which is what the diagnosis
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, Hermes Desktop, OpenClaw, and the Perplexity desktop app.
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too.
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Installing it also hands Hermes the Omarchy theme as a skin named `omarchy`, which every Hermes surface follows as you switch themes; pick another under Hermes' Appearance settings or with `/skin` if you'd rather it didn't, and Omarchy leaves that choice alone. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself unless you tell it otherwise: it asks, defaulting to no, whether that data and your connection settings should go too.
OpenClaw's desktop experience is its Control UI, which opens as a web app backed by its own local gateway. OpenClaw updates arrive through Omarchy's package updates, so skip the Control UI's own "Update Gateway" button: it would try to write into the package-managed install and fail. Removing OpenClaw under _Remove > AI_ takes the gateway service and the app with it and then asks whether `~/.openclaw` should go too, since that holds your chats and credentials alongside the plugin runtimes OpenClaw downloads for itself; the default keeps it.
+9
View File
@@ -0,0 +1,9 @@
echo "Hand Hermes Desktop the Omarchy theme as a skin"
# Only the app Omarchy installed under Install > AI follows the theme by itself.
# A Hermes the user set up some other way keeps whatever skin they chose.
omarchy-pkg-present hermes-desktop || exit 0
# The same hand-over a fresh install does. A Hermes that is not ready or refuses
# the write is reported and done with there; only Omarchy's own failures return.
omarchy-theme-set-hermes --activate
+13
View File
@@ -34,6 +34,11 @@ cat >"$mock_bin/gum" <<'SH'
printf '%s\0' "$@" >>"$OMARCHY_TEST_GUM_LOG"
exit "${OMARCHY_TEST_GUM_STATUS:-1}"
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
echo "systemctl $*" >>"$OMARCHY_TEST_SYSTEMCTL_LOG"
SH
chmod +x "$mock_bin"/*
seed_install() {
@@ -58,9 +63,11 @@ seed_install() {
remove() {
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_INSTALLER_STATUS="${OMARCHY_TEST_INSTALLER_STATUS:-0}" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-remove-ai-hermes" </dev/null >/dev/null 2>&1
@@ -71,8 +78,10 @@ remove() {
remove_tty() {
: >"$test_tmp/installer-log"
: >"$test_tmp/gum-log"
: >"$test_tmp/systemctl-log"
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" \
OMARCHY_TEST_INSTALLER_LOG="$test_tmp/installer-log" \
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
@@ -89,6 +98,10 @@ remove || fail "remove succeeds"
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
pass "removal takes the whole runtime the app installed"
grep -Fxq 'systemctl --user stop omarchy-hermes-theme.service' "$test_tmp/systemctl-log" ||
fail "the unit the installer left waiting to hand over the theme is stopped" "$(cat "$test_tmp/systemctl-log")"
pass "removal stops the installer's theme hand-over"
[[ -d $test_home/.config/Hermes ]] ||
fail "gateway connections, tokens and settings survive removal"
pass "removal keeps the app's connections and settings"
@@ -0,0 +1,59 @@
#!/bin/bash
set -euo pipefail
# The migration hands an existing Hermes Desktop install the Omarchy skin. It
# is exercised here with the package probe and the skin hook stubbed, so a
# migration that reached a Hermes Omarchy did not install, or that marked a
# failed hand-over done, shows up in what it ran.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
migration="$ROOT/migrations/1788619462.sh"
[[ -f $migration ]] || fail "Hermes skin migration exists"
[[ $(stat -c %a "$migration") == "644" ]] || fail "migration is a plain 0644 file"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-theme-set-hermes" <<'SH'
#!/bin/bash
echo "omarchy-theme-set-hermes $*" >>"$OMARCHY_TEST_CALLS"
[[ ${OMARCHY_TEST_HOOK_FAILS:-0} == 0 ]]
SH
chmod +x "$mock_bin"/*
run_migration() {
: >"$calls"
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_HOOK_FAILS="${OMARCHY_TEST_HOOK_FAILS:-0}" \
OMARCHY_TEST_CALLS="$calls" \
PATH="$mock_bin:$PATH" \
HOME="$test_tmp/home" \
OMARCHY_PATH="$ROOT" \
bash -euo pipefail "$migration" >/dev/null
}
OMARCHY_TEST_DESKTOP_INSTALLED=0 run_migration || fail "migration exits clean without Hermes Desktop"
[[ ! -s $calls ]] || fail "a machine without Hermes Desktop is left alone" "$(cat "$calls")"
pass "migration only applies where Omarchy installed Hermes Desktop"
run_migration || fail "migration exits clean with Hermes Desktop installed"
[[ $(cat "$calls") == "omarchy-theme-set-hermes --activate" ]] ||
fail "the skin is rendered, published and activated through the hook's deliberate form" "$(cat "$calls")"
pass "migration hands the skin over through the hook"
if OMARCHY_TEST_HOOK_FAILS=1 run_migration; then
fail "a hand-over that failed on Omarchy's side stays pending"
fi
pass "migration stays pending when the hand-over fails"
+401
View File
@@ -0,0 +1,401 @@
#!/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
# The hook runs the hermes the probe vets, ~/.local/bin/hermes, not one on PATH;
# reset_home installs this stub there and a decoy on PATH that must never run.
cat >"$mock_bin/hermes" <<'SH'
#!/bin/bash
echo "PATH hermes ran: $*" >>"$OMARCHY_TEST_HERMES_CALLS"
exit 1
SH
cat >"$mock_bin/hermes-stub" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HERMES_CALLS"
if [[ $1 == "config" && $2 == "get" ]]; then
[[ ${OMARCHY_TEST_HERMES_GET_FAILS:-0} == 0 ]] || exit 1
printf '%s\n' "${OMARCHY_TEST_HERMES_SKIN-default}"
fi
if [[ $1 == "config" && $2 == "set" ]]; then
[[ ${OMARCHY_TEST_HERMES_SET_FAILS:-0} == 0 ]] || exit 1
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"
if [[ $1 == 60 && -n ${OMARCHY_TEST_SWAP_SOURCE:-} ]]; then
printf '%s\n' "$OMARCHY_TEST_SWAP_SOURCE" >"$HOME/.local/state/omarchy/current/theme/hermes.yaml"
fi
if [[ $1 == 60 && ${OMARCHY_TEST_DROP_SKINS:-0} == 1 ]]; then
rm -f "$HOME/.hermes/skins/omarchy.yaml" "$HOME"/.hermes/profiles/*/skins/omarchy.yaml
fi
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" "$test_home/.local/bin" "$hermes_home/skills"
cp "$mock_bin/hermes-stub" "$test_home/.local/bin/hermes"
: >"$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_GET_FAILS="${OMARCHY_TEST_HERMES_GET_FAILS:-0}" \
OMARCHY_TEST_HERMES_SET_FAILS="${OMARCHY_TEST_HERMES_SET_FAILS:-0}" \
OMARCHY_TEST_HERMES_CALLS="$hermes_calls" \
OMARCHY_TEST_DESKTOP_INSTALLED="${OMARCHY_TEST_DESKTOP_INSTALLED:-1}" \
OMARCHY_TEST_SWAP_SOURCE="${OMARCHY_TEST_SWAP_SOURCE:-}" \
OMARCHY_TEST_DROP_SKINS="${OMARCHY_TEST_DROP_SKINS:-0}" \
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"
# mv would otherwise move the temp file inside a directory at the skin's path,
# leaving Hermes a directory to read and the temp file behind.
reset_home --set-up
mkdir -p "$skin"
if run_hook 2>/dev/null; then
fail "a directory at the skin's path is an error, not a place to put the skin"
fi
[[ -z $(ls -A "$skin") && $(ls "$hermes_home/skins") == "omarchy.yaml" ]] ||
fail "nothing is left inside or beside a directory at the skin's path" "$(ls -R "$hermes_home/skins")"
pass "a directory at the skin's path is refused cleanly"
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"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\rbanner_logo: "[link=file:///etc/passwd]x[/link]"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xe2\x80\xa8banner_logo: "evil"' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\n#\xc2\x85banner_logo: "evil"' \
$'name: omarchy\n background: "#1a1b26"\ncolors:' \
$'name: omarchy\ncolors:\n background: "#1a1b26"\ncolors:' \
$'colors:\n background: "#1a1b26"\nname: omarchy' \
$'name: omarchy\ncolors:'; 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, on the lines Hermes' YAML reader sees"
# A NUL cannot travel through a shell string, so it is written straight to the
# source; grep reads past one where YAML stops.
reset_home --set-up
printf 'name: omarchy\ncolors:\n background: "#1a1b26"\0\n' >"$test_home/.local/state/omarchy/current/theme/hermes.yaml"
run_hook 2>/dev/null
[[ ! -e $skin ]] || fail "a NUL byte in the skin is rejected"
pass "a skin carrying a NUL byte is not published"
reset_home --set-up $'# rendered by Omarchy\nname: omarchy\n\ndescription: Omarchy system theme\ncolors:\n background: "#1a1b26"\n'
run_hook 2>/dev/null
[[ -f $skin ]] || fail "comments and blank lines are allowed around the palette"
pass "a well-formed skin with comments and blank lines is published"
# -- 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"
[[ -f $skin ]] || fail "a chosen skin still gets the Omarchy skin published beside it"
[[ ! -s $hermes_calls ]] || fail "a theme switch does not start a Hermes whose config names 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"
# Hermes reads the config of the profile named in active_profile, so that is
# the config that says whether there is anything left to do.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: default\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile still on its default is switched even when the root config names the skin" "$(cat "$hermes_calls")"
pass "a theme switch follows the active Hermes profile"
reset_home --set-up
mkdir -p "$hermes_home/profiles/work"
printf 'display:\n skin: ares\n' >"$hermes_home/profiles/work/config.yaml"
echo work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
[[ ! -s $hermes_calls ]] || fail "a skin chosen in the active profile is not replaced" "$(cat "$hermes_calls")"
pass "a theme switch leaves a skin chosen in the active Hermes profile"
# Hermes selects a profile once its directory exists; without a config of its
# own it is on the default skin whatever the root config says.
reset_home --on omarchy
mkdir -p "$hermes_home/profiles/work"
echo Work >"$hermes_home/active_profile"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "an active profile without a config of its own is on the default and gets switched" "$(cat "$hermes_calls")"
pass "a theme switch follows an active profile that has no config yet"
# Only a plainly named skin ends a switch early; anything Hermes might read as
# its default is left for Hermes to answer.
for line in 'skin: "default"' 'skin: default # chosen long ago' 'skin: null' 'skin: false'; do
reset_home --set-up
printf 'display:\n %s\n' "$line" >"$hermes_home/config.yaml"
OMARCHY_TEST_HERMES_READY=1 run_hook
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "a config line Hermes reads as the default still gets the skin activated" "$line: $(cat "$hermes_calls")"
done
pass "a theme switch asks Hermes about any skin line that is not a plain name"
reset_home --set-up
mkdir -p "$hermes_home/profiles/broken"
: >"$hermes_home/profiles/broken/skins"
OMARCHY_TEST_HERMES_READY=1 run_hook 2>"$test_tmp/stderr" || fail "a profile that cannot take the skin does not fail the hook"
[[ ! -s $test_tmp/stderr ]] || fail "a theme switch stays quiet about a profile it could not reach" "$(cat "$test_tmp/stderr")"
[[ -f $skin ]] || fail "the Hermes home still gets the skin beside a broken profile"
grep -Fxq 'config set display.skin omarchy' "$hermes_calls" ||
fail "activation still happens beside a broken profile" "$(cat "$hermes_calls")"
pass "a profile that cannot take the skin costs nobody else"
# -- 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"
reset_home --set-up
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_GET_FAILS=1 run_hook --activate 2>"$test_tmp/stderr" || fail "a Hermes that does not answer is not an error"
! grep -q 'config set' "$hermes_calls" || fail "no answer from Hermes is not taken for the default" "$(cat "$hermes_calls")"
grep -q 'did not say' "$test_tmp/stderr" || fail "an unanswered question is reported"
pass "--activate does not switch a Hermes that did not say which skin it is on"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SET_FAILS=1 run_hook --wait 2>"$test_tmp/stderr" || fail "a Hermes that refuses the write is not an error"
grep -q 'refused' "$test_tmp/stderr" || fail "a refused write is reported"
! grep -q 'sleep 60' "$hermes_calls" || fail "nothing is announced for a write that did not happen" "$(cat "$hermes_calls")"
pass "--activate reports a Hermes that refused the skin and moves on"
# -- 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 --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_SWAP_SOURCE=$'name: omarchy\ncolors:\n background: "#1a1b26"\nbanner_logo: "evil"' run_hook --wait 2>/dev/null
! grep -q 'banner_logo' "$skin" || fail "the republish after the gateway is up does not publish a theme that changed underneath into something rejected"
pass "--wait checks the skin again before republishing it"
reset_home --set-up
mkdir -p "$hermes_home/hermes-agent" "$hermes_home/profiles/work"
touch "$hermes_home/hermes-agent/.hermes-bootstrap-complete"
OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_DROP_SKINS=1 run_hook --wait 2>/dev/null
[[ -f $skin && -f $hermes_home/profiles/work/skins/omarchy.yaml ]] ||
fail "the republish after the gateway is up writes the skin to the Hermes home and every profile again" "$(ls -R "$hermes_home")"
pass "--wait republishes the skin everywhere once the gateway is up"
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"
+1 -1
View File
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
# generates. Every generated theme file is either denied to an installed theme or
# recorded here as carrying colour, so a new template fails until it is placed.
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
for tpl in "$ROOT"/default/themed/*.tpl; do
generated=$(basename "$tpl" .tpl)