Harden the Hermes skin hand-over

Hermes' YAML reader breaks lines on carriage return, NEL and the Unicode line and paragraph separators, and stops at NUL, none of which grep treats as a line end, so a comment line carrying one could put a root-level key such as banner_logo past the validator and into Rich markup on Hermes' terminal surfaces. The lines grep accepted also did not add up to the YAML Hermes needs: a colour before colors:, a second colors:, or a key over YAML's simple-key limit all passed and loaded as no palette at all, which Hermes shows as its default. The validator now counts every byte outside printable ASCII first, then walks the file in order: the name, at most one plain description, colors:, and only #rrggbb colour lines after it.

omarchy-theme-set releases its lock before the hooks run, so the rendered skin can change under this one between the check and the copy. The check is made on a private copy and that copy is what gets published, both on the first pass and on the republish a minute after activation, which used to copy whatever the theme had become by then, unchecked.

A theme switch reads the config of the profile named in active_profile, which is the one Hermes reads, and a profile exists to Hermes once its directory does, with or without a config; it ends early only for a config plainly naming another skin, since only the default is ever replaced, and leaves anything Hermes might read as the default for Hermes to answer. Hermes is run by the path the readiness probe vets, ~/.local/bin/hermes, bounded the way the probe bounds it; an answer that did not come is not taken for the default, and a write Hermes refuses is reported rather than failed, being cosmetic.

A profile that cannot take the skin no longer costs the others or the activation; a directory at the skin's path is an error rather than a place mv puts the temp file; a temp file the copy could not fill is removed. Remove stops the unit the installer left waiting, so a removal within the waiter's half hour does not hand the theme to a Hermes installed some other way or recreate the skin under a home the user asked to delete. The migration no longer swallows the hook's exit: what is not ready or refused is reported and done with inside the hook, so only Omarchy's own failures return, and those keep the migration pending as the guide requires.

Comments are cut to what the code cannot say; the reasoning is here.

Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
Spencer BullandCodex XHigh committed 2026-09-05 23:28:46 -05:00
1 parent 6baae0f556
commit 8569d1cadc
7 files changed
+280 -102

No files matched your search

+2 -7
View File
@@ -21,13 +21,8 @@ omarchy-install-hermes-cli || true
echo "Opening Hermes Desktop..."
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
# The app builds its runtime on first launch, which takes minutes, and only a
# running Hermes can be told which skin to show. So the theme is handed over
# after the launch above, by a helper that waits for that runtime and then
# names the Omarchy skin through Hermes itself; the running app repaints. Only
# Hermes' own default is replaced, so a skin chosen in Hermes is never undone.
# It runs as its own unit, so this terminal closing cannot take it along and
# what it did is in the journal: journalctl --user -u omarchy-hermes-theme
# 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
+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
+112 -78
View File
@@ -4,21 +4,8 @@
# omarchy:args=[--activate] [--wait]
# omarchy:hidden=true
# Hermes has one theme unit for all of its surfaces, the skin: a YAML file in
# ~/.hermes/skins/ that the desktop app, the TUI and the CLI each turn into
# their own palette. The active one is named by display.skin in Hermes' config,
# and the gateway watches that file's mtime, so a rewrite of the active skin
# repaints every connected surface within a second -- no Omarchy code in
# Hermes, and nothing here that Hermes did not already offer any other system.
#
# Every run publishes the skin file, and for the desktop app Omarchy installed
# names it in the config through Hermes' own config writer if Hermes is still
# on its default skin, so a theme switch also picks up an install whose
# hand-over was missed. --activate is the deliberate form, for the installer
# and the migration: it renders a skin the current theme does not have yet and
# says why nothing happened when it cannot finish. --wait holds the whole thing
# until the desktop app has finished building the runtime its first launch
# provisions.
# 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
@@ -26,11 +13,10 @@ 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"
# The filename is the skin name Hermes shows and display.skin refers to, and it
# stays put while the colors change underneath on every theme switch.
HERMES_SKIN_NAME="omarchy"
# The app writes this when the runtime it provisions has landed; the CLI
# installer's readiness probe passes only once it has.
# 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
@@ -60,17 +46,14 @@ for arg in "$@"; do
esac
done
# Only the deliberate form explains itself: the theme switch runs this beside
# a dozen other hooks, where a note about Hermes on every switch is noise.
# A theme switch runs this beside a dozen other hooks; only --activate explains.
note() {
if (( HERMES_ACTIVATE == 1 )); then
echo "$*" >&2
fi
}
# The skin is generated with the rest of the theme, so a theme applied before
# the template existed has none yet. Asked to activate, re-stage the current
# theme to render it; a theme switch simply has nothing to publish.
# A theme applied before the template existed has no skin rendered yet.
if [[ ! -f $HERMES_SOURCE_PATH ]]; then
(( HERMES_ACTIVATE == 1 )) || exit 0
@@ -88,9 +71,8 @@ if [[ ! -f $HERMES_SOURCE_PATH ]]; then
fi
fi
# The desktop app builds its runtime on first launch, which takes minutes and
# may be abandoned. Poll for the marker rather than the readiness probe, which
# starts Hermes to answer, and give up quietly on a launch that never came.
# 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
@@ -104,96 +86,148 @@ if (( HERMES_WAIT == 1 )); then
done
fi
# Only follow a Hermes that has been set up. Its config is what Hermes writes
# when it has; ~/.hermes itself proves nothing, because provisioning creates it
# on every machine to hold the Omarchy skill.
# 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 the strings to every surface, so the
# generated file is held to the shape the template promises before it is
# published: the name Hermes loads it by, a plain description, and colors that
# are all #rrggbb. A theme missing a palette key leaves its placeholder
# unresolved, and a theme cloned from a git repo may ship its own hermes.yaml;
# neither gets to write anything else into a file another program reads. The
# previous skin stays in place instead.
# 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"
grep -qx "name: $HERMES_SKIN_NAME" "$skin" &&
grep -qx 'colors:' "$skin" &&
! grep -qvE "^(#.*|[[:space:]]*|name: $HERMES_SKIN_NAME|description: [A-Za-z0-9 ,.()-]*|colors:| [a-z_]+: \"#[0-9a-fA-F]{6}\")$" "$skin"
[[ -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"
}
if ! skin_is_well_formed "$HERMES_SOURCE_PATH"; then
# 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 watches the skin file's mtime and reads it whole on change, so
# the write has to be atomic: a half-written file would read as no skin at all.
# 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"
tmp=$(mktemp "$skins_dir/$HERMES_SKIN_NAME.yaml.XXXXXX")
cp "$HERMES_SOURCE_PATH" "$tmp"
mv "$tmp" "$skins_dir/$HERMES_SKIN_NAME.yaml"
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
}
publish_skin "$HERMES_HOME/skins"
# 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
# A Hermes profile is a home of its own, with its own skins and config, and the
# gateway watches whichever profile is active. Existing profiles get the skin
# too; none are created here.
for profile in "$HERMES_HOME"/profiles/*/; do
[[ -d $profile ]] || continue
publish_skin "${profile%/}/skins"
done
publish_skin "$HERMES_HOME/skins" || {
echo "Could not publish the Hermes skin to $HERMES_HOME/skins." >&2
return 1
}
# A theme switch finishes the hand-over only for the app Omarchy installed
# under Install > AI; a Hermes set up some other way keeps whatever skin it
# has unless --activate asks for it by name. Hermes writes the active skin
# under display: at that indent, and once it names ours a theme switch is done
# here without starting Hermes; anything else is for Hermes itself to answer
# below, so the grep is only ever a shortcut.
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
grep -qx " skin: $HERMES_SKIN_NAME" "$HERMES_CONFIG_PATH" && 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
# Naming the skin in the config is Hermes' own job: `hermes config set` is what
# `hermes skin use` runs, it writes the active profile's config, and it touches
# the skin file so a running gateway broadcasts the change and every surface
# repaints at once. Asked through the installer's readiness probe rather than by
# running whatever `hermes` is on PATH, because Omarchy's cold stub installs
# Hermes when run, and that takes minutes.
# 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
# A skin the user picked in Hermes stays theirs. Only Hermes' own default is
# replaced, so a reinstall, a migration or a theme switch never undoes a choice.
current_skin=$(hermes config get display.skin 2>/dev/null || true)
# 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 config set display.skin "$HERMES_SKIN_NAME" >/dev/null
# 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 app adopts a skin from a change broadcast, not from the config it
# finds at connect time, and the gateway seeds its change watcher when it starts.
# A first launch brings the gateway up moments after the marker, so a config
# written in that gap goes unannounced; republishing once the gateway is surely
# up moves the file's mtime again, and that broadcast the desktop does apply.
# 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
publish_skin "$HERMES_HOME/skins"
if take_snapshot; then
publish_skin_everywhere
fi
fi
+3 -6
View File
@@ -4,9 +4,6 @@ echo "Hand Hermes Desktop the Omarchy theme as a skin"
# A Hermes the user set up some other way keeps whatever skin they chose.
omarchy-pkg-present hermes-desktop || exit 0
# Renders the skin for a theme applied before the template existed, publishes
# it, and names it in Hermes' config the same way a fresh install does: through
# Hermes itself, and only when Hermes is still on its default skin, so a skin
# the user chose stays. A Hermes that is not ready is told and skipped; a Hermes
# that refuses the write is cosmetic and must not hold up later migrations.
omarchy-theme-set-hermes --activate || true
# 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"
+7 -5
View File
@@ -4,8 +4,8 @@ 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 let a
# Hermes refusing the write hold up later migrations, shows up in what it ran.
# 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"
@@ -22,7 +22,7 @@ mkdir -p "$mock_bin"
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
[[ $1 == "hermes-desktop" && ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
SH
cat >"$mock_bin/omarchy-theme-set-hermes" <<'SH'
@@ -53,5 +53,7 @@ run_migration || fail "migration exits clean with Hermes Desktop installed"
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"
OMARCHY_TEST_HOOK_FAILS=1 run_migration || fail "a skin the hook cannot hand over does not hold up later migrations"
pass "migration tolerates a skin the hook could not activate"
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"
+140 -6
View File
@@ -30,11 +30,23 @@ cat >"$mock_bin/omarchy-pkg-present" <<'SH'
[[ $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
printf '%s\n' "${OMARCHY_TEST_HERMES_SKIN:-default}"
[[ ${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
@@ -51,6 +63,12 @@ SH
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"/*
@@ -75,7 +93,8 @@ reset_home() {
local source="$good_skin"
rm -rf "$test_home"
mkdir -p "$test_home/.local/state/omarchy/current/theme" "$hermes_home/skills"
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
@@ -92,9 +111,13 @@ reset_home() {
run_hook() {
OMARCHY_TEST_HERMES_READY="${OMARCHY_TEST_HERMES_READY:-0}" \
OMARCHY_TEST_HERMES_SKIN="${OMARCHY_TEST_HERMES_SKIN:-default}" \
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='' \
@@ -132,17 +155,48 @@ grep -q '#000000' "$skin" || fail "an unresolved placeholder keeps the previous
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"'; do
$'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"
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 ---------------------------------
@@ -173,10 +227,61 @@ 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")"
[[ -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
@@ -221,6 +326,20 @@ OMARCHY_TEST_HERMES_READY=1 OMARCHY_TEST_HERMES_SKIN=ares run_hook --activate 2>
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
@@ -258,6 +377,21 @@ OMARCHY_TEST_HERMES_READY=1 run_hook --wait 2>/dev/null
[[ -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 ]] ||