diff --git a/bin/omarchy-install-ai-hermes b/bin/omarchy-install-ai-hermes index 3f6abafe..55a3a271 100755 --- a/bin/omarchy-install-ai-hermes +++ b/bin/omarchy-install-ai-hermes @@ -21,6 +21,17 @@ 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 +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." diff --git a/bin/omarchy-theme-set b/bin/omarchy-theme-set index b0c1cda4..b6733803 100755 --- a/bin/omarchy-theme-set +++ b/bin/omarchy-theme-set @@ -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 diff --git a/bin/omarchy-theme-set-hermes b/bin/omarchy-theme-set-hermes new file mode 100755 index 00000000..a4663738 --- /dev/null +++ b/bin/omarchy-theme-set-hermes @@ -0,0 +1,199 @@ +#!/bin/bash + +# omarchy:summary=Sync the generated Omarchy theme to Hermes as a skin +# 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. + +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" +# 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. +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 + +# 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. +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. +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 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. +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 + +# 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. +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. +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" +} + +if ! skin_is_well_formed "$HERMES_SOURCE_PATH"; 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. +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" +} + +publish_skin "$HERMES_HOME/skins" + +# 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 + +# 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. +if (( HERMES_ACTIVATE == 0 )); then + omarchy-pkg-present hermes-desktop || exit 0 + grep -qx " skin: $HERMES_SKIN_NAME" "$HERMES_CONFIG_PATH" && exit 0 +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. +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) +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 +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. +if (( HERMES_WAIT == 1 )); then + sleep 60 + publish_skin "$HERMES_HOME/skins" +fi diff --git a/default/themed/hermes.yaml.tpl b/default/themed/hermes.yaml.tpl new file mode 100644 index 00000000..2e39710e --- /dev/null +++ b/default/themed/hermes.yaml.tpl @@ -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 }}" diff --git a/manual/17-ai.md b/manual/17-ai.md index 9987cb5b..90d3a10e 100644 --- a/manual/17-ai.md +++ b/manual/17-ai.md @@ -26,7 +26,7 @@ Pick your default agent with `omarchy default agent ` 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. diff --git a/migrations/1788619462.sh b/migrations/1788619462.sh new file mode 100644 index 00000000..56f6c39a --- /dev/null +++ b/migrations/1788619462.sh @@ -0,0 +1,12 @@ +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 + +# 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 diff --git a/test/shell.d/hermes-skin-migration-test.sh b/test/shell.d/hermes-skin-migration-test.sh new file mode 100644 index 00000000..9e27bf96 --- /dev/null +++ b/test/shell.d/hermes-skin-migration-test.sh @@ -0,0 +1,57 @@ +#!/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 let a +# Hermes refusing the write hold up later migrations, 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 +[[ ${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" + +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" diff --git a/test/shell.d/hermes-theme-test.sh b/test/shell.d/hermes-theme-test.sh new file mode 100644 index 00000000..6a68cb70 --- /dev/null +++ b/test/shell.d/hermes-theme-test.sh @@ -0,0 +1,267 @@ +#!/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" diff --git a/test/shell.d/theme-staging-test.sh b/test/shell.d/theme-staging-test.sh index cc749a9e..9ec3b0f9 100755 --- a/test/shell.d/theme-staging-test.sh +++ b/test/shell.d/theme-staging-test.sh @@ -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)