Restrict third-party shell plugin capabilities
Reported-by: Roger Piñol <rogerpicar@gmail.com>
This commit is contained in:
1 parent
4d017913d0
commit
1702cf0bee
25 files changed
+1403
-49
No files matched your search
@@ -20,9 +20,7 @@ Run `omarchy-restart-shell` after making changes to QML files.
|
||||
[`docs/omarchy-shell.md`](../../docs/omarchy-shell.md) and
|
||||
`shell/services/PluginRegistry.qml` for the current contract; fields such as
|
||||
`activation` are optional.
|
||||
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the
|
||||
shell-injected properties `omarchyPath`, `shell`, `manifest`, and
|
||||
`pluginRegistry` / `barWidgetRegistry` as appropriate.
|
||||
- Entry-point QML files are `Item`s (not `ShellRoot`), and accept the shell-injected properties `omarchyPath`, `shell`, `manifest`, and `pluginRegistry` / `barWidgetRegistry` as appropriate. First-party plugins receive the host objects. Third-party plugins receive capability-scoped facades: ordinary plugins may look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state; full-bar plugins additionally receive detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities must be stamped from trusted first-party manifests, and third-party registry views must be detached snapshots rather than shared objects. Do not expose host objects or authentication services through new third-party-facing properties.
|
||||
- Panel / overlay / menu plugins must expose `open(payloadJson)` and
|
||||
`close()` lifecycle methods for `shell summon` and `shell hide`.
|
||||
|
||||
|
||||
+2
-10
@@ -41,10 +41,7 @@ Panels, overlays, and menus are loaded when summoned. Plugins can set the
|
||||
top-level manifest key `keepLoaded: true` to survive between summons.
|
||||
First-party services are loaded at startup.
|
||||
|
||||
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose
|
||||
`open(payloadJson)` and `close()` for summon/hide; on load the host injects
|
||||
`omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` /
|
||||
`barWidgetRegistry`) as properties.
|
||||
Entry points are QML `Item`s. Panel, overlay, and menu entry points expose `open(payloadJson)` and `close()` for summon/hide; on load the host injects `omarchyPath`, `shell`, `manifest`, and the registries (`pluginRegistry` / `barWidgetRegistry`) as properties. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades instead: ordinary plugins may look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are kept out of the host's public service map and QML object tree, and third-party registry snapshots can be changed only locally without mutating the host registries.
|
||||
|
||||
Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegistry.qml).
|
||||
|
||||
@@ -81,12 +78,7 @@ one replaces the active bar, and it is therefore never offered under Disable.
|
||||
Bar widgets may set `barWidget.defaultSection` to `left`, `center`, or `right`;
|
||||
widgets that omit it default to `center`.
|
||||
|
||||
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you
|
||||
before cloning, plugins land disabled so you can review the code before
|
||||
`omarchy plugin enable`, and updates show a diff before touching anything.
|
||||
Commands confirm in a terminal even when given arguments; without one they
|
||||
refuse rather than guess. Add `--yes` to skip every prompt (the path for
|
||||
scripts and agents).
|
||||
Plugins run as **unsandboxed code** inside `omarchy-shell`. Adding warns you before cloning, plugins land disabled so you can review the code before `omarchy plugin enable`, and updates show a diff before touching anything. Commands confirm in a terminal even when given arguments; without one they refuse rather than guess. Add `--yes` to skip every prompt (the path for scripts and agents). The scoped QML interfaces protect shell-owned credentials and cross-plugin controls; they are not an operating-system sandbox, so plugin code still has the same user-level file and process access as the shell.
|
||||
|
||||
You can still install by hand: drop a plugin into
|
||||
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
|
||||
|
||||
@@ -4,7 +4,7 @@ The Omarchy desktop runs as a single long-lived Quickshell process called `omarc
|
||||
|
||||
That's not just an implementation detail. It means you can turn pieces of the desktop off, swap them out, or write your own without touching a line of Omarchy's source.
|
||||
|
||||
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup; the only difference is where they sit on disk.
|
||||
The first-party plugins ship with Omarchy and live in `$OMARCHY_PATH/shell/plugins/`. Anything you add yourself — your own experiments, or something you found on GitHub — lives in `~/.config/omarchy/plugins/`. Both are discovered the same way at startup, but built-ins receive trusted shell interfaces while third-party plugins receive a limited interface scoped to their own service and lifecycle.
|
||||
|
||||
## Seeing what you have
|
||||
|
||||
@@ -37,7 +37,7 @@ A third-party plugin is just a git repo with a `manifest.json` at its root.
|
||||
omarchy plugin add https://github.com/acme/omarchy-weather.git --enable
|
||||
```
|
||||
|
||||
Before it does anything, it tells you plainly that plugins run as arbitrary, unsandboxed code inside your long-lived shell process, shows you the URL, and asks you to confirm. Take that seriously. A plugin isn't a config file — it's code that runs for as long as your session does, with everything your user account can reach. Only add repos you're willing to run, and read them before you enable them.
|
||||
Before it does anything, it tells you plainly that plugins run as arbitrary, unsandboxed code inside your long-lived shell process, shows you the URL, and asks you to confirm. Take that seriously. The shell does not expose its authentication state or raw host object tree through the third-party plugin interface. A replacement bar receives additional limited capabilities so it can render built-in widgets and orchestrate the configured non-authentication UI, but it still cannot reach authentication services. This is not an operating-system sandbox: plugin code still runs for as long as your session does, with everything your user account can reach. Only add repos you're willing to run, and read them before you enable them.
|
||||
|
||||
Then it clones the repo into a staging directory, validates the manifest, refuses the install if another plugin already claims that id, and moves it into `~/.config/omarchy/plugins/<id>/`. Without `--enable` it asks whether you want it on now, and you can say no and go read the code first. It never runs anything from the plugin, never executes an install hook, and never asks for sudo — it clones files, checks the manifest, and flips a bit over IPC.
|
||||
|
||||
|
||||
+3
-4
@@ -89,6 +89,8 @@ to outlive a single summon can set `keepLoaded: true` (e.g. the image
|
||||
picker keeps its overlay window mounted between summons). First-party
|
||||
services are loaded at startup.
|
||||
|
||||
Entry points may declare `omarchyPath`, `shell`, `manifest`, `pluginRegistry`, and `barWidgetRegistry` properties for host injection. Built-in plugins receive the trusted host objects. Third-party plugins receive capability-scoped facades: ordinary plugins can look up and control only their own service and lifecycle, menu plugins receive an application-library facade, and plugins can read detached scalar bar state. A full-bar plugin additionally receives detached bar configuration and widget-catalog snapshots, narrow proxies for the non-authentication services used by built-in bar widgets, and lifecycle control over configured non-authentication UI plugins. Authentication capabilities are stamped from trusted first-party manifests, authentication services are retained outside the host's public service map and QML object tree, and changing a third-party registry snapshot cannot mutate the host registry.
|
||||
|
||||
The full schema lives in `services/PluginRegistry.qml`.
|
||||
|
||||
## Installing a third-party plugin
|
||||
@@ -104,10 +106,7 @@ omarchy plugin update # updates every git-managed plugin
|
||||
omarchy plugin remove acme.weather
|
||||
```
|
||||
|
||||
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns
|
||||
> you before cloning, plugins land disabled so you can review the code before
|
||||
> enabling, and updates show a diff of the changes before touching anything.
|
||||
> Only add repos whose code you are willing to run.
|
||||
> ⚠️ **Plugins run as unsandboxed code inside `omarchy-shell`.** Adding warns you before cloning, plugins land disabled so you can review the code before enabling, and updates show a diff of the changes before touching anything. The scoped QML interfaces protect shell-owned credentials and cross-plugin controls; they are not an operating-system sandbox. Only add repos whose code you are willing to run.
|
||||
|
||||
Each command is **interactive** when run bare in a terminal (gum pickers,
|
||||
confirmation, a diff to review) and fully **non-interactive** when given
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import QtQuick
|
||||
|
||||
// Bar surface exposed to an installed third-party widget. Scalar presentation
|
||||
// state is mirrored by Bar.qml and operations are delegated through scoped
|
||||
// callbacks, so the actual Bar (and its root-shell property) is never retained.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
required property string moduleName
|
||||
property var shell: null
|
||||
|
||||
property color foreground: "transparent"
|
||||
property color barForeground: "transparent"
|
||||
property color background: "transparent"
|
||||
property color urgent: "transparent"
|
||||
property string fontFamily: ""
|
||||
property string position: "top"
|
||||
property bool vertical: false
|
||||
property int barSize: 0
|
||||
property bool transparent: false
|
||||
property bool foregroundAnimationEnabled: true
|
||||
property bool centerSectionRevealHeld: false
|
||||
property bool _centerHoverRevealSuppressed: false
|
||||
readonly property bool centerHoverRevealSuppressed: _centerHoverRevealSuppressed
|
||||
property var activePopout: null
|
||||
property var clickTargets: []
|
||||
property var layoutConfig: ({})
|
||||
readonly property var foreignPopoutMarker: ({ foreign: true })
|
||||
|
||||
property var _showTooltip: null
|
||||
property var _hideTooltip: null
|
||||
property var _registerClickTarget: null
|
||||
property var _unregisterClickTarget: null
|
||||
property var _requestPopout: null
|
||||
property var _releasePopout: null
|
||||
property var _switchPanelFrom: null
|
||||
property var _targetBelongsToWindow: null
|
||||
property var _moduleWidgets: null
|
||||
property var _run: null
|
||||
property var _setCenterHoverRevealSuppressed: null
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (_setCenterHoverRevealSuppressed) _setCenterHoverRevealSuppressed(!!value)
|
||||
}
|
||||
|
||||
function showTooltip(target, text) {
|
||||
if (_showTooltip) _showTooltip(target, String(text || ""))
|
||||
}
|
||||
|
||||
function hideTooltip(target) {
|
||||
if (_hideTooltip) _hideTooltip(target)
|
||||
}
|
||||
|
||||
function registerClickTarget(target) {
|
||||
if (_registerClickTarget) _registerClickTarget(target)
|
||||
}
|
||||
|
||||
function unregisterClickTarget(target) {
|
||||
if (_unregisterClickTarget) _unregisterClickTarget(target)
|
||||
}
|
||||
|
||||
function requestPopout(owner) {
|
||||
if (_requestPopout) _requestPopout(owner)
|
||||
}
|
||||
|
||||
function releasePopout(owner) {
|
||||
if (_releasePopout) _releasePopout(owner)
|
||||
}
|
||||
|
||||
function switchPanelFrom(owner, direction) {
|
||||
return _switchPanelFrom ? _switchPanelFrom(owner, direction) : false
|
||||
}
|
||||
|
||||
function targetBelongsToWindow(target, window) {
|
||||
return _targetBelongsToWindow ? _targetBelongsToWindow(target, window) : false
|
||||
}
|
||||
|
||||
function moduleWidgets(id) {
|
||||
return _moduleWidgets ? _moduleWidgets(String(id || "")) : []
|
||||
}
|
||||
|
||||
function run(command) {
|
||||
if (_run) _run(String(command || ""))
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
|
||||
PanelSeparator 1.0 PanelSeparator.qml
|
||||
PanelSlider 1.0 PanelSlider.qml
|
||||
PanelToolTip 1.0 PanelToolTip.qml
|
||||
PluginBarApi 1.0 PluginBarApi.qml
|
||||
PointerMoveGate 1.0 PointerMoveGate.qml
|
||||
ScreenMoveRemap 1.0 ScreenMoveRemap.qml
|
||||
PopupCard 1.0 PopupCard.qml
|
||||
|
||||
+228
-4
@@ -12,20 +12,29 @@ Item {
|
||||
id: root
|
||||
|
||||
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
|
||||
required property string omarchyPath
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
// Injected by the host shell so bar slots can resolve enabled widgets.
|
||||
required property var barWidgetRegistry
|
||||
property var barWidgetRegistry: fallbackBarWidgetRegistry
|
||||
// Read-only registry view for third-party full bars; the built-in bar does
|
||||
// not otherwise need it, but declaring it keeps clone construction atomic.
|
||||
property var pluginRegistry: null
|
||||
// Injected by the host shell every time shell.json is reloaded. Holds the
|
||||
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
|
||||
// the bar just renders whatever it's handed. The bar font follows the
|
||||
// OS-level fontconfig monospace binding — it is not stored in shell.json.
|
||||
required property var barConfig
|
||||
property var barConfig: ({})
|
||||
// Injected by the host shell. Used for shell-wide actions such as opening
|
||||
// settings and persisting inline widget state.
|
||||
property var shell: null
|
||||
// Manifest for the active bar option. Present for custom bars and useful for
|
||||
// diagnostics; the built-in bar does not otherwise need it.
|
||||
property var manifest: null
|
||||
QtObject {
|
||||
id: fallbackBarWidgetRegistry
|
||||
property var widgets: ({})
|
||||
property int revision: 0
|
||||
function metadataFor(id) { return null }
|
||||
}
|
||||
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
|
||||
// killing the entire shell. Hidden panels stay mapped but park off-screen
|
||||
// without an exclusion zone; updated by the FileView watcher further down.
|
||||
@@ -100,6 +109,213 @@ Item {
|
||||
property var barMoveScreen: null
|
||||
property var clickTargets: []
|
||||
property var moduleSlots: []
|
||||
property var pluginBarApis: ({})
|
||||
property var pluginObjectOwners: []
|
||||
|
||||
Component {
|
||||
id: pluginBarApiComponent
|
||||
PluginBarApi { }
|
||||
}
|
||||
|
||||
function publicLayoutConfig() {
|
||||
return JSON.parse(JSON.stringify(root.layoutConfig || {}))
|
||||
}
|
||||
|
||||
function bindPluginBarApi(api) {
|
||||
if (!api) return
|
||||
api.foreground = Qt.binding(function() { return root.foreground })
|
||||
api.barForeground = Qt.binding(function() { return root.barForeground })
|
||||
api.background = Qt.binding(function() { return root.background })
|
||||
api.urgent = Qt.binding(function() { return root.urgent })
|
||||
api.fontFamily = Qt.binding(function() { return root.fontFamily })
|
||||
api.position = Qt.binding(function() { return root.position })
|
||||
api.vertical = Qt.binding(function() { return root.vertical })
|
||||
api.barSize = Qt.binding(function() { return root.barSize })
|
||||
api.transparent = Qt.binding(function() { return root.transparent })
|
||||
api.foregroundAnimationEnabled = Qt.binding(function() { return root.foregroundAnimationEnabled })
|
||||
api.centerSectionRevealHeld = Qt.binding(function() { return root.centerSectionRevealHeld })
|
||||
api._centerHoverRevealSuppressed = Qt.binding(function() { return root.centerHoverRevealSuppressed })
|
||||
root.syncPluginBarApiObjects(api)
|
||||
}
|
||||
|
||||
function syncPluginBarApiObjects(api) {
|
||||
if (!api) return
|
||||
api.activePopout = root.pluginOwnsBarObject(api.pluginId, root.activePopout)
|
||||
? root.activePopout : (root.activePopout ? api.foreignPopoutMarker : null)
|
||||
api.clickTargets = root.pluginClickTargets(api.pluginId)
|
||||
api.layoutConfig = root.publicLayoutConfig()
|
||||
}
|
||||
|
||||
function pluginObjectRecord(target) {
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var record = pluginObjectOwners[i]
|
||||
if (record && record.target === target) return record
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function markPluginObject(pluginId, target, role) {
|
||||
var key = String(pluginId || "")
|
||||
if (!key || !target) return false
|
||||
var record = root.pluginObjectRecord(target)
|
||||
if (record && record.pluginId !== key) return false
|
||||
var next = []
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var existing = pluginObjectOwners[i]
|
||||
if (!existing || existing.target !== target) next.push(existing)
|
||||
}
|
||||
var updated = record || { target: target, pluginId: key, clickTarget: false, popout: false }
|
||||
updated[role] = true
|
||||
next.push(updated)
|
||||
pluginObjectOwners = next
|
||||
return true
|
||||
}
|
||||
|
||||
function unmarkPluginObject(pluginId, target, role) {
|
||||
var key = String(pluginId || "")
|
||||
var next = []
|
||||
for (var i = 0; i < pluginObjectOwners.length; i++) {
|
||||
var record = pluginObjectOwners[i]
|
||||
if (!record || record.target !== target || record.pluginId !== key) {
|
||||
next.push(record)
|
||||
continue
|
||||
}
|
||||
record[role] = false
|
||||
if (record.clickTarget || record.popout) next.push(record)
|
||||
}
|
||||
pluginObjectOwners = next
|
||||
}
|
||||
|
||||
function pluginOwnsBarObject(pluginId, target) {
|
||||
var record = target ? root.pluginObjectRecord(target) : null
|
||||
return !!record && record.pluginId === String(pluginId || "")
|
||||
}
|
||||
|
||||
function pluginClickTargets(pluginId) {
|
||||
var out = []
|
||||
for (var i = 0; i < root.clickTargets.length; i++) {
|
||||
var target = root.clickTargets[i]
|
||||
if (root.pluginOwnsBarObject(pluginId, target)) out.push(target)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function syncAllPluginBarApiObjects() {
|
||||
for (var id in pluginBarApis) root.syncPluginBarApiObjects(pluginBarApis[id])
|
||||
}
|
||||
|
||||
function registerPluginClickTarget(pluginId, target) {
|
||||
if (!root.markPluginObject(pluginId, target, "clickTarget")) return
|
||||
root.registerClickTarget(target)
|
||||
}
|
||||
|
||||
function unregisterPluginClickTarget(pluginId, target) {
|
||||
if (!root.pluginOwnsBarObject(pluginId, target)) return
|
||||
root.unregisterClickTarget(target)
|
||||
root.unmarkPluginObject(pluginId, target, "clickTarget")
|
||||
}
|
||||
|
||||
function requestPluginPopout(pluginId, owner) {
|
||||
if (!root.markPluginObject(pluginId, owner, "popout")) return
|
||||
root.requestPopout(owner)
|
||||
}
|
||||
|
||||
function releasePluginPopout(pluginId, owner) {
|
||||
if (!root.pluginOwnsBarObject(pluginId, owner)) return
|
||||
root.releasePopout(owner)
|
||||
root.unmarkPluginObject(pluginId, owner, "popout")
|
||||
}
|
||||
|
||||
function pluginBarApiFor(pluginId, moduleName, registered) {
|
||||
var key = String(pluginId || "")
|
||||
if (!key) return null
|
||||
if (pluginBarApis[key]) return pluginBarApis[key]
|
||||
|
||||
var pluginShell = null
|
||||
if (registered && root.shell && typeof root.shell.pluginShellForId === "function")
|
||||
pluginShell = root.shell.pluginShellForId(moduleName)
|
||||
else if (!registered && root.shell && typeof root.shell.pluginShellForBarEntry === "function")
|
||||
pluginShell = root.shell.pluginShellForBarEntry(key, moduleName)
|
||||
|
||||
var api = pluginBarApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
moduleName: String(moduleName || ""),
|
||||
shell: pluginShell,
|
||||
_showTooltip: function(target, text) { root.showTooltip(target, text) },
|
||||
_hideTooltip: function(target) { root.hideTooltip(target) },
|
||||
_registerClickTarget: function(target) { root.registerPluginClickTarget(key, target) },
|
||||
_unregisterClickTarget: function(target) { root.unregisterPluginClickTarget(key, target) },
|
||||
_requestPopout: function(owner) { root.requestPluginPopout(key, owner) },
|
||||
_releasePopout: function(owner) { root.releasePluginPopout(key, owner) },
|
||||
_switchPanelFrom: function(owner, direction) { return root.switchPanelFrom(owner, direction) },
|
||||
_targetBelongsToWindow: function(target, window) { return root.targetBelongsToWindow(target, window) },
|
||||
_moduleWidgets: function(requestedId) {
|
||||
return String(requestedId || "") === String(moduleName || "")
|
||||
? root.moduleWidgets(moduleName) : []
|
||||
},
|
||||
_run: function(command) { root.run(command) },
|
||||
_setCenterHoverRevealSuppressed: function(value) {
|
||||
root.centerHoverRevealSuppressed = !!value
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
root.bindPluginBarApi(api)
|
||||
|
||||
var next = ({})
|
||||
for (var id in pluginBarApis) next[id] = pluginBarApis[id]
|
||||
next[key] = api
|
||||
pluginBarApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarApiUsed(pluginId) {
|
||||
for (var i = 0; i < moduleSlots.length; i++) {
|
||||
var slot = moduleSlots[i]
|
||||
if (slot && slot.pluginApiId === pluginId) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function releasePluginObjects(pluginId) {
|
||||
var owned = pluginObjectOwners.slice()
|
||||
for (var i = 0; i < owned.length; i++) {
|
||||
var record = owned[i]
|
||||
if (!record || record.pluginId !== pluginId) continue
|
||||
if (record.clickTarget) root.unregisterClickTarget(record.target)
|
||||
if (record.popout && root.activePopout === record.target) root.releasePopout(record.target)
|
||||
}
|
||||
pluginObjectOwners = pluginObjectOwners.filter(function(record) {
|
||||
return record && record.pluginId !== pluginId
|
||||
})
|
||||
}
|
||||
|
||||
function prunePluginBarApis() {
|
||||
var next = ({})
|
||||
for (var id in pluginBarApis) {
|
||||
var api = pluginBarApis[id]
|
||||
if (root.pluginBarApiUsed(id)) {
|
||||
next[id] = api
|
||||
continue
|
||||
}
|
||||
root.releasePluginObjects(id)
|
||||
if (api && typeof api.destroy === "function") api.destroy()
|
||||
}
|
||||
pluginBarApis = next
|
||||
}
|
||||
|
||||
onActivePopoutChanged: syncAllPluginBarApiObjects()
|
||||
onClickTargetsChanged: syncAllPluginBarApiObjects()
|
||||
onLayoutConfigChanged: syncAllPluginBarApiObjects()
|
||||
onModuleSlotsChanged: Qt.callLater(prunePluginBarApis)
|
||||
|
||||
Component.onDestruction: {
|
||||
for (var id in pluginBarApis) {
|
||||
root.releasePluginObjects(id)
|
||||
if (pluginBarApis[id] && typeof pluginBarApis[id].destroy === "function")
|
||||
pluginBarApis[id].destroy()
|
||||
}
|
||||
pluginBarApis = ({})
|
||||
}
|
||||
|
||||
function registerClickTarget(target) {
|
||||
if (!target || clickTargets.indexOf(target) !== -1) return
|
||||
@@ -599,6 +815,10 @@ Item {
|
||||
if (barHoverCount === 0) centerSectionRevealTimer.restart()
|
||||
}
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
centerHoverRevealSuppressed = !!value
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: centerSectionRevealTimer
|
||||
interval: 120
|
||||
@@ -1548,6 +1768,9 @@ Item {
|
||||
readonly property string moduleName: root.entryId(entry)
|
||||
readonly property var moduleSettings: root.entrySettings(entry)
|
||||
readonly property string customType: root.customModuleType(entry)
|
||||
readonly property var registryMetadata: root.barWidgetRegistry.metadataFor(root.canonicalWidgetId(moduleName))
|
||||
readonly property bool firstParty: registryMetadata && registryMetadata.firstParty === true
|
||||
readonly property string pluginApiId: registered ? root.canonicalWidgetId(moduleName) : "bar-entry:" + moduleName
|
||||
// Re-evaluate when the registry mutates (Component reference changes,
|
||||
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
|
||||
// the binding dependency — the wrapped function call alone wouldn't.
|
||||
@@ -1766,7 +1989,8 @@ Item {
|
||||
function injectProps() {
|
||||
var target = activeItem
|
||||
if (!target) return
|
||||
if ("bar" in target) target.bar = root
|
||||
if ("bar" in target) target.bar = firstParty
|
||||
? root : root.pluginBarApiFor(pluginApiId, moduleName, registered)
|
||||
if ("moduleName" in target) target.moduleName = moduleName
|
||||
if ("settings" in target) target.settings = moduleSettings
|
||||
}
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Quickshell session lock with separate password and fingerprint PAM flows.",
|
||||
"omarchy": {
|
||||
"capabilities": [
|
||||
"authentication"
|
||||
]
|
||||
},
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
|
||||
@@ -119,7 +119,9 @@ Panel {
|
||||
// Summoning by hotkey moves no pointer, so a hover the bar was still
|
||||
// holding must not keep the center indicators revealed behind the panel.
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
|
||||
root.bar.setCenterHoverRevealSuppressed(value)
|
||||
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
root.bar.centerHoverRevealSuppressed = value
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ Panel {
|
||||
}
|
||||
|
||||
function setCenterHoverRevealSuppressed(value) {
|
||||
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function")
|
||||
root.bar.setCenterHoverRevealSuppressed(value)
|
||||
else if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
||||
root.bar.centerHoverRevealSuppressed = value
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Theme-aware authentication dialog for privileged actions.",
|
||||
"omarchy": {
|
||||
"capabilities": [
|
||||
"authentication"
|
||||
]
|
||||
},
|
||||
"kinds": [
|
||||
"service"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Intentionally not `.pragma library`: QML JavaScript imports get a private
|
||||
// module instance per importing component. shell.qml's instance retains the
|
||||
// authentication services; a third-party plugin importing this file receives
|
||||
// a separate empty store rather than a shared path to credential-bearing QML.
|
||||
|
||||
var services = ({})
|
||||
|
||||
function has(id) {
|
||||
return services[String(id || "")] !== undefined
|
||||
}
|
||||
|
||||
function put(id, service) {
|
||||
var key = String(id || "")
|
||||
if (!key || !service) return
|
||||
if (services[key] && services[key] !== service && typeof services[key].destroy === "function")
|
||||
services[key].destroy()
|
||||
services[key] = service
|
||||
}
|
||||
|
||||
function ids() {
|
||||
return Object.keys(services)
|
||||
}
|
||||
|
||||
function destroy(id) {
|
||||
var key = String(id || "")
|
||||
var service = services[key]
|
||||
if (service && typeof service.destroy === "function") service.destroy()
|
||||
delete services[key]
|
||||
}
|
||||
|
||||
function destroyAll() {
|
||||
var keys = ids()
|
||||
for (var i = 0; i < keys.length; i++) destroy(keys[i])
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached application-library capability for third-party menus. Callbacks
|
||||
// expose the supported app-list operations without retaining AppLibrary or its
|
||||
// ShellRoot parent in the plugin-visible object graph.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
signal appsChanged()
|
||||
|
||||
property var _entryName: null
|
||||
property var _entrySubtext: null
|
||||
property var _sortedEntries: null
|
||||
property var _iconSource: null
|
||||
property var _refreshIcons: null
|
||||
property var _launch: null
|
||||
property var _remove: null
|
||||
|
||||
function entryName(entry) {
|
||||
return _entryName ? _entryName(entry) : ""
|
||||
}
|
||||
|
||||
function entrySubtext(entry) {
|
||||
return _entrySubtext ? _entrySubtext(entry) : ""
|
||||
}
|
||||
|
||||
function sortedEntries(query) {
|
||||
return _sortedEntries ? _sortedEntries(String(query || "")) : []
|
||||
}
|
||||
|
||||
function iconSource(icon) {
|
||||
return _iconSource ? _iconSource(icon) : ""
|
||||
}
|
||||
|
||||
function refreshIcons() {
|
||||
if (_refreshIcons) _refreshIcons()
|
||||
}
|
||||
|
||||
function launch(desktopId, name) {
|
||||
if (_launch) _launch(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
|
||||
function remove(desktopId, name) {
|
||||
if (_remove) _remove(String(desktopId || ""), String(name || ""))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
|
||||
// Scalar-only view of the active bar for plugins that position independent
|
||||
// windows. The active Bar QObject is never retained here.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
|
||||
property bool barHidden: false
|
||||
property int barSize: 0
|
||||
property string fontFamily: ""
|
||||
property string position: "top"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
|
||||
// Detached widget-catalogue snapshot for third-party full-bar implementations.
|
||||
// Plugins can render the referenced components, but mutating this local view
|
||||
// cannot replace a registration in the host registry.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
property var widgets: ({})
|
||||
property int revision: 0
|
||||
|
||||
function metadataFor(id) {
|
||||
var entry = widgets[String(id || "")]
|
||||
return entry ? entry.metadata : null
|
||||
}
|
||||
|
||||
function availableIds() {
|
||||
return Object.keys(widgets)
|
||||
}
|
||||
|
||||
function has(id) {
|
||||
return widgets[String(id || "")] !== undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
|
||||
// Narrow proxy for the non-authentication first-party services used by the
|
||||
// built-in bar. It intentionally has no generic property or method forwarding.
|
||||
QtObject {
|
||||
required property string ownerPluginId
|
||||
required property string serviceId
|
||||
|
||||
property bool stayAwake: false
|
||||
property bool enabled: false
|
||||
property bool doNotDisturb: false
|
||||
property var activePlayer: null
|
||||
property var sourcePlayers: []
|
||||
|
||||
property var _setIdleEnabled: null
|
||||
property var _setNightlight: null
|
||||
property var _setDoNotDisturb: null
|
||||
property var _runAction: null
|
||||
property var _playerKey: null
|
||||
property var _selectPlayer: null
|
||||
|
||||
function setIdleEnabled(value) {
|
||||
if (serviceId === "omarchy.idle" && _setIdleEnabled) _setIdleEnabled(!!value)
|
||||
}
|
||||
|
||||
function setNightlight(value) {
|
||||
if (serviceId === "omarchy.nightlight" && _setNightlight) _setNightlight(!!value)
|
||||
}
|
||||
|
||||
function setDoNotDisturb(value) {
|
||||
if (serviceId === "omarchy.notifications" && _setDoNotDisturb) _setDoNotDisturb(!!value)
|
||||
}
|
||||
|
||||
function runAction(action, showFeedback, playerId) {
|
||||
if (serviceId === "omarchy.media" && _runAction)
|
||||
_runAction(String(action || ""), !!showFeedback, String(playerId || ""))
|
||||
}
|
||||
|
||||
function playerKey(player) {
|
||||
return serviceId === "omarchy.media" && _playerKey ? _playerKey(player) : ""
|
||||
}
|
||||
|
||||
function selectPlayer(playerId) {
|
||||
if (serviceId === "omarchy.media" && _selectPlayer) _selectPlayer(String(playerId || ""))
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ QtObject {
|
||||
property var shellConfigProvider: null
|
||||
property var shellConfigMutator: null
|
||||
|
||||
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
|
||||
// { pluginId: manifest } — manifests have source/trust metadata stamped in.
|
||||
property var installedPlugins: ({})
|
||||
property int registryRevision: 0
|
||||
property bool scanning: false
|
||||
@@ -90,6 +90,32 @@ QtObject {
|
||||
return manifest
|
||||
}
|
||||
|
||||
function trustedCapabilities(manifest) {
|
||||
if (!manifest || !manifest.__isFirstParty) return []
|
||||
var metadata = Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var declared = metadata && Array.isArray(metadata.capabilities) ? metadata.capabilities : []
|
||||
var out = []
|
||||
for (var i = 0; i < declared.length; i++) {
|
||||
var capability = String(declared[i] || "")
|
||||
if (capability && out.indexOf(capability) === -1) out.push(capability)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function stampHostCapabilities(firstParty, thirdParty) {
|
||||
for (var firstPartyId in firstParty)
|
||||
firstParty[firstPartyId].__hostCapabilities = trustedCapabilities(firstParty[firstPartyId])
|
||||
|
||||
for (var thirdPartyId in thirdParty) {
|
||||
var manifest = thirdParty[thirdPartyId]
|
||||
var metadata = manifest && Util.isPlainObject(manifest.omarchy) ? manifest.omarchy : null
|
||||
var clonedFrom = metadata ? String(metadata.clonedFrom || "") : ""
|
||||
var source = clonedFrom ? firstParty[clonedFrom] : null
|
||||
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
|
||||
? source.__hostCapabilities.slice() : []
|
||||
}
|
||||
}
|
||||
|
||||
function entryPointUrl(manifest, kind) {
|
||||
if (!Util.isPlainObject(manifest)) return ""
|
||||
var ep = manifest.entryPoints ? manifest.entryPoints[kind] : null
|
||||
@@ -594,6 +620,8 @@ QtObject {
|
||||
}
|
||||
flush()
|
||||
|
||||
stampHostCapabilities(firstParty, thirdParty)
|
||||
|
||||
var merged = {}
|
||||
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
||||
// Third-party plugins never shadow first-party ids. The whole
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
|
||||
// Read-only, self-scoped registry view for an installed third-party plugin.
|
||||
// The host updates manifest/enabled when it rescans; no host registry object is
|
||||
// retained here, so `parent` and property traversal cannot reach ShellRoot.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
property var manifest: null
|
||||
property bool enabled: false
|
||||
property var _entryPointUrl: null
|
||||
|
||||
readonly property var installedPlugins: {
|
||||
var out = ({})
|
||||
if (manifest) out[pluginId] = manifest
|
||||
return out
|
||||
}
|
||||
|
||||
function isEnabled(id) {
|
||||
return String(id || "") === pluginId && enabled
|
||||
}
|
||||
|
||||
function resolveEnabledId(id) {
|
||||
return String(id || "") === pluginId ? pluginId : ""
|
||||
}
|
||||
|
||||
function entryPointUrl(candidate, kind) {
|
||||
if (!candidate || String(candidate.id || "") !== pluginId) return ""
|
||||
return _entryPointUrl ? _entryPointUrl(String(kind || "")) : ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
|
||||
// Capability-scoped shell surface for installed third-party plugins.
|
||||
//
|
||||
// The callbacks are closed over one plugin id by shell.qml. A plugin can call
|
||||
// them directly, but it cannot widen their scope: ordinary plugins are limited
|
||||
// to their own id, and full-bar callbacks independently enforce their explicit
|
||||
// non-authentication UI scope. Keeping the host shell out of this object's
|
||||
// properties also prevents ordinary QML object traversal from turning the
|
||||
// facade back into the root ShellRoot.
|
||||
QtObject {
|
||||
id: api
|
||||
|
||||
required property string pluginId
|
||||
|
||||
property var appLibrary: null
|
||||
property var bar: null
|
||||
property var barConfig: ({})
|
||||
|
||||
property var _serviceLookup: null
|
||||
property var _firstPartyServiceLookup: null
|
||||
property var _pluginShellLookup: null
|
||||
property var _barEntryShellLookup: null
|
||||
property var _summon: null
|
||||
property var _hide: null
|
||||
property var _toggle: null
|
||||
property var _isOpen: null
|
||||
property var _updateSettings: null
|
||||
property var _mutateBarConfig: null
|
||||
|
||||
function serviceFor(id) {
|
||||
return _serviceLookup ? _serviceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
// Only full-bar facades receive narrow proxies for the specific
|
||||
// non-authentication services used by the built-in bar widgets.
|
||||
function firstPartyServiceFor(id) {
|
||||
return _firstPartyServiceLookup
|
||||
? _firstPartyServiceLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
function pluginShellForId(id) {
|
||||
return _pluginShellLookup ? _pluginShellLookup(String(id || "")) : null
|
||||
}
|
||||
|
||||
function pluginShellForBarEntry(ownerId, moduleName) {
|
||||
return _barEntryShellLookup
|
||||
? _barEntryShellLookup(String(ownerId || ""), String(moduleName || "")) : null
|
||||
}
|
||||
|
||||
function summon(id, payloadJson) {
|
||||
return _summon ? _summon(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function hide(id) {
|
||||
return _hide ? _hide(String(id || "")) : false
|
||||
}
|
||||
|
||||
function toggle(id, payloadJson) {
|
||||
return _toggle ? _toggle(String(id || ""), String(payloadJson || "")) : false
|
||||
}
|
||||
|
||||
function isPluginOpen(id) {
|
||||
return _isOpen ? _isOpen(String(id || "")) : false
|
||||
}
|
||||
|
||||
function updateEntryInline(id, settings) {
|
||||
return _updateSettings ? _updateSettings(String(id || ""), settings) : false
|
||||
}
|
||||
|
||||
function mutateShellConfig(mutator) {
|
||||
return _mutateBarConfig && typeof mutator === "function"
|
||||
? _mutateBarConfig(mutator) : false
|
||||
}
|
||||
}
|
||||
+524
-23
@@ -7,6 +7,7 @@ import qs.Commons
|
||||
|
||||
import "plugins/bar"
|
||||
import "services"
|
||||
import "services/AuthServiceStore.js" as AuthServiceStore
|
||||
|
||||
ShellRoot {
|
||||
id: shell
|
||||
@@ -214,10 +215,10 @@ ShellRoot {
|
||||
function configureBar(target, manifest) {
|
||||
if (!target) return
|
||||
if ("omarchyPath" in target) target.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in target) target.shell = shell
|
||||
if ("manifest" in target) target.manifest = manifest
|
||||
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistry
|
||||
if ("shell" in target) target.shell = shell.pluginShellFor(manifest)
|
||||
if ("manifest" in target) target.manifest = shell.publicPluginManifest(manifest)
|
||||
if ("barWidgetRegistry" in target) target.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
|
||||
if ("pluginRegistry" in target) target.pluginRegistry = shell.pluginRegistryFor(manifest)
|
||||
if ("barConfig" in target) target.barConfig = shell.barConfig
|
||||
shell.bar = target
|
||||
}
|
||||
@@ -253,8 +254,7 @@ ShellRoot {
|
||||
onActiveChanged: if (!active) shell.bar = null
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
var detail = errorString && errorString() ? errorString() : ""
|
||||
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId + ":", detail)
|
||||
console.warn("bar option " + shell.activeBarId + " failed to load, falling back to " + shell.defaultBarId)
|
||||
shell.failedBarId = shell.activeBarId
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,462 @@ ShellRoot {
|
||||
}
|
||||
|
||||
property var _services: ({})
|
||||
property var _pluginShellApis: ({})
|
||||
property var _pluginBarEntryShellApis: ({})
|
||||
property var _pluginRegistryApis: ({})
|
||||
property var _pluginBarWidgetRegistryApis: ({})
|
||||
property var _pluginAppLibraryApis: ({})
|
||||
property var _pluginBarStateApis: ({})
|
||||
property var _pluginFirstPartyServiceApis: ({})
|
||||
|
||||
Component {
|
||||
id: pluginShellApiComponent
|
||||
PluginShellApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginRegistryApiComponent
|
||||
PluginRegistryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginBarWidgetRegistryApiComponent
|
||||
PluginBarWidgetRegistryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginAppLibraryApiComponent
|
||||
PluginAppLibraryApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginBarStateApiComponent
|
||||
PluginBarStateApi { }
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginFirstPartyServiceApiComponent
|
||||
PluginFirstPartyServiceApi { }
|
||||
}
|
||||
|
||||
function publicPluginManifest(manifest) {
|
||||
if (!manifest) return null
|
||||
if (manifest.__isFirstParty) return manifest
|
||||
var copy = JSON.parse(JSON.stringify(manifest))
|
||||
delete copy.__sourceDir
|
||||
delete copy.__isFirstParty
|
||||
delete copy.__hostCapabilities
|
||||
return copy
|
||||
}
|
||||
|
||||
function publicBarConfig() {
|
||||
return JSON.parse(JSON.stringify(shell.barConfig || {}))
|
||||
}
|
||||
|
||||
function publicBarWidgetSnapshot() {
|
||||
var source = shell.barWidgetRegistry.widgets || {}
|
||||
var snapshot = {}
|
||||
for (var id in source) {
|
||||
var entry = source[id]
|
||||
if (!entry) continue
|
||||
snapshot[id] = {
|
||||
component: entry.component,
|
||||
metadata: JSON.parse(JSON.stringify(entry.metadata || {}))
|
||||
}
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function manifestHasKind(manifest, kind) {
|
||||
return !!manifest && Array.isArray(manifest.kinds)
|
||||
&& manifest.kinds.indexOf(kind) !== -1
|
||||
}
|
||||
|
||||
function pluginHasBarCapabilities(manifest) {
|
||||
return shell.manifestHasKind(manifest, "bar")
|
||||
}
|
||||
|
||||
function pluginOwnsTarget(pluginId, requestedId) {
|
||||
var caller = String(pluginId || "")
|
||||
if (!caller) return false
|
||||
return shell.pluginRegistry.resolveEnabledId(String(requestedId || "")) === caller
|
||||
}
|
||||
|
||||
function pluginServiceFor(pluginId, requestedId) {
|
||||
if (!shell.pluginOwnsTarget(pluginId, requestedId)) return null
|
||||
return shell.serviceFor(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
}
|
||||
|
||||
function barEntryConfigured(pluginId) {
|
||||
var location = shell.pluginRegistry.findEntryLocation(shell.shellConfig, pluginId)
|
||||
return location && location.kind === "bar"
|
||||
}
|
||||
|
||||
function barPluginMayControl(manifest, requestedId) {
|
||||
if (!shell.pluginHasBarCapabilities(manifest)) return false
|
||||
var id = shell.pluginRegistry.resolveEnabledId(String(requestedId || ""))
|
||||
var target = shell.pluginRegistry.installedPlugins[id]
|
||||
if (!target || shell.isAuthenticationService(target)) return false
|
||||
if (shell.barEntryConfigured(id)) return true
|
||||
var uiKinds = ["bar-widget", "panel", "overlay", "menu"]
|
||||
for (var i = 0; i < uiKinds.length; i++)
|
||||
if (shell.manifestHasKind(target, uiKinds[i])) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function mutatePluginBarConfig(mutator) {
|
||||
if (typeof mutator !== "function") return false
|
||||
shell.mutateShellConfig(function(config) {
|
||||
var scoped = { bar: JSON.parse(JSON.stringify(config.bar || {})) }
|
||||
mutator(scoped)
|
||||
if (Util.isPlainObject(scoped.bar)) config.bar = JSON.parse(JSON.stringify(scoped.bar))
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
function pluginAppLibraryFor(cacheKey, pluginId) {
|
||||
if (_pluginAppLibraryApis[cacheKey]) return _pluginAppLibraryApis[cacheKey]
|
||||
var api = pluginAppLibraryApiComponent.createObject(null, {
|
||||
ownerPluginId: pluginId,
|
||||
_entryName: function(entry) { return shell.appLibrary.entryName(entry) },
|
||||
_entrySubtext: function(entry) { return shell.appLibrary.entrySubtext(entry) },
|
||||
_sortedEntries: function(query) { return shell.appLibrary.sortedEntries(query) },
|
||||
_iconSource: function(icon) { return shell.appLibrary.iconSource(icon) },
|
||||
_refreshIcons: function() { shell.appLibrary.refreshIcons() },
|
||||
_launch: function(desktopId, name) { shell.appLibrary.launch(desktopId, name) },
|
||||
_remove: function(desktopId, name) { shell.appLibrary.remove(desktopId, name) }
|
||||
})
|
||||
if (!api) return null
|
||||
var next = ({})
|
||||
for (var id in _pluginAppLibraryApis) next[id] = _pluginAppLibraryApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginAppLibraryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarStateFor(cacheKey, pluginId) {
|
||||
if (_pluginBarStateApis[cacheKey]) return _pluginBarStateApis[cacheKey]
|
||||
var api = pluginBarStateApiComponent.createObject(null, { ownerPluginId: pluginId })
|
||||
if (!api) return null
|
||||
api.barHidden = Qt.binding(function() { return shell.bar ? shell.bar.barHidden === true : false })
|
||||
api.barSize = Qt.binding(function() { return shell.bar ? Math.max(0, shell.bar.barSize || 0) : 0 })
|
||||
api.fontFamily = Qt.binding(function() { return shell.bar ? String(shell.bar.fontFamily || "") : "" })
|
||||
api.position = Qt.binding(function() { return shell.bar ? String(shell.bar.position || "top") : "top" })
|
||||
var next = ({})
|
||||
for (var id in _pluginBarStateApis) next[id] = _pluginBarStateApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginBarStateApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginFirstPartyServiceFor(cacheKey, pluginId, requestedId) {
|
||||
var id = String(requestedId || "")
|
||||
var allowed = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
|
||||
if (allowed.indexOf(id) === -1) return null
|
||||
var proxyKey = cacheKey + "::" + id
|
||||
if (_pluginFirstPartyServiceApis[proxyKey]) return _pluginFirstPartyServiceApis[proxyKey]
|
||||
|
||||
function service() { return shell.serviceFor(id) }
|
||||
var api = pluginFirstPartyServiceApiComponent.createObject(null, {
|
||||
ownerPluginId: pluginId,
|
||||
serviceId: id,
|
||||
_setIdleEnabled: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setIdleEnabled === "function") target.setIdleEnabled(value)
|
||||
},
|
||||
_setNightlight: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setNightlight === "function") target.setNightlight(value)
|
||||
},
|
||||
_setDoNotDisturb: function(value) {
|
||||
var target = service()
|
||||
if (target && typeof target.setDoNotDisturb === "function") target.setDoNotDisturb(value)
|
||||
},
|
||||
_runAction: function(action, showFeedback, targetKey) {
|
||||
var target = service()
|
||||
if (target && typeof target.runAction === "function") target.runAction(action, showFeedback, targetKey)
|
||||
},
|
||||
_playerKey: function(player) {
|
||||
var target = service()
|
||||
return target && typeof target.playerKey === "function" ? target.playerKey(player) : ""
|
||||
},
|
||||
_selectPlayer: function(playerKey) {
|
||||
var target = service()
|
||||
if (target && typeof target.selectPlayer === "function") target.selectPlayer(playerKey)
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
api.stayAwake = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.stayAwake === true : false
|
||||
})
|
||||
api.enabled = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.enabled === true : false
|
||||
})
|
||||
api.doNotDisturb = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.doNotDisturb === true : false
|
||||
})
|
||||
api.activePlayer = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target ? target.activePlayer : null
|
||||
})
|
||||
api.sourcePlayers = Qt.binding(function() {
|
||||
var target = service()
|
||||
return target && Array.isArray(target.sourcePlayers) ? target.sourcePlayers : []
|
||||
})
|
||||
var next = ({})
|
||||
for (var existing in _pluginFirstPartyServiceApis) next[existing] = _pluginFirstPartyServiceApis[existing]
|
||||
next[proxyKey] = api
|
||||
_pluginFirstPartyServiceApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function createScopedPluginShell(manifest, cacheKey, allowOwnService, barCapabilities) {
|
||||
if (_pluginShellApis[cacheKey]) return _pluginShellApis[cacheKey]
|
||||
var key = String(manifest && manifest.id || "")
|
||||
if (!key) return null
|
||||
|
||||
// Construct the narrow service proxies before any plugin binding can call
|
||||
// firstPartyServiceFor(). Creating a QObject while evaluating that binding
|
||||
// makes QML re-enter the binding and report a loop on the caller's service
|
||||
// property, even though the resulting proxy is otherwise acyclic.
|
||||
var firstPartyServices = ({})
|
||||
if (barCapabilities) {
|
||||
var serviceIds = ["omarchy.idle", "omarchy.media", "omarchy.nightlight", "omarchy.notifications"]
|
||||
for (var i = 0; i < serviceIds.length; i++) {
|
||||
var serviceId = serviceIds[i]
|
||||
firstPartyServices[serviceId] = shell.pluginFirstPartyServiceFor(cacheKey, key, serviceId)
|
||||
}
|
||||
}
|
||||
|
||||
var api = pluginShellApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
appLibrary: shell.manifestHasKind(manifest, "menu")
|
||||
? shell.pluginAppLibraryFor(cacheKey, key) : null,
|
||||
bar: shell.pluginBarStateFor(cacheKey, key),
|
||||
barConfig: shell.publicBarConfig(),
|
||||
_serviceLookup: function(requestedId) {
|
||||
return allowOwnService ? shell.pluginServiceFor(key, requestedId) : null
|
||||
},
|
||||
_firstPartyServiceLookup: function(requestedId) {
|
||||
return barCapabilities ? (firstPartyServices[requestedId] || null) : null
|
||||
},
|
||||
_pluginShellLookup: function(requestedId) {
|
||||
return barCapabilities ? shell.scopedPluginShellForId(requestedId) : null
|
||||
},
|
||||
_barEntryShellLookup: function(ownerId, moduleName) {
|
||||
return barCapabilities
|
||||
? shell.pluginShellForBarEntry(cacheKey + ":" + ownerId, moduleName) : null
|
||||
},
|
||||
_summon: function(requestedId, payloadJson) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(manifest, requestedId)) return false
|
||||
return shell.summon(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
|
||||
},
|
||||
_hide: function(requestedId) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(manifest, requestedId)) return false
|
||||
return shell.hide(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
},
|
||||
_toggle: function(requestedId, payloadJson) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(manifest, requestedId)) return false
|
||||
return shell.toggle(shell.pluginRegistry.resolveEnabledId(requestedId), payloadJson)
|
||||
},
|
||||
_isOpen: function(requestedId) {
|
||||
if (!shell.pluginOwnsTarget(key, requestedId)
|
||||
&& !shell.barPluginMayControl(manifest, requestedId)) return false
|
||||
return shell.isPluginOpen(shell.pluginRegistry.resolveEnabledId(requestedId))
|
||||
},
|
||||
_updateSettings: function(requestedId, settings) {
|
||||
if (shell.pluginOwnsTarget(key, requestedId)) return shell.updateEntryInline(key, settings)
|
||||
if (barCapabilities && shell.barEntryConfigured(requestedId))
|
||||
return shell.updateEntryInline(requestedId, settings)
|
||||
return false
|
||||
},
|
||||
_mutateBarConfig: function(mutator) {
|
||||
return barCapabilities ? shell.mutatePluginBarConfig(mutator) : false
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginShellApis) next[id] = _pluginShellApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginShellApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function scopedPluginShellForId(pluginId) {
|
||||
var key = String(pluginId || "")
|
||||
var manifest = shell.pluginRegistry.installedPlugins[key]
|
||||
if (!manifest) return null
|
||||
if (!manifest.__isFirstParty) return shell.pluginShellFor(manifest)
|
||||
return shell.createScopedPluginShell(manifest, "hosted:" + key, false, false)
|
||||
}
|
||||
|
||||
function pluginShellForId(pluginId) {
|
||||
return shell.scopedPluginShellForId(pluginId)
|
||||
}
|
||||
|
||||
function pluginShellForBarEntry(ownerId, moduleName) {
|
||||
var owner = String(ownerId || "")
|
||||
var target = String(moduleName || "")
|
||||
if (!owner || !target) return null
|
||||
var cacheKey = owner + "::" + target
|
||||
if (_pluginBarEntryShellApis[cacheKey]) return _pluginBarEntryShellApis[cacheKey]
|
||||
var api = pluginShellApiComponent.createObject(null, {
|
||||
pluginId: target,
|
||||
barConfig: shell.publicBarConfig(),
|
||||
_updateSettings: function(requestedId, settings) {
|
||||
return String(requestedId || "") === target
|
||||
? shell.updateEntryInline(target, settings) : false
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
var next = ({})
|
||||
for (var id in _pluginBarEntryShellApis) next[id] = _pluginBarEntryShellApis[id]
|
||||
next[cacheKey] = api
|
||||
_pluginBarEntryShellApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginShellFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
|
||||
}
|
||||
|
||||
function pluginRegistryFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell.pluginRegistry
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
if (_pluginRegistryApis[key]) return _pluginRegistryApis[key]
|
||||
|
||||
var api = pluginRegistryApiComponent.createObject(null, {
|
||||
pluginId: key,
|
||||
manifest: shell.publicPluginManifest(manifest),
|
||||
enabled: shell.pluginRegistry.isEnabled(key),
|
||||
_entryPointUrl: function(kind) {
|
||||
var current = shell.pluginRegistry.installedPlugins[key]
|
||||
return current ? shell.pluginRegistry.entryPointUrl(current, kind) : ""
|
||||
}
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginRegistryApis) next[id] = _pluginRegistryApis[id]
|
||||
next[key] = api
|
||||
_pluginRegistryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginBarWidgetRegistryFor(manifest) {
|
||||
if (!manifest || manifest.__isFirstParty) return shell.barWidgetRegistry
|
||||
var key = String(manifest.id || "")
|
||||
if (!key) return null
|
||||
if (_pluginBarWidgetRegistryApis[key]) return _pluginBarWidgetRegistryApis[key]
|
||||
|
||||
var api = pluginBarWidgetRegistryApiComponent.createObject(null, {
|
||||
widgets: shell.publicBarWidgetSnapshot(),
|
||||
revision: shell.barWidgetRegistry.revision
|
||||
})
|
||||
if (!api) return null
|
||||
|
||||
var next = ({})
|
||||
for (var id in _pluginBarWidgetRegistryApis) next[id] = _pluginBarWidgetRegistryApis[id]
|
||||
next[key] = api
|
||||
_pluginBarWidgetRegistryApis = next
|
||||
return api
|
||||
}
|
||||
|
||||
function pluginApiActive(api, plugins) {
|
||||
var id = api ? String(api.pluginId || api.ownerPluginId || "") : ""
|
||||
var manifest = id ? plugins[id] : null
|
||||
return !!manifest && shell.pluginRegistry.isEnabled(id)
|
||||
}
|
||||
|
||||
function prunePluginApis() {
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
var shellNext = ({})
|
||||
for (var shellKey in _pluginShellApis) {
|
||||
var shellApi = _pluginShellApis[shellKey]
|
||||
if (shell.pluginApiActive(shellApi, plugins)) shellNext[shellKey] = shellApi
|
||||
else if (shellApi && typeof shellApi.destroy === "function") shellApi.destroy()
|
||||
}
|
||||
_pluginShellApis = shellNext
|
||||
|
||||
var registryNext = ({})
|
||||
for (var registryKey in _pluginRegistryApis) {
|
||||
var registryApi = _pluginRegistryApis[registryKey]
|
||||
if (shell.pluginApiActive(registryApi, plugins)) registryNext[registryKey] = registryApi
|
||||
else if (registryApi && typeof registryApi.destroy === "function") registryApi.destroy()
|
||||
}
|
||||
_pluginRegistryApis = registryNext
|
||||
|
||||
var widgetNext = ({})
|
||||
for (var widgetKey in _pluginBarWidgetRegistryApis) {
|
||||
var widgetApi = _pluginBarWidgetRegistryApis[widgetKey]
|
||||
if (plugins[widgetKey] && shell.pluginRegistry.isEnabled(widgetKey)) widgetNext[widgetKey] = widgetApi
|
||||
else if (widgetApi && typeof widgetApi.destroy === "function") widgetApi.destroy()
|
||||
}
|
||||
_pluginBarWidgetRegistryApis = widgetNext
|
||||
|
||||
var appNext = ({})
|
||||
for (var appKey in _pluginAppLibraryApis) {
|
||||
var appApi = _pluginAppLibraryApis[appKey]
|
||||
if (shell.pluginApiActive(appApi, plugins)) appNext[appKey] = appApi
|
||||
else if (appApi && typeof appApi.destroy === "function") appApi.destroy()
|
||||
}
|
||||
_pluginAppLibraryApis = appNext
|
||||
|
||||
var barStateNext = ({})
|
||||
for (var barStateKey in _pluginBarStateApis) {
|
||||
var barStateApi = _pluginBarStateApis[barStateKey]
|
||||
if (shell.pluginApiActive(barStateApi, plugins)) barStateNext[barStateKey] = barStateApi
|
||||
else if (barStateApi && typeof barStateApi.destroy === "function") barStateApi.destroy()
|
||||
}
|
||||
_pluginBarStateApis = barStateNext
|
||||
|
||||
var serviceNext = ({})
|
||||
for (var serviceKey in _pluginFirstPartyServiceApis) {
|
||||
var serviceApi = _pluginFirstPartyServiceApis[serviceKey]
|
||||
if (shell.pluginApiActive(serviceApi, plugins)) serviceNext[serviceKey] = serviceApi
|
||||
else if (serviceApi && typeof serviceApi.destroy === "function") serviceApi.destroy()
|
||||
}
|
||||
_pluginFirstPartyServiceApis = serviceNext
|
||||
|
||||
var entryNext = ({})
|
||||
for (var entryKey in _pluginBarEntryShellApis) {
|
||||
var entryApi = _pluginBarEntryShellApis[entryKey]
|
||||
if (entryApi && shell.barEntryConfigured(entryApi.pluginId)) entryNext[entryKey] = entryApi
|
||||
else if (entryApi && typeof entryApi.destroy === "function") entryApi.destroy()
|
||||
}
|
||||
_pluginBarEntryShellApis = entryNext
|
||||
}
|
||||
|
||||
function syncPluginApis() {
|
||||
shell.prunePluginApis()
|
||||
var plugins = shell.pluginRegistry.installedPlugins
|
||||
for (var id in _pluginRegistryApis) {
|
||||
var registryApi = _pluginRegistryApis[id]
|
||||
var manifest = plugins[id]
|
||||
registryApi.manifest = shell.publicPluginManifest(manifest)
|
||||
registryApi.enabled = !!manifest && shell.pluginRegistry.isEnabled(id)
|
||||
}
|
||||
for (var widgetId in _pluginBarWidgetRegistryApis) {
|
||||
var widgetApi = _pluginBarWidgetRegistryApis[widgetId]
|
||||
widgetApi.widgets = shell.publicBarWidgetSnapshot()
|
||||
widgetApi.revision = shell.barWidgetRegistry.revision
|
||||
}
|
||||
for (var shellKey in _pluginShellApis)
|
||||
_pluginShellApis[shellKey].barConfig = shell.publicBarConfig()
|
||||
for (var entryKey in _pluginBarEntryShellApis)
|
||||
_pluginBarEntryShellApis[entryKey].barConfig = shell.publicBarConfig()
|
||||
}
|
||||
|
||||
function serviceFor(pluginId) {
|
||||
return _services[String(pluginId)] || null
|
||||
@@ -280,6 +736,11 @@ ShellRoot {
|
||||
return serviceFor(pluginId)
|
||||
}
|
||||
|
||||
function isAuthenticationService(manifest) {
|
||||
return !!manifest && Array.isArray(manifest.__hostCapabilities)
|
||||
&& manifest.__hostCapabilities.indexOf("authentication") !== -1
|
||||
}
|
||||
|
||||
function ensureService(pluginId) {
|
||||
var key = String(pluginId)
|
||||
if (_services[key]) return _services[key]
|
||||
@@ -290,6 +751,8 @@ ShellRoot {
|
||||
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
|
||||
var url = pluginRegistry.entryPointUrl(manifest, "service")
|
||||
if (!url) return null
|
||||
var authenticationService = shell.isAuthenticationService(manifest)
|
||||
if (authenticationService && AuthServiceStore.has(key)) return null
|
||||
|
||||
var comp = Qt.createComponent(url, Component.PreferSynchronous)
|
||||
function finalize() {
|
||||
@@ -297,27 +760,37 @@ ShellRoot {
|
||||
console.warn("service plugin load failed for " + key + ": " + comp.errorString())
|
||||
return
|
||||
}
|
||||
var inst = comp.createObject(serviceHost)
|
||||
// Authentication services and third-party services have no visual
|
||||
// parent. Parenting either to serviceHost would let a plugin's object
|
||||
// traversal walk between the host and credential-bearing QML.
|
||||
var inst = comp.createObject(manifest.__isFirstParty && !authenticationService ? serviceHost : null)
|
||||
if (!inst) {
|
||||
console.warn("service plugin createObject returned null for", key)
|
||||
return
|
||||
}
|
||||
if ("omarchyPath" in inst) inst.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in inst) inst.shell = shell
|
||||
if ("manifest" in inst) inst.manifest = manifest
|
||||
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistry
|
||||
var snext = ({})
|
||||
for (var sk in _services) snext[sk] = _services[sk]
|
||||
snext[key] = inst
|
||||
_services = snext
|
||||
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
|
||||
if ("manifest" in inst) inst.manifest = shell.publicPluginManifest(manifest)
|
||||
if ("barWidgetRegistry" in inst) inst.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(manifest)
|
||||
if ("pluginRegistry" in inst) inst.pluginRegistry = shell.pluginRegistryFor(manifest)
|
||||
if (authenticationService) {
|
||||
// Never publish lock/polkit through ShellRoot._services. The private JS
|
||||
// import retains their lifetime without adding a traversable property
|
||||
// or QObject parent back to the host shell.
|
||||
AuthServiceStore.put(key, inst)
|
||||
} else {
|
||||
var snext = ({})
|
||||
for (var sk in _services) snext[sk] = _services[sk]
|
||||
snext[key] = inst
|
||||
_services = snext
|
||||
}
|
||||
}
|
||||
if (comp.status === Component.Loading) {
|
||||
comp.statusChanged.connect(finalize)
|
||||
return null
|
||||
}
|
||||
finalize()
|
||||
return _services[key] || null
|
||||
return authenticationService ? null : (_services[key] || null)
|
||||
}
|
||||
|
||||
function _syncServices() {
|
||||
@@ -329,7 +802,7 @@ ShellRoot {
|
||||
if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue
|
||||
if (!m.entryPoints || !m.entryPoints.service) continue
|
||||
if (!pluginRegistry.isEnabled(id)) continue
|
||||
if (_services[id]) continue
|
||||
if (_services[id] || (shell.isAuthenticationService(m) && AuthServiceStore.has(id))) continue
|
||||
ensureService(id)
|
||||
}
|
||||
// Drop services for plugins that have been disabled or removed.
|
||||
@@ -343,6 +816,16 @@ ShellRoot {
|
||||
for (var k in _services) if (k !== existingId) next[k] = _services[k]
|
||||
_services = next
|
||||
}
|
||||
// Authentication services are retained outside the root object graph, so
|
||||
// reconcile their disable/remove lifecycle separately from _services.
|
||||
var authenticationIds = AuthServiceStore.ids()
|
||||
for (var ai = 0; ai < authenticationIds.length; ai++) {
|
||||
var authenticationId = authenticationIds[ai]
|
||||
var authenticationManifest = plugins[authenticationId]
|
||||
if (authenticationManifest && pluginRegistry.isEnabled(authenticationId)
|
||||
&& shell.isAuthenticationService(authenticationManifest)) continue
|
||||
AuthServiceStore.destroy(authenticationId)
|
||||
}
|
||||
}
|
||||
|
||||
function unloadPluginServices() {
|
||||
@@ -351,11 +834,28 @@ ShellRoot {
|
||||
if (inst && typeof inst.destroy === "function") inst.destroy()
|
||||
}
|
||||
_services = ({})
|
||||
AuthServiceStore.destroyAll()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.pluginRegistry
|
||||
function onPluginsChanged() { if (!shell.pluginReloading) shell._syncServices() }
|
||||
function onPluginsChanged() {
|
||||
shell.syncPluginApis()
|
||||
if (!shell.pluginReloading) shell._syncServices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.barWidgetRegistry
|
||||
function onChanged() { shell.syncPluginApis() }
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell.appLibrary
|
||||
function onAppsChanged() {
|
||||
for (var id in shell._pluginAppLibraryApis)
|
||||
shell._pluginAppLibraryApis[id].appsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Writes inline settings to a bar layout entry or top-level plugin entry in
|
||||
@@ -627,10 +1127,10 @@ ShellRoot {
|
||||
onLoaded: {
|
||||
if (!item) return
|
||||
if ("omarchyPath" in item) item.omarchyPath = shell.omarchyPath
|
||||
if ("shell" in item) item.shell = shell
|
||||
if ("manifest" in item) item.manifest = panelEntry.manifest
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
|
||||
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)
|
||||
if ("manifest" in item) item.manifest = shell.publicPluginManifest(panelEntry.manifest)
|
||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.pluginBarWidgetRegistryFor(panelEntry.manifest)
|
||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistryFor(panelEntry.manifest)
|
||||
// Plugins that pair a panel UI with a service entry read shared
|
||||
// state off `service`. Hand them the matching singleton if one was
|
||||
// loaded.
|
||||
@@ -696,7 +1196,8 @@ ShellRoot {
|
||||
schema: meta.schema || [],
|
||||
pluginId: manifest.id,
|
||||
sourceDir: manifest.__sourceDir || "",
|
||||
source: "plugin"
|
||||
source: "plugin",
|
||||
firstParty: !!manifest.__isFirstParty
|
||||
}
|
||||
|
||||
// A load already in flight for this URL registers itself when it
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick
|
||||
import "services/AuthServiceStore.js" as AuthServiceStore
|
||||
|
||||
QtObject {
|
||||
function retain(id, service) {
|
||||
AuthServiceStore.put(id, service)
|
||||
}
|
||||
|
||||
function has(id) {
|
||||
return AuthServiceStore.has(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
import "services/AuthServiceStore.js" as AuthServiceStore
|
||||
|
||||
QtObject {
|
||||
function has(id) {
|
||||
return AuthServiceStore.has(id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "services"
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property var calls: []
|
||||
property QtObject ownService: QtObject { property string marker: "own" }
|
||||
|
||||
AuthStoreOwner { id: authStoreOwner }
|
||||
AuthStoreReader { id: authStoreReader }
|
||||
|
||||
Component {
|
||||
id: apiComponent
|
||||
PluginShellApi { }
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: resultFile
|
||||
path: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
||||
atomicWrites: true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
var caller = "example.safe"
|
||||
authStoreOwner.retain("omarchy.lock", root.ownService)
|
||||
var api = apiComponent.createObject(null, {
|
||||
pluginId: caller,
|
||||
_serviceLookup: function(requestedId) {
|
||||
return requestedId === caller ? root.ownService : null
|
||||
},
|
||||
_summon: function(requestedId) {
|
||||
if (requestedId !== caller) return false
|
||||
root.calls = root.calls.concat(["summon"])
|
||||
return true
|
||||
},
|
||||
_hide: function(requestedId) {
|
||||
if (requestedId !== caller) return false
|
||||
root.calls = root.calls.concat(["hide"])
|
||||
return true
|
||||
},
|
||||
_toggle: function(requestedId) {
|
||||
if (requestedId !== caller) return false
|
||||
root.calls = root.calls.concat(["toggle"])
|
||||
return true
|
||||
},
|
||||
_isOpen: function(requestedId) { return requestedId === caller },
|
||||
_updateSettings: function(requestedId) {
|
||||
if (requestedId !== caller) return false
|
||||
root.calls = root.calls.concat(["settings"])
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
var own = api.serviceFor(caller)
|
||||
var result = {
|
||||
detached: api.parent === undefined || api.parent === null,
|
||||
ownService: own && own.marker === "own",
|
||||
foreignService: api.serviceFor("omarchy.lock") === null,
|
||||
firstPartyService: api.firstPartyServiceFor("omarchy.polkit") === null,
|
||||
ownSummon: api.summon(caller, "{}") === true,
|
||||
foreignSummon: api.summon("omarchy.lock", "{}") === false,
|
||||
ownHide: api.hide(caller) === true,
|
||||
foreignHide: api.hide("omarchy.lock") === false,
|
||||
ownToggle: api.toggle(caller, "{}") === true,
|
||||
foreignToggle: api.toggle("omarchy.lock", "{}") === false,
|
||||
ownOpen: api.isPluginOpen(caller) === true,
|
||||
foreignOpen: api.isPluginOpen("omarchy.lock") === false,
|
||||
ownSettings: api.updateEntryInline(caller, {}) === true,
|
||||
foreignSettings: api.updateEntryInline("omarchy.lock", {}) === false,
|
||||
authStoreOwnerRetains: authStoreOwner.has("omarchy.lock") === true,
|
||||
authStoreImportIsolated: authStoreReader.has("omarchy.lock") === false,
|
||||
calls: root.calls
|
||||
}
|
||||
result.ok = Object.keys(result).every(function(key) {
|
||||
return key === "ok" || key === "calls" || result[key] === true
|
||||
}) && JSON.stringify(result.calls) === JSON.stringify(["summon", "hide", "toggle", "settings"])
|
||||
resultFile.setText(JSON.stringify(result))
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,9 @@ ShellRoot {
|
||||
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" }))
|
||||
var futureAuth = manifest("omarchy.future-auth", ["service"], { service: "Service.qml" })
|
||||
futureAuth.omarchy = { capabilities: ["authentication"] }
|
||||
scan += block("firstparty", "/first/future-auth", futureAuth)
|
||||
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" }))
|
||||
scan += block("thirdparty", "/third/center-widget", manifest("third.center-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
|
||||
@@ -103,6 +106,12 @@ ShellRoot {
|
||||
localBar.omarchy = { clonedFrom: "omarchy.bar" }
|
||||
scan += block("thirdparty", "/third/local-bar", localBar)
|
||||
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
|
||||
var localFutureAuth = manifest("local.future-auth", ["service"], { service: "Service.qml" })
|
||||
localFutureAuth.omarchy = { clonedFrom: "omarchy.future-auth" }
|
||||
scan += block("thirdparty", "/third/local-future-auth", localFutureAuth)
|
||||
var spoofedAuth = manifest("third.spoofed-auth", ["service"], { service: "Service.qml" })
|
||||
spoofedAuth.omarchy = { capabilities: ["authentication"] }
|
||||
scan += block("thirdparty", "/third/spoofed-auth", spoofedAuth)
|
||||
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" }))
|
||||
@@ -116,22 +125,28 @@ ShellRoot {
|
||||
root.assertDeepEqual(pluginIds(), [
|
||||
"local.bar",
|
||||
"local.first-widget",
|
||||
"local.future-auth",
|
||||
"local.grouped-panel",
|
||||
"local.hybrid",
|
||||
"local.weather",
|
||||
"omarchy.bar",
|
||||
"omarchy.first-widget",
|
||||
"omarchy.future-auth",
|
||||
"omarchy.grouped-panel",
|
||||
"omarchy.hybrid",
|
||||
"third.bar",
|
||||
"third.center-widget",
|
||||
"third.panel",
|
||||
"third.right-widget",
|
||||
"third.spoofed-auth",
|
||||
"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.assertDeepEqual(registry.installedPlugins["omarchy.future-auth"].__hostCapabilities, ["authentication"], "trusted manifests stamp authentication capability")
|
||||
root.assertDeepEqual(registry.installedPlugins["local.future-auth"].__hostCapabilities, ["authentication"], "clones inherit trusted host capabilities")
|
||||
root.assertDeepEqual(registry.installedPlugins["third.spoofed-auth"].__hostCapabilities, [], "third-party manifests cannot self-grant host capabilities")
|
||||
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")
|
||||
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
TMPDIR=""
|
||||
QS_PID=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
|
||||
kill "$QS_PID" 2>/dev/null || true
|
||||
wait "$QS_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
|
||||
rm -rf "$TMPDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
shell_qml="$ROOT/shell/shell.qml"
|
||||
bar_qml="$ROOT/shell/plugins/bar/Bar.qml"
|
||||
|
||||
# Normalize horizontal and vertical whitespace so the wiring assertions survive
|
||||
# harmless QML reflow. The runtime fixture below behaviorally covers
|
||||
# PluginShellApi and AuthServiceStore; these checks remain the guard for their
|
||||
# integration through shell.qml and Bar.qml, including without a compositor.
|
||||
qml_matches() {
|
||||
local file=$1
|
||||
local pattern=$2
|
||||
|
||||
tr '\n\r\t' ' ' < "$file" | grep -Eq "$pattern"
|
||||
}
|
||||
|
||||
qml_matches "$shell_qml" 'comp\.createObject\( *manifest\.__isFirstParty *&& *!authenticationService *\? *serviceHost *: *null *\)' ||
|
||||
fail "third-party and authentication services are detached from the host object tree"
|
||||
qml_matches "$shell_qml" 'AuthServiceStore\.put\( *key, *inst *\)' ||
|
||||
fail "authentication services are retained outside the host service map"
|
||||
pass "third-party and authentication services are detached from the host object tree"
|
||||
|
||||
qml_matches "$shell_qml" 'inst\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
|
||||
fail "service plugins receive a scoped shell facade"
|
||||
qml_matches "$shell_qml" 'item\.shell *= *shell\.pluginShellFor\( *panelEntry\.manifest *\)' ||
|
||||
fail "panel plugins receive a scoped shell facade"
|
||||
qml_matches "$shell_qml" 'target\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
|
||||
fail "full-bar plugins receive a scoped shell facade"
|
||||
pass "third-party entry points receive scoped shell facades"
|
||||
|
||||
qml_matches "$bar_qml" 'target\.bar *= *firstParty *\? *root *: *root\.pluginBarApiFor\( *pluginApiId, *moduleName, *registered *\)' ||
|
||||
fail "third-party widgets receive a bar facade instead of the host bar"
|
||||
qml_matches "$bar_qml" 'api\.clickTargets *= *root\.pluginClickTargets\( *api\.pluginId *\)' ||
|
||||
fail "third-party bar facades exclude other widgets from their object graph"
|
||||
pass "third-party widgets receive a bar facade instead of the host bar"
|
||||
|
||||
qml_matches "$shell_qml" 'widgets: *shell\.publicBarWidgetSnapshot\( *\)' ||
|
||||
fail "third-party widget registries receive detached snapshots"
|
||||
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *target, *"clickTarget" *\)' ||
|
||||
fail "third-party bar-object ownership is stamped by the host callback"
|
||||
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *owner, *"popout" *\)' ||
|
||||
fail "owner-less popouts receive trusted ownership before activation"
|
||||
qml_matches "$shell_qml" 'manifest\.__hostCapabilities\.indexOf\( *"authentication" *\)' ||
|
||||
fail "authentication isolation follows host-stamped capabilities"
|
||||
pass "registry mutation and ownership boundaries are host-controlled"
|
||||
|
||||
qml_matches "$bar_qml" 'root\.moduleWidgets\( *moduleName *\)' ||
|
||||
fail "custom bar module widget lookups use their real module name"
|
||||
qml_matches "$shell_qml" 'shell\.pluginShellForBarEntry\( *cacheKey *\+ *":" *\+ *ownerId, *moduleName *\)' ||
|
||||
fail "full-bar plugins receive a scoped settings facade for custom modules"
|
||||
pass "custom bar modules retain settings and popout identity"
|
||||
|
||||
if qml_matches "$bar_qml" 'on(Foreground|BarForeground|Background|Urgent|FontFamily|Vertical|BarSize|Transparent)Changed: *sync'; then
|
||||
fail "animated scalar properties still trigger full facade resyncs"
|
||||
fi
|
||||
qml_matches "$bar_qml" 'api\.foreground *= *Qt\.binding\( *function\( *\) *\{ *return root\.foreground *\} *\)' ||
|
||||
fail "third-party bar scalar mirrors use bindings"
|
||||
qml_matches "$shell_qml" 'shell\.prunePluginApis\( *\)' ||
|
||||
fail "disabled plugin facade caches are pruned"
|
||||
pass "plugin facade synchronization is bounded"
|
||||
|
||||
require_compositor "plugin authentication boundary runtime test"
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping plugin authentication boundary runtime test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_command jq
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
result="$TMPDIR/result.json"
|
||||
log="$TMPDIR/quickshell.log"
|
||||
config_dir="$TMPDIR/plugin-auth-boundary"
|
||||
mkdir -p "$config_dir" "$TMPDIR/home"
|
||||
cp "$SHELL_TEST_DIR/fixtures/plugin-auth-boundary/"*.qml "$config_dir/"
|
||||
ln -s "$ROOT/shell/services" "$config_dir/services"
|
||||
|
||||
OMARCHY_QML_TEST_RESULT="$result" \
|
||||
HOME="$TMPDIR/home" \
|
||||
XDG_CONFIG_HOME="$TMPDIR/home/.config" \
|
||||
XDG_CACHE_HOME="$TMPDIR/home/.cache" \
|
||||
XDG_STATE_HOME="$TMPDIR/home/.local/state" \
|
||||
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
|
||||
QS_PID=$!
|
||||
|
||||
for _ in {1..80}; do
|
||||
[[ -s $result ]] && break
|
||||
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "plugin authentication boundary fixture exited before writing result"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
[[ -s $result ]] || {
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "plugin authentication boundary runtime test timed out"
|
||||
}
|
||||
|
||||
if ! jq -e '.ok == true' "$result" >/dev/null; then
|
||||
jq . "$result" >&2
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "plugin authentication boundary runtime behavior"
|
||||
fi
|
||||
|
||||
pass "plugin authentication boundary runtime behavior"
|
||||
Reference in new issue
Block a user