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:
+18
-10
@@ -100,7 +100,7 @@ manifest id); updating is a fast-forward pull of that checkout.
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git
|
||||
omarchy plugin update acme.weather # fetches, shows a diff, fast-forwards
|
||||
omarchy plugin update --all
|
||||
omarchy plugin update # updates every git-managed plugin
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ AI agents:
|
||||
|
||||
```bash
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable --yes
|
||||
omarchy plugin update --all --yes
|
||||
omarchy plugin update --yes
|
||||
```
|
||||
|
||||
The installer never runs plugin code, install hooks, or sudo — it only clones
|
||||
@@ -130,26 +130,34 @@ You can still drop a plugin in without git:
|
||||
|
||||
1. Put it in `~/.config/omarchy/plugins/<plugin-id>/` with a `manifest.json`
|
||||
plus the QML referenced from its `entryPoints`.
|
||||
2. `omarchy plugin rescan`.
|
||||
2. `omarchy-shell shell rescanPlugins`.
|
||||
3. `omarchy plugin enable <id>`. Bar widgets start in
|
||||
`barWidget.defaultSection`, or in the center when it is omitted, and can be
|
||||
moved with `omarchy bar plugin move`; a full bar replaces the one in use.
|
||||
|
||||
The lower-level IPC equivalents remain available via `omarchy-shell shell rescanPlugins`,
|
||||
`omarchy-shell shell setPluginEnabled <id> true`, and `omarchy-shell shell listPlugins`.
|
||||
The `omarchy plugin` command wraps those calls and can also edit the persisted
|
||||
The `omarchy plugin` commands wrap those calls and can also edit the persisted
|
||||
bar layout in `shell.json`.
|
||||
|
||||
To hack on an existing widget safely, clone it into a user plugin instead of
|
||||
editing the built-in source. Third-party ids must be namespaced and may not use
|
||||
the reserved `omarchy.*` prefix.
|
||||
To hack on a built-in plugin safely, clone it into user config instead of
|
||||
editing the built-in source. The complete plugin directory is copied, including
|
||||
every declared kind and local dependency. A built-in id such as
|
||||
`omarchy.clock` becomes `local.clock`, with `My Clock` as its display name.
|
||||
|
||||
```bash
|
||||
omarchy plugin clone omarchy.clock local.clock --replace
|
||||
omarchy plugin clone # interactive source/name picker
|
||||
omarchy plugin edit local.clock # cd into the plugin directory
|
||||
omarchy plugin clone omarchy.clock
|
||||
```
|
||||
|
||||
Cloning switches from the built-in to the new local plugin, preserving an
|
||||
existing bar widget's position and settings. Setup > Plugins > Clone provides
|
||||
the interactive picker, then opens the new `local.*` directory in `$EDITOR`.
|
||||
Existing shortcuts and shell IPC calls made to the built-in id are routed to
|
||||
the enabled clone, so cloning does not require changing its callers. Removing
|
||||
an active clone switches back to its built-in source.
|
||||
Saving a file anywhere inside a `local.*` plugin reloads plugin code
|
||||
automatically; `omarchy-shell shell rescanPlugins` remains available to force a reload.
|
||||
|
||||
First-party plugins under `shell/plugins/` are discovered the same way and load
|
||||
by default. Disabling a non-widget records it in `disabledPlugins[]`; disabling
|
||||
a widget removes it from the bar layout while leaving its component available
|
||||
|
||||
@@ -180,4 +180,4 @@ Third-party widgets ship as separate plugins under
|
||||
declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See
|
||||
[../../README.md](../../README.md) for the manifest schema. Enable,
|
||||
rescan, and place third-party plugins with `omarchy plugin enable`,
|
||||
`omarchy plugin rescan`, and `omarchy bar plugin add`.
|
||||
`omarchy-shell shell rescanPlugins`, and `omarchy bar plugin add`.
|
||||
|
||||
@@ -66,5 +66,13 @@
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"omarchy": {
|
||||
"clonePaths": [
|
||||
{
|
||||
"source": "../indicators",
|
||||
"target": "indicators"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,13 @@
|
||||
"description": "Status notifier items",
|
||||
"category": "Status",
|
||||
"allowMultiple": false
|
||||
},
|
||||
"omarchy": {
|
||||
"clonePaths": [
|
||||
{
|
||||
"source": "TrayModel.js",
|
||||
"target": "TrayModel.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ QtObject {
|
||||
signal pluginsChanged()
|
||||
signal scanFinished()
|
||||
signal pluginLoadFailed(string id, string error)
|
||||
signal localPluginChanged(string id)
|
||||
|
||||
// ---------------------------------------------------------------- helpers
|
||||
|
||||
@@ -141,6 +142,19 @@ QtObject {
|
||||
&& config.disabledPlugins.indexOf(Util.canonicalWidgetId(String(id))) !== -1
|
||||
}
|
||||
|
||||
function resolveEnabledId(id) {
|
||||
var key = Util.canonicalWidgetId(String(id || ""))
|
||||
// Callers keep using the built-in id after cloning; the enabled local
|
||||
// manifest is the implementation that should receive the call.
|
||||
for (var candidate in installedPlugins) {
|
||||
var manifest = installedPlugins[candidate]
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
if (metadata && String(metadata.clonedFrom || "") === key && isEnabled(candidate))
|
||||
return candidate
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// A bar widget is on when it sits in the bar, whoever shipped it. That is a
|
||||
// different question from isEnabled(), which decides whether the widget's
|
||||
// component is loaded at all — a built-in stays loadable so it can be put
|
||||
@@ -200,6 +214,8 @@ QtObject {
|
||||
}
|
||||
var isBarOption = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1
|
||||
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||
var hasNonWidgetKind = manifest && Array.isArray(manifest.kinds)
|
||||
&& manifest.kinds.some(function(kind) { return kind !== "bar-widget" })
|
||||
shellConfigMutator(function(config) {
|
||||
// Ensure shape exists.
|
||||
if (!Util.isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
|
||||
@@ -241,7 +257,7 @@ QtObject {
|
||||
else if (location.kind === "plugin") config.plugins.splice(location.index, 1)
|
||||
// Dropping the layout entry is the whole story for a widget. Anything
|
||||
// else built-in loads by default, so switching it off has to be stated.
|
||||
if (isFirstParty && !isBarWidget && !isDisabled(config, key)) {
|
||||
if (isFirstParty && (!isBarWidget || hasNonWidgetKind) && !isDisabled(config, key)) {
|
||||
if (!Array.isArray(config.disabledPlugins)) config.disabledPlugins = []
|
||||
config.disabledPlugins.push(key)
|
||||
}
|
||||
@@ -337,7 +353,36 @@ QtObject {
|
||||
}
|
||||
|
||||
property Process initProcess: Process {
|
||||
onExited: registry.rescan()
|
||||
onExited: {
|
||||
localPluginWatcher.running = true
|
||||
registry.rescan()
|
||||
}
|
||||
}
|
||||
|
||||
property Process localPluginWatcher: Process {
|
||||
command: [
|
||||
"inotifywait",
|
||||
"-m",
|
||||
"-r",
|
||||
"-q",
|
||||
"-e",
|
||||
"close_write,create,delete,move",
|
||||
"--format",
|
||||
"%w%f",
|
||||
registry.pluginsDir
|
||||
]
|
||||
stdout: SplitParser {
|
||||
onRead: function(path) {
|
||||
var pluginId = registry.localPluginIdForPath(path)
|
||||
if (pluginId) registry.localPluginChanged(pluginId)
|
||||
}
|
||||
}
|
||||
onExited: localPluginWatcherRestart.restart()
|
||||
}
|
||||
|
||||
property Timer localPluginWatcherRestart: Timer {
|
||||
interval: 1000
|
||||
onTriggered: localPluginWatcher.running = true
|
||||
}
|
||||
|
||||
function rescan() {
|
||||
@@ -379,5 +424,17 @@ QtObject {
|
||||
initProcess.running = true
|
||||
}
|
||||
|
||||
function localPluginIdForPath(filePath) {
|
||||
var base = pluginsDir.replace(/\/$/, "") + "/"
|
||||
var path = String(filePath || "").trim()
|
||||
if (path.indexOf(base + "local.") !== 0) return ""
|
||||
|
||||
var relative = path.slice(base.length)
|
||||
if (relative.indexOf("/.git/") !== -1 || relative.endsWith("/.git")) return ""
|
||||
|
||||
var slash = relative.indexOf("/")
|
||||
return slash === -1 ? relative : relative.slice(0, slash)
|
||||
}
|
||||
|
||||
Component.onCompleted: ensureUserDir()
|
||||
}
|
||||
|
||||
+17
-6
@@ -58,6 +58,12 @@ ShellRoot {
|
||||
property bool pluginReloading: false
|
||||
property bool pluginReloadPending: false
|
||||
|
||||
Timer {
|
||||
id: localPluginReloadTimer
|
||||
interval: 150
|
||||
onTriggered: shell.reloadPlugins()
|
||||
}
|
||||
|
||||
onShellConfigChanged: {
|
||||
if (failedBarId !== "") failedBarId = ""
|
||||
pluginRegistry.registryRevision++
|
||||
@@ -440,7 +446,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function summon(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (!id) return false
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
if (!plugins[id]) {
|
||||
@@ -480,7 +486,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function hide(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (!id) return false
|
||||
if (shell.isBarWidgetPanelPlugin(id)) {
|
||||
var hidden = shell.bar && typeof shell.bar.hideBarWidget === "function"
|
||||
@@ -497,7 +503,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function isPluginOpen(pluginId) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
if (shell.isBarWidgetPanelPlugin(id)) {
|
||||
return shell.bar && typeof shell.bar.isBarWidgetOpen === "function"
|
||||
? shell.bar.isBarWidgetOpen(id)
|
||||
@@ -510,7 +516,7 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function toggle(pluginId, payloadJson) {
|
||||
var id = String(pluginId || "")
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
return isPluginOpen(id) ? hide(id) : summon(id, payloadJson)
|
||||
}
|
||||
|
||||
@@ -567,14 +573,15 @@ ShellRoot {
|
||||
}
|
||||
|
||||
function callIfLoaded(pluginId, method, arg) {
|
||||
var loader = panelLoaders[pluginId]
|
||||
var id = shell.pluginRegistry.resolveEnabledId(pluginId)
|
||||
var loader = panelLoaders[id]
|
||||
if (!loader || !loader.item) return "unknown"
|
||||
if (typeof loader.item[method] !== "function") return "unknown"
|
||||
try {
|
||||
var result = loader.item[method](arg)
|
||||
return result === undefined || result === null ? "ok" : String(result)
|
||||
} catch (e) {
|
||||
console.warn("plugin " + pluginId + " " + method + "() threw:", e)
|
||||
console.warn("plugin " + id + " " + method + "() threw:", e)
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
@@ -761,6 +768,10 @@ ShellRoot {
|
||||
|
||||
Connections {
|
||||
target: shell.pluginRegistry
|
||||
function onLocalPluginChanged(pluginId) {
|
||||
console.log("Local plugin changed, reloading:", pluginId)
|
||||
localPluginReloadTimer.restart()
|
||||
}
|
||||
function onScanFinished() {
|
||||
if (shell.pluginReloadPending) {
|
||||
shell.pluginReloadPending = false
|
||||
|
||||
Reference in New Issue
Block a user