mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-04 12:47:51 +02:00
351 lines
12 KiB
QML
351 lines
12 KiB
QML
import QtQuick
|
|
import QtQml.Models
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
import "plugins/bar"
|
|
import "services"
|
|
|
|
ShellRoot {
|
|
id: shell
|
|
|
|
// Shared service instances. Plugins receive these via property injection
|
|
// rather than re-importing them as singletons — relative-path imports do
|
|
// not share singleton state, which silently leaves consumers with their
|
|
// own empty copies.
|
|
property PluginRegistry pluginRegistry: PluginRegistry { }
|
|
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }
|
|
|
|
property string home: Quickshell.env("HOME")
|
|
|
|
// The omarchy-shell host is the long-running entry point. Plugins live in
|
|
// sibling directories under plugins/. Most child code (the bar, future
|
|
// plugins) accept omarchyPath as a required property; resolve it once here
|
|
// from the shellDir so we don't depend on OMARCHY_PATH being set in the env.
|
|
function deriveOmarchyPath() {
|
|
var env = Quickshell.env("OMARCHY_PATH")
|
|
if (env) return env
|
|
var dir = String(Quickshell.shellDir || "")
|
|
if (dir.indexOf("/default/quickshell/omarchy-shell") !== -1)
|
|
return dir.substring(0, dir.indexOf("/default/quickshell/omarchy-shell"))
|
|
return home + "/.local/share/omarchy"
|
|
}
|
|
property string omarchyPath: deriveOmarchyPath()
|
|
readonly property string firstPartyPluginsDir: omarchyPath + "/default/quickshell/omarchy-shell/plugins"
|
|
|
|
Component.onCompleted: {
|
|
console.log("omarchy-shell paths",
|
|
"omarchyPath=" + shell.omarchyPath,
|
|
"shellDir=" + Quickshell.shellDir,
|
|
"firstPartyPluginsDir=" + shell.firstPartyPluginsDir)
|
|
pluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
|
|
// PluginRegistry.ensureUserDir() runs in its own Component.onCompleted and
|
|
// chains rescan() once the directory exists. We also kick a scan here in
|
|
// case the user dir already existed at startup.
|
|
pluginRegistry.rescan()
|
|
}
|
|
|
|
Bar {
|
|
id: bar
|
|
omarchyPath: shell.omarchyPath
|
|
barWidgetRegistry: shell.barWidgetRegistry
|
|
}
|
|
|
|
// ---------------------------------------------------------- on-demand panels
|
|
|
|
// openPanelIds is a plain object treated as a set. A plugin id maps to
|
|
// `true` while the panel is summoned; deleting the key (well, building a new
|
|
// object without it) hides it. Reassigning the whole object is required for
|
|
// QML to notice the change.
|
|
property var openPanelIds: ({})
|
|
|
|
function isPanelOpen(id) { return openPanelIds[id] === true }
|
|
|
|
// Pending payloads to deliver to a plugin's open() once its loader resolves.
|
|
// Keyed by plugin id; the value is an array so two summon() calls before
|
|
// the Loader resolves both reach the plugin in arrival order rather than
|
|
// the second clobbering the first.
|
|
property var pendingPayloads: ({})
|
|
|
|
function summon(pluginId, payloadJson) {
|
|
var id = String(pluginId || "")
|
|
if (!id) return false
|
|
var plugins = shell.pluginRegistry.installedPlugins
|
|
if (!plugins[id]) {
|
|
console.warn("summon: unknown plugin", id)
|
|
return false
|
|
}
|
|
var next = ({})
|
|
for (var k in openPanelIds) next[k] = openPanelIds[k]
|
|
next[id] = true
|
|
openPanelIds = next
|
|
|
|
// Stash payload so the Loader.onLoaded handler can hand it to open().
|
|
var pending = ({})
|
|
for (var p in pendingPayloads) pending[p] = pendingPayloads[p].slice()
|
|
var queue = pending[id] || []
|
|
queue.push(payloadJson || "")
|
|
pending[id] = queue
|
|
pendingPayloads = pending
|
|
|
|
// If the plugin is keepLoaded and already mounted, deliver immediately.
|
|
deliverIfLoaded(id)
|
|
return true
|
|
}
|
|
|
|
function hide(pluginId) {
|
|
var id = String(pluginId || "")
|
|
if (!id) return false
|
|
invokeIfLoaded(id, "close", null)
|
|
if (!openPanelIds[id]) return true
|
|
var next = ({})
|
|
for (var k in openPanelIds) if (k !== id) next[k] = openPanelIds[k]
|
|
openPanelIds = next
|
|
return true
|
|
}
|
|
|
|
function toggle(pluginId, payloadJson) {
|
|
var id = String(pluginId || "")
|
|
return openPanelIds[id] ? hide(id) : summon(id, payloadJson)
|
|
}
|
|
|
|
// Map of pluginId -> Loader, populated by the Instantiator delegate below.
|
|
property var panelLoaders: ({})
|
|
|
|
function registerPanelLoader(pluginId, loader) {
|
|
var next = ({})
|
|
for (var k in panelLoaders) next[k] = panelLoaders[k]
|
|
next[pluginId] = loader
|
|
panelLoaders = next
|
|
deliverIfLoaded(pluginId)
|
|
}
|
|
|
|
function unregisterPanelLoader(pluginId) {
|
|
if (!panelLoaders[pluginId]) return
|
|
var next = ({})
|
|
for (var k in panelLoaders) if (k !== pluginId) next[k] = panelLoaders[k]
|
|
panelLoaders = next
|
|
}
|
|
|
|
function deliverIfLoaded(pluginId) {
|
|
var loader = panelLoaders[pluginId]
|
|
if (!loader || !loader.item) return
|
|
var queue = pendingPayloads[pluginId]
|
|
if (!Array.isArray(queue) || queue.length === 0) return
|
|
if (typeof loader.item.open === "function") {
|
|
for (var i = 0; i < queue.length; i++) {
|
|
try { loader.item.open(queue[i]) } catch (e) {
|
|
console.warn("plugin " + pluginId + " open() threw:", e)
|
|
}
|
|
}
|
|
}
|
|
var next = ({})
|
|
for (var k in pendingPayloads) if (k !== pluginId) next[k] = pendingPayloads[k].slice()
|
|
pendingPayloads = next
|
|
}
|
|
|
|
function invokeIfLoaded(pluginId, method, arg) {
|
|
var loader = panelLoaders[pluginId]
|
|
if (!loader || !loader.item) return
|
|
if (typeof loader.item[method] !== "function") return
|
|
try { loader.item[method](arg) } catch (e) {
|
|
console.warn("plugin " + pluginId + " " + method + "() threw:", e)
|
|
}
|
|
}
|
|
|
|
// One Loader per discoverable panel/overlay/menu plugin. Active when the
|
|
// host marks it open. The Loader holds onto the instance while active so the
|
|
// plugin's FloatingWindow + state survive between summons within a session.
|
|
property var panelEntries: []
|
|
|
|
function computePanelEntries() {
|
|
var out = []
|
|
var plugins = shell.pluginRegistry.installedPlugins
|
|
var panelKinds = ["panel", "overlay", "menu"]
|
|
for (var id in plugins) {
|
|
var m = plugins[id]
|
|
if (!m || !Array.isArray(m.kinds)) continue
|
|
var matched = false
|
|
for (var i = 0; i < panelKinds.length; i++)
|
|
if (m.kinds.indexOf(panelKinds[i]) !== -1) { matched = true; break }
|
|
if (!matched) continue
|
|
if (!shell.pluginRegistry.isEnabled(id)) continue
|
|
var kind = m.kinds.indexOf("panel") !== -1 ? "panel"
|
|
: (m.kinds.indexOf("overlay") !== -1 ? "overlay" : "menu")
|
|
out.push({ id: id, manifest: m, kind: kind, keepLoaded: m.keepLoaded === true })
|
|
}
|
|
return out
|
|
}
|
|
|
|
Connections {
|
|
target: shell.pluginRegistry
|
|
function onPluginsChanged() { shell.panelEntries = shell.computePanelEntries() }
|
|
}
|
|
|
|
Instantiator {
|
|
model: shell.panelEntries
|
|
active: true
|
|
|
|
delegate: QtObject {
|
|
id: panelEntry
|
|
required property var modelData
|
|
readonly property string pluginId: modelData.id
|
|
readonly property var manifest: modelData.manifest
|
|
readonly property string entryKind: modelData.kind
|
|
readonly property bool keepLoaded: modelData.keepLoaded === true
|
|
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
|
|
|
|
property Loader panelLoader: Loader {
|
|
source: panelEntry.sourceUrl
|
|
active: panelEntry.sourceUrl !== "" && (panelEntry.keepLoaded || shell.openPanelIds[panelEntry.pluginId] === true)
|
|
asynchronous: true
|
|
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
|
|
shell.registerPanelLoader(panelEntry.pluginId, this)
|
|
}
|
|
onStatusChanged: {
|
|
if (status === Loader.Error) {
|
|
console.warn("panel plugin " + panelEntry.pluginId + " failed:",
|
|
sourceComponent ? sourceComponent.errorString() : "")
|
|
shell.hide(panelEntry.pluginId)
|
|
}
|
|
}
|
|
Component.onDestruction: shell.unregisterPanelLoader(panelEntry.pluginId)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------- plugin loader
|
|
|
|
// Mirror plugin registry state into BarWidgetRegistry whenever it changes.
|
|
// Each enabled plugin with kind "bar-widget" gets a Component created from
|
|
// its manifest entry point and registered under the id "plugin:<id>".
|
|
Connections {
|
|
target: shell.pluginRegistry
|
|
function onPluginsChanged() { shell.syncPluginWidgets() }
|
|
}
|
|
|
|
property var pluginWidgetComponents: ({})
|
|
|
|
function syncPluginWidgets() {
|
|
var plugins = shell.pluginRegistry.installedPlugins
|
|
var seen = ({})
|
|
|
|
for (var pluginId in plugins) {
|
|
var manifest = plugins[pluginId]
|
|
if (!manifest || !manifest.kinds || manifest.kinds.indexOf("bar-widget") === -1) continue
|
|
if (!shell.pluginRegistry.isEnabled(pluginId)) continue
|
|
|
|
var registryKey = "plugin:" + manifest.id
|
|
seen[registryKey] = true
|
|
|
|
// Already loaded with matching source — leave it alone.
|
|
var existing = pluginWidgetComponents[registryKey]
|
|
var url = shell.pluginRegistry.entryPointUrl(manifest, "barWidget")
|
|
if (!url) {
|
|
console.warn("Plugin " + manifest.id + " has no barWidget entry point")
|
|
continue
|
|
}
|
|
if (existing && existing.url === url && shell.barWidgetRegistry.has(registryKey)) continue
|
|
|
|
var meta = manifest.barWidget || {}
|
|
meta = {
|
|
displayName: meta.displayName || manifest.name,
|
|
description: meta.description || manifest.description,
|
|
category: meta.category || "Plugin",
|
|
allowMultiple: meta.allowMultiple === true,
|
|
defaults: meta.defaults || {},
|
|
schema: meta.schema || [],
|
|
pluginId: manifest.id,
|
|
source: "plugin"
|
|
}
|
|
|
|
loadPluginWidget(registryKey, url, meta)
|
|
}
|
|
|
|
// Drop registrations for plugins that are no longer present or enabled.
|
|
var allIds = shell.barWidgetRegistry.availableIds()
|
|
for (var i = 0; i < allIds.length; i++) {
|
|
var id = allIds[i]
|
|
if (id.indexOf("plugin:") !== 0) continue
|
|
if (!seen[id]) {
|
|
shell.barWidgetRegistry.unregister(id)
|
|
var next = ({})
|
|
for (var k in pluginWidgetComponents) if (k !== id) next[k] = pluginWidgetComponents[k]
|
|
pluginWidgetComponents = next
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadPluginWidget(registryKey, url, meta) {
|
|
var comp = Qt.createComponent(url, Component.Asynchronous)
|
|
function finalize() {
|
|
if (comp.status === Component.Ready) {
|
|
shell.barWidgetRegistry.register(registryKey, comp, meta)
|
|
var next = ({})
|
|
for (var k in pluginWidgetComponents) next[k] = pluginWidgetComponents[k]
|
|
next[registryKey] = { url: url, component: comp }
|
|
pluginWidgetComponents = next
|
|
} else if (comp.status === Component.Error) {
|
|
console.warn("Plugin widget " + registryKey + " failed: " + comp.errorString())
|
|
shell.pluginRegistry.pluginLoadFailed(registryKey, comp.errorString())
|
|
}
|
|
}
|
|
if (comp.status === Component.Loading) {
|
|
comp.statusChanged.connect(finalize)
|
|
} else {
|
|
finalize()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------- shell IPC
|
|
|
|
IpcHandler {
|
|
target: "shell"
|
|
|
|
function ping(): string {
|
|
return "ok"
|
|
}
|
|
|
|
function rescanPlugins(): void {
|
|
shell.pluginRegistry.rescan()
|
|
}
|
|
|
|
function setPluginEnabled(id: string, enabled: string): void {
|
|
shell.pluginRegistry.setEnabled(id, enabled === "true")
|
|
}
|
|
|
|
function listPlugins(): string {
|
|
var out = []
|
|
var plugins = shell.pluginRegistry.installedPlugins
|
|
for (var id in plugins) {
|
|
out.push({
|
|
id: id,
|
|
name: plugins[id].name,
|
|
kinds: plugins[id].kinds,
|
|
enabled: shell.pluginRegistry.isEnabled(id),
|
|
firstParty: !!plugins[id].__isFirstParty
|
|
})
|
|
}
|
|
return JSON.stringify(out)
|
|
}
|
|
|
|
function summon(id: string, payloadJson: string): string {
|
|
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
|
}
|
|
|
|
function hide(id: string): void {
|
|
shell.hide(id)
|
|
}
|
|
|
|
function toggle(id: string, payloadJson: string): void {
|
|
shell.toggle(id, payloadJson)
|
|
}
|
|
}
|
|
}
|