pragma Singleton pragma ComponentBehavior: Bound import QtCore import QtQuick import Qt.labs.folderlistmodel import Quickshell import Quickshell.Io import qs.Common Singleton { id: root property var availablePlugins: ({}) property var loadedPlugins: ({}) property var pluginWidgetComponents: ({}) property var pluginDaemonComponents: ({}) property string pluginDirectory: { var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation) var configDirStr = configDir.toString() if (configDirStr.startsWith("file://")) { configDirStr = configDirStr.substring(7) } return configDirStr + "/DankMaterialShell/plugins" } property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins" property var knownManifests: ({}) property var pathToPluginId: ({}) property var pluginInstances: ({}) signal pluginLoaded(string pluginId) signal pluginUnloaded(string pluginId) signal pluginLoadFailed(string pluginId, string error) signal pluginDataChanged(string pluginId) signal pluginListUpdated() Timer { id: resyncDebounce interval: 120 repeat: false onTriggered: resyncAll() } Component.onCompleted: { userWatcher.folder = Paths.toFileUrl(root.pluginDirectory) systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory) Qt.callLater(resyncAll) } FolderListModel { id: userWatcher showDirs: true showFiles: false showDotAndDotDot: false nameFilters: ["plugin.json"] onCountChanged: resyncDebounce.restart() onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart() } FolderListModel { id: systemWatcher showDirs: true showFiles: false showDotAndDotDot: false nameFilters: ["plugin.json"] onCountChanged: resyncDebounce.restart() onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart() } function snapshotModel(model, sourceTag) { const out = [] const n = model.count const baseDir = sourceTag === "user" ? pluginDirectory : systemPluginDirectory for (let i = 0; i < n; i++) { let dirPath = model.get(i, "filePath") if (dirPath.startsWith("file://")) { dirPath = dirPath.substring(7) } if (!dirPath.startsWith(baseDir)) { continue } const manifestPath = dirPath + "/plugin.json" out.push({ path: manifestPath, source: sourceTag }) } return out } function resyncAll() { const userList = snapshotModel(userWatcher, "user") const sysList = snapshotModel(systemWatcher, "system") const seenPaths = {} function consider(entry) { const key = entry.path seenPaths[key] = true const prev = knownManifests[key] if (!prev) { loadPluginManifestFile(entry.path, entry.source, Date.now()) } } for (let i=0;i { console.warn("PluginService: manifest load failed", absPath, err) fv.destroy() } } ` const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId) loader.absPath = manifestPathNoScheme loader.path = manifestPathNoScheme } function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) { if (!manifest || !manifest.id || !manifest.name || !manifest.component) { console.error("PluginService: invalid manifest fields:", absPath) knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, bad: true } return } const dir = absPath.substring(0, absPath.lastIndexOf('/')) let comp = manifest.component if (comp.startsWith("./")) comp = comp.slice(2) let settings = manifest.settings if (settings && settings.startsWith("./")) settings = settings.slice(2) const info = {} for (const k in manifest) info[k] = manifest[k] let perms = manifest.permissions if (typeof perms === "string") { perms = perms.split(/\s*,\s*/) } if (!Array.isArray(perms)) { perms = [] } info.permissions = perms.map(p => String(p).trim()) info.manifestPath = absPath info.pluginDirectory = dir info.componentPath = dir + "/" + comp info.settingsPath = settings ? (dir + "/" + settings) : null info.loaded = isPluginLoaded(manifest.id) info.type = manifest.type || "widget" info.source = sourceTag const existing = availablePlugins[manifest.id] const shouldReplace = (!existing) || (existing && existing.source === "system" && sourceTag === "user") if (shouldReplace) { if (existing && existing.loaded && existing.source !== sourceTag) { unloadPlugin(manifest.id) } const newMap = Object.assign({}, availablePlugins) newMap[manifest.id] = info availablePlugins = newMap pathToPluginId[absPath] = manifest.id knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag } pluginListUpdated() const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false) if (enabled && !info.loaded) loadPlugin(manifest.id) } else { knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, shadowedBy: existing.source } pathToPluginId[absPath] = manifest.id } } function unregisterPluginByPath(absPath, pluginId) { const current = availablePlugins[pluginId] if (current && current.manifestPath === absPath) { if (current.loaded) unloadPlugin(pluginId) const newMap = Object.assign({}, availablePlugins) delete newMap[pluginId] availablePlugins = newMap } } function loadPlugin(pluginId) { const plugin = availablePlugins[pluginId] if (!plugin) { console.error("PluginService: Plugin not found:", pluginId) pluginLoadFailed(pluginId, "Plugin not found") return false } if (plugin.loaded) { return true } const isDaemon = plugin.type === "daemon" const map = isDaemon ? pluginDaemonComponents : pluginWidgetComponents const prevInstance = pluginInstances[pluginId] if (prevInstance) { prevInstance.destroy() const newInstances = Object.assign({}, pluginInstances) delete newInstances[pluginId] pluginInstances = newInstances } try { const url = "file://" + plugin.componentPath const comp = Qt.createComponent(url, Component.PreferSynchronous) if (comp.status === Component.Error) { console.error("PluginService: component error", pluginId, comp.errorString()) pluginLoadFailed(pluginId, comp.errorString()) return false } if (isDaemon) { const instance = comp.createObject(root, { "pluginId": pluginId }) if (!instance) { console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString()) pluginLoadFailed(pluginId, comp.errorString()) return false } const newInstances = Object.assign({}, pluginInstances) newInstances[pluginId] = instance pluginInstances = newInstances const newDaemons = Object.assign({}, pluginDaemonComponents) newDaemons[pluginId] = comp pluginDaemonComponents = newDaemons } else { const newComponents = Object.assign({}, pluginWidgetComponents) newComponents[pluginId] = comp pluginWidgetComponents = newComponents } plugin.loaded = true loadedPlugins[pluginId] = plugin pluginLoaded(pluginId) return true } catch (e) { console.error("PluginService: Error loading plugin:", pluginId, e.message) pluginLoadFailed(pluginId, e.message) return false } } function unloadPlugin(pluginId) { const plugin = loadedPlugins[pluginId] if (!plugin) { console.warn("PluginService: Plugin not loaded:", pluginId) return false } try { const isDaemon = plugin.type === "daemon" const instance = pluginInstances[pluginId] if (instance) { instance.destroy() const newInstances = Object.assign({}, pluginInstances) delete newInstances[pluginId] pluginInstances = newInstances } if (isDaemon && pluginDaemonComponents[pluginId]) { const newDaemons = Object.assign({}, pluginDaemonComponents) delete newDaemons[pluginId] pluginDaemonComponents = newDaemons } else if (pluginWidgetComponents[pluginId]) { const newComponents = Object.assign({}, pluginWidgetComponents) delete newComponents[pluginId] pluginWidgetComponents = newComponents } plugin.loaded = false delete loadedPlugins[pluginId] pluginUnloaded(pluginId) return true } catch (error) { console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message) return false } } function getWidgetComponents() { return pluginWidgetComponents } function getDaemonComponents() { return pluginDaemonComponents } function getAvailablePlugins() { const result = [] for (const key in availablePlugins) { result.push(availablePlugins[key]) } return result } function getPluginVariants(pluginId) { const plugin = availablePlugins[pluginId] if (!plugin) { return [] } const variants = SettingsData.getPluginSetting(pluginId, "variants", []) return variants } function getAllPluginVariants() { const result = [] for (const pluginId in availablePlugins) { const plugin = availablePlugins[pluginId] if (plugin.type !== "widget") { continue } const variants = getPluginVariants(pluginId) if (variants.length === 0) { result.push({ pluginId: pluginId, variantId: null, fullId: pluginId, name: plugin.name, icon: plugin.icon || "extension", description: plugin.description || "Plugin widget", loaded: plugin.loaded }) } else { for (let i = 0; i < variants.length; i++) { const variant = variants[i] result.push({ pluginId: pluginId, variantId: variant.id, fullId: pluginId + ":" + variant.id, name: plugin.name + " - " + variant.name, icon: variant.icon || plugin.icon || "extension", description: variant.description || plugin.description || "Plugin widget variant", loaded: plugin.loaded }) } } } return result } function createPluginVariant(pluginId, variantName, variantConfig) { const variants = getPluginVariants(pluginId) const variantId = "variant_" + Date.now() const newVariant = Object.assign({}, variantConfig, { id: variantId, name: variantName }) variants.push(newVariant) SettingsData.setPluginSetting(pluginId, "variants", variants) pluginDataChanged(pluginId) return variantId } function removePluginVariant(pluginId, variantId) { const variants = getPluginVariants(pluginId) const newVariants = variants.filter(function(v) { return v.id !== variantId }) SettingsData.setPluginSetting(pluginId, "variants", newVariants) const fullId = pluginId + ":" + variantId removeWidgetFromDankBar(fullId) pluginDataChanged(pluginId) } function removeWidgetFromDankBar(widgetId) { function filterWidget(widget) { const id = typeof widget === "string" ? widget : widget.id return id !== widgetId } const leftWidgets = SettingsData.dankBarLeftWidgets const centerWidgets = SettingsData.dankBarCenterWidgets const rightWidgets = SettingsData.dankBarRightWidgets const newLeft = leftWidgets.filter(filterWidget) const newCenter = centerWidgets.filter(filterWidget) const newRight = rightWidgets.filter(filterWidget) if (newLeft.length !== leftWidgets.length) { SettingsData.setDankBarLeftWidgets(newLeft) } if (newCenter.length !== centerWidgets.length) { SettingsData.setDankBarCenterWidgets(newCenter) } if (newRight.length !== rightWidgets.length) { SettingsData.setDankBarRightWidgets(newRight) } } function updatePluginVariant(pluginId, variantId, variantConfig) { const variants = getPluginVariants(pluginId) for (let i = 0; i < variants.length; i++) { if (variants[i].id === variantId) { variants[i] = Object.assign({}, variants[i], variantConfig) break } } SettingsData.setPluginSetting(pluginId, "variants", variants) pluginDataChanged(pluginId) } function getPluginVariantData(pluginId, variantId) { const variants = getPluginVariants(pluginId) for (let i = 0; i < variants.length; i++) { if (variants[i].id === variantId) { return variants[i] } } return null } function getLoadedPlugins() { const result = [] for (const key in loadedPlugins) { result.push(loadedPlugins[key]) } return result } function isPluginLoaded(pluginId) { return loadedPlugins[pluginId] !== undefined } function enablePlugin(pluginId) { SettingsData.setPluginSetting(pluginId, "enabled", true) return loadPlugin(pluginId) } function disablePlugin(pluginId) { SettingsData.setPluginSetting(pluginId, "enabled", false) return unloadPlugin(pluginId) } function reloadPlugin(pluginId) { if (isPluginLoaded(pluginId)) { unloadPlugin(pluginId) } return loadPlugin(pluginId) } function savePluginData(pluginId, key, value) { SettingsData.setPluginSetting(pluginId, key, value) pluginDataChanged(pluginId) return true } function loadPluginData(pluginId, key, defaultValue) { return SettingsData.getPluginSetting(pluginId, key, defaultValue) } function saveAllPluginSettings() { SettingsData.savePluginSettings() } function scanPlugins() { resyncDebounce.restart() } function createPluginDirectory() { const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }") if (mkdirProcess.status === Component.Ready) { const process = mkdirProcess.createObject(root) process.command = ["mkdir", "-p", pluginDirectory] process.exited.connect(function(exitCode) { if (exitCode !== 0) { console.error("PluginService: Failed to create plugin directory, exit code:", exitCode) } process.destroy() }) process.running = true return true } else { console.error("PluginService: Failed to create mkdir process") return false } } }