From 4e8bf99240b8612e45810f854a9a02ef500479f3 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Wed, 13 May 2026 18:03:01 -0400 Subject: [PATCH] Polish docs and harden plugin loading --- AGENTS.md | 39 +++++ bin/omarchy-shell-ipc | 36 ++++- default/quickshell/omarchy-shell/README.md | 153 ++++++++++++++++++ .../omarchy-shell/plugins/README.md | 48 ++++++ .../BackgroundSwitcher.qml | 44 ++++- .../plugins/bar-settings/BarSettingsPanel.qml | 45 +++++- .../omarchy-shell/plugins/bar/Bar.qml | 2 +- .../omarchy-shell/plugins/bar/README.md | 17 +- .../omarchy-shell/services/PluginRegistry.qml | 41 ++++- .../quickshell/omarchy-shell/services/qmldir | 3 - default/quickshell/omarchy-shell/shell.qml | 23 +-- .../omarchy-shell/ui/settings/qmldir | 2 - 12 files changed, 414 insertions(+), 39 deletions(-) create mode 100644 default/quickshell/omarchy-shell/README.md create mode 100644 default/quickshell/omarchy-shell/plugins/README.md delete mode 100644 default/quickshell/omarchy-shell/services/qmldir delete mode 100644 default/quickshell/omarchy-shell/ui/settings/qmldir diff --git a/AGENTS.md b/AGENTS.md index 617ef28d..4a9c3d71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,6 +101,45 @@ For interactive UI work, use `wtype` to simulate keyboard input when available. When testing layer-shell UI, capture the reference and candidate states as separate screenshots, then compare them visually before further edits. If a launched UI would otherwise remain open, keep track of its PID and stop it after the screenshot; avoid broad process kills unless checking with `ps` first. +# Omarchy shell + +The Quickshell desktop runs as a single long-running process out of +`default/quickshell/omarchy-shell/`. Hyprland's autostart launches it; do +not start additional standalone `quickshell -p` instances for individual +components. + +Plugin contract: + +- Each plugin lives in its own directory under + `default/quickshell/omarchy-shell/plugins//` (first-party) or + `~/.config/omarchy/plugins//` (third-party). +- Every plugin ships a `manifest.json` declaring `id`, `kinds`, + `activation`, and `entryPoints`. The full schema is in + `default/quickshell/omarchy-shell/README.md`. +- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the + shell-injected properties `omarchyPath`, `shell`, `manifest`, and + `pluginRegistry` / `barWidgetRegistry` as appropriate. +- Panel / overlay / menu plugins must expose `open(payloadJson)` and + `close()` lifecycle methods for `shell summon` and `shell hide`. + +IPC: + +- `bin/omarchy-shell-ipc` is the canonical IPC entry point. It starts + the shell on first call, then forwards to `quickshell ipc call`. + Prefer it over re-implementing the wait-for-instance dance in every + CLI. +- The `shell` IPC target exposes `ping`, `summon`, `hide`, `toggle`, + `rescanPlugins`, `setPluginEnabled`, and `listPlugins`. Individual + plugins can register additional IPC targets (the bar registers `bar`, + the background switcher registers `image-selector`). + +Widget files in `plugins/bar/widgets/` contain Nerd Font glyphs as raw +unicode characters. The `Write` and `Edit` tools strip multi-byte +codepoints in some positions — do **not** rewrite widget files wholesale +through those tools. For glyph fixes, use the targeted `Edit` tool with +the surrounding context, or a Python script that inserts codepoints via +`chr(0xXXXXX)`. + # Refresh Pattern To copy a default config to user config with automatic backup: diff --git a/bin/omarchy-shell-ipc b/bin/omarchy-shell-ipc index a58644bd..6ecec8a0 100755 --- a/bin/omarchy-shell-ipc +++ b/bin/omarchy-shell-ipc @@ -3,15 +3,37 @@ # omarchy:summary=Send an IPC call to omarchy-shell, starting it if not running # omarchy:hidden=true +if [[ $# -eq 0 || $1 == "-h" || $1 == "--help" ]]; then + cat < [args...] + +Starts omarchy-shell if not running, then forwards a quickshell ipc call. + +Examples: + omarchy-shell-ipc shell ping + omarchy-shell-ipc shell summon omarchy.bar-settings "{}" + omarchy-shell-ipc shell hide omarchy.background-switcher + omarchy-shell-ipc shell listPlugins + omarchy-shell-ipc shell rescanPlugins +USAGE + exit 0 +fi + OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}" SHELL_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell" -if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then - setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 & - for _ in 1 2 3 4 5 6 7 8 9 10; do - sleep 0.2 - quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break - done -fi +# Serialize concurrent invocations so two CLI callers don't both spawn the shell +# when no instance is running yet. +lockfile="${XDG_RUNTIME_DIR:-/tmp}/omarchy-shell-ipc.lock" +{ + flock 9 + if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then + setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 & + for _ in 1 2 3 4 5 6 7 8 9 10; do + sleep 0.2 + quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break + done + fi +} 9>"$lockfile" exec quickshell ipc -p "$SHELL_DIR" call "$@" diff --git a/default/quickshell/omarchy-shell/README.md b/default/quickshell/omarchy-shell/README.md new file mode 100644 index 00000000..00c966cf --- /dev/null +++ b/default/quickshell/omarchy-shell/README.md @@ -0,0 +1,153 @@ +# Omarchy shell + +`omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/) +instance that hosts the Omarchy desktop. Hyprland autostarts one shell per +session; everything else — the bar, the bar settings UI, the background +switcher, future panels and overlays — runs **inside** the shell as a +plugin. + +Hosting everything inside one shell means: + +- shared services and singletons live once, not once per process +- summoning a panel is an IPC call into a process that is already running, + not a fresh `quickshell -p ...` cold start +- third-party plugins can be loaded from disk without changing any source + code in Omarchy itself + +The runtime layout in this branch: + +``` +default/quickshell/omarchy-shell/ + shell.qml entry point (ShellRoot) + services/ + PluginRegistry.qml discovers, validates, persists plugin state + BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p) + ui/ + settings/ + DynamicSettingsForm.qml renders plugin-declared schemas + plugins/ + bar/ first-party plugins (see plugins/README.md) + bar-settings/ + background-switcher/ +``` + +The plugin discovery path is documented in [plugins/README.md](plugins/README.md). + +## Plugin manifest + +Every plugin ships a `manifest.json` describing what it is and how the +shell should load it. Minimal example: + +```json +{ + "schemaVersion": 1, + "id": "my.org.cool-clock", + "name": "Cool clock", + "version": "1.0.0", + "author": "You", + "description": "A clock that does cool things", + "kinds": ["bar-widget"], + "activation": "on-demand", + "entryPoints": { "barWidget": "Widget.qml" }, + "barWidget": { + "displayName": "Cool clock", + "category": "Time", + "allowMultiple": false, + "defaults": { "format": "HH:mm" }, + "schema": [ + { "key": "format", "type": "string", "label": "Format" } + ] + } +} +``` + +Supported `kinds`: + +| Kind | What it is | +|--------------|--------------------------------------------------------------| +| `bar-widget` | A component that the bar can drop into a section | +| `panel` | A persistent or summoned floating window (e.g. bar settings) | +| `overlay` | A fullscreen overlay (e.g. background switcher) | +| `menu` | A summoned menu surface | +| `service` | A headless singleton, no UI | + +`activation` is either `persistent` (loaded on startup, never unloaded) or +`on-demand` (loaded by `shell summon ` and unloaded by `shell hide`). +Plugins that need their IPC socket to outlive a single summon can set +`keepLoaded: true` (e.g. background-switcher's legacy unix socket). + +The full schema lives in `services/PluginRegistry.qml`. + +## Installing a third-party plugin + +1. Drop the plugin into `~/.config/omarchy/plugins//`. + The directory must contain a `manifest.json` plus the QML files + referenced from its `entryPoints`. +2. `omarchy-shell-ipc shell rescanPlugins` — or open the Plugin Manager + tab in `omarchy launch bar-settings` and click **Rescan**. +3. Enable the plugin (Plugin Manager **Enable** toggle, or + `omarchy-shell-ipc shell setPluginEnabled true`). +4. If it's a `bar-widget`, add it to a layout section from the bar editor. + +First-party plugins under `default/quickshell/omarchy-shell/plugins/` +are discovered the same way and cannot be disabled. + +## IPC contract + +The shell exposes a single `shell` IPC target plus whatever extra targets +individual plugins register (e.g. the bar's `bar` target for refresh +hooks, the background switcher's `image-selector` target). + +| Method | Returns | Effect | +|------------------------------------------|---------|-------------------------------------------------------| +| `ping` | `ok` | health check | +| `summon ` | `ok` / `unknown` | load + open a panel/overlay plugin | +| `hide ` | — | close a previously-summoned plugin | +| `toggle ` | — | summon if closed, hide if open | +| `rescanPlugins` | — | re-walk plugin dirs and pick up new/changed manifests | +| `setPluginEnabled ` | — | flip the persisted enabled bit (see note) | +| `listPlugins` | JSON | every discovered plugin (id, name, kinds, enabled) | + +Direct invocation: + +``` +quickshell ipc -p $OMARCHY_PATH/default/quickshell/omarchy-shell call shell ping +``` + +A convenience wrapper, [`omarchy-shell-ipc`](../../../bin/omarchy-shell-ipc), +starts the shell if it is not already running, then forwards a `call`. It +is the canonical way for other Omarchy CLIs to talk to the shell. + +``` +omarchy-shell-ipc shell ping +omarchy-shell-ipc shell summon omarchy.bar-settings "{}" +omarchy-shell-ipc shell listPlugins +omarchy-shell-ipc shell rescanPlugins +``` + +**Note on `setPluginEnabled`:** the `enabled` argument is a string. Only the +literal `"true"` enables the plugin; every other value (including `"True"`, +`"1"`, `"yes"`, or omitted) disables it. This keeps the IPC surface +type-stable across QML's `string`-only IPC arguments. + +## Persisted state + +| Path | Owner | Purpose | +|-------------------------------------------|----------------|--------------------------------------| +| `~/.config/omarchy/bar.json` | bar plugin | section layout + per-entry settings | +| `~/.config/omarchy/plugins.json` | PluginRegistry | enabled/disabled state | +| `~/.config/omarchy/plugins//` | user | manifest + entry points + assets | +| `~/.config/omarchy/plugins//settings.json` | user | optional per-plugin overrides | + +## Implementation history + +Built up in phases on this branch: + +- Phase 1 — `omarchy-shell phase 1: host the existing bar in a single shell` +- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry` +- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin` +- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin` +- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs` (this commit) + +Shared services and Pipewire/UPower/Hyprland consolidation are explicitly +out of scope here and deferred to a follow-up after a review pass. diff --git a/default/quickshell/omarchy-shell/plugins/README.md b/default/quickshell/omarchy-shell/plugins/README.md new file mode 100644 index 00000000..08353ddb --- /dev/null +++ b/default/quickshell/omarchy-shell/plugins/README.md @@ -0,0 +1,48 @@ +# First-party plugins + +These plugins ship with Omarchy and are loaded by the shell at startup. +They use the same `manifest.json` contract as third-party plugins; the +only difference is that the shell flags them with `__isFirstParty: true` +so they cannot be disabled. + +User-installed plugins live alongside these conceptually but on disk under +`~/.config/omarchy/plugins//` rather than in this directory. + +| Plugin | id | kinds | activation | entry point | +|-----------------------|-------------------------------|--------------|-------------|----------------------------| +| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` | +| Bar settings | `omarchy.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` | +| Background switcher | `omarchy.background-switcher` | `overlay` | on-demand | `background-switcher/BackgroundSwitcher.qml` | + +## Bar + +The status bar. Mounted at startup, lives forever. Layout is configured +through `~/.config/omarchy/bar.json` (deep-merged over +[`bar/bar-defaults.json`](bar/bar-defaults.json)). Owns the `bar` IPC +target for refresh hooks fired by indicator scripts. See +[`bar/README.md`](bar/README.md) for the widget catalogue and customization +schema. + +## Bar settings + +Visual editor for the bar layout. Summoned by +`omarchy-shell-ipc shell summon omarchy.bar-settings "{}"` (which is what +`omarchy launch bar-settings` ultimately calls). Provides: + +- per-section add/move/remove/edit of widget entries +- a Plugin Manager tab for enabling/disabling third-party plugins +- a dynamic settings form driven by each widget's manifest schema + +## Background switcher + +Fullscreen wallpaper / image picker overlay. Summoned for ad-hoc wallpaper +selection. Keeps its legacy unix socket protocol at +`/run/user//omarchy-image-selector.sock` so existing callers like +`omarchy-menu-images` keep working without any wire-format change. The +plugin has `keepLoaded: true` so the socket survives between summons. + +## Coming soon + +- `omarchy.menu` — folds the existing `omarchy-menu` (currently on another + branch) into the shell as a `menu` plugin. Not present on this branch. +- `omarchy.theme-switcher` — folds theme switching into the shell. diff --git a/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml b/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml index ca05a2eb..096c0b06 100644 --- a/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml +++ b/default/quickshell/omarchy-shell/plugins/background-switcher/BackgroundSwitcher.qml @@ -5,9 +5,19 @@ import QtQuick import QtQuick.Effects import QtQuick.Shapes -ShellRoot { +Item { id: root + // Injected by omarchy-shell. Optional here — the picker doesn't need + // omarchyPath itself, but every plugin gets it so user-installed scripts + // referenced by other plugins can stay path-portable. + property string omarchyPath: "" + // Set by omarchy-shell when summoning the overlay; not currently consumed but + // declared so the host's onLoaded injection doesn't trip a missing-property + // warning. + property var shell: null + property var manifest: null + property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/backgrounds") property string imageRows: "" property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE") @@ -293,9 +303,35 @@ ShellRoot { } } - Component.onCompleted: { - if (selectionFile) - openSelector(imageDirs, "", selectedImage, selectionFile, Quickshell.env("OMARCHY_IMAGE_SELECTOR_DONE_FILE"), colorsFile, "", false, false) + // The plugin is keep-loaded inside omarchy-shell, so the env-driven + // auto-open path that the standalone background-switcher.qml used would + // now fire once at shell startup with stale env. The legacy callers + // (omarchy-menu-images) deliver their request over the unix socket + // declared below; modern callers go through `shell summon` -> open(payload). + + // Lifecycle hooks invoked by omarchy-shell summon/hide. The legacy entry + // point remains the unix socket below — callers that already have a + // selection_file/done_file flow keep using it. summon() with no payload + // simply opens the picker against the user's current theme backgrounds. + function open(payload) { + var args = {} + if (payload) { + try { args = JSON.parse(payload) || {} } catch (e) { args = {} } + } + var dirs = String(args.imageDirs || imageDirs) + var rows = String(args.imageRows || "") + var sel = String(args.selectedImage || selectedImage) + var selFile = String(args.selectionFile || "") + var doneF = String(args.doneFile || "") + var colors = String(args.colorsFile || colorsFile) + var colorsRaw = String(args.colorsRaw || "") + var labels = args.showLabels === true || args.showLabels === "true" + var filter = args.filterable === true || args.filterable === "true" + openSelector(dirs, rows, sel, selFile, doneF, colors, colorsRaw, labels, filter) + } + + function close() { + cancel() } IpcHandler { diff --git a/default/quickshell/omarchy-shell/plugins/bar-settings/BarSettingsPanel.qml b/default/quickshell/omarchy-shell/plugins/bar-settings/BarSettingsPanel.qml index a5652978..d2d440c0 100644 --- a/default/quickshell/omarchy-shell/plugins/bar-settings/BarSettingsPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/bar-settings/BarSettingsPanel.qml @@ -9,6 +9,12 @@ import "../../ui/settings" as SettingsUi Item { id: root + // Plugin lifecycle hooks. omarchy-shell calls open(payloadJson) on summon + // and close() on hide. We don't consume payloads yet, and visibility is + // driven by the host Loader's `active`, so both are no-ops for now. + function open(payloadJson) { /* no payload schema yet; reserved for future use */ } + function close() { /* visibility handled by parent Loader; nothing to clean up */ } + // Injected by the host shell when the panel is summoned. Shared instances // so the panel sees the same registry state the bar wrote into. property var barWidgetRegistry: null @@ -342,12 +348,16 @@ Item { return result } - Component.onCompleted: { + // Only log once the registry has actually been injected by the host. The + // raw Component.onCompleted fires before the Loader's onLoaded property + // injection so it would always print `(null)` widgets, which is noise. + onBarWidgetRegistryChanged: { + if (!root.barWidgetRegistry) return console.log("bar-settings open. omarchyPath=" + root.omarchyPath, "defaultsPath=" + root.defaultsPath, "userConfigPath=" + root.userConfigPath, "registry has", - root.barWidgetRegistry ? root.barWidgetRegistry.availableIds().length : "(null)", + root.barWidgetRegistry.availableIds().length, "widgets") } @@ -1251,12 +1261,33 @@ Item { Item { width: 8; height: 1 } - Switch { - checked: row.pluginEnabled - enabled: !row.firstParty - opacity: row.firstParty ? 0.5 : 1 + Item { + implicitWidth: enabledSwitch.implicitWidth + implicitHeight: enabledSwitch.implicitHeight anchors.verticalCenter: parent.verticalCenter - onToggled: root.pluginRegistry.setEnabled(row.pluginId, checked) + + Switch { + id: enabledSwitch + checked: row.pluginEnabled + enabled: !row.firstParty + opacity: row.firstParty ? 0.45 : 1 + ToolTip.visible: row.firstParty && hoverArea.containsMouse + ToolTip.delay: 300 + ToolTip.text: "First-party plugin — always enabled" + onToggled: root.pluginRegistry.setEnabled(row.pluginId, checked) + } + + // Switch.enabled=false also disables mouse tracking, so the tooltip + // never sees a hover event. Layer a transparent hover-only MouseArea + // on top to surface the explanation. + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + visible: row.firstParty + cursorShape: Qt.ForbiddenCursor + } } } diff --git a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml index c93e10da..0099580c 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml @@ -714,7 +714,7 @@ Item { } FileView { - path: root.omarchyPath + "/default/quickshell/bar/bar-defaults.json" + path: root.omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json" watchChanges: true printErrors: false onLoaded: root.loadDefaultBarConfig(text()) diff --git a/default/quickshell/omarchy-shell/plugins/bar/README.md b/default/quickshell/omarchy-shell/plugins/bar/README.md index 8d36d79c..0317ca7b 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/README.md +++ b/default/quickshell/omarchy-shell/plugins/bar/README.md @@ -1,7 +1,11 @@ # Omarchy bar -This is the Quickshell implementation of the Omarchy status bar. +This is the Quickshell implementation of the Omarchy status bar. It is +shipped as a first-party plugin of [`omarchy-shell`](../../README.md), the +long-running shell host. The bar is mounted at startup and lives inside +the shell for its whole session. +- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`, `activation: persistent`) and points at `Bar.qml` as the entry point. - `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly. - `bar-defaults.json` is the Omarchy-owned default layout and module settings. - `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy. @@ -156,4 +160,13 @@ Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (o - `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup - `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator -Drop new widgets into `widgets/.qml`, add the name to the `firstPartyWidgets` registry in `shell.qml`, and reference it by name in any layout list. +First-party widgets live in `widgets/.qml` and are picked up by the +shell's `BarWidgetRegistry` at startup; reference one by `id` in any +layout list. + +Third-party widgets ship as separate plugins under +`~/.config/omarchy/plugins//` with their own `manifest.json` +declaring `kinds: ["bar-widget"]` and a `barWidget` entry point. See +[../../README.md](../../README.md) for the manifest schema and the +Plugin Manager tab in `omarchy launch bar-settings` for enable/disable +controls. diff --git a/default/quickshell/omarchy-shell/services/PluginRegistry.qml b/default/quickshell/omarchy-shell/services/PluginRegistry.qml index ec453683..23d137d3 100644 --- a/default/quickshell/omarchy-shell/services/PluginRegistry.qml +++ b/default/quickshell/omarchy-shell/services/PluginRegistry.qml @@ -33,6 +33,13 @@ QtObject { return "file://" + String(path).split("/").map(encodeURIComponent).join("/") } + function isSafeEntryPoint(value) { + if (typeof value !== "string" || value.length === 0) return false + if (value.charAt(0) === "/") return false + if (value.indexOf("..") !== -1) return false + return true + } + function validateManifest(manifest, sourcePath) { if (!isPlainObject(manifest)) { console.warn("PluginRegistry: manifest is not an object at " + sourcePath) @@ -62,6 +69,16 @@ QtObject { console.warn("PluginRegistry: entryPoints must be an object at " + sourcePath) return null } + // Every entry point must be a relative path inside the plugin's source + // directory. Reject the whole manifest if anything looks like an attempt + // to escape the plugin's sandbox. + for (var key in manifest.entryPoints) { + if (!isSafeEntryPoint(manifest.entryPoints[key])) { + console.warn("PluginRegistry: unsafe entryPoint '" + key + "'='" + + manifest.entryPoints[key] + "' at " + sourcePath) + return null + } + } return manifest } @@ -71,7 +88,15 @@ QtObject { if (!ep) return "" var dir = manifest.__sourceDir || "" if (!dir) return "" - return fileUrl(dir + "/" + ep) + // Defense in depth: even after validateManifest, confirm the resolved + // path stays inside the plugin's sourceDir. + var resolved = dir.replace(/\/$/, "") + "/" + String(ep) + var expectedPrefix = dir.replace(/\/$/, "") + "/" + if (resolved.indexOf(expectedPrefix) !== 0) { + console.warn("PluginRegistry: entry point escapes sourceDir: " + resolved) + return "" + } + return fileUrl(resolved) } function isEnabled(id) { @@ -194,7 +219,15 @@ QtObject { var merged = {} for (var fk in firstParty) merged[fk] = firstParty[fk] - for (var tk in thirdParty) merged[tk] = thirdParty[tk] + // Third-party plugins never shadow a first-party one with the same id. + for (var tk in thirdParty) { + if (firstParty[tk]) { + console.warn("PluginRegistry: plugin " + tk + + " rejected: id collides with first-party plugin") + continue + } + merged[tk] = thirdParty[tk] + } pluginStates = nextStates installedPlugins = merged @@ -234,12 +267,12 @@ QtObject { + "}; " + "scan \"$0\" firstparty; " + "scan \"$1\" thirdparty" - scanProcess.command = ["bash", "-lc", script, registry.firstPartyDir, registry.pluginsDir] + scanProcess.command = ["bash", "-c", script, registry.firstPartyDir, registry.pluginsDir] scanProcess.running = true } function ensureUserDir() { - initProcess.command = ["bash", "-lc", "mkdir -p \"$0\"", registry.pluginsDir] + initProcess.command = ["bash", "-c", "mkdir -p \"$0\"", registry.pluginsDir] initProcess.running = true } diff --git a/default/quickshell/omarchy-shell/services/qmldir b/default/quickshell/omarchy-shell/services/qmldir deleted file mode 100644 index e7678c8f..00000000 --- a/default/quickshell/omarchy-shell/services/qmldir +++ /dev/null @@ -1,3 +0,0 @@ -module omarchy.services -PluginRegistry 1.0 PluginRegistry.qml -BarWidgetRegistry 1.0 BarWidgetRegistry.qml diff --git a/default/quickshell/omarchy-shell/shell.qml b/default/quickshell/omarchy-shell/shell.qml index 26f5e4b7..911ded62 100644 --- a/default/quickshell/omarchy-shell/shell.qml +++ b/default/quickshell/omarchy-shell/shell.qml @@ -58,12 +58,13 @@ ShellRoot { // object without it) hides it. Reassigning the whole object is required for // QML to notice the change. property var openPanelIds: ({}) - property var panelCache: ({}) function isPanelOpen(id) { return openPanelIds[id] === true } // Pending payloads to deliver to a plugin's open() once its loader resolves. - // Keyed by plugin id; consumed by the Loader.onLoaded handler below. + // Keyed by plugin id; the value is an array so two summon() calls before + // the Loader resolves both reach the plugin in arrival order rather than + // the second clobbering the first. property var pendingPayloads: ({}) function summon(pluginId, payloadJson) { @@ -81,8 +82,10 @@ ShellRoot { // Stash payload so the Loader.onLoaded handler can hand it to open(). var pending = ({}) - for (var p in pendingPayloads) pending[p] = pendingPayloads[p] - pending[id] = payloadJson || "" + for (var p in pendingPayloads) pending[p] = pendingPayloads[p].slice() + var queue = pending[id] || [] + queue.push(payloadJson || "") + pending[id] = queue pendingPayloads = pending // If the plugin is keepLoaded and already mounted, deliver immediately. @@ -127,15 +130,17 @@ ShellRoot { function deliverIfLoaded(pluginId) { var loader = panelLoaders[pluginId] if (!loader || !loader.item) return - var payload = pendingPayloads[pluginId] - if (payload === undefined) return + var queue = pendingPayloads[pluginId] + if (!Array.isArray(queue) || queue.length === 0) return if (typeof loader.item.open === "function") { - try { loader.item.open(payload) } catch (e) { - console.warn("plugin " + pluginId + " open() threw:", e) + for (var i = 0; i < queue.length; i++) { + try { loader.item.open(queue[i]) } catch (e) { + console.warn("plugin " + pluginId + " open() threw:", e) + } } } var next = ({}) - for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k] + for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k].slice() pendingPayloads = next } diff --git a/default/quickshell/omarchy-shell/ui/settings/qmldir b/default/quickshell/omarchy-shell/ui/settings/qmldir deleted file mode 100644 index 341bb0e2..00000000 --- a/default/quickshell/omarchy-shell/ui/settings/qmldir +++ /dev/null @@ -1,2 +0,0 @@ -module omarchy.ui.settings -DynamicSettingsForm 1.0 DynamicSettingsForm.qml