Plugin cloning via menu (#6433)
* Add plugin cloning via menu * Split plugin commands by action * Keep plugin enablement in action commands * Remove unused plugin edit command * Simplify plugin rescan arguments * Assume Omarchy shell is running for plugin commands * Remove plugin compatibility dispatcher * Flatten plugin clone command * Keep only shared plugin helpers * Remove plugin rescan wrapper * Keep plugin commands self-contained * Simplify plugin clone lifecycle
This commit is contained in:
@@ -82,8 +82,12 @@ ShellRoot {
|
||||
scan += block("firstparty", "/first/widgets/clock", manifest("omarchy.first-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
|
||||
scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
|
||||
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" }))
|
||||
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "left" }))
|
||||
var localWidget = manifest("local.first-widget", ["bar-widget"], { barWidget: "Widget.qml" })
|
||||
localWidget.omarchy = { clonedFrom: "omarchy.first-widget" }
|
||||
scan += block("thirdparty", "/third/local-widget", localWidget)
|
||||
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
|
||||
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
|
||||
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
|
||||
@@ -96,9 +100,11 @@ ShellRoot {
|
||||
registry.parseScanOutput(scan)
|
||||
|
||||
root.assertDeepEqual(pluginIds(), [
|
||||
"local.first-widget",
|
||||
"omarchy.bar",
|
||||
"omarchy.first-widget",
|
||||
"omarchy.grouped-panel",
|
||||
"omarchy.hybrid",
|
||||
"third.bar",
|
||||
"third.panel",
|
||||
"third.widget"
|
||||
@@ -120,6 +126,7 @@ ShellRoot {
|
||||
root.assertTrue(registry.isEnabled("omarchy.bar"), "built-in bar option is active by default")
|
||||
root.assertTrue(!registry.isEnabled("third.bar"), "third-party bar options start inactive")
|
||||
root.assertTrue(!registry.isEnabled("third.panel"), "third-party plugins start disabled")
|
||||
root.assertEqual(registry.resolveEnabledId("omarchy.first-widget"), "omarchy.first-widget", "inactive clones do not replace their source id")
|
||||
|
||||
registry.setEnabled("third.bar", true)
|
||||
root.assertEqual(root.config.bar.id, "third.bar", "enabling third-party bar options writes bar id")
|
||||
@@ -141,6 +148,10 @@ ShellRoot {
|
||||
registry.setEnabled("third.widget", false)
|
||||
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling bar widgets removes layout entry")
|
||||
|
||||
registry.setEnabled("local.first-widget", true)
|
||||
root.assertEqual(registry.resolveEnabledId("omarchy.first-widget"), "local.first-widget", "enabled clones receive calls made to their source id")
|
||||
registry.setEnabled("local.first-widget", false)
|
||||
|
||||
root.config = {
|
||||
version: 1,
|
||||
bar: { layout: { left: [], center: [{ id: "third.widget", size: 4 }], right: [] } },
|
||||
@@ -183,6 +194,21 @@ ShellRoot {
|
||||
registry.setEnabled("omarchy.first-widget", true)
|
||||
root.assertDeepEqual(root.config.bar.layout.center, [{ id: "omarchy.first-widget" }], "a widget without a default section falls back to center")
|
||||
|
||||
root.config = {
|
||||
version: 1,
|
||||
bar: { layout: { left: [{ id: "omarchy.hybrid" }], center: [], right: [] } },
|
||||
plugins: []
|
||||
}
|
||||
registry.setEnabled("omarchy.hybrid", false)
|
||||
root.assertDeepEqual(root.config.bar.layout.left, [], "disabling a multi-kind built-in removes its widget")
|
||||
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.hybrid"], "disabling a multi-kind built-in unloads its other kinds")
|
||||
root.assertTrue(!registry.isEnabled("omarchy.hybrid"), "a disabled multi-kind built-in is not loadable")
|
||||
|
||||
var localBase = registry.pluginsDir + "/local.clock"
|
||||
root.assertEqual(registry.localPluginIdForPath(localBase + "/BarWidget.qml"), "local.clock", "local clone changes are watched")
|
||||
root.assertEqual(registry.localPluginIdForPath(registry.pluginsDir + "/acme.clock/BarWidget.qml"), "", "installed plugins are not treated as local clones")
|
||||
root.assertEqual(registry.localPluginIdForPath(localBase + "/.git/index"), "", "clone git metadata is ignored")
|
||||
|
||||
root.assertTrue(changeCount > 0, "registry emits change notifications")
|
||||
writeResult()
|
||||
}
|
||||
|
||||
@@ -12,15 +12,21 @@ trap 'rm -rf "$TMPDIR"' EXIT
|
||||
STUB_DIR="$TMPDIR/stub"
|
||||
mkdir -p "$STUB_DIR"
|
||||
|
||||
# The picker reads the plugin list from omarchy-plugin and hands what it decided
|
||||
# back to it, so stubbing both ends shows which plugin a pick actually resolved
|
||||
# to -- the thing a source-level check cannot see.
|
||||
cat >"$STUB_DIR/omarchy-plugin" <<'STUB'
|
||||
# The picker reads the plugin list from omarchy-plugin-list and hands what it
|
||||
# decided to a verb-specific command, so stubbing both ends shows which plugin
|
||||
# a pick actually resolved to -- the thing a source-level check cannot see.
|
||||
cat >"$STUB_DIR/omarchy-plugin-list" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == list ]] && { cat "$FAKE_PLUGINS"; exit 0; }
|
||||
printf 'omarchy-plugin %s\n' "$*" >>"$FAKE_CALLS"
|
||||
cat "$FAKE_PLUGINS"
|
||||
STUB
|
||||
|
||||
for command in omarchy-plugin-enable omarchy-plugin-disable; do
|
||||
cat >"$STUB_DIR/$command" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
STUB
|
||||
done
|
||||
|
||||
# Records the rows it was offered, then answers with the pick under test.
|
||||
cat >"$STUB_DIR/omarchy-menu-select" <<'STUB'
|
||||
#!/bin/bash
|
||||
@@ -47,7 +53,8 @@ pick() {
|
||||
|
||||
: >"$TMPDIR/calls"
|
||||
: >"$TMPDIR/rows"
|
||||
PATH="$STUB_DIR:$PATH" \
|
||||
HOME="$TMPDIR/home" \
|
||||
PATH="$STUB_DIR:$PATH" \
|
||||
FAKE_PLUGINS="$TMPDIR/plugins.json" \
|
||||
FAKE_CALLS="$TMPDIR/calls" \
|
||||
FAKE_ROWS="$TMPDIR/rows" \
|
||||
@@ -58,9 +65,8 @@ pick() {
|
||||
CALLS=$(cat "$TMPDIR/calls")
|
||||
}
|
||||
|
||||
# Cloning a plugin keeps the name it was cloned from, so two plugins really can
|
||||
# arrive at the picker calling themselves Clock. Both eligible for the same
|
||||
# verb: neither row can stand on the name alone.
|
||||
# Two plugins can declare the same display name. When both are eligible for the
|
||||
# same verb, neither row can stand on the name alone.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": true},
|
||||
@@ -72,7 +78,7 @@ pick enable "Clock (local.clock)"
|
||||
[[ $ROWS == *"Clock (omarchy.clock)"* && $ROWS == *"Clock (local.clock)"* ]] \
|
||||
|| fail "picker tells two plugins of the same name apart" "$ROWS"
|
||||
pass "picker tells two plugins of the same name apart"
|
||||
[[ $CALLS == *"omarchy-plugin enable local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable local.clock"* ]] \
|
||||
|| fail "picker acts on the row that was picked, not the one that shares its name" "$CALLS"
|
||||
pass "picker acts on the row that was picked, not the one that shares its name"
|
||||
|
||||
@@ -88,12 +94,12 @@ JSON
|
||||
pick enable "Clock"
|
||||
[[ $ROWS != *"("* ]] || fail "picker adorns a row only when its name is taken twice over" "$ROWS"
|
||||
pass "picker adorns a row only when its name is taken twice over"
|
||||
[[ $CALLS == *"omarchy-plugin enable local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable local.clock"* ]] \
|
||||
|| fail "picker resolves a lone row to the plugin the verb offered, not a namesake it filtered out" "$CALLS"
|
||||
pass "picker resolves a lone row to the plugin the verb offered, not a namesake it filtered out"
|
||||
|
||||
pick remove "Clock"
|
||||
[[ $CALLS == *"omarchy-plugin remove local.clock"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-remove local.clock"* ]] \
|
||||
|| fail "picker removes the plugin whose row was picked" "$CALLS"
|
||||
pass "picker removes the plugin whose row was picked"
|
||||
|
||||
@@ -108,10 +114,40 @@ pick enable "Weather"
|
||||
[[ $ROWS == *"Weather"* && $ROWS != *"acme.weather)"* ]] \
|
||||
|| fail "picker leaves an unambiguous name unadorned" "$ROWS"
|
||||
pass "picker leaves an unambiguous name unadorned"
|
||||
[[ $CALLS == *"omarchy-plugin enable acme.weather"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable acme.weather"* ]] \
|
||||
|| fail "picker delegates plugin enablement to the plugin command" "$CALLS"
|
||||
pass "picker delegates plugin enablement to the plugin command"
|
||||
|
||||
# Clone offers only first-party plugins without an existing local counterpart,
|
||||
# then performs the clone and opens its deterministic path in $EDITOR.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": true, "active": false, "canDisable": true, "firstParty": true},
|
||||
{"id": "acme.weather", "name": "Weather", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": false}
|
||||
]
|
||||
JSON
|
||||
|
||||
pick clone "Clock"
|
||||
[[ $ROWS == *"Clock"* && $ROWS != *"Weather"* ]] ||
|
||||
fail "clone picker offers only built-in plugins" "$ROWS"
|
||||
pass "clone picker offers built-in plugins"
|
||||
[[ $CALLS == *'terminal: omarchy-plugin-clone omarchy.clock && exec $EDITOR '*"/.config/omarchy/plugins/local.clock" ]] ||
|
||||
fail "clone picker opens the cloned path in EDITOR" "$CALLS"
|
||||
pass "clone picker clones and opens the local plugin"
|
||||
|
||||
# Once local.<id> is discovered, the source no longer belongs in Clone.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
[
|
||||
{"id": "omarchy.clock", "name": "Clock", "kinds": ["bar-widget"], "enabled": true, "active": false, "canDisable": true, "firstParty": true},
|
||||
{"id": "local.clock", "name": "My Clock", "kinds": ["bar-widget"], "enabled": false, "active": false, "canDisable": true, "firstParty": false}
|
||||
]
|
||||
JSON
|
||||
|
||||
pick clone ""
|
||||
[[ $CALLS == *"notification: No plugin to clone"* ]] ||
|
||||
fail "clone picker offers an already cloned plugin" "$CALLS"
|
||||
pass "clone picker omits plugins already cloned locally"
|
||||
|
||||
# The picker treats every plugin alike and leaves kind-specific behavior to the
|
||||
# plugin command.
|
||||
cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
@@ -121,7 +157,7 @@ cat >"$TMPDIR/plugins.json" <<'JSON'
|
||||
JSON
|
||||
|
||||
pick enable "Fancy"
|
||||
[[ $CALLS == *"omarchy-plugin enable acme.fancy"* && $CALLS != *"--section"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable acme.fancy"* && $CALLS != *"--section"* ]] \
|
||||
|| fail "picker delegates kind-specific enablement" "$CALLS"
|
||||
pass "picker delegates kind-specific enablement"
|
||||
|
||||
@@ -151,7 +187,7 @@ pick enable "Bar"
|
||||
[[ $ROWS == *"Bar"* && $ROWS != *"Neon Bar"* ]] \
|
||||
|| fail "picker offers every bar except the one already running" "$ROWS"
|
||||
pass "picker offers every bar except the one already running"
|
||||
[[ $CALLS == *"omarchy-plugin enable omarchy.bar"* ]] \
|
||||
[[ $CALLS == *"omarchy-plugin-enable omarchy.bar"* ]] \
|
||||
|| fail "picker returns to the built-in bar by enabling it" "$CALLS"
|
||||
pass "picker returns to the built-in bar by enabling it"
|
||||
|
||||
|
||||
+12
-10
@@ -183,11 +183,11 @@ assertEqual(
|
||||
)
|
||||
assertDeepEqual(
|
||||
defaultItems.filter(item => item.parent === 'setup.plugin').map(item => item.label),
|
||||
['Enable Plugin', 'Disable Plugin', 'Add Plugin', 'Remove Plugin'],
|
||||
['Enable Plugin', 'Disable Plugin', 'Add Plugin', 'Clone Plugin', 'Remove Plugin'],
|
||||
'menu manages plugins from Setup > Plugins'
|
||||
)
|
||||
assert(
|
||||
['enable', 'disable', 'remove'].every(
|
||||
['enable', 'disable', 'clone', 'remove'].every(
|
||||
verb => defaultById[`setup.plugin.${verb}`].action === `omarchy-menu-plugin ${verb}`
|
||||
),
|
||||
'menu picks a plugin the way it already picks a theme or a timezone'
|
||||
@@ -201,7 +201,7 @@ assert(
|
||||
'menu hides Remove until a plugin the user installed exists to delete'
|
||||
)
|
||||
assert(
|
||||
defaultById['setup.plugin.add'].action.includes('omarchy-plugin add'),
|
||||
defaultById['setup.plugin.add'].action.includes('omarchy-plugin-add'),
|
||||
'menu adds a plugin through the CLI, where the trust warning and clone output are visible'
|
||||
)
|
||||
|
||||
@@ -212,23 +212,25 @@ assert(
|
||||
)
|
||||
assert(
|
||||
/remove\).*\(\.firstParty \| not\)/.test(pluginPicker)
|
||||
&& /clone\).*\.firstParty/.test(pluginPicker)
|
||||
&& !/kinds|bar-widget|A_BAR_OPTION|NOT_A_BAR_OPTION|BAR_ICON/.test(pluginPicker),
|
||||
'plugin picker leaves plugin-kind decisions to its data and the plugin command'
|
||||
)
|
||||
|
||||
const pluginCli = fs.readFileSync(path.join(root, 'bin/omarchy-plugin'), 'utf8')
|
||||
const pluginAdd = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-add'), 'utf8')
|
||||
const pluginEnable = fs.readFileSync(path.join(root, 'bin/omarchy-plugin-enable'), 'utf8')
|
||||
assert(
|
||||
/Now using \$id as the bar/.test(pluginCli)
|
||||
&& /enabled_message "\$id"[\s\S]*?place_bar_widget/.test(pluginCli),
|
||||
/Now using \$id as the bar/.test(pluginEnable)
|
||||
&& /Now using \$id as the bar[\s\S]*?place_bar_widget/.test(pluginAdd),
|
||||
'plugin enable reports a bar as replacing the one in use, whether enabled or freshly added'
|
||||
)
|
||||
assert(
|
||||
/\.barWidget\.defaultSection \/\/ "center"/.test(pluginCli)
|
||||
&& /gum choose[\s\S]*?--selected "\$default_section"/.test(pluginCli),
|
||||
/\.barWidget\.defaultSection \/\/ "center"/.test(pluginAdd)
|
||||
&& /gum choose[\s\S]*?--selected "\$default_section"/.test(pluginAdd),
|
||||
'interactive plugin add selects the manifest placement or center fallback by default'
|
||||
)
|
||||
assert(
|
||||
/omarchy-plugin "\$1" "\$id"/.test(pluginPicker),
|
||||
/"omarchy-plugin-\$1" "\$id"/.test(pluginPicker),
|
||||
'plugin picker delegates enable and disable without interpreting plugin kinds'
|
||||
)
|
||||
// Icons ride along as "<glyph>\tlabel"; the menu shows the glyph and hands
|
||||
@@ -245,7 +247,7 @@ assert(
|
||||
'menu select mode reads a leading icon off an option and filters on the label alone'
|
||||
)
|
||||
assert(
|
||||
/omarchy-launch-floating-terminal-with-presentation "omarchy-plugin remove/.test(pluginPicker),
|
||||
/omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove/.test(pluginPicker),
|
||||
'plugin picker removes where the confirmation and backup path are visible'
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ git -C "$incoming" add .
|
||||
git -C "$incoming" -c user.name=Test -c user.email=test@example.com commit -qm "Initial"
|
||||
|
||||
output=$(HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$stub_dir:$ROOT/bin:$PATH" \
|
||||
omarchy-plugin add "$incoming" --yes 2>&1) &&
|
||||
omarchy-plugin-add "$incoming" --yes 2>&1) &&
|
||||
fail "plugin add accepts an id already installed under another directory" "$output"
|
||||
grep -qF "plugin id 'acme.same' is already used by" <<<"$output" ||
|
||||
fail "plugin add explains the installed id collision" "$output"
|
||||
|
||||
@@ -4,45 +4,216 @@ set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
require_command jq
|
||||
require_command python3
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy"
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy" "$TMPDIR/bin"
|
||||
CALLS="$TMPDIR/calls"
|
||||
|
||||
clone() {
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" \
|
||||
omarchy-plugin-clone "$1" "$2" --name "$3"
|
||||
cat >"$TMPDIR/bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ $* == *"listShellConfig"* ]]; then
|
||||
if [[ -n ${FAKE_SHELL_CONFIG:-} ]]; then
|
||||
printf '%s\n' "$FAKE_SHELL_CONFIG"
|
||||
else
|
||||
printf '{}\n'
|
||||
fi
|
||||
elif [[ $* == *"listPlugins"* ]]; then
|
||||
if [[ ${FAKE_NO_DISCOVERY:-0} == 1 ]]; then
|
||||
printf '[]\n'
|
||||
else
|
||||
find "$HOME/.config/omarchy/plugins" -mindepth 2 -maxdepth 2 -name manifest.json -print0 |
|
||||
xargs -0 -r jq -s 'map({id: .id, enabled: true})'
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
|
||||
for command in omarchy-bar omarchy-bar-plugin; do
|
||||
cat >"$TMPDIR/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
exec "$OMARCHY_TEST_ROOT/bin/${0##*/}" "$@"
|
||||
SH
|
||||
done
|
||||
|
||||
for command in omarchy-plugin-enable omarchy-plugin-disable omarchy-notification-send; do
|
||||
cat >"$TMPDIR/bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s %s\n' "${0##*/}" "$*" >>"$FAKE_CALLS"
|
||||
SH
|
||||
done
|
||||
chmod +x "$TMPDIR/bin/"*
|
||||
|
||||
clone_plugin() {
|
||||
local default_config='{
|
||||
"bar": {
|
||||
"layout": {
|
||||
"left": [{"id": "omarchy.menu"}],
|
||||
"center": [{"id": "omarchy.clock", "format": "HH:mm"}],
|
||||
"right": []
|
||||
}
|
||||
}
|
||||
}'
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
|
||||
FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
|
||||
FAKE_SHELL_CONFIG="${FAKE_CLONE_CONFIG:-$default_config}" \
|
||||
omarchy-plugin-clone "$@"
|
||||
}
|
||||
|
||||
# A bar widget that pulls in a sibling JS module clones into a directory that
|
||||
# does not contain it, so the import has to be rewritten back to the bundled
|
||||
# file or the cloned widget fails to load.
|
||||
clone omarchy.clock local.clock-clone "Cloned Clock" >/dev/null
|
||||
widget="$TMPDIR/home/.config/omarchy/plugins/local.clock-clone/Widget.qml"
|
||||
clone_plugin omarchy.clock >/dev/null
|
||||
clock="$TMPDIR/home/.config/omarchy/plugins/local.clock"
|
||||
|
||||
[[ -f $widget ]] || fail "clone produces a widget file"
|
||||
pass "clone produces a widget file"
|
||||
|
||||
grep -q 'moduleName: "local.clock-clone"' "$widget" ||
|
||||
fail "clone rewrites the module name"
|
||||
pass "clone rewrites the module name"
|
||||
|
||||
while read -r ref; do
|
||||
[[ $ref == file://* ]] || fail "clone leaves a relative reference behind" "$ref"
|
||||
done < <(grep -oE '(import|Qt\.resolvedUrl\() *"[^"]+"' "$widget" |
|
||||
grep -oE '"[^"]+"' | tr -d '"' | grep -E '\.(js|qml)$')
|
||||
pass "clone resolves every relative QML and JS reference to the bundled file"
|
||||
|
||||
for ref in $(grep -oE 'file://[^"]+\.(js|qml)' "$widget"); do
|
||||
path=${ref#file://}
|
||||
[[ -f $path ]] || fail "cloned reference points at a real file" "$ref"
|
||||
for file in manifest.json BarWidget.qml Panel.qml Model.js; do
|
||||
[[ -f $clock/$file ]] || fail "clock clone is missing $file"
|
||||
done
|
||||
pass "cloned references point at files that exist"
|
||||
pass "clone copies the complete plugin"
|
||||
|
||||
# The bundled widget genuinely has such an import, so the check above is not
|
||||
# passing by accident.
|
||||
grep -qE '^import "[^"/][^"]*\.js"' "$ROOT/shell/plugins/panels/clock/BarWidget.qml" ||
|
||||
fail "the clock widget still imports a sibling JS module"
|
||||
pass "the clock widget still imports a sibling JS module"
|
||||
grep -q 'import "Model.js"' "$clock/BarWidget.qml" &&
|
||||
grep -q 'Qt.resolvedUrl("Panel.qml")' "$clock/BarWidget.qml" ||
|
||||
fail "clock clone does not preserve local dependencies"
|
||||
pass "clone keeps plugin dependencies local"
|
||||
|
||||
rg -qF "omarchy.clock" "$clock" -g '*.qml' -g '*.js' ||
|
||||
fail "clock clone does not preserve the stable runtime id"
|
||||
pass "clone preserves the built-in runtime IPC id"
|
||||
|
||||
jq -e '
|
||||
.id == "local.clock" and
|
||||
.name == "My Clock" and
|
||||
.barWidget.displayName == "My Clock" and
|
||||
.omarchy.clonedFrom == "omarchy.clock" and
|
||||
.kinds == ["bar-widget"] and
|
||||
.entryPoints.barWidget == "BarWidget.qml"
|
||||
' "$clock/manifest.json" >/dev/null || fail "clock clone manifest is incorrect"
|
||||
pass "clone updates identity without replacing the manifest"
|
||||
|
||||
grep -qx 'omarchy-bar-plugin replace omarchy.clock local.clock' "$CALLS" ||
|
||||
fail "clone does not replace an active bar widget"
|
||||
grep -qx 'omarchy-notification-send -g Editing Cloned Plugin Original plugin has been replace by clone.' "$CALLS" ||
|
||||
fail "clone does not notify that the editable clone is active"
|
||||
jq -e '
|
||||
any(.bar.layout.center[];
|
||||
.id == "local.clock" and
|
||||
.format == "dddd HH:mm" and
|
||||
.formatAlt == "d MMMM \u0027W\u0027ww yyyy")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not preserve the active widget's placement and settings"
|
||||
pass "clone switches bar widgets in place and confirms the editable clone"
|
||||
|
||||
jq '.bar.layout.center += ["omarchy.keyboard-layout"]' \
|
||||
"$TMPDIR/home/.config/omarchy/shell.json" >"$TMPDIR/shell.json"
|
||||
mv "$TMPDIR/shell.json" "$TMPDIR/home/.config/omarchy/shell.json"
|
||||
FAKE_CLONE_CONFIG='{
|
||||
"bar": {
|
||||
"layout": {
|
||||
"left": [],
|
||||
"center": ["omarchy.keyboard-layout"],
|
||||
"right": []
|
||||
}
|
||||
}
|
||||
}' clone_plugin omarchy.keyboard-layout >/dev/null
|
||||
jq -e '
|
||||
any(.bar.layout.center[]; .id == "local.keyboard-layout")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not replace a string-form bar entry"
|
||||
pass "clone replaces legacy string-form bar entries"
|
||||
|
||||
clone_plugin omarchy.menu >/dev/null
|
||||
menu="$TMPDIR/home/.config/omarchy/plugins/local.menu"
|
||||
|
||||
for file in manifest.json Menu.qml MenuModel.js BarWidget.qml; do
|
||||
[[ -f $menu/$file ]] || fail "menu clone is missing $file"
|
||||
done
|
||||
jq -e '
|
||||
.id == "local.menu" and
|
||||
.kinds == ["menu", "bar-widget"] and
|
||||
.entryPoints.menu == "Menu.qml" and
|
||||
.entryPoints.barWidget == "BarWidget.qml"
|
||||
' "$menu/manifest.json" >/dev/null || fail "menu clone loses plugin kinds"
|
||||
grep -qx 'omarchy-plugin-disable omarchy.menu' "$CALLS" ||
|
||||
fail "clone leaves the built-in half of a multi-kind plugin enabled"
|
||||
pass "clone preserves multi-kind plugins"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
|
||||
FAKE_CALLS="$CALLS" OMARCHY_TEST_ROOT="$ROOT" \
|
||||
omarchy-plugin-remove local.menu --yes >/dev/null
|
||||
grep -qx 'omarchy-bar-plugin replace local.menu omarchy.menu' "$CALLS" &&
|
||||
grep -qx 'omarchy-plugin-enable omarchy.menu' "$CALLS" ||
|
||||
fail "removing a clone does not restore its built-in source"
|
||||
pass "removing a clone restores its built-in source"
|
||||
|
||||
clone_plugin omarchy.active-window >/dev/null
|
||||
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.active-window/ActiveWindow.qml ]] ||
|
||||
fail "flat bar plugin clone is incomplete"
|
||||
pass "flat bar plugins clone from adjacent manifests"
|
||||
|
||||
grep -qx 'omarchy-bar-plugin add local.active-window' "$CALLS" ||
|
||||
fail "clone does not activate a bar widget whose source is absent"
|
||||
jq -e '
|
||||
any(.bar.layout.left[]; .id == "local.active-window")
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not add the absent widget using its default placement"
|
||||
pass "clone activates an absent bar widget"
|
||||
|
||||
clone_plugin omarchy.indicators >/dev/null
|
||||
indicators="$TMPDIR/home/.config/omarchy/plugins/local.indicators"
|
||||
for file in Indicators.qml indicators/Dnd.qml indicators/Reminder.qml; do
|
||||
[[ -f $indicators/$file ]] || fail "indicators clone is missing $file"
|
||||
done
|
||||
grep -q 'Qt.resolvedUrl("indicators/"' "$indicators/Indicators.qml" ||
|
||||
fail "indicators clone does not point at its copied components"
|
||||
pass "flat bar plugins declare extra clone dependencies"
|
||||
|
||||
clone_plugin omarchy.tray >/dev/null
|
||||
[[ -f $TMPDIR/home/.config/omarchy/plugins/local.tray/TrayModel.js ]] ||
|
||||
fail "tray clone is missing its model"
|
||||
pass "flat bar plugins keep local script dependencies"
|
||||
|
||||
clone_plugin omarchy.bar >/dev/null
|
||||
grep -qx 'omarchy-bar use local.bar' "$CALLS" ||
|
||||
fail "clone does not select a cloned bar"
|
||||
jq -e '.bar.id == "local.bar"' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null ||
|
||||
fail "clone does not persist the cloned bar as active"
|
||||
pass "clone switches full bars"
|
||||
|
||||
clone_plugin omarchy.background >/dev/null
|
||||
grep -qx 'omarchy-plugin-enable local.background' "$CALLS" ||
|
||||
fail "clone does not enable an ordinary cloned plugin"
|
||||
grep -qx 'omarchy-plugin-disable omarchy.background' "$CALLS" ||
|
||||
fail "clone does not disable the ordinary source plugin"
|
||||
pass "clone switches ordinary plugins"
|
||||
|
||||
mkdir -p "$TMPDIR/home/.config/omarchy/plugins/acme.example"
|
||||
cat >"$TMPDIR/home/.config/omarchy/plugins/acme.example/manifest.json" <<'JSON'
|
||||
{"id":"acme.example","name":"Example","kinds":["bar-widget"],"entryPoints":{"barWidget":"Widget.qml"}}
|
||||
JSON
|
||||
if clone_plugin acme.example >/dev/null 2>&1; then
|
||||
fail "clone accepts a user plugin"
|
||||
fi
|
||||
pass "clone is limited to built-in plugins"
|
||||
|
||||
if clone_plugin omarchy.weather custom.weather >/dev/null 2>&1; then
|
||||
fail "clone accepts a custom id"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.weather ]] ||
|
||||
fail "rejected custom id leaves a clone behind"
|
||||
pass "clone derives the local id"
|
||||
|
||||
if clone_plugin omarchy.weather --replace >/dev/null 2>&1; then
|
||||
fail "clone still accepts bar layout actions"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.weather ]] ||
|
||||
fail "rejected bar action leaves a clone behind"
|
||||
pass "clone does not accept manual switch options"
|
||||
|
||||
if clone_plugin >/dev/null 2>&1; then
|
||||
fail "clone opens an interactive picker without a source id"
|
||||
fi
|
||||
pass "clone requires an explicit source id"
|
||||
|
||||
if FAKE_NO_DISCOVERY=1 clone_plugin omarchy.osd >/dev/null 2>&1; then
|
||||
fail "clone succeeds before the shell discovers it"
|
||||
fi
|
||||
[[ ! -e $TMPDIR/home/.config/omarchy/plugins/local.osd ]] ||
|
||||
fail "failed clone discovery leaves a partial clone behind"
|
||||
pass "clone removes a partial clone when switching fails"
|
||||
|
||||
@@ -156,6 +156,33 @@ for (const manifestPath of manifests) {
|
||||
if (relativePath.endsWith('.manifest.json')) {
|
||||
check(JSON.stringify(manifest.kinds) === JSON.stringify(['bar-widget']), `${manifest.id} sibling manifest must be a bar widget`)
|
||||
}
|
||||
|
||||
const clonePaths = manifest.omarchy?.clonePaths
|
||||
if (clonePaths !== undefined) {
|
||||
check(Array.isArray(clonePaths), `${manifest.id} omarchy.clonePaths must be an array`)
|
||||
const cloneTargets = new Set()
|
||||
for (const clonePath of Array.isArray(clonePaths) ? clonePaths : []) {
|
||||
const valid = isPlainObject(clonePath)
|
||||
&& typeof clonePath.source === 'string'
|
||||
&& /^[A-Za-z0-9_./-]+$/.test(clonePath.source)
|
||||
&& typeof clonePath.target === 'string'
|
||||
&& /^[A-Za-z0-9_./-]+$/.test(clonePath.target)
|
||||
&& !clonePath.target.startsWith('/')
|
||||
&& !clonePath.target.includes('..')
|
||||
check(
|
||||
valid,
|
||||
`${manifest.id} clone paths must have safe source and target paths`
|
||||
)
|
||||
if (valid) {
|
||||
check(
|
||||
fs.existsSync(path.resolve(path.dirname(manifestPath), clonePath.source)),
|
||||
`${manifest.id} clone source ${clonePath.source} must exist`
|
||||
)
|
||||
check(!cloneTargets.has(clonePath.target), `${manifest.id} clone target ${clonePath.target} must be unique`)
|
||||
cloneTargets.add(clonePath.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const byId = Object.fromEntries(manifests.map(manifestPath => {
|
||||
|
||||
@@ -53,6 +53,28 @@ cp -a "$ROOT/shell" "$test_root/shell"
|
||||
ln -s "$ROOT/config" "$test_root/config"
|
||||
ln -s "$ROOT/bin" "$test_root/bin"
|
||||
|
||||
hot_reload_dir="$test_home/.config/omarchy/plugins/local.hot-reload"
|
||||
mkdir -p "$hot_reload_dir"
|
||||
cat >"$hot_reload_dir/manifest.json" <<'JSON'
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "local.hot-reload",
|
||||
"name": "Before Hot Reload",
|
||||
"version": "1.0.0",
|
||||
"kinds": ["overlay"],
|
||||
"entryPoints": {"overlay": "Overlay.qml"},
|
||||
"omarchy": {"clonedFrom": "omarchy.emojis"}
|
||||
}
|
||||
JSON
|
||||
cat >"$hot_reload_dir/Overlay.qml" <<'QML'
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
function open(payloadJson) {}
|
||||
function close() {}
|
||||
}
|
||||
QML
|
||||
|
||||
cat >"$stub_bin/omarchy-update-available" <<'SH'
|
||||
#!/bin/bash
|
||||
echo "Omarchy update available (test)"
|
||||
@@ -116,6 +138,31 @@ jq -e '
|
||||
}
|
||||
pass "shell IPC lists plugin metadata"
|
||||
|
||||
jq '.name = "After Hot Reload"' "$hot_reload_dir/manifest.json" >"$hot_reload_dir/manifest.json.tmp"
|
||||
mv "$hot_reload_dir/manifest.json.tmp" "$hot_reload_dir/manifest.json"
|
||||
|
||||
hot_reload_name=""
|
||||
for _ in {1..80}; do
|
||||
hot_reload_name=$(shell_ipc shell listPlugins 2>/dev/null |
|
||||
jq -r '.[] | select(.id == "local.hot-reload") | .name' 2>/dev/null || true)
|
||||
[[ $hot_reload_name == "After Hot Reload" ]] && break
|
||||
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||
fail_with_log "test shell exited while reloading a changed local clone"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
[[ $hot_reload_name == "After Hot Reload" ]] ||
|
||||
fail_with_log "local clone changes reload without an explicit rescan"
|
||||
pass "local clone changes reload without an explicit rescan"
|
||||
|
||||
[[ $(shell_ipc shell setPluginEnabled local.hot-reload true) == "ok" ]] ||
|
||||
fail_with_log "local clone could not be enabled"
|
||||
[[ $(shell_ipc shell summon omarchy.emojis "{}") == "ok" ]] ||
|
||||
fail_with_log "calls to a cloned source id do not reach its enabled clone"
|
||||
shell_ipc_quiet shell hide omarchy.emojis >/dev/null
|
||||
shell_ipc_quiet shell setPluginEnabled local.hot-reload false >/dev/null
|
||||
pass "shell IPC routes built-in ids to enabled clones"
|
||||
|
||||
shell_config=$(shell_ipc shell listShellConfig)
|
||||
jq -e '
|
||||
.version == 1 and
|
||||
|
||||
Reference in New Issue
Block a user