summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml
diff options
context:
space:
mode:
authorNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
committerNippy <nippy@rp1.hu>2026-05-08 19:50:49 +0200
commit3462bcc46ecf74f576ea4397f16492c16bd75b10 (patch)
treec5ab7aef4498647e057bda8d49c11e5f9dda74c9 /raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml
parent56616f693b4f263cbf0d47da43b67424efa6022d (diff)
downloadRaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.tar.gz
RaveOS-PKGBUILD-3462bcc46ecf74f576ea4397f16492c16bd75b10.zip
raveos update
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml876
1 files changed, 876 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml b/raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml
new file mode 100644
index 0000000..03f9be6
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Services/PluginService.qml
@@ -0,0 +1,876 @@
+pragma Singleton
+pragma ComponentBehavior: Bound
+
+import QtCore
+import QtQuick
+import Qt.labs.folderlistmodel
+import Quickshell
+import qs.Common
+import qs.Services
+
+Singleton {
+ id: root
+
+ property var availablePlugins: ({})
+ property var loadedPlugins: ({})
+ property var pluginWidgetComponents: ({})
+ property var pluginDaemonComponents: ({})
+ property var pluginLauncherComponents: ({})
+ property var pluginDesktopComponents: ({})
+ property var availablePluginsList: []
+ 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: ({})
+ property var globalVars: ({})
+
+ property var _stateCache: ({})
+ property var _stateLoaded: ({})
+ property var _stateWriters: ({})
+ property var _stateDirtyPlugins: ({})
+ property bool _stateDirCreated: false
+
+ signal pluginLoaded(string pluginId)
+ signal pluginUnloaded(string pluginId)
+ signal pluginLoadFailed(string pluginId, string error)
+ signal pluginDataChanged(string pluginId)
+ signal pluginStateChanged(string pluginId)
+ signal pluginListUpdated
+ signal globalVarChanged(string pluginId, string varName)
+ signal requestLauncherUpdate(string pluginId)
+
+ Timer {
+ id: resyncDebounce
+ interval: 120
+ repeat: false
+ onTriggered: resyncAll()
+ }
+
+ Timer {
+ id: _stateWriteTimer
+ interval: 150
+ repeat: false
+ onTriggered: root._flushDirtyStates()
+ }
+
+ 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
+
+ onCountChanged: resyncDebounce.restart()
+ onStatusChanged: {
+ if (status === FolderListModel.Ready)
+ resyncDebounce.restart();
+ }
+ }
+
+ FolderListModel {
+ id: systemWatcher
+ showDirs: true
+ showFiles: false
+ showDotAndDotDot: false
+
+ 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 < userList.length; i++)
+ consider(userList[i]);
+ for (let i = 0; i < sysList.length; i++)
+ consider(sysList[i]);
+
+ const removed = [];
+ for (const path in knownManifests) {
+ if (!seenPaths[path])
+ removed.push(path);
+ }
+ if (removed.length) {
+ removed.forEach(function (path) {
+ const pid = pathToPluginId[path];
+ if (pid) {
+ unregisterPluginByPath(path, pid);
+ }
+ delete knownManifests[path];
+ delete pathToPluginId[path];
+ });
+ _updateAvailablePluginsList();
+ pluginListUpdated();
+ }
+ }
+
+ function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
+ const manifestId = "m_" + Math.random().toString(36).slice(2);
+ const qml = `
+ import QtQuick
+ import Quickshell.Io
+ FileView {
+ id: fv
+ property string absPath: ""
+ onLoaded: {
+ try {
+ let raw = text()
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
+ const manifest = JSON.parse(raw)
+ root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs})
+ } catch (e) {
+ console.error("PluginService: bad manifest", absPath, e.message)
+ knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true }
+ }
+ fv.destroy()
+ }
+ onLoadFailed: (err) => {
+ 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;
+ info.requires_dms = manifest.requires_dms || null;
+
+ 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
+ };
+ _updateAvailablePluginsList();
+ pluginListUpdated();
+ const enabled = info.type === "desktop" || 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, bustCache) {
+ 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 isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
+ const isDesktop = plugin.type === "desktop";
+
+ const prevInstance = pluginInstances[pluginId];
+ if (prevInstance) {
+ prevInstance.destroy();
+ const newInstances = Object.assign({}, pluginInstances);
+ delete newInstances[pluginId];
+ pluginInstances = newInstances;
+ }
+
+ try {
+ let url = "file://" + plugin.componentPath;
+ if (bustCache)
+ url += "?t=" + Date.now();
+ 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 newDaemons = Object.assign({}, pluginDaemonComponents);
+ newDaemons[pluginId] = comp;
+ pluginDaemonComponents = newDaemons;
+ } else if (isLauncher) {
+ const instance = comp.createObject(root, {
+ "pluginService": root
+ });
+ if (!instance) {
+ console.error("PluginService: failed to instantiate plugin:", pluginId, comp.errorString());
+ pluginLoadFailed(pluginId, comp.errorString());
+ return false;
+ }
+ const newInstances = Object.assign({}, pluginInstances);
+ newInstances[pluginId] = instance;
+ pluginInstances = newInstances;
+
+ const newLaunchers = Object.assign({}, pluginLauncherComponents);
+ newLaunchers[pluginId] = comp;
+ pluginLauncherComponents = newLaunchers;
+ } else if (isDesktop) {
+ const newDesktop = Object.assign({}, pluginDesktopComponents);
+ newDesktop[pluginId] = comp;
+ pluginDesktopComponents = newDesktop;
+ } else {
+ const newComponents = Object.assign({}, pluginWidgetComponents);
+ newComponents[pluginId] = comp;
+ pluginWidgetComponents = newComponents;
+ }
+
+ plugin.loaded = true;
+ const newLoaded = Object.assign({}, loadedPlugins);
+ newLoaded[pluginId] = plugin;
+ loadedPlugins = newLoaded;
+
+ 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 isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
+ const isDesktop = plugin.type === "desktop";
+
+ 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 (isLauncher && pluginLauncherComponents[pluginId]) {
+ const newLaunchers = Object.assign({}, pluginLauncherComponents);
+ delete newLaunchers[pluginId];
+ pluginLauncherComponents = newLaunchers;
+ } else if (isDesktop && pluginDesktopComponents[pluginId]) {
+ const newDesktop = Object.assign({}, pluginDesktopComponents);
+ delete newDesktop[pluginId];
+ pluginDesktopComponents = newDesktop;
+ } else if (pluginWidgetComponents[pluginId]) {
+ const newComponents = Object.assign({}, pluginWidgetComponents);
+ delete newComponents[pluginId];
+ pluginWidgetComponents = newComponents;
+ }
+
+ plugin.loaded = false;
+ const newLoaded = Object.assign({}, loadedPlugins);
+ delete newLoaded[pluginId];
+ loadedPlugins = newLoaded;
+
+ _cleanupPluginStateWriter(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 getDesktopComponents() {
+ return pluginDesktopComponents;
+ }
+
+ function getAvailablePlugins() {
+ return availablePluginsList;
+ }
+
+ function _updateAvailablePluginsList() {
+ const result = [];
+ for (const key in availablePlugins) {
+ result.push(availablePlugins[key]);
+ }
+ availablePluginsList = 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 defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ if (!defaultBar)
+ return;
+ const leftWidgets = defaultBar.leftWidgets || [];
+ const centerWidgets = defaultBar.centerWidgets || [];
+ const rightWidgets = defaultBar.rightWidgets || [];
+
+ 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, true);
+ }
+
+ function togglePlugin(pluginId) {
+ let instance = pluginInstances[pluginId];
+
+ // Lazy instantiate daemon plugins on first toggle
+ // This respects the daemon lifecycle (not instantiated on load)
+ // while supporting toggle functionality for slideout-capable daemons
+ if (!instance && pluginDaemonComponents[pluginId]) {
+ const comp = pluginDaemonComponents[pluginId];
+ const newInstance = comp.createObject(root, {
+ "pluginId": pluginId,
+ "pluginService": root
+ });
+ if (newInstance) {
+ const newInstances = Object.assign({}, pluginInstances);
+ newInstances[pluginId] = newInstance;
+ pluginInstances = newInstances;
+ instance = newInstance;
+ }
+ }
+
+ if (instance && typeof instance.toggle === "function") {
+ instance.toggle();
+ return true;
+ }
+ return false;
+ }
+
+ 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 getPluginPath(pluginId) {
+ const plugin = availablePlugins[pluginId];
+ if (!plugin)
+ return "";
+ return plugin.pluginDirectory || "";
+ }
+
+ function saveAllPluginSettings() {
+ SettingsData.savePluginSettings();
+ }
+
+ function getPluginStatePath(pluginId) {
+ return Paths.strip(Paths.state) + "/plugins/" + pluginId + "_state.json";
+ }
+
+ function loadPluginState(pluginId, key, defaultValue) {
+ if (!_stateLoaded[pluginId])
+ _loadStateFromDisk(pluginId);
+ const state = _stateCache[pluginId];
+ if (!state)
+ return defaultValue;
+ return state[key] !== undefined ? state[key] : defaultValue;
+ }
+
+ function savePluginState(pluginId, key, value) {
+ if (!_stateLoaded[pluginId])
+ _loadStateFromDisk(pluginId);
+ if (!_stateCache[pluginId])
+ _stateCache[pluginId] = {};
+ _stateCache[pluginId][key] = value;
+ _stateDirtyPlugins[pluginId] = true;
+ _stateWriteTimer.restart();
+ pluginStateChanged(pluginId);
+ }
+
+ function clearPluginState(pluginId) {
+ _stateCache[pluginId] = {};
+ _stateLoaded[pluginId] = true;
+ _flushStateToDisk(pluginId);
+ pluginStateChanged(pluginId);
+ }
+
+ function removePluginStateKey(pluginId, key) {
+ if (!_stateCache[pluginId])
+ return;
+ delete _stateCache[pluginId][key];
+ _stateDirtyPlugins[pluginId] = true;
+ _stateWriteTimer.restart();
+ pluginStateChanged(pluginId);
+ }
+
+ function _ensureStateDir() {
+ if (_stateDirCreated)
+ return;
+ _stateDirCreated = true;
+ Paths.mkdir(Paths.state + "/plugins");
+ }
+
+ function _loadStateFromDisk(pluginId) {
+ _stateLoaded[pluginId] = true;
+ _ensureStateDir();
+ const path = getPluginStatePath(pluginId);
+ const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
+ try {
+ const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockLoading: true; blockWrites: true; atomicWrites: true }';
+ const fv = Qt.createQmlObject(qml, root, "sf_" + pluginId);
+ const raw = fv.text();
+ if (raw && raw.trim()) {
+ _stateCache[pluginId] = JSON.parse(raw);
+ } else {
+ _stateCache[pluginId] = {};
+ }
+ _stateWriters[pluginId] = fv;
+ } catch (e) {
+ _stateCache[pluginId] = {};
+ }
+ }
+
+ function _flushStateToDisk(pluginId) {
+ _ensureStateDir();
+ const content = JSON.stringify(_stateCache[pluginId] || {}, null, 2);
+ if (_stateWriters[pluginId]) {
+ _stateWriters[pluginId].setText(content);
+ return;
+ }
+ const path = getPluginStatePath(pluginId);
+ const escapedPath = path.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
+ try {
+ const qml = 'import QtQuick; import Quickshell.Io; FileView { path: "' + escapedPath + '"; blockWrites: true; atomicWrites: true }';
+ const fv = Qt.createQmlObject(qml, root, "sw_" + pluginId);
+ _stateWriters[pluginId] = fv;
+ fv.loaded.connect(function () {
+ fv.setText(content);
+ });
+ fv.loadFailed.connect(function () {
+ fv.setText(content);
+ });
+ } catch (e) {
+ console.warn("PluginService: Failed to write state for", pluginId, e.message);
+ }
+ }
+
+ function _flushDirtyStates() {
+ const dirty = _stateDirtyPlugins;
+ _stateDirtyPlugins = {};
+ for (const pluginId in dirty)
+ _flushStateToDisk(pluginId);
+ }
+
+ function _cleanupPluginStateWriter(pluginId) {
+ if (!_stateWriters[pluginId])
+ return;
+ _stateWriters[pluginId].destroy();
+ delete _stateWriters[pluginId];
+ }
+
+ function scanPlugins() {
+ const userUrl = Paths.toFileUrl(root.pluginDirectory);
+ const systemUrl = Paths.toFileUrl(root.systemPluginDirectory);
+ userWatcher.folder = "";
+ userWatcher.folder = userUrl;
+ systemWatcher.folder = "";
+ systemWatcher.folder = systemUrl;
+ resyncDebounce.restart();
+ }
+
+ function forceRescanPlugin(pluginId) {
+ const plugin = availablePlugins[pluginId];
+ if (plugin && plugin.manifestPath) {
+ const manifestPath = plugin.manifestPath;
+ const source = plugin.source || "user";
+ delete knownManifests[manifestPath];
+ const newMap = Object.assign({}, availablePlugins);
+ delete newMap[pluginId];
+ availablePlugins = newMap;
+ loadPluginManifestFile(manifestPath, source, Date.now());
+ }
+ }
+
+ 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;
+ }
+ }
+
+ // Launcher plugin helper functions
+ function getLauncherPlugins() {
+ const launchers = {};
+
+ // Check plugins that have launcher components
+ for (const pluginId in pluginLauncherComponents) {
+ const plugin = availablePlugins[pluginId];
+ if (plugin && plugin.loaded) {
+ launchers[pluginId] = plugin;
+ }
+ }
+ return launchers;
+ }
+
+ function getLauncherPlugin(pluginId) {
+ const plugin = availablePlugins[pluginId];
+ if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
+ return plugin;
+ }
+ return null;
+ }
+
+ function getPluginTrigger(pluginId) {
+ const plugin = getLauncherPlugin(pluginId);
+ if (plugin) {
+ // Check if noTrigger is set (always active mode)
+ const noTrigger = SettingsData.getPluginSetting(pluginId, "noTrigger", false);
+ if (noTrigger) {
+ return "";
+ }
+ // Otherwise load the custom trigger, defaulting to plugin manifest trigger
+ const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!");
+ return customTrigger;
+ }
+ return null;
+ }
+
+ function getAllPluginTriggers() {
+ const triggers = {};
+ const launchers = getLauncherPlugins();
+
+ for (const pluginId in launchers) {
+ const trigger = getPluginTrigger(pluginId);
+ if (trigger && trigger.trim() !== "") {
+ triggers[trigger] = pluginId;
+ }
+ }
+ return triggers;
+ }
+
+ function getPluginsWithEmptyTrigger() {
+ const plugins = [];
+ const launchers = getLauncherPlugins();
+
+ for (const pluginId in launchers) {
+ const trigger = getPluginTrigger(pluginId);
+ if (!trigger || trigger.trim() === "") {
+ plugins.push(pluginId);
+ }
+ }
+ return plugins;
+ }
+
+ function getPluginViewPreference(pluginId) {
+ const plugin = availablePlugins[pluginId];
+ if (!plugin)
+ return null;
+
+ return {
+ mode: plugin.viewMode || null,
+ enforced: plugin.viewModeEnforced === true
+ };
+ }
+
+ function getGlobalVar(pluginId, varName, defaultValue) {
+ if (globalVars[pluginId] && varName in globalVars[pluginId]) {
+ return globalVars[pluginId][varName];
+ }
+ return defaultValue;
+ }
+
+ function setGlobalVar(pluginId, varName, value) {
+ const newGlobals = Object.assign({}, globalVars);
+ if (!newGlobals[pluginId]) {
+ newGlobals[pluginId] = {};
+ }
+ newGlobals[pluginId] = Object.assign({}, newGlobals[pluginId]);
+ newGlobals[pluginId][varName] = value;
+ globalVars = newGlobals;
+ globalVarChanged(pluginId, varName);
+ }
+
+ function checkPluginCompatibility(requiresDms) {
+ if (!requiresDms)
+ return true;
+ return SystemUpdateService.checkVersionRequirement(requiresDms, SystemUpdateService.getParsedShellVersion());
+ }
+
+ function getIncompatiblePlugins() {
+ const result = [];
+ for (const pluginId in availablePlugins) {
+ const plugin = availablePlugins[pluginId];
+ if (plugin.loaded && plugin.requires_dms && !checkPluginCompatibility(plugin.requires_dms)) {
+ result.push(plugin);
+ }
+ }
+ return result;
+ }
+}