* 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
236 lines
12 KiB
QML
236 lines
12 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import "services"
|
|
|
|
ShellRoot {
|
|
id: root
|
|
|
|
readonly property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
|
property var failures: []
|
|
property int changeCount: 0
|
|
property var config: ({
|
|
version: 1,
|
|
bar: { layout: { left: [], center: [], right: [] } },
|
|
plugins: []
|
|
})
|
|
|
|
function fail(message) {
|
|
failures.push(String(message))
|
|
}
|
|
|
|
function assertTrue(condition, message) {
|
|
if (!condition) fail(message)
|
|
}
|
|
|
|
function assertEqual(actual, expected, message) {
|
|
if (actual !== expected) fail(message + " expected=" + expected + " actual=" + actual)
|
|
}
|
|
|
|
function assertDeepEqual(actual, expected, message) {
|
|
var actualJson = JSON.stringify(actual)
|
|
var expectedJson = JSON.stringify(expected)
|
|
if (actualJson !== expectedJson) fail(message + " expected=" + expectedJson + " actual=" + actualJson)
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
|
}
|
|
|
|
function writeResult() {
|
|
var payload = JSON.stringify({
|
|
ok: failures.length === 0,
|
|
failures: failures,
|
|
changeCount: changeCount,
|
|
config: config,
|
|
ids: Object.keys(registry.installedPlugins).sort()
|
|
})
|
|
|
|
if (resultPath) {
|
|
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
|
|
}
|
|
}
|
|
|
|
function manifest(id, kinds, entryPoints, barWidget) {
|
|
var value = {
|
|
schemaVersion: 1,
|
|
id: id,
|
|
name: id,
|
|
version: "1.0.0",
|
|
kinds: kinds,
|
|
entryPoints: entryPoints
|
|
}
|
|
if (barWidget) value.barWidget = barWidget
|
|
return value
|
|
}
|
|
|
|
function block(kind, source, payload) {
|
|
return "===" + kind + "::" + source + "===\n"
|
|
+ (typeof payload === "string" ? payload : JSON.stringify(payload))
|
|
+ "\n=== EOM ===\n"
|
|
}
|
|
|
|
function has(id) {
|
|
return registry.installedPlugins[String(id)] !== undefined
|
|
}
|
|
|
|
function pluginIds() {
|
|
return Object.keys(registry.installedPlugins).sort()
|
|
}
|
|
|
|
function runChecks() {
|
|
var scan = ""
|
|
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" }))
|
|
scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" }))
|
|
scan += block("thirdparty", "/third/missing", { schemaVersion: 1, id: "third.missing", name: "missing", version: "1.0.0", kinds: ["panel"] })
|
|
scan += block("thirdparty", "/third/bad-section", manifest("third.bad-section", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "bottom" }))
|
|
scan += block("thirdparty", "/third/schema", { schemaVersion: 2, id: "third.schema", name: "schema", version: "1.0.0", kinds: ["panel"], entryPoints: { panel: "Panel.qml" } })
|
|
scan += block("thirdparty", "/third/bad-json", "{")
|
|
|
|
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"
|
|
], "registry merges valid first-party and third-party manifests")
|
|
|
|
root.assertTrue(registry.installedPlugins["omarchy.first-widget"].__isFirstParty === true, "first-party manifests are stamped")
|
|
root.assertTrue(registry.installedPlugins["third.panel"].__isFirstParty === false, "third-party manifests are stamped")
|
|
root.assertEqual(registry.installedPlugins["omarchy.grouped-panel"].__sourceDir, "/first/panels/grouped", "grouped plugin source paths are preserved")
|
|
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.panel"], "panel"), "file:///third/panel/Panel.qml", "entryPointUrl resolves plugin-relative paths")
|
|
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.widget"], "barWidget"), "file:///third/widget/Widget.qml", "entryPointUrl resolves bar widget paths")
|
|
|
|
root.assertTrue(!has("omarchy.reserved"), "third-party omarchy namespace ids are rejected")
|
|
root.assertTrue(!has("third.unsafe"), "unsafe entry points are rejected")
|
|
root.assertTrue(!has("third.missing"), "incomplete manifests are rejected")
|
|
root.assertTrue(!has("third.bad-section"), "invalid default bar widget sections are rejected")
|
|
root.assertTrue(!has("third.schema"), "unsupported schema versions are rejected")
|
|
|
|
root.assertTrue(registry.isEnabled("omarchy.first-widget"), "first-party plugins are implicitly enabled")
|
|
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")
|
|
root.assertTrue(registry.isEnabled("third.bar"), "selected third-party bar options are enabled")
|
|
root.assertTrue(!registry.isEnabled("omarchy.bar"), "selecting third-party bar options deactivates built-in bar")
|
|
registry.setEnabled("third.bar", false)
|
|
root.assertTrue(root.config.bar.id === undefined, "disabling active bar options resets to built-in")
|
|
root.assertTrue(registry.isEnabled("omarchy.bar"), "built-in bar option returns after reset")
|
|
|
|
registry.setEnabled("third.panel", true)
|
|
root.assertDeepEqual(root.config.plugins, [{ id: "third.panel" }], "enabling third-party panels writes plugins array")
|
|
root.assertTrue(registry.isEnabled("third.panel"), "enabled third-party panels are found")
|
|
registry.setEnabled("third.panel", false)
|
|
root.assertDeepEqual(root.config.plugins, [], "disabling third-party panels removes plugins array entry")
|
|
|
|
registry.setEnabled("third.widget", true)
|
|
root.assertDeepEqual(root.config.bar.layout.left, [{ id: "third.widget" }], "enabling bar widgets uses their default section")
|
|
root.assertTrue(registry.isEnabled("third.widget"), "enabled bar widgets are found")
|
|
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: [] } },
|
|
plugins: []
|
|
}
|
|
root.assertTrue(registry.isEnabled("third.widget"), "existing layout entries enable bar widgets")
|
|
registry.setEnabled("third.widget", false)
|
|
root.assertDeepEqual(root.config.bar.layout.center, [], "disabling existing bar widgets removes the original layout entry")
|
|
|
|
root.config = { version: 1 }
|
|
registry.setEnabled("third.panel", true)
|
|
root.assertDeepEqual(root.config.plugins, [{ id: "third.panel" }], "setEnabled repairs missing plugin config shape")
|
|
|
|
// A built-in loads by default, so switching one off is recorded the other
|
|
// way round and has to survive round-tripping back on.
|
|
root.config = { version: 1, bar: { layout: { left: [], center: [], right: [] } }, plugins: [] }
|
|
registry.setEnabled("omarchy.grouped-panel", false)
|
|
root.assertDeepEqual(root.config.disabledPlugins, ["omarchy.grouped-panel"], "disabling a first-party plugin records it")
|
|
root.assertTrue(!registry.isEnabled("omarchy.grouped-panel"), "a recorded first-party plugin is disabled")
|
|
root.assertDeepEqual(root.config.plugins, [], "disabling a first-party plugin leaves the plugins array alone")
|
|
registry.setEnabled("omarchy.grouped-panel", true)
|
|
root.assertTrue(root.config.disabledPlugins === undefined, "re-enabling drops the disabled record entirely")
|
|
root.assertTrue(registry.isEnabled("omarchy.grouped-panel"), "a first-party plugin returns to enabled")
|
|
root.assertDeepEqual(root.config.plugins, [], "re-enabling a first-party plugin adds no redundant entry")
|
|
|
|
// A widget's place in the bar is its on/off switch. Loadability must not
|
|
// follow it down, or a plugin that is both widget and menu (omarchy.menu)
|
|
// would be locked out of the shell by taking its button off the bar.
|
|
root.config = {
|
|
version: 1,
|
|
bar: { layout: { left: [], center: [], right: [{ id: "omarchy.first-widget" }] } },
|
|
plugins: []
|
|
}
|
|
root.assertTrue(registry.inBar("omarchy.first-widget"), "inBar sees a widget in the layout")
|
|
registry.setEnabled("omarchy.first-widget", false)
|
|
root.assertDeepEqual(root.config.bar.layout.right, [], "disabling a first-party widget removes its layout entry")
|
|
root.assertTrue(root.config.disabledPlugins === undefined, "disabling a first-party widget records nothing else")
|
|
root.assertTrue(!registry.inBar("omarchy.first-widget"), "inBar follows the widget out of the layout")
|
|
root.assertTrue(registry.isEnabled("omarchy.first-widget"), "a first-party widget stays loadable off the bar")
|
|
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()
|
|
}
|
|
|
|
PluginRegistry {
|
|
id: registry
|
|
firstPartyDir: ""
|
|
pluginsDir: Quickshell.env("HOME") + "/.config/omarchy/plugins"
|
|
shellConfigProvider: function() { return root.config }
|
|
shellConfigMutator: function(mutator) {
|
|
var next = JSON.parse(JSON.stringify(root.config || {}))
|
|
mutator(next)
|
|
root.config = next
|
|
}
|
|
onPluginsChanged: root.changeCount++
|
|
}
|
|
|
|
Timer {
|
|
interval: 100
|
|
running: true
|
|
repeat: false
|
|
onTriggered: root.runChecks()
|
|
}
|
|
}
|