From 531c3e890ff0b3c062fa046c8115f72a9122bf09 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Wed, 16 Sep 2026 13:09:54 -0500 Subject: [PATCH 1/5] Install Elsewhen, the world clock plugin, by default Elsewhen (omacom.elsewhen) arrives as the elsewhen package under /usr/share/omarchy/plugins, the packaged root the shell scans between its bundled plugins and the user's. It opens the right section of the default bar, just before the tray, and a migration installs the package, writes the widget into a customized shell.json in the same spot, and retires a pristine pre-package clone of the upstream repo that the package now shadows. --- config/omarchy/shell.json | 3 + default/agents/skills/omarchy/plugins.md | 5 + docs/file-layout.md | 3 + docs/omarchy-shell.md | 13 + install/omarchy-base.packages | 1 + migrations/1789581661.sh | 85 ++++++ shell/README.md | 14 + shell/plugins/README.md | 6 +- test/shell.d/config-test.sh | 5 +- .../elsewhen-default-migration-test.sh | 285 ++++++++++++++++++ 10 files changed, 417 insertions(+), 3 deletions(-) create mode 100644 migrations/1789581661.sh create mode 100644 test/shell.d/elsewhen-default-migration-test.sh diff --git a/config/omarchy/shell.json b/config/omarchy/shell.json index e6202da8..64cc4075 100644 --- a/config/omarchy/shell.json +++ b/config/omarchy/shell.json @@ -38,6 +38,9 @@ } ], "right": [ + { + "id": "omacom.elsewhen" + }, { "id": "omarchy.tray" }, diff --git a/default/agents/skills/omarchy/plugins.md b/default/agents/skills/omarchy/plugins.md index 6e3ab069..bf1033b3 100644 --- a/default/agents/skills/omarchy/plugins.md +++ b/default/agents/skills/omarchy/plugins.md @@ -9,9 +9,14 @@ inside a single long-running Quickshell process (`omarchy-shell`). ``` ~/.config/omarchy/shell.json # User overrides: bar, plugins, idle ~/.config/omarchy/plugins// # User-owned shell plugins +/usr/share/omarchy/plugins// # Packaged plugins (pacman), e.g. omacom.elsewhen $OMARCHY_PATH/config/omarchy/shell.json # Canonical defaults ``` +Packaged plugins such as Elsewhen (`elsewhen` package) update with `omarchy update` and are removed with pacman; +`omarchy plugin update` and `omarchy plugin remove` refuse them and name the +package. + The shell hot-reloads `shell.json` on save — no restart needed for layout changes. `idle.screensaver` and `idle.lock` are seconds since user idle began. diff --git a/docs/file-layout.md b/docs/file-layout.md index f261085f..8bbcd5b2 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -78,6 +78,9 @@ install/** ──► omarchy /usr/share/omarchy migrations/** ──► omarchy /usr/share/omarchy/migrations/ themes/** ──► omarchy /usr/share/omarchy/themes/ shell/** ──► omarchy /usr/share/omarchy/shell/ + (packaged plugins from their own + packages, e.g. elsewhen, land beside it + in /usr/share/omarchy/plugins//) version ──► omarchy /usr/share/omarchy/version + /etc/skel/.local/state/omarchy/migrations/* diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index d62ff743..18a61896 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -88,6 +88,19 @@ bar from the CLI — `use | reset | defaults | position | transparent | put | move | set`, with placement flags such as `--section` and `--index`. The lower-level IPC methods remain available through `omarchy-shell shell ...`. +## Packaged plugins + +A default plugin can ship as its own Arch package instead of in the Omarchy +checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs them at +`/usr/share/omarchy/plugins//`, a root the shell scans between +`$OMARCHY_PATH/shell/plugins` and `~/.config/omarchy/plugins`. A packaged +`omarchy.*` id is trusted like a bundled plugin and loads by default; any other +packaged id behaves like an installed plugin. Precedence is bundled, then +packaged, then user, so a checkout under `omarchy dev link` overrides the +package. Packaged plugins update through `omarchy update` and leave through +pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and +name the package, and Setup › Plugins › Remove does not list them. + ## IPC The shell exposes a `shell` target (the host also registers diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 76e74110..0a8e27cc 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -26,6 +26,7 @@ docker-compose dosfstools dotnet-runtime dua-cli +elsewhen evince exfatprogs expac diff --git a/migrations/1789581661.sh b/migrations/1789581661.sh new file mode 100644 index 00000000..fd5d30be --- /dev/null +++ b/migrations/1789581661.sh @@ -0,0 +1,85 @@ +echo "Install Elsewhen, the world clock plugin" + +# Elsewhen ships as the elsewhen package rather than in this checkout; pacman +# puts it under /usr/share/omarchy/plugins, where the shell looks next to its +# bundled plugins. A package the mirror does not carry yet fails the migration +# on purpose: nothing before this line has changed anything, and the runner +# retries it on the next update or login rather than quietly leaving the +# machine without the plugin. +omarchy-pkg-add elsewhen + +# A shell that already knows the packaged root (dev-linked, or restarted +# since) picks the plugin up here; the one an update runs under predates it +# and is replaced by omarchy-update-restart right after the migrations. +omarchy-shell -q shell rescanPlugins || true + +# Before the package, the README had users run +# `omarchy plugin add https://github.com/omacom/elsewhen.git`, which left a +# git clone under ~/.config/omarchy/plugins. The packaged copy now shadows +# it: the shell logs a rejection on every scan, and `omarchy plugin update` +# keeps pulling a checkout nothing loads. A pristine clone of the upstream +# repo is retired the way `omarchy plugin remove` retires one (the repo +# remains upstream). Anything else -- a symlink, a plain directory, local +# changes, a fork, a checkout git cannot read -- is the user's and stays. This +# runs only once the package is in place, so the plugin never leaves a +# machine; its id stays in shell.json either way, which is what keeps it +# enabled. The shell an update runs under predates the packaged root, and it +# watches this directory: dropping the checkout empties the widget's slot in +# that shell until omarchy-update-restart replaces it a few steps later with +# one that finds the package. A one-time gap of a few minutes, during the +# update itself. +upstream_checkout() { + local dir="$1" origin status + [[ -d $dir && ! -L $dir && -d $dir/.git ]] || return 1 + origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 1 + origin=${origin,,} + origin=${origin%/} + origin=${origin%.git} + [[ $origin =~ ^([a-z+]+://)?([^/@]+@)?github\.com[/:]omacom/elsewhen$ ]] || return 1 + status=$(git -C "$dir" status --porcelain 2>/dev/null) || return 1 + [[ -z $status ]] +} + +checkout="$HOME/.config/omarchy/plugins/omacom.elsewhen" +if [[ -e $checkout || -L $checkout ]]; then + if upstream_checkout "$checkout"; then + rm -rf "$checkout" + echo "Retired the omacom.elsewhen checkout at $checkout in favour of the elsewhen package." + else + echo "The elsewhen package takes precedence over $checkout, which is left as it is." + fi +fi + +# The widget opens the right section of the default bar, just before the +# tray, which a machine without a shell.json takes on as soon as the shell +# restarts. A customized bar gets the entry written into its file rather than +# placed over IPC: the shell this runs under cannot see a widget its scan +# never reached and would refuse it, whereas the file it hot-reloads carries +# the entry through to the restart. A bar that already carries the widget +# keeps it where the user put it. +source omarchy-shell-config + +[[ -s $CONFIG_FILE ]] || exit 0 +# A file the shell cannot read is left for its owner to repair. +jq empty "$CONFIG_FILE" 2>/dev/null || exit 0 + +entry_id='def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;' + +if jq -e "$entry_id"' + any((.bar.layout // {} | .left, .center, .right | arrays)[]; entry_id == "omacom.elsewhen") +' "$CONFIG_FILE" >/dev/null; then + exit 0 +fi + +commit "$NORMALIZE | $entry_id"' + def ids: map(entry_id); + def insert_at($section; $index): + .bar.layout[$section] = .bar.layout[$section][:$index] + [{id: "omacom.elsewhen"}] + .bar.layout[$section][$index:]; + . as $config + | (["left", "center", "right"] | map(select($config.bar.layout[.] | ids | index("omarchy.tray") != null)) | first) as $section + | if $section != null then + insert_at($section; .bar.layout[$section] | ids | index("omarchy.tray")) + else + insert_at("right"; 0) + end +' diff --git a/shell/README.md b/shell/README.md index ca07b520..23a25ca6 100644 --- a/shell/README.md +++ b/shell/README.md @@ -140,6 +140,19 @@ You can still drop a plugin in without git: `barWidget.defaultSection`, or in the center when it is omitted, and can be moved with `omarchy bar move`; a full bar replaces the one in use. +### Packaged plugins + +Some default plugins ship as their own Arch package rather than in this +checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs such a plugin, +root-owned, at `/usr/share/omarchy/plugins//`, and +the shell scans that root between its bundled plugins and the user's. A +packaged `omarchy.*` id is trusted exactly like a bundled one; a packaged +plugin under any other id is treated like a user plugin. They update with +`omarchy update` and are removed with pacman, so `omarchy plugin update` and +`omarchy plugin remove` refuse them and name the package instead. A bundled +copy in `$OMARCHY_PATH/shell/plugins` wins over the packaged one, which is how +a packaged plugin is developed under `omarchy dev link`. + The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`, `omarchy-shell shell enablePlugin '{}'`, and `omarchy-shell shell listPlugins`. The `omarchy plugin` commands wrap those calls. `omarchy bar move` and @@ -224,6 +237,7 @@ customization from the shipped defaults lives in it. |-----------------------------------|----------------|--------------------------------------------------------| | `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list | | `~/.config/omarchy/plugins//` | user | drop-in third-party plugin source files | +| `/usr/share/omarchy/plugins//` | pacman | packaged plugins (e.g. `elsewhen`), updated by `omarchy update` | The `config/omarchy/shell.json` default config describes the fresh-install state. When the user has no `shell.json`, the shell uses diff --git a/shell/plugins/README.md b/shell/plugins/README.md index 66cb74e1..1a2d26fb 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -9,7 +9,10 @@ First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`; at startup; other panels, overlays, and menus are loaded on demand. User-installed plugins live alongside these conceptually but on disk under -`~/.config/omarchy/plugins//` rather than in this directory. +`~/.config/omarchy/plugins//` rather than in this directory. A few +default plugins arrive as their own package instead: pacman installs them at +`/usr/share/omarchy/plugins//`, where an `omarchy.*` id is trusted +like the plugins here, and a copy in this directory shadows the packaged one. | Plugin | id | kinds | entry point | |---------------|---------------------------|-------------------------|---------------------------------------| @@ -28,6 +31,7 @@ User-installed plugins live alongside these conceptually but on disk under | Power | `omarchy.power` | `bar-widget` | `panels/power/Panel.qml` | | Tailscale | `omarchy.tailscale` | `bar-widget` | `panels/tailscale/Panel.qml` | | Agents | `omarchy.agents` | `bar-widget` | `agents/Panel.qml` | +| Elsewhen | `omacom.elsewhen` | `bar-widget` | packaged: `elsewhen` installs `/usr/share/omarchy/plugins/omacom.elsewhen/` | | Weather | `omarchy.weather` | `bar-widget` | `panels/weather/BarWidget.qml` | | Media | `omarchy.media` | `service`, `bar-widget` | `services/media/Service.qml`, `services/media/BarWidget.qml` | | Battery | `omarchy.battery` | `service` | `services/battery/Service.qml` | diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 355ca26b..d9086d44 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -41,11 +41,12 @@ pass "default clock date format has no leading zero" jq -e ' def ids: map(.id // .); (.bar.layout.right | ids) as $ids | + ($ids | index("omacom.elsewhen")) as $elsewhen | ($ids | index("omarchy.tray")) as $tray | ($ids | index("omarchy.agents")) as $agents | - $tray != null and $agents == $tray + 1 + $elsewhen == 0 and $tray == 1 and $agents == $tray + 1 ' "$ROOT/config/omarchy/shell.json" >/dev/null -pass "default right layout keeps agents next to the tray" +pass "default right layout opens with elsewhen and the tray, then agents" ROOT="$ROOT" python3 <<'PY' import json diff --git a/test/shell.d/elsewhen-default-migration-test.sh b/test/shell.d/elsewhen-default-migration-test.sh new file mode 100644 index 00000000..438ba1d3 --- /dev/null +++ b/test/shell.d/elsewhen-default-migration-test.sh @@ -0,0 +1,285 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +require_command jq +require_command git + +migration="$ROOT/migrations/1789581661.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +mkdir -p "$test_dir/bin" +export INSTALLED_PACKAGES="$test_dir/installed" CALL_LOG="$test_dir/calls" SHELL_CALLS="$test_dir/shell-calls" +# The checkout cases use the real git; keep the developer's config (signing, +# hooks, safe.directory) out of what it sees. +export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 +export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.com +export GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.com + +# Keep the real package helpers, but contain every pacman transaction here. +cat >"$test_dir/bin/pacman" <<'SH' +#!/bin/bash +case "$1" in + -Q) grep -Fxq -- "$2" "$INSTALLED_PACKAGES" ;; + -S) + [[ ${FAIL_INSTALL:-0} == 0 ]] || exit 1 + shift 3 # -S --noconfirm --needed + printf '%s\n' "$@" >>"$INSTALLED_PACKAGES" + printf '%s\n' "$@" >>"$CALL_LOG" + ;; + *) exit 1 ;; +esac +SH +cat >"$test_dir/bin/sudo" <<'SH' +#!/bin/bash +[[ $1 == "pacman" ]] || exit 1 +"$@" +SH +# The migration runs under a shell that predates the packaged root, or under +# none at all, so it must only ever ask the shell for best-effort refreshes. +cat >"$test_dir/bin/omarchy-shell" <<'SH' +#!/bin/bash +printf '%s\n' "$*" >>"$SHELL_CALLS" +[[ $1 != "-q" ]] || exit 0 +exit "${SHELL_STATUS:-0}" +SH +chmod +x "$test_dir/bin/"* + +home="$test_dir/home" +config="$home/.config/omarchy/shell.json" +checkout="$home/.config/omarchy/plugins/omacom.elsewhen" +output="$test_dir/output" +mkdir -p "$home/.config/omarchy" + +run_migration() { + : >"$CALL_LOG" + : >"$SHELL_CALLS" + HOME="$home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \ + bash -euo pipefail "$migration" >"$output" +} + +ids() { + jq -c ".bar.layout.$1 | map(if type == \"object\" then .id else . end)" "$config" +} + +# ------------------------------------------------------------ package install +: >"$INSTALLED_PACKAGES" +cat >"$config" <<'JSON' +{ + "version": 1, + "bar": { + "layout": { + "left": [{ "id": "omarchy.menu" }], + "center": [{ "id": "omarchy.clock", "format": "HH:mm" }], + "right": [ + { "id": "omarchy.tray" }, + { "id": "omarchy.agents", "syncMode": "On" }, + { "id": "omarchy.power" } + ] + } + }, + "plugins": [] +} +JSON + +run_migration +grep -Fxq elsewhen "$CALL_LOG" || fail "migration installs the elsewhen package" "$(cat "$CALL_LOG")" +pass "migration installs the elsewhen package" + +[[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.agents","omarchy.power"]' ]] || + fail "migration puts the widget just before the tray" "$(cat "$config")" +pass "migration puts the widget just before the tray" + +[[ $(jq -c '.bar.layout.right[2]' "$config") == '{"id":"omarchy.agents","syncMode":"On"}' ]] || + fail "migration keeps the settings of its neighbours" "$(cat "$config")" +[[ $(jq -c '.bar.layout.center[0]' "$config") == '{"format":"HH:mm","id":"omarchy.clock"}' ]] || + fail "migration leaves other sections alone" "$(cat "$config")" +pass "migration leaves the rest of the layout alone" + +grep -q 'shell rescanPlugins' "$SHELL_CALLS" && grep -q 'shell reloadConfig' "$SHELL_CALLS" || + fail "migration asks the running shell to rescan and reload" "$(cat "$SHELL_CALLS")" +pass "migration asks the running shell to rescan and reload" + +before=$(sha256sum "$config") +run_migration +[[ $before == $(sha256sum "$config") ]] || fail "migration is idempotent" "$(cat "$config")" +[[ ! -s $CALL_LOG ]] || fail "migration does not reinstall a present package" "$(cat "$CALL_LOG")" +pass "migration is idempotent" + +# ------------------------------------------------------------- placements +cat >"$config" <<'JSON' +{ + "version": 1, + "bar": { "layout": { "left": [], "center": ["omarchy.clock", "omarchy.tray"], "right": ["omarchy.power"] } } +} +JSON +run_migration +[[ $(ids center) == '["omarchy.clock","omacom.elsewhen","omarchy.tray"]' && $(ids right) == '["omarchy.power"]' ]] || + fail "migration follows the tray into another section and reads string entries" "$(cat "$config")" +pass "migration follows the tray into another section and reads string entries" + +cat >"$config" <<'JSON' +{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.agents" }, { "id": "omarchy.power" }] } } } +JSON +run_migration +[[ $(ids right) == '["omacom.elsewhen","omarchy.agents","omarchy.power"]' ]] || + fail "migration prepends to the right section when the tray is off the bar" "$(cat "$config")" +pass "migration prepends to the right section when the tray is off the bar" + +cat >"$config" <<'JSON' +{ + "version": 1, + "bar": { "layout": { "left": [{ "id": "omacom.elsewhen" }], "center": [], "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } +} +JSON +before=$(sha256sum "$config") +run_migration +[[ $before == $(sha256sum "$config") ]] || + fail "migration leaves a widget the user already placed where it is" "$(cat "$config")" +pass "migration leaves a widget the user already placed where it is" + +# ------------------------------------------------------------- edge cases +rm -f "$config" +run_migration +[[ ! -e $config ]] || fail "migration writes no shell.json where the defaults apply" "$(cat "$config")" +pass "migration writes no shell.json where the defaults apply" + +printf '{ not json' >"$config" +run_migration +[[ $(cat "$config") == '{ not json' ]] || fail "migration leaves an unparsable config untouched" "$(cat "$config")" +pass "migration leaves an unparsable config untouched" + +cat >"$config" <<'JSON' +{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } } +JSON +SHELL_STATUS=1 run_migration +[[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.agents"]' ]] || + fail "migration places the widget with no shell running" "$(cat "$config")" +pass "migration places the widget with no shell running" + +# A package the mirror does not carry yet leaves the migration pending rather +# than half-done: the layout must not name a widget nothing can install. +: >"$INSTALLED_PACKAGES" +cat >"$config" <<'JSON' +{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } } +JSON +before=$(sha256sum "$config") +if FAIL_INSTALL=1 run_migration 2>/dev/null; then + fail "a failed package installation must leave the migration pending" +fi +[[ $before == $(sha256sum "$config") ]] || + fail "a failed package installation leaves the layout untouched" "$(cat "$config")" +pass "a failed package installation is propagated and changes nothing" + +# ------------------------------------------------- pre-package checkouts +# Users who followed the README before the package existed hold a git clone +# under ~/.config/omarchy/plugins, which the packaged copy now shadows. Only a +# clone of the upstream repo with nothing local in it is retired; everything +# else is the user's and stays, and shell.json gets the widget either way. + +# A clone made by `omarchy plugin add`: one commit, the given origin, nothing +# local. Built with init rather than clone so the origin can be any URL form. +make_checkout() { + local origin="$1" + rm -rf "$checkout" + mkdir -p "$checkout" + git -C "$checkout" init -q + git -C "$checkout" remote add origin "$origin" + printf '{ "id": "omacom.elsewhen" }\n' >"$checkout/manifest.json" + printf 'import QtQuick\nItem {}\n' >"$checkout/Panel.qml" + git -C "$checkout" add -A + git -C "$checkout" commit -q -m "Elsewhen" +} + +reset_layout() { + cat >"$config" <<'JSON' +{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.power" }] } } } +JSON +} + +assert_widget_placed() { + [[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.power"]' ]] || + fail "$1 still gets the widget" "$(cat "$config")" +} + +assert_retired() { + local description="$1" + [[ ! -e $checkout && ! -L $checkout ]] || fail "$description is retired" "$(ls -la "$checkout")" + grep -q 'Retired the omacom.elsewhen checkout' "$output" || fail "$description is reported as retired" "$(cat "$output")" + assert_widget_placed "$description" + pass "$description is retired" +} + +assert_kept() { + local description="$1" + [[ -e $checkout || -L $checkout ]] || fail "$description is kept" + grep -q 'takes precedence over' "$output" || fail "$description is reported as shadowed" "$(cat "$output")" + assert_widget_placed "$description" + pass "$description is kept" +} + +for origin in \ + https://github.com/omacom/elsewhen.git \ + https://github.com/omacom/elsewhen \ + git@github.com:omacom/elsewhen.git \ + ssh://git@github.com/omacom/elsewhen.git \ + https://GitHub.com/Omacom/Elsewhen/; do + make_checkout "$origin" + reset_layout + run_migration + assert_retired "a pristine clone with origin $origin" +done + +make_checkout https://github.com/omacom/elsewhen.git +printf 'import QtQuick\nItem { id: mine }\n' >"$checkout/Panel.qml" +reset_layout +run_migration +assert_kept "a clone with a modified tracked file" +[[ $(cat "$checkout/Panel.qml") == $'import QtQuick\nItem { id: mine }' ]] || + fail "a kept clone keeps its local change" "$(cat "$checkout/Panel.qml")" +pass "a kept clone keeps its local change" + +make_checkout https://github.com/omacom/elsewhen.git +printf 'notes\n' >"$checkout/NOTES.md" +reset_layout +run_migration +assert_kept "a clone with an untracked file" + +make_checkout https://github.com/someone/elsewhen-fork.git +reset_layout +run_migration +assert_kept "a clone of another repository" + +make_checkout https://github.com/omacom/elsewhen.git +git -C "$checkout" remote remove origin +reset_layout +run_migration +assert_kept "a clone without an origin remote" + +rm -rf "$checkout" +mkdir -p "$checkout" +printf '{ "id": "omacom.elsewhen" }\n' >"$checkout/manifest.json" +reset_layout +run_migration +assert_kept "a plain directory without .git" + +rm -rf "$checkout" +make_checkout https://github.com/omacom/elsewhen.git +mv "$checkout" "$test_dir/elsewhen-src" +ln -s "$test_dir/elsewhen-src" "$checkout" +reset_layout +run_migration +assert_kept "a symlinked checkout" +[[ -L $checkout && -f $test_dir/elsewhen-src/manifest.json ]] || + fail "a symlinked checkout stays a symlink with its target intact" "$(ls -la "$checkout" "$test_dir/elsewhen-src")" +pass "a symlinked checkout stays a symlink with its target intact" + +rm -f "$checkout" +reset_layout +run_migration +grep -q 'omacom.elsewhen checkout\|takes precedence' "$output" && fail "no checkout means no checkout report" "$(cat "$output")" +assert_widget_placed "a machine without a checkout" +pass "a machine without a checkout is left alone" From 49286d0e66b22d6b91e21c0f8c8fe8bd1a6ce064 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 17 Sep 2026 21:07:08 -0500 Subject: [PATCH 2/5] Place Elsewhen immediately before the center clock --- config/omarchy/shell.json | 6 +-- default/agents/skills/omarchy/plugins.md | 4 +- docs/omarchy-shell.md | 11 +----- migrations/1789581661.sh | 10 ++--- shell/README.md | 11 +----- test/shell.d/config-test.sh | 14 +++++-- .../elsewhen-default-migration-test.sh | 37 ++++++++++++------- 7 files changed, 45 insertions(+), 48 deletions(-) diff --git a/config/omarchy/shell.json b/config/omarchy/shell.json index 64cc4075..b8aa5de4 100644 --- a/config/omarchy/shell.json +++ b/config/omarchy/shell.json @@ -21,6 +21,9 @@ { "id": "omarchy.indicators" }, + { + "id": "omacom.elsewhen" + }, { "id": "omarchy.clock", "format": "dddd HH:mm", @@ -38,9 +41,6 @@ } ], "right": [ - { - "id": "omacom.elsewhen" - }, { "id": "omarchy.tray" }, diff --git a/default/agents/skills/omarchy/plugins.md b/default/agents/skills/omarchy/plugins.md index bf1033b3..1c58c83a 100644 --- a/default/agents/skills/omarchy/plugins.md +++ b/default/agents/skills/omarchy/plugins.md @@ -13,9 +13,7 @@ inside a single long-running Quickshell process (`omarchy-shell`). $OMARCHY_PATH/config/omarchy/shell.json # Canonical defaults ``` -Packaged plugins such as Elsewhen (`elsewhen` package) update with `omarchy update` and are removed with pacman; -`omarchy plugin update` and `omarchy plugin remove` refuse them and name the -package. +Packaged plugins such as Elsewhen (`elsewhen` package) update with `omarchy update` and are removed with pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package. The shell hot-reloads `shell.json` on save — no restart needed for layout changes. `idle.screensaver` and `idle.lock` are seconds since user idle began. diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 18a61896..dd8cc18a 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -90,16 +90,7 @@ The lower-level IPC methods remain available through `omarchy-shell shell ...`. ## Packaged plugins -A default plugin can ship as its own Arch package instead of in the Omarchy -checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs them at -`/usr/share/omarchy/plugins//`, a root the shell scans between -`$OMARCHY_PATH/shell/plugins` and `~/.config/omarchy/plugins`. A packaged -`omarchy.*` id is trusted like a bundled plugin and loads by default; any other -packaged id behaves like an installed plugin. Precedence is bundled, then -packaged, then user, so a checkout under `omarchy dev link` overrides the -package. Packaged plugins update through `omarchy update` and leave through -pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and -name the package, and Setup › Plugins › Remove does not list them. +A default plugin can ship as its own Arch package instead of in the Omarchy checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs it at `/usr/share/omarchy/plugins//`, a root the shell scans between `$OMARCHY_PATH/shell/plugins` and `~/.config/omarchy/plugins`. A packaged `omarchy.*` id is trusted like a bundled plugin and loads by default; any other packaged id behaves like an installed plugin. Precedence is bundled, then packaged, then user, so a checkout under `omarchy dev link` overrides the package. Packaged plugins update through `omarchy update` and leave through pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package, and Setup › Plugins › Remove does not list them. ## IPC diff --git a/migrations/1789581661.sh b/migrations/1789581661.sh index fd5d30be..163deb9c 100644 --- a/migrations/1789581661.sh +++ b/migrations/1789581661.sh @@ -50,8 +50,8 @@ if [[ -e $checkout || -L $checkout ]]; then fi fi -# The widget opens the right section of the default bar, just before the -# tray, which a machine without a shell.json takes on as soon as the shell +# The widget sits just before the clock in the center of the default bar, +# which a machine without a shell.json takes on as soon as the shell # restarts. A customized bar gets the entry written into its file rather than # placed over IPC: the shell this runs under cannot see a widget its scan # never reached and would refuse it, whereas the file it hot-reloads carries @@ -76,10 +76,10 @@ commit "$NORMALIZE | $entry_id"' def insert_at($section; $index): .bar.layout[$section] = .bar.layout[$section][:$index] + [{id: "omacom.elsewhen"}] + .bar.layout[$section][$index:]; . as $config - | (["left", "center", "right"] | map(select($config.bar.layout[.] | ids | index("omarchy.tray") != null)) | first) as $section + | (["left", "center", "right"] | map(select($config.bar.layout[.] | ids | index("omarchy.clock") != null)) | first) as $section | if $section != null then - insert_at($section; .bar.layout[$section] | ids | index("omarchy.tray")) + insert_at($section; .bar.layout[$section] | ids | index("omarchy.clock")) else - insert_at("right"; 0) + insert_at("center"; 0) end ' diff --git a/shell/README.md b/shell/README.md index 23a25ca6..53425139 100644 --- a/shell/README.md +++ b/shell/README.md @@ -142,16 +142,7 @@ You can still drop a plugin in without git: ### Packaged plugins -Some default plugins ship as their own Arch package rather than in this -checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs such a plugin, -root-owned, at `/usr/share/omarchy/plugins//`, and -the shell scans that root between its bundled plugins and the user's. A -packaged `omarchy.*` id is trusted exactly like a bundled one; a packaged -plugin under any other id is treated like a user plugin. They update with -`omarchy update` and are removed with pacman, so `omarchy plugin update` and -`omarchy plugin remove` refuse them and name the package instead. A bundled -copy in `$OMARCHY_PATH/shell/plugins` wins over the packaged one, which is how -a packaged plugin is developed under `omarchy dev link`. +Some default plugins ship as their own Arch package rather than in this checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs such a plugin, root-owned, at `/usr/share/omarchy/plugins//`, and the shell scans that root between its bundled plugins and the user's. A packaged `omarchy.*` id is trusted exactly like a bundled one; a packaged plugin under any other id is treated like a user plugin. They update with `omarchy update` and are removed with pacman, so `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package instead. A bundled copy in `$OMARCHY_PATH/shell/plugins` wins over the packaged one, which is how a packaged plugin is developed under `omarchy dev link`. The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`, `omarchy-shell shell enablePlugin '{}'`, and `omarchy-shell shell listPlugins`. diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index d9086d44..1cd47e33 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -33,6 +33,15 @@ jq -e ' ' "$ROOT/config/omarchy/shell.json" >/dev/null pass "default center anchor exists in center layout" +jq -e ' + def ids: map(.id // .); + (.bar.layout.center | ids) as $ids | + ($ids | index("omacom.elsewhen")) as $elsewhen | + ($ids | index("omarchy.clock")) as $clock | + $elsewhen != null and $clock == $elsewhen + 1 +' "$ROOT/config/omarchy/shell.json" >/dev/null +pass "default center layout puts elsewhen immediately before the clock" + jq -e ' any(.bar.layout.center[]; (.id // .) == "omarchy.clock" and (.formatAlt // "") == "d MMMM \u0027W\u0027ww yyyy") ' "$ROOT/config/omarchy/shell.json" >/dev/null @@ -41,12 +50,11 @@ pass "default clock date format has no leading zero" jq -e ' def ids: map(.id // .); (.bar.layout.right | ids) as $ids | - ($ids | index("omacom.elsewhen")) as $elsewhen | ($ids | index("omarchy.tray")) as $tray | ($ids | index("omarchy.agents")) as $agents | - $elsewhen == 0 and $tray == 1 and $agents == $tray + 1 + $tray == 0 and $agents == $tray + 1 ' "$ROOT/config/omarchy/shell.json" >/dev/null -pass "default right layout opens with elsewhen and the tray, then agents" +pass "default right layout opens with the tray, then agents" ROOT="$ROOT" python3 <<'PY' import json diff --git a/test/shell.d/elsewhen-default-migration-test.sh b/test/shell.d/elsewhen-default-migration-test.sh index 438ba1d3..89585f22 100644 --- a/test/shell.d/elsewhen-default-migration-test.sh +++ b/test/shell.d/elsewhen-default-migration-test.sh @@ -89,14 +89,14 @@ run_migration grep -Fxq elsewhen "$CALL_LOG" || fail "migration installs the elsewhen package" "$(cat "$CALL_LOG")" pass "migration installs the elsewhen package" -[[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.agents","omarchy.power"]' ]] || - fail "migration puts the widget just before the tray" "$(cat "$config")" -pass "migration puts the widget just before the tray" +[[ $(ids center) == '["omacom.elsewhen","omarchy.clock"]' && $(ids right) == '["omarchy.tray","omarchy.agents","omarchy.power"]' ]] || + fail "migration puts the widget just before the center clock" "$(cat "$config")" +pass "migration puts the widget just before the center clock" -[[ $(jq -c '.bar.layout.right[2]' "$config") == '{"id":"omarchy.agents","syncMode":"On"}' ]] || +[[ $(jq -c '.bar.layout.right[1]' "$config") == '{"id":"omarchy.agents","syncMode":"On"}' ]] || fail "migration keeps the settings of its neighbours" "$(cat "$config")" -[[ $(jq -c '.bar.layout.center[0]' "$config") == '{"format":"HH:mm","id":"omarchy.clock"}' ]] || - fail "migration leaves other sections alone" "$(cat "$config")" +[[ $(jq -c '.bar.layout.center[1]' "$config") == '{"format":"HH:mm","id":"omarchy.clock"}' ]] || + fail "migration keeps the clock settings" "$(cat "$config")" pass "migration leaves the rest of the layout alone" grep -q 'shell rescanPlugins' "$SHELL_CALLS" && grep -q 'shell reloadConfig' "$SHELL_CALLS" || @@ -117,17 +117,17 @@ cat >"$config" <<'JSON' } JSON run_migration -[[ $(ids center) == '["omarchy.clock","omacom.elsewhen","omarchy.tray"]' && $(ids right) == '["omarchy.power"]' ]] || - fail "migration follows the tray into another section and reads string entries" "$(cat "$config")" -pass "migration follows the tray into another section and reads string entries" +[[ $(ids center) == '["omacom.elsewhen","omarchy.clock","omarchy.tray"]' && $(ids right) == '["omarchy.power"]' ]] || + fail "migration reads string clock entries" "$(cat "$config")" +pass "migration reads string clock entries" cat >"$config" <<'JSON' { "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.agents" }, { "id": "omarchy.power" }] } } } JSON run_migration -[[ $(ids right) == '["omacom.elsewhen","omarchy.agents","omarchy.power"]' ]] || - fail "migration prepends to the right section when the tray is off the bar" "$(cat "$config")" -pass "migration prepends to the right section when the tray is off the bar" +[[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.agents","omarchy.power"]' ]] || + fail "migration prepends to the center section when the clock is off the bar" "$(cat "$config")" +pass "migration prepends to the center section when the clock is off the bar" cat >"$config" <<'JSON' { @@ -141,6 +141,15 @@ run_migration fail "migration leaves a widget the user already placed where it is" "$(cat "$config")" pass "migration leaves a widget the user already placed where it is" +# A customized clock keeps its section and settings; insert only its neighbour. +for section in left right; do + jq -n --arg section "$section" '{version: 1, bar: {layout: {center: ["omarchy.weather"], ($section): ["omarchy.menu", {id: "omarchy.clock", format: "HH:mm"}]}}}' >"$config" + run_migration + [[ $(ids "$section") == '["omarchy.menu","omacom.elsewhen","omarchy.clock"]' && $(ids center) == '["omarchy.weather"]' ]] || + fail "migration follows a clock customized into $section" "$(cat "$config")" + pass "migration follows a clock customized into $section" +done + # ------------------------------------------------------------- edge cases rm -f "$config" run_migration @@ -156,7 +165,7 @@ cat >"$config" <<'JSON' { "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } } JSON SHELL_STATUS=1 run_migration -[[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.agents"]' ]] || +[[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.tray","omarchy.agents"]' ]] || fail "migration places the widget with no shell running" "$(cat "$config")" pass "migration places the widget with no shell running" @@ -201,7 +210,7 @@ JSON } assert_widget_placed() { - [[ $(ids right) == '["omacom.elsewhen","omarchy.tray","omarchy.power"]' ]] || + [[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.tray","omarchy.power"]' ]] || fail "$1 still gets the widget" "$(cat "$config")" } From e8a8236a22e9d05f4fccded51639d522a88903ca Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Thu, 17 Sep 2026 22:07:54 -0500 Subject: [PATCH 3/5] Preserve local Elsewhen work and fallback bar layouts Retire only checkouts whose refs and reflogs are reachable from recorded origin history, and preserve ignored files. Leave configs without an explicit supported bar layout untouched so migration does not replace the shell fallback with an almost empty bar. Co-Authored-By: Codex XHigh --- migrations/1789581661.sh | 13 ++-- .../elsewhen-default-migration-test.sh | 69 +++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/migrations/1789581661.sh b/migrations/1789581661.sh index 163deb9c..f581d4d0 100644 --- a/migrations/1789581661.sh +++ b/migrations/1789581661.sh @@ -29,15 +29,18 @@ omarchy-shell -q shell rescanPlugins || true # one that finds the package. A one-time gap of a few minutes, during the # update itself. upstream_checkout() { - local dir="$1" origin status + local dir="$1" origin status unpublished [[ -d $dir && ! -L $dir && -d $dir/.git ]] || return 1 origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 1 origin=${origin,,} origin=${origin%/} origin=${origin%.git} [[ $origin =~ ^([a-z+]+://)?([^/@]+@)?github\.com[/:]omacom/elsewhen$ ]] || return 1 - status=$(git -C "$dir" status --porcelain 2>/dev/null) || return 1 - [[ -z $status ]] + status=$(git -C "$dir" status --porcelain --untracked-files=all --ignored 2>/dev/null) || return 1 + [[ -z $status ]] || return 1 + # Local branches, stashes, and reflogs may hold work absent from a clean tree. + unpublished=$(git -C "$dir" rev-list HEAD --all --reflog --not --remotes=origin 2>/dev/null) || return 1 + [[ -z $unpublished ]] } checkout="$HOME/.config/omarchy/plugins/omacom.elsewhen" @@ -60,8 +63,8 @@ fi source omarchy-shell-config [[ -s $CONFIG_FILE ]] || exit 0 -# A file the shell cannot read is left for its owner to repair. -jq empty "$CONFIG_FILE" 2>/dev/null || exit 0 +# Preserve configs whose bar comes from the shell fallback instead of an explicit layout. +jq -e 'type == "object" and .version == 1 and (.bar | type == "object") and (.bar.layout | type == "object")' "$CONFIG_FILE" >/dev/null 2>&1 || exit 0 entry_id='def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;' diff --git a/test/shell.d/elsewhen-default-migration-test.sh b/test/shell.d/elsewhen-default-migration-test.sh index 89585f22..525094e5 100644 --- a/test/shell.d/elsewhen-default-migration-test.sh +++ b/test/shell.d/elsewhen-default-migration-test.sh @@ -156,6 +156,18 @@ run_migration [[ ! -e $config ]] || fail "migration writes no shell.json where the defaults apply" "$(cat "$config")" pass "migration writes no shell.json where the defaults apply" +for partial in \ + '{"version":1,"idle":{"lock":600}}' \ + '{"version":1,"bar":null,"idle":{"lock":600}}' \ + '{"version":1,"bar":{"position":"bottom"}}' \ + '{"bar":{"layout":{"center":["omarchy.clock"]}}}'; do + printf '%s\n' "$partial" >"$config" + before=$(sha256sum "$config") + run_migration + [[ $before == $(sha256sum "$config") ]] || fail "migration preserves fallback configuration" "$(cat "$config")" + pass "migration preserves fallback configuration: $partial" +done + printf '{ not json' >"$config" run_migration [[ $(cat "$config") == '{ not json' ]] || fail "migration leaves an unparsable config untouched" "$(cat "$config")" @@ -201,6 +213,7 @@ make_checkout() { printf 'import QtQuick\nItem {}\n' >"$checkout/Panel.qml" git -C "$checkout" add -A git -C "$checkout" commit -q -m "Elsewhen" + git -C "$checkout" update-ref refs/remotes/origin/main HEAD } reset_layout() { @@ -257,6 +270,62 @@ reset_layout run_migration assert_kept "a clone with an untracked file" +make_checkout https://github.com/omacom/elsewhen.git +printf 'import QtQuick\nItem { id: mine }\n' >"$checkout/Panel.qml" +git -C "$checkout" add Panel.qml +git -C "$checkout" commit -q -m "Local customization" +local_commit=$(git -C "$checkout" rev-parse HEAD) +reset_layout +run_migration +assert_kept "a clean clone with an unpublished commit" +[[ $(git -C "$checkout" rev-parse HEAD) == "$local_commit" ]] || fail "local commit survives" + +make_checkout https://github.com/omacom/elsewhen.git +git -C "$checkout" checkout -qb local-work +printf 'local branch\n' >"$checkout/Panel.qml" +git -C "$checkout" commit -qam "Unpublished branch" +git -C "$checkout" checkout -q --detach refs/remotes/origin/main +reset_layout +run_migration +assert_kept "an unpublished commit on another branch" + +make_checkout https://github.com/omacom/elsewhen.git +printf 'stashed work\n' >"$checkout/Panel.qml" +git -C "$checkout" stash push -q +reset_layout +run_migration +assert_kept "a clean clone with stashed work" + +make_checkout https://github.com/omacom/elsewhen.git +git -C "$checkout" config status.showUntrackedFiles no +printf 'private-notes\n' >"$checkout/.git/info/exclude" +printf 'ignored work\n' >"$checkout/private-notes" +reset_layout +run_migration +assert_kept "a clone with an ignored file" +[[ $(cat "$checkout/private-notes") == "ignored work" ]] || fail "ignored file survives" + +make_checkout https://github.com/omacom/elsewhen.git +printf 'recoverable work\n' >"$checkout/Panel.qml" +git -C "$checkout" commit -qam "Recoverable local commit" +git -C "$checkout" reset -q --hard refs/remotes/origin/main +reset_layout +run_migration +assert_kept "a local commit retained only by the reflog" + +make_checkout https://github.com/omacom/elsewhen.git +git -C "$checkout" update-ref -d refs/remotes/origin/main +reset_layout +run_migration +assert_kept "a clone without recorded upstream history" + +make_checkout https://github.com/omacom/elsewhen.git +git -C "$checkout" config status.showUntrackedFiles no +printf 'untracked work\n' >"$checkout/NOTES.md" +reset_layout +run_migration +assert_kept "untracked files hidden by the user Git configuration" + make_checkout https://github.com/someone/elsewhen-fork.git reset_layout run_migration From 9e3ff71f4805a4deb20605c5178a78dce7c578b9 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Sat, 19 Sep 2026 00:00:15 -0500 Subject: [PATCH 4/5] Simplify Elsewhen installation and bar migration Link the package into the existing plugin directory and let bar put handle enablement, clock-relative placement, and the missing-clock fallback. Preserve user checkouts and existing placements, and seed the same link for new users. This removes the Atreyu packaged-discovery prerequisite and the checkout cleanup and JSON rewrite machinery. --- config/omarchy/plugins/omacom.elsewhen | 1 + default/agents/skills/omarchy/plugins.md | 3 - docs/file-layout.md | 3 - docs/omarchy-shell.md | 4 +- migrations/1789581661.sh | 89 +--- shell/README.md | 5 - shell/plugins/README.md | 6 +- test/shell.d/config-test.sh | 4 +- .../elsewhen-default-migration-test.sh | 382 +++--------------- 9 files changed, 61 insertions(+), 436 deletions(-) create mode 120000 config/omarchy/plugins/omacom.elsewhen diff --git a/config/omarchy/plugins/omacom.elsewhen b/config/omarchy/plugins/omacom.elsewhen new file mode 120000 index 00000000..19840c51 --- /dev/null +++ b/config/omarchy/plugins/omacom.elsewhen @@ -0,0 +1 @@ +/usr/share/omarchy/plugins/omacom.elsewhen \ No newline at end of file diff --git a/default/agents/skills/omarchy/plugins.md b/default/agents/skills/omarchy/plugins.md index 1c58c83a..6e3ab069 100644 --- a/default/agents/skills/omarchy/plugins.md +++ b/default/agents/skills/omarchy/plugins.md @@ -9,12 +9,9 @@ inside a single long-running Quickshell process (`omarchy-shell`). ``` ~/.config/omarchy/shell.json # User overrides: bar, plugins, idle ~/.config/omarchy/plugins// # User-owned shell plugins -/usr/share/omarchy/plugins// # Packaged plugins (pacman), e.g. omacom.elsewhen $OMARCHY_PATH/config/omarchy/shell.json # Canonical defaults ``` -Packaged plugins such as Elsewhen (`elsewhen` package) update with `omarchy update` and are removed with pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package. - The shell hot-reloads `shell.json` on save — no restart needed for layout changes. `idle.screensaver` and `idle.lock` are seconds since user idle began. diff --git a/docs/file-layout.md b/docs/file-layout.md index 8bbcd5b2..f261085f 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -78,9 +78,6 @@ install/** ──► omarchy /usr/share/omarchy migrations/** ──► omarchy /usr/share/omarchy/migrations/ themes/** ──► omarchy /usr/share/omarchy/themes/ shell/** ──► omarchy /usr/share/omarchy/shell/ - (packaged plugins from their own - packages, e.g. elsewhen, land beside it - in /usr/share/omarchy/plugins//) version ──► omarchy /usr/share/omarchy/version + /etc/skel/.local/state/omarchy/migrations/* diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index dd8cc18a..6e999682 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -88,9 +88,9 @@ bar from the CLI — `use | reset | defaults | position | transparent | put | move | set`, with placement flags such as `--section` and `--index`. The lower-level IPC methods remain available through `omarchy-shell shell ...`. -## Packaged plugins +## Elsewhen -A default plugin can ship as its own Arch package instead of in the Omarchy checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs it at `/usr/share/omarchy/plugins//`, a root the shell scans between `$OMARCHY_PATH/shell/plugins` and `~/.config/omarchy/plugins`. A packaged `omarchy.*` id is trusted like a bundled plugin and loads by default; any other packaged id behaves like an installed plugin. Precedence is bundled, then packaged, then user, so a checkout under `omarchy dev link` overrides the package. Packaged plugins update through `omarchy update` and leave through pacman; `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package, and Setup › Plugins › Remove does not list them. +Elsewhen (`omacom.elsewhen`) ships in the `elsewhen` package at `/usr/share/omarchy/plugins/omacom.elsewhen`. A symlink in `~/.config/omarchy/plugins/` makes it available to the shell. New installs place it immediately before the clock; the migration uses `omarchy bar put omacom.elsewhen --before omarchy.clock`, which preserves an existing placement and uses Elsewhen's normal right-side placement if the clock is absent. Existing plugin directories and symlinks are left intact. ## IPC diff --git a/migrations/1789581661.sh b/migrations/1789581661.sh index f581d4d0..10f18e66 100644 --- a/migrations/1789581661.sh +++ b/migrations/1789581661.sh @@ -1,88 +1,13 @@ echo "Install Elsewhen, the world clock plugin" -# Elsewhen ships as the elsewhen package rather than in this checkout; pacman -# puts it under /usr/share/omarchy/plugins, where the shell looks next to its -# bundled plugins. A package the mirror does not carry yet fails the migration -# on purpose: nothing before this line has changed anything, and the runner -# retries it on the next update or login rather than quietly leaving the -# machine without the plugin. omarchy-pkg-add elsewhen -# A shell that already knows the packaged root (dev-linked, or restarted -# since) picks the plugin up here; the one an update runs under predates it -# and is replaced by omarchy-update-restart right after the migrations. -omarchy-shell -q shell rescanPlugins || true - -# Before the package, the README had users run -# `omarchy plugin add https://github.com/omacom/elsewhen.git`, which left a -# git clone under ~/.config/omarchy/plugins. The packaged copy now shadows -# it: the shell logs a rejection on every scan, and `omarchy plugin update` -# keeps pulling a checkout nothing loads. A pristine clone of the upstream -# repo is retired the way `omarchy plugin remove` retires one (the repo -# remains upstream). Anything else -- a symlink, a plain directory, local -# changes, a fork, a checkout git cannot read -- is the user's and stays. This -# runs only once the package is in place, so the plugin never leaves a -# machine; its id stays in shell.json either way, which is what keeps it -# enabled. The shell an update runs under predates the packaged root, and it -# watches this directory: dropping the checkout empties the widget's slot in -# that shell until omarchy-update-restart replaces it a few steps later with -# one that finds the package. A one-time gap of a few minutes, during the -# update itself. -upstream_checkout() { - local dir="$1" origin status unpublished - [[ -d $dir && ! -L $dir && -d $dir/.git ]] || return 1 - origin=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 1 - origin=${origin,,} - origin=${origin%/} - origin=${origin%.git} - [[ $origin =~ ^([a-z+]+://)?([^/@]+@)?github\.com[/:]omacom/elsewhen$ ]] || return 1 - status=$(git -C "$dir" status --porcelain --untracked-files=all --ignored 2>/dev/null) || return 1 - [[ -z $status ]] || return 1 - # Local branches, stashes, and reflogs may hold work absent from a clean tree. - unpublished=$(git -C "$dir" rev-list HEAD --all --reflog --not --remotes=origin 2>/dev/null) || return 1 - [[ -z $unpublished ]] -} - -checkout="$HOME/.config/omarchy/plugins/omacom.elsewhen" -if [[ -e $checkout || -L $checkout ]]; then - if upstream_checkout "$checkout"; then - rm -rf "$checkout" - echo "Retired the omacom.elsewhen checkout at $checkout in favour of the elsewhen package." - else - echo "The elsewhen package takes precedence over $checkout, which is left as it is." - fi +plugin="$HOME/.config/omarchy/plugins/omacom.elsewhen" +mkdir -p "$(dirname "$plugin")" +if [[ ! -e $plugin && ! -L $plugin ]]; then + ln -s /usr/share/omarchy/plugins/omacom.elsewhen "$plugin" fi -# The widget sits just before the clock in the center of the default bar, -# which a machine without a shell.json takes on as soon as the shell -# restarts. A customized bar gets the entry written into its file rather than -# placed over IPC: the shell this runs under cannot see a widget its scan -# never reached and would refuse it, whereas the file it hot-reloads carries -# the entry through to the restart. A bar that already carries the widget -# keeps it where the user put it. -source omarchy-shell-config - -[[ -s $CONFIG_FILE ]] || exit 0 -# Preserve configs whose bar comes from the shell fallback instead of an explicit layout. -jq -e 'type == "object" and .version == 1 and (.bar | type == "object") and (.bar.layout | type == "object")' "$CONFIG_FILE" >/dev/null 2>&1 || exit 0 - -entry_id='def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;' - -if jq -e "$entry_id"' - any((.bar.layout // {} | .left, .center, .right | arrays)[]; entry_id == "omacom.elsewhen") -' "$CONFIG_FILE" >/dev/null; then - exit 0 -fi - -commit "$NORMALIZE | $entry_id"' - def ids: map(entry_id); - def insert_at($section; $index): - .bar.layout[$section] = .bar.layout[$section][:$index] + [{id: "omacom.elsewhen"}] + .bar.layout[$section][$index:]; - . as $config - | (["left", "center", "right"] | map(select($config.bar.layout[.] | ids | index("omarchy.clock") != null)) | first) as $section - | if $section != null then - insert_at($section; .bar.layout[$section] | ids | index("omarchy.clock")) - else - insert_at("center"; 0) - end -' +omarchy-shell shell rescanPlugins +omarchy-bar put omacom.elsewhen --before omarchy.clock +omarchy-restart-shell diff --git a/shell/README.md b/shell/README.md index 53425139..ca07b520 100644 --- a/shell/README.md +++ b/shell/README.md @@ -140,10 +140,6 @@ You can still drop a plugin in without git: `barWidget.defaultSection`, or in the center when it is omitted, and can be moved with `omarchy bar move`; a full bar replaces the one in use. -### Packaged plugins - -Some default plugins ship as their own Arch package rather than in this checkout: Elsewhen (`omacom.elsewhen`) is the `elsewhen` package. pacman installs such a plugin, root-owned, at `/usr/share/omarchy/plugins//`, and the shell scans that root between its bundled plugins and the user's. A packaged `omarchy.*` id is trusted exactly like a bundled one; a packaged plugin under any other id is treated like a user plugin. They update with `omarchy update` and are removed with pacman, so `omarchy plugin update` and `omarchy plugin remove` refuse them and name the package instead. A bundled copy in `$OMARCHY_PATH/shell/plugins` wins over the packaged one, which is how a packaged plugin is developed under `omarchy dev link`. - The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`, `omarchy-shell shell enablePlugin '{}'`, and `omarchy-shell shell listPlugins`. The `omarchy plugin` commands wrap those calls. `omarchy bar move` and @@ -228,7 +224,6 @@ customization from the shipped defaults lives in it. |-----------------------------------|----------------|--------------------------------------------------------| | `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list | | `~/.config/omarchy/plugins//` | user | drop-in third-party plugin source files | -| `/usr/share/omarchy/plugins//` | pacman | packaged plugins (e.g. `elsewhen`), updated by `omarchy update` | The `config/omarchy/shell.json` default config describes the fresh-install state. When the user has no `shell.json`, the shell uses diff --git a/shell/plugins/README.md b/shell/plugins/README.md index 1a2d26fb..66cb74e1 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -9,10 +9,7 @@ First-party non-bar plugins are enabled unless listed in `disabledPlugins[]`; at startup; other panels, overlays, and menus are loaded on demand. User-installed plugins live alongside these conceptually but on disk under -`~/.config/omarchy/plugins//` rather than in this directory. A few -default plugins arrive as their own package instead: pacman installs them at -`/usr/share/omarchy/plugins//`, where an `omarchy.*` id is trusted -like the plugins here, and a copy in this directory shadows the packaged one. +`~/.config/omarchy/plugins//` rather than in this directory. | Plugin | id | kinds | entry point | |---------------|---------------------------|-------------------------|---------------------------------------| @@ -31,7 +28,6 @@ like the plugins here, and a copy in this directory shadows the packaged one. | Power | `omarchy.power` | `bar-widget` | `panels/power/Panel.qml` | | Tailscale | `omarchy.tailscale` | `bar-widget` | `panels/tailscale/Panel.qml` | | Agents | `omarchy.agents` | `bar-widget` | `agents/Panel.qml` | -| Elsewhen | `omacom.elsewhen` | `bar-widget` | packaged: `elsewhen` installs `/usr/share/omarchy/plugins/omacom.elsewhen/` | | Weather | `omarchy.weather` | `bar-widget` | `panels/weather/BarWidget.qml` | | Media | `omarchy.media` | `service`, `bar-widget` | `services/media/Service.qml`, `services/media/BarWidget.qml` | | Battery | `omarchy.battery` | `service` | `services/battery/Service.qml` | diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 1cd47e33..82a51e6f 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -52,9 +52,9 @@ jq -e ' (.bar.layout.right | ids) as $ids | ($ids | index("omarchy.tray")) as $tray | ($ids | index("omarchy.agents")) as $agents | - $tray == 0 and $agents == $tray + 1 + $tray != null and $agents == $tray + 1 ' "$ROOT/config/omarchy/shell.json" >/dev/null -pass "default right layout opens with the tray, then agents" +pass "default right layout keeps agents next to the tray" ROOT="$ROOT" python3 <<'PY' import json diff --git a/test/shell.d/elsewhen-default-migration-test.sh b/test/shell.d/elsewhen-default-migration-test.sh index 525094e5..bda62fd6 100644 --- a/test/shell.d/elsewhen-default-migration-test.sh +++ b/test/shell.d/elsewhen-default-migration-test.sh @@ -3,361 +3,75 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" - require_command jq -require_command git -migration="$ROOT/migrations/1789581661.sh" test_dir=$(mktemp -d) trap 'rm -rf "$test_dir"' EXIT +mkdir -p "$test_dir/bin" "$test_dir/home" +export CALL_LOG="$test_dir/calls" -mkdir -p "$test_dir/bin" -export INSTALLED_PACKAGES="$test_dir/installed" CALL_LOG="$test_dir/calls" SHELL_CALLS="$test_dir/shell-calls" -# The checkout cases use the real git; keep the developer's config (signing, -# hooks, safe.directory) out of what it sees. -export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 -export GIT_AUTHOR_NAME=test GIT_AUTHOR_EMAIL=test@example.com -export GIT_COMMITTER_NAME=test GIT_COMMITTER_EMAIL=test@example.com - -# Keep the real package helpers, but contain every pacman transaction here. -cat >"$test_dir/bin/pacman" <<'SH' +cat >"$test_dir/bin/omarchy-pkg-add" <<'SH' #!/bin/bash -case "$1" in - -Q) grep -Fxq -- "$2" "$INSTALLED_PACKAGES" ;; - -S) - [[ ${FAIL_INSTALL:-0} == 0 ]] || exit 1 - shift 3 # -S --noconfirm --needed - printf '%s\n' "$@" >>"$INSTALLED_PACKAGES" - printf '%s\n' "$@" >>"$CALL_LOG" - ;; - *) exit 1 ;; -esac +printf 'package %s\n' "$*" >>"$CALL_LOG" +exit "${PACKAGE_STATUS:-0}" SH -cat >"$test_dir/bin/sudo" <<'SH' -#!/bin/bash -[[ $1 == "pacman" ]] || exit 1 -"$@" -SH -# The migration runs under a shell that predates the packaged root, or under -# none at all, so it must only ever ask the shell for best-effort refreshes. cat >"$test_dir/bin/omarchy-shell" <<'SH' #!/bin/bash -printf '%s\n' "$*" >>"$SHELL_CALLS" -[[ $1 != "-q" ]] || exit 0 -exit "${SHELL_STATUS:-0}" +printf '%s\n' "$*" >>"$CALL_LOG" +[[ $2 != "rescanPlugins" ]] || exit "${SCAN_STATUS:-0}" +printf '%s\n' "${TEST_PUT_RESULT:-ok}" +SH +cat >"$test_dir/bin/omarchy-restart-shell" <<'SH' +#!/bin/bash +printf 'restart\n' >>"$CALL_LOG" SH chmod +x "$test_dir/bin/"* -home="$test_dir/home" -config="$home/.config/omarchy/shell.json" -checkout="$home/.config/omarchy/plugins/omacom.elsewhen" -output="$test_dir/output" -mkdir -p "$home/.config/omarchy" - +plugin="$test_dir/home/.config/omarchy/plugins/omacom.elsewhen" run_migration() { : >"$CALL_LOG" - : >"$SHELL_CALLS" - HOME="$home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \ - bash -euo pipefail "$migration" >"$output" + HOME="$test_dir/home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \ + bash -euo pipefail "$ROOT/migrations/1789581661.sh" >"$test_dir/output" 2>&1 } -ids() { - jq -c ".bar.layout.$1 | map(if type == \"object\" then .id else . end)" "$config" -} - -# ------------------------------------------------------------ package install -: >"$INSTALLED_PACKAGES" -cat >"$config" <<'JSON' -{ - "version": 1, - "bar": { - "layout": { - "left": [{ "id": "omarchy.menu" }], - "center": [{ "id": "omarchy.clock", "format": "HH:mm" }], - "right": [ - { "id": "omarchy.tray" }, - { "id": "omarchy.agents", "syncMode": "On" }, - { "id": "omarchy.power" } - ] - } - }, - "plugins": [] -} -JSON - -run_migration -grep -Fxq elsewhen "$CALL_LOG" || fail "migration installs the elsewhen package" "$(cat "$CALL_LOG")" -pass "migration installs the elsewhen package" - -[[ $(ids center) == '["omacom.elsewhen","omarchy.clock"]' && $(ids right) == '["omarchy.tray","omarchy.agents","omarchy.power"]' ]] || - fail "migration puts the widget just before the center clock" "$(cat "$config")" -pass "migration puts the widget just before the center clock" - -[[ $(jq -c '.bar.layout.right[1]' "$config") == '{"id":"omarchy.agents","syncMode":"On"}' ]] || - fail "migration keeps the settings of its neighbours" "$(cat "$config")" -[[ $(jq -c '.bar.layout.center[1]' "$config") == '{"format":"HH:mm","id":"omarchy.clock"}' ]] || - fail "migration keeps the clock settings" "$(cat "$config")" -pass "migration leaves the rest of the layout alone" - -grep -q 'shell rescanPlugins' "$SHELL_CALLS" && grep -q 'shell reloadConfig' "$SHELL_CALLS" || - fail "migration asks the running shell to rescan and reload" "$(cat "$SHELL_CALLS")" -pass "migration asks the running shell to rescan and reload" - -before=$(sha256sum "$config") -run_migration -[[ $before == $(sha256sum "$config") ]] || fail "migration is idempotent" "$(cat "$config")" -[[ ! -s $CALL_LOG ]] || fail "migration does not reinstall a present package" "$(cat "$CALL_LOG")" -pass "migration is idempotent" - -# ------------------------------------------------------------- placements -cat >"$config" <<'JSON' -{ - "version": 1, - "bar": { "layout": { "left": [], "center": ["omarchy.clock", "omarchy.tray"], "right": ["omarchy.power"] } } -} -JSON -run_migration -[[ $(ids center) == '["omacom.elsewhen","omarchy.clock","omarchy.tray"]' && $(ids right) == '["omarchy.power"]' ]] || - fail "migration reads string clock entries" "$(cat "$config")" -pass "migration reads string clock entries" - -cat >"$config" <<'JSON' -{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.agents" }, { "id": "omarchy.power" }] } } } -JSON -run_migration -[[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.agents","omarchy.power"]' ]] || - fail "migration prepends to the center section when the clock is off the bar" "$(cat "$config")" -pass "migration prepends to the center section when the clock is off the bar" - -cat >"$config" <<'JSON' -{ - "version": 1, - "bar": { "layout": { "left": [{ "id": "omacom.elsewhen" }], "center": [], "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } -} -JSON -before=$(sha256sum "$config") -run_migration -[[ $before == $(sha256sum "$config") ]] || - fail "migration leaves a widget the user already placed where it is" "$(cat "$config")" -pass "migration leaves a widget the user already placed where it is" - -# A customized clock keeps its section and settings; insert only its neighbour. -for section in left right; do - jq -n --arg section "$section" '{version: 1, bar: {layout: {center: ["omarchy.weather"], ($section): ["omarchy.menu", {id: "omarchy.clock", format: "HH:mm"}]}}}' >"$config" - run_migration - [[ $(ids "$section") == '["omarchy.menu","omacom.elsewhen","omarchy.clock"]' && $(ids center) == '["omarchy.weather"]' ]] || - fail "migration follows a clock customized into $section" "$(cat "$config")" - pass "migration follows a clock customized into $section" -done - -# ------------------------------------------------------------- edge cases -rm -f "$config" -run_migration -[[ ! -e $config ]] || fail "migration writes no shell.json where the defaults apply" "$(cat "$config")" -pass "migration writes no shell.json where the defaults apply" - -for partial in \ - '{"version":1,"idle":{"lock":600}}' \ - '{"version":1,"bar":null,"idle":{"lock":600}}' \ - '{"version":1,"bar":{"position":"bottom"}}' \ - '{"bar":{"layout":{"center":["omarchy.clock"]}}}'; do - printf '%s\n' "$partial" >"$config" - before=$(sha256sum "$config") - run_migration - [[ $before == $(sha256sum "$config") ]] || fail "migration preserves fallback configuration" "$(cat "$config")" - pass "migration preserves fallback configuration: $partial" -done - -printf '{ not json' >"$config" -run_migration -[[ $(cat "$config") == '{ not json' ]] || fail "migration leaves an unparsable config untouched" "$(cat "$config")" -pass "migration leaves an unparsable config untouched" - -cat >"$config" <<'JSON' -{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } } -JSON -SHELL_STATUS=1 run_migration -[[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.tray","omarchy.agents"]' ]] || - fail "migration places the widget with no shell running" "$(cat "$config")" -pass "migration places the widget with no shell running" - -# A package the mirror does not carry yet leaves the migration pending rather -# than half-done: the layout must not name a widget nothing can install. -: >"$INSTALLED_PACKAGES" -cat >"$config" <<'JSON' -{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.agents" }] } } } -JSON -before=$(sha256sum "$config") -if FAIL_INSTALL=1 run_migration 2>/dev/null; then - fail "a failed package installation must leave the migration pending" +if PACKAGE_STATUS=1 run_migration; then + fail "package failure stops the migration" fi -[[ $before == $(sha256sum "$config") ]] || - fail "a failed package installation leaves the layout untouched" "$(cat "$config")" -pass "a failed package installation is propagated and changes nothing" +[[ ! -e $plugin && ! -L $plugin && $(cat "$CALL_LOG") == "package elsewhen" ]] || + fail "package failure leaves the plugin and shell untouched" +pass "package failure stops before linking or changing the shell" -# ------------------------------------------------- pre-package checkouts -# Users who followed the README before the package existed hold a git clone -# under ~/.config/omarchy/plugins, which the packaged copy now shadows. Only a -# clone of the upstream repo with nothing local in it is retired; everything -# else is the user's and stays, and shell.json gets the widget either way. +run_migration +[[ $(readlink "$plugin") == /usr/share/omarchy/plugins/omacom.elsewhen ]] || fail "package link is installed" +[[ $(readlink "$ROOT/config/omarchy/plugins/omacom.elsewhen") == "$(readlink "$plugin")" ]] || fail "fresh installs use the same package link" +pass "migration and fresh installs link to the package" -# A clone made by `omarchy plugin add`: one commit, the given origin, nothing -# local. Built with init rather than clone so the origin can be any URL form. -make_checkout() { - local origin="$1" - rm -rf "$checkout" - mkdir -p "$checkout" - git -C "$checkout" init -q - git -C "$checkout" remote add origin "$origin" - printf '{ "id": "omacom.elsewhen" }\n' >"$checkout/manifest.json" - printf 'import QtQuick\nItem {}\n' >"$checkout/Panel.qml" - git -C "$checkout" add -A - git -C "$checkout" commit -q -m "Elsewhen" - git -C "$checkout" update-ref refs/remotes/origin/main HEAD -} +expected=$'package elsewhen\nshell rescanPlugins\nshell putBarWidget omacom.elsewhen {"before":"omarchy.clock"}\nrestart' +[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "install, scan, placement and restart run in order" "$(cat "$CALL_LOG")" +pass "real bar helper enables and places before the clock, then restarts" -reset_layout() { - cat >"$config" <<'JSON' -{ "version": 1, "bar": { "layout": { "right": [{ "id": "omarchy.tray" }, { "id": "omarchy.power" }] } } } -JSON -} +run_migration +[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "migration can be rerun" +pass "migration can be rerun with its package link present" -assert_widget_placed() { - [[ $(ids center) == '["omacom.elsewhen"]' && $(ids right) == '["omarchy.tray","omarchy.power"]' ]] || - fail "$1 still gets the widget" "$(cat "$config")" -} +rm "$plugin" +mkdir "$plugin" +printf 'local work\n' >"$plugin/notes" +run_migration +[[ ! -L $plugin && $(cat "$plugin/notes") == "local work" ]] || fail "existing checkout is preserved" +pass "existing checkout and local files are preserved" -assert_retired() { - local description="$1" - [[ ! -e $checkout && ! -L $checkout ]] || fail "$description is retired" "$(ls -la "$checkout")" - grep -q 'Retired the omacom.elsewhen checkout' "$output" || fail "$description is reported as retired" "$(cat "$output")" - assert_widget_placed "$description" - pass "$description is retired" -} +rm "$plugin/notes" +rmdir "$plugin" +ln -s "$test_dir/custom-plugin" "$plugin" +run_migration +[[ $(readlink "$plugin") == "$test_dir/custom-plugin" ]] || fail "existing symlink is preserved" +pass "existing symlink is preserved, including a missing target" -assert_kept() { - local description="$1" - [[ -e $checkout || -L $checkout ]] || fail "$description is kept" - grep -q 'takes precedence over' "$output" || fail "$description is reported as shadowed" "$(cat "$output")" - assert_widget_placed "$description" - pass "$description is kept" -} - -for origin in \ - https://github.com/omacom/elsewhen.git \ - https://github.com/omacom/elsewhen \ - git@github.com:omacom/elsewhen.git \ - ssh://git@github.com/omacom/elsewhen.git \ - https://GitHub.com/Omacom/Elsewhen/; do - make_checkout "$origin" - reset_layout - run_migration - assert_retired "a pristine clone with origin $origin" +for failure in 'SCAN_STATUS=1' 'TEST_PUT_RESULT=unknown'; do + if env "$failure" HOME="$test_dir/home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \ + bash -euo pipefail "$ROOT/migrations/1789581661.sh" >"$test_dir/output" 2>&1; then + fail "$failure must leave the migration pending" + fi + pass "$failure leaves the migration pending" done - -make_checkout https://github.com/omacom/elsewhen.git -printf 'import QtQuick\nItem { id: mine }\n' >"$checkout/Panel.qml" -reset_layout -run_migration -assert_kept "a clone with a modified tracked file" -[[ $(cat "$checkout/Panel.qml") == $'import QtQuick\nItem { id: mine }' ]] || - fail "a kept clone keeps its local change" "$(cat "$checkout/Panel.qml")" -pass "a kept clone keeps its local change" - -make_checkout https://github.com/omacom/elsewhen.git -printf 'notes\n' >"$checkout/NOTES.md" -reset_layout -run_migration -assert_kept "a clone with an untracked file" - -make_checkout https://github.com/omacom/elsewhen.git -printf 'import QtQuick\nItem { id: mine }\n' >"$checkout/Panel.qml" -git -C "$checkout" add Panel.qml -git -C "$checkout" commit -q -m "Local customization" -local_commit=$(git -C "$checkout" rev-parse HEAD) -reset_layout -run_migration -assert_kept "a clean clone with an unpublished commit" -[[ $(git -C "$checkout" rev-parse HEAD) == "$local_commit" ]] || fail "local commit survives" - -make_checkout https://github.com/omacom/elsewhen.git -git -C "$checkout" checkout -qb local-work -printf 'local branch\n' >"$checkout/Panel.qml" -git -C "$checkout" commit -qam "Unpublished branch" -git -C "$checkout" checkout -q --detach refs/remotes/origin/main -reset_layout -run_migration -assert_kept "an unpublished commit on another branch" - -make_checkout https://github.com/omacom/elsewhen.git -printf 'stashed work\n' >"$checkout/Panel.qml" -git -C "$checkout" stash push -q -reset_layout -run_migration -assert_kept "a clean clone with stashed work" - -make_checkout https://github.com/omacom/elsewhen.git -git -C "$checkout" config status.showUntrackedFiles no -printf 'private-notes\n' >"$checkout/.git/info/exclude" -printf 'ignored work\n' >"$checkout/private-notes" -reset_layout -run_migration -assert_kept "a clone with an ignored file" -[[ $(cat "$checkout/private-notes") == "ignored work" ]] || fail "ignored file survives" - -make_checkout https://github.com/omacom/elsewhen.git -printf 'recoverable work\n' >"$checkout/Panel.qml" -git -C "$checkout" commit -qam "Recoverable local commit" -git -C "$checkout" reset -q --hard refs/remotes/origin/main -reset_layout -run_migration -assert_kept "a local commit retained only by the reflog" - -make_checkout https://github.com/omacom/elsewhen.git -git -C "$checkout" update-ref -d refs/remotes/origin/main -reset_layout -run_migration -assert_kept "a clone without recorded upstream history" - -make_checkout https://github.com/omacom/elsewhen.git -git -C "$checkout" config status.showUntrackedFiles no -printf 'untracked work\n' >"$checkout/NOTES.md" -reset_layout -run_migration -assert_kept "untracked files hidden by the user Git configuration" - -make_checkout https://github.com/someone/elsewhen-fork.git -reset_layout -run_migration -assert_kept "a clone of another repository" - -make_checkout https://github.com/omacom/elsewhen.git -git -C "$checkout" remote remove origin -reset_layout -run_migration -assert_kept "a clone without an origin remote" - -rm -rf "$checkout" -mkdir -p "$checkout" -printf '{ "id": "omacom.elsewhen" }\n' >"$checkout/manifest.json" -reset_layout -run_migration -assert_kept "a plain directory without .git" - -rm -rf "$checkout" -make_checkout https://github.com/omacom/elsewhen.git -mv "$checkout" "$test_dir/elsewhen-src" -ln -s "$test_dir/elsewhen-src" "$checkout" -reset_layout -run_migration -assert_kept "a symlinked checkout" -[[ -L $checkout && -f $test_dir/elsewhen-src/manifest.json ]] || - fail "a symlinked checkout stays a symlink with its target intact" "$(ls -la "$checkout" "$test_dir/elsewhen-src")" -pass "a symlinked checkout stays a symlink with its target intact" - -rm -f "$checkout" -reset_layout -run_migration -grep -q 'omacom.elsewhen checkout\|takes precedence' "$output" && fail "no checkout means no checkout report" "$(cat "$output")" -assert_widget_placed "a machine without a checkout" -pass "a machine without a checkout is left alone" From 16cc7d7a9dcf40320016dae37da8b97889d7cb58 Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Sat, 19 Sep 2026 00:06:29 -0500 Subject: [PATCH 5/5] Leave the shell restart to the update flow Restarting immediately after the plugin rescan races Quickshell IPC handler creation and can crash the exiting shell. The normal update flow already restarts after migrations. Let this migration finish through live enablement and placement without adding timing workarounds. --- docs/omarchy-shell.md | 2 +- migrations/1789581661.sh | 1 - test/shell.d/elsewhen-default-migration-test.sh | 9 +++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 6e999682..d62661a7 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -90,7 +90,7 @@ The lower-level IPC methods remain available through `omarchy-shell shell ...`. ## Elsewhen -Elsewhen (`omacom.elsewhen`) ships in the `elsewhen` package at `/usr/share/omarchy/plugins/omacom.elsewhen`. A symlink in `~/.config/omarchy/plugins/` makes it available to the shell. New installs place it immediately before the clock; the migration uses `omarchy bar put omacom.elsewhen --before omarchy.clock`, which preserves an existing placement and uses Elsewhen's normal right-side placement if the clock is absent. Existing plugin directories and symlinks are left intact. +Elsewhen (`omacom.elsewhen`) ships in the `elsewhen` package at `/usr/share/omarchy/plugins/omacom.elsewhen`. A symlink in `~/.config/omarchy/plugins/` makes it available to the shell. New installs place it immediately before the clock; the migration uses `omarchy bar put omacom.elsewhen --before omarchy.clock`, which preserves an existing placement and uses Elsewhen's normal right-side placement if the clock is absent. Existing plugin directories and symlinks are left intact. The normal update flow restarts the shell after migrations; the migration does not interrupt plugin loading with an immediate restart. ## IPC diff --git a/migrations/1789581661.sh b/migrations/1789581661.sh index 10f18e66..5c022e1c 100644 --- a/migrations/1789581661.sh +++ b/migrations/1789581661.sh @@ -10,4 +10,3 @@ fi omarchy-shell shell rescanPlugins omarchy-bar put omacom.elsewhen --before omarchy.clock -omarchy-restart-shell diff --git a/test/shell.d/elsewhen-default-migration-test.sh b/test/shell.d/elsewhen-default-migration-test.sh index bda62fd6..c0a2de8f 100644 --- a/test/shell.d/elsewhen-default-migration-test.sh +++ b/test/shell.d/elsewhen-default-migration-test.sh @@ -23,7 +23,8 @@ printf '%s\n' "${TEST_PUT_RESULT:-ok}" SH cat >"$test_dir/bin/omarchy-restart-shell" <<'SH' #!/bin/bash -printf 'restart\n' >>"$CALL_LOG" +echo 'migration must leave the restart to omarchy update' >&2 +exit 1 SH chmod +x "$test_dir/bin/"* @@ -46,9 +47,9 @@ run_migration [[ $(readlink "$ROOT/config/omarchy/plugins/omacom.elsewhen") == "$(readlink "$plugin")" ]] || fail "fresh installs use the same package link" pass "migration and fresh installs link to the package" -expected=$'package elsewhen\nshell rescanPlugins\nshell putBarWidget omacom.elsewhen {"before":"omarchy.clock"}\nrestart' -[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "install, scan, placement and restart run in order" "$(cat "$CALL_LOG")" -pass "real bar helper enables and places before the clock, then restarts" +expected=$'package elsewhen\nshell rescanPlugins\nshell putBarWidget omacom.elsewhen {"before":"omarchy.clock"}' +[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "install, scan and placement run in order" "$(cat "$CALL_LOG")" +pass "real bar helper enables and places before the clock without restarting during reload" run_migration [[ $(cat "$CALL_LOG") == "$expected" ]] || fail "migration can be rerun"