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
+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
|
||||
|
||||
Reference in new issue
Block a user