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:". 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) } } }