1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

plugins: fix reactivity, tooltips, new IPCs to reload

This commit is contained in:
bbedward
2025-11-25 11:02:38 -05:00
parent 3a365f6807
commit 4035c9cc5f
4 changed files with 423 additions and 350 deletions

View File

@@ -601,4 +601,61 @@ Item {
target: "widget" target: "widget"
} }
IpcHandler {
function reload(pluginId: string): string {
if (!pluginId)
return "ERROR: No plugin ID specified";
if (!PluginService.availablePlugins[pluginId])
return `PLUGIN_NOT_FOUND: ${pluginId}`;
if (!PluginService.isPluginLoaded(pluginId))
return `PLUGIN_NOT_LOADED: ${pluginId}`;
const success = PluginService.reloadPlugin(pluginId);
return success ? `PLUGIN_RELOAD_SUCCESS: ${pluginId}` : `PLUGIN_RELOAD_FAILED: ${pluginId}`;
}
function enable(pluginId: string): string {
if (!pluginId)
return "ERROR: No plugin ID specified";
if (!PluginService.availablePlugins[pluginId])
return `PLUGIN_NOT_FOUND: ${pluginId}`;
const success = PluginService.enablePlugin(pluginId);
return success ? `PLUGIN_ENABLE_SUCCESS: ${pluginId}` : `PLUGIN_ENABLE_FAILED: ${pluginId}`;
}
function disable(pluginId: string): string {
if (!pluginId)
return "ERROR: No plugin ID specified";
if (!PluginService.availablePlugins[pluginId])
return `PLUGIN_NOT_FOUND: ${pluginId}`;
const success = PluginService.disablePlugin(pluginId);
return success ? `PLUGIN_DISABLE_SUCCESS: ${pluginId}` : `PLUGIN_DISABLE_FAILED: ${pluginId}`;
}
function list(): string {
const plugins = PluginService.getAvailablePlugins();
if (plugins.length === 0)
return "No plugins available";
return plugins.map(p => `${p.id} [${p.loaded ? "loaded" : "disabled"}]`).join("\n");
}
function status(pluginId: string): string {
if (!pluginId)
return "ERROR: No plugin ID specified";
if (!PluginService.availablePlugins[pluginId])
return `PLUGIN_NOT_FOUND: ${pluginId}`;
return PluginService.isPluginLoaded(pluginId) ? "loaded" : "disabled";
}
target: "plugins"
}
} }

View File

@@ -10,6 +10,7 @@ StyledRect {
property string expandedPluginId: "" property string expandedPluginId: ""
property bool hasUpdate: false property bool hasUpdate: false
property bool isReloading: false property bool isReloading: false
property var sharedTooltip: null
property string pluginId: pluginData ? pluginData.id : "" property string pluginId: pluginData ? pluginData.id : ""
property string pluginDirectoryName: { property string pluginDirectoryName: {
@@ -28,6 +29,10 @@ StyledRect {
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : [] property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== "" property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
property bool isExpanded: expandedPluginId === pluginId property bool isExpanded: expandedPluginId === pluginId
property bool isLoaded: {
PluginService.loadedPlugins;
return PluginService.loadedPlugins[pluginId] !== undefined;
}
width: parent.width width: parent.width
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
@@ -63,7 +68,7 @@ StyledRect {
DankIcon { DankIcon {
name: root.pluginIcon name: root.pluginIcon
size: Theme.iconSize size: Theme.iconSize
color: PluginService.isPluginLoaded(root.pluginId) ? Theme.primary : Theme.surfaceVariantText color: root.isLoaded ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -111,7 +116,7 @@ StyledRect {
height: 28 height: 28
radius: 14 radius: 14
color: updateArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent" color: updateArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent"
visible: DMSService.dmsAvailable && PluginService.isPluginLoaded(root.pluginId) && root.hasUpdate visible: DMSService.dmsAvailable && root.isLoaded && root.hasUpdate
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -131,27 +136,21 @@ StyledRect {
DMSService.update(currentPluginName, response => { DMSService.update(currentPluginName, response => {
if (response.error) { if (response.error) {
ToastService.showError("Update failed: " + response.error); ToastService.showError("Update failed: " + response.error);
} else { return;
ToastService.showInfo("Plugin updated: " + currentPluginName);
PluginService.forceRescanPlugin(currentPluginId);
if (DMSService.apiVersion >= 8) {
DMSService.listInstalled();
}
} }
ToastService.showInfo("Plugin updated: " + currentPluginName);
PluginService.forceRescanPlugin(currentPluginId);
if (DMSService.apiVersion >= 8)
DMSService.listInstalled();
}); });
} }
onEntered: { onEntered: {
tooltipLoader.active = true; if (root.sharedTooltip)
if (tooltipLoader.item) { root.sharedTooltip.show(I18n.tr("Update Plugin"), parent, 0, 0, "top");
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Update Plugin"), p.x, p.y - 40, null);
}
} }
onExited: { onExited: {
if (tooltipLoader.item) { if (root.sharedTooltip)
tooltipLoader.item.hide(); root.sharedTooltip.hide();
}
tooltipLoader.active = false;
} }
} }
} }
@@ -180,27 +179,21 @@ StyledRect {
DMSService.uninstall(currentPluginName, response => { DMSService.uninstall(currentPluginName, response => {
if (response.error) { if (response.error) {
ToastService.showError("Uninstall failed: " + response.error); ToastService.showError("Uninstall failed: " + response.error);
} else { return;
ToastService.showInfo("Plugin uninstalled: " + currentPluginName);
PluginService.scanPlugins();
if (root.isExpanded) {
root.expandedPluginId = "";
}
} }
ToastService.showInfo("Plugin uninstalled: " + currentPluginName);
PluginService.scanPlugins();
if (root.isExpanded)
root.expandedPluginId = "";
}); });
} }
onEntered: { onEntered: {
tooltipLoader.active = true; if (root.sharedTooltip)
if (tooltipLoader.item) { root.sharedTooltip.show(I18n.tr("Uninstall Plugin"), parent, 0, 0, "top");
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Uninstall Plugin"), p.x, p.y - 40, null);
}
} }
onExited: { onExited: {
if (tooltipLoader.item) { if (root.sharedTooltip)
tooltipLoader.item.hide(); root.sharedTooltip.hide();
}
tooltipLoader.active = false;
} }
} }
} }
@@ -210,7 +203,7 @@ StyledRect {
height: 28 height: 28
radius: 14 radius: 14
color: reloadArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent" color: reloadArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent"
visible: PluginService.isPluginLoaded(root.pluginId) visible: root.isLoaded
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -230,23 +223,18 @@ StyledRect {
root.isReloading = true; root.isReloading = true;
if (PluginService.reloadPlugin(currentPluginId)) { if (PluginService.reloadPlugin(currentPluginId)) {
ToastService.showInfo("Plugin reloaded: " + currentPluginName); ToastService.showInfo("Plugin reloaded: " + currentPluginName);
} else { return;
ToastService.showError("Failed to reload plugin: " + currentPluginName);
root.isReloading = false;
} }
ToastService.showError("Failed to reload plugin: " + currentPluginName);
root.isReloading = false;
} }
onEntered: { onEntered: {
tooltipLoader.active = true; if (root.sharedTooltip)
if (tooltipLoader.item) { root.sharedTooltip.show(I18n.tr("Reload Plugin"), parent, 0, 0, "top");
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Reload Plugin"), p.x, p.y - 40, null);
}
} }
onExited: { onExited: {
if (tooltipLoader.item) { if (root.sharedTooltip)
tooltipLoader.item.hide(); root.sharedTooltip.hide();
}
tooltipLoader.active = false;
} }
} }
} }
@@ -254,7 +242,7 @@ StyledRect {
DankToggle { DankToggle {
id: pluginToggle id: pluginToggle
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(root.pluginId) checked: root.isLoaded
onToggled: isChecked => { onToggled: isChecked => {
const currentPluginId = root.pluginId; const currentPluginId = root.pluginId;
const currentPluginName = root.pluginName; const currentPluginName = root.pluginName;
@@ -262,21 +250,18 @@ StyledRect {
if (isChecked) { if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) { if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName); ToastService.showInfo("Plugin enabled: " + currentPluginName);
} else { return;
ToastService.showError("Failed to enable plugin: " + currentPluginName);
checked = false;
}
} else {
if (PluginService.disablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin disabled: " + currentPluginName);
if (root.isExpanded) {
root.expandedPluginId = "";
}
} else {
ToastService.showError("Failed to disable plugin: " + currentPluginName);
checked = true;
} }
ToastService.showError("Failed to enable plugin: " + currentPluginName);
return;
} }
if (PluginService.disablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin disabled: " + currentPluginName);
if (root.isExpanded)
root.expandedPluginId = "";
return;
}
ToastService.showError("Failed to disable plugin: " + currentPluginName);
} }
} }
} }
@@ -344,7 +329,7 @@ StyledRect {
id: settingsLoader id: settingsLoader
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
active: root.isExpanded && root.hasSettings && PluginService.isPluginLoaded(root.pluginId) active: root.isExpanded && root.hasSettings && root.isLoaded
asynchronous: false asynchronous: false
source: { source: {
@@ -376,16 +361,10 @@ StyledRect {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: !PluginService.isPluginLoaded(root.pluginId) ? "Enable plugin to access settings" : (settingsLoader.status === Loader.Error ? "Failed to load settings" : "No configurable settings") text: !root.isLoaded ? "Enable plugin to access settings" : (settingsLoader.status === Loader.Error ? "Failed to load settings" : "No configurable settings")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: root.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error) visible: root.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error)
} }
} }
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
} }

View File

@@ -11,9 +11,14 @@ FocusScope {
property var parentModal: null property var parentModal: null
property var installedPluginsData: ({}) property var installedPluginsData: ({})
property bool isReloading: false property bool isReloading: false
property alias sharedTooltip: sharedTooltip
focus: true focus: true
DankTooltipV2 {
id: sharedTooltip
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
@@ -218,7 +223,7 @@ FocusScope {
Repeater { Repeater {
id: pluginRepeater id: pluginRepeater
model: PluginService.getAvailablePlugins() model: PluginService.availablePluginsList
PluginListItem { PluginListItem {
pluginData: modelData pluginData: modelData
@@ -229,6 +234,7 @@ FocusScope {
return pluginsTab.installedPluginsData[pluginId] || pluginsTab.installedPluginsData[pluginName] || false; return pluginsTab.installedPluginsData[pluginId] || pluginsTab.installedPluginsData[pluginName] || false;
} }
isReloading: pluginsTab.isReloading isReloading: pluginsTab.isReloading
sharedTooltip: pluginsTab.sharedTooltip
onExpandedPluginIdChanged: { onExpandedPluginIdChanged: {
pluginsTab.expandedPluginId = expandedPluginId; pluginsTab.expandedPluginId = expandedPluginId;
} }
@@ -253,12 +259,7 @@ FocusScope {
} }
function refreshPluginList() { function refreshPluginList() {
Qt.callLater(() => { pluginsTab.isRefreshingPlugins = false;
var plugins = PluginService.getAvailablePlugins();
pluginRepeater.model = null;
pluginRepeater.model = plugins;
pluginsTab.isRefreshingPlugins = false;
});
} }
Connections { Connections {

View File

@@ -1,12 +1,10 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick import QtQuick
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
Singleton { Singleton {
@@ -17,13 +15,14 @@ Singleton {
property var pluginWidgetComponents: ({}) property var pluginWidgetComponents: ({})
property var pluginDaemonComponents: ({}) property var pluginDaemonComponents: ({})
property var pluginLauncherComponents: ({}) property var pluginLauncherComponents: ({})
property var availablePluginsList: []
property string pluginDirectory: { property string pluginDirectory: {
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation) var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation);
var configDirStr = configDir.toString() var configDirStr = configDir.toString();
if (configDirStr.startsWith("file://")) { if (configDirStr.startsWith("file://")) {
configDirStr = configDirStr.substring(7) configDirStr = configDirStr.substring(7);
} }
return configDirStr + "/DankMaterialShell/plugins" return configDirStr + "/DankMaterialShell/plugins";
} }
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins" property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
@@ -36,7 +35,7 @@ Singleton {
signal pluginUnloaded(string pluginId) signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error) signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId) signal pluginDataChanged(string pluginId)
signal pluginListUpdated() signal pluginListUpdated
signal globalVarChanged(string pluginId, string varName) signal globalVarChanged(string pluginId, string varName)
Timer { Timer {
@@ -47,9 +46,9 @@ Singleton {
} }
Component.onCompleted: { Component.onCompleted: {
userWatcher.folder = Paths.toFileUrl(root.pluginDirectory) userWatcher.folder = Paths.toFileUrl(root.pluginDirectory);
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory) systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory);
Qt.callLater(resyncAll) Qt.callLater(resyncAll);
} }
FolderListModel { FolderListModel {
@@ -57,10 +56,12 @@ Singleton {
showDirs: true showDirs: true
showFiles: false showFiles: false
showDotAndDotDot: false showDotAndDotDot: false
nameFilters: ["plugin.json"]
onCountChanged: resyncDebounce.restart() onCountChanged: resyncDebounce.restart()
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart() onStatusChanged: {
if (status === FolderListModel.Ready)
resyncDebounce.restart();
}
} }
FolderListModel { FolderListModel {
@@ -68,65 +69,74 @@ Singleton {
showDirs: true showDirs: true
showFiles: false showFiles: false
showDotAndDotDot: false showDotAndDotDot: false
nameFilters: ["plugin.json"]
onCountChanged: resyncDebounce.restart() onCountChanged: resyncDebounce.restart()
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart() onStatusChanged: {
if (status === FolderListModel.Ready)
resyncDebounce.restart();
}
} }
function snapshotModel(model, sourceTag) { function snapshotModel(model, sourceTag) {
const out = [] const out = [];
const n = model.count const n = model.count;
const baseDir = sourceTag === "user" ? pluginDirectory : systemPluginDirectory const baseDir = sourceTag === "user" ? pluginDirectory : systemPluginDirectory;
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
let dirPath = model.get(i, "filePath") let dirPath = model.get(i, "filePath");
if (dirPath.startsWith("file://")) { if (dirPath.startsWith("file://")) {
dirPath = dirPath.substring(7) dirPath = dirPath.substring(7);
} }
if (!dirPath.startsWith(baseDir)) { if (!dirPath.startsWith(baseDir)) {
continue continue;
} }
const manifestPath = dirPath + "/plugin.json" const manifestPath = dirPath + "/plugin.json";
out.push({ path: manifestPath, source: sourceTag }) out.push({
path: manifestPath,
source: sourceTag
});
} }
return out return out;
} }
function resyncAll() { function resyncAll() {
const userList = snapshotModel(userWatcher, "user") const userList = snapshotModel(userWatcher, "user");
const sysList = snapshotModel(systemWatcher, "system") const sysList = snapshotModel(systemWatcher, "system");
const seenPaths = {} const seenPaths = {};
function consider(entry) { function consider(entry) {
const key = entry.path const key = entry.path;
seenPaths[key] = true seenPaths[key] = true;
const prev = knownManifests[key] const prev = knownManifests[key];
if (!prev) { if (!prev) {
loadPluginManifestFile(entry.path, entry.source, Date.now()) loadPluginManifestFile(entry.path, entry.source, Date.now());
} }
} }
for (let i=0;i<userList.length;i++) consider(userList[i]) for (let i = 0; i < userList.length; i++)
for (let i=0;i<sysList.length;i++) consider(sysList[i]) consider(userList[i]);
for (let i = 0; i < sysList.length; i++)
consider(sysList[i]);
const removed = [] const removed = [];
for (const path in knownManifests) { for (const path in knownManifests) {
if (!seenPaths[path]) removed.push(path) if (!seenPaths[path])
removed.push(path);
} }
if (removed.length) { if (removed.length) {
removed.forEach(function(path) { removed.forEach(function (path) {
const pid = pathToPluginId[path] const pid = pathToPluginId[path];
if (pid) { if (pid) {
unregisterPluginByPath(path, pid) unregisterPluginByPath(path, pid);
} }
delete knownManifests[path] delete knownManifests[path];
delete pathToPluginId[path] delete pathToPluginId[path];
}) });
pluginListUpdated() _updateAvailablePluginsList();
pluginListUpdated();
} }
} }
function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) { function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
const manifestId = "m_" + Math.random().toString(36).slice(2) const manifestId = "m_" + Math.random().toString(36).slice(2);
const qml = ` const qml = `
import QtQuick import QtQuick
import Quickshell.Io import Quickshell.Io
@@ -150,226 +160,251 @@ Singleton {
fv.destroy() fv.destroy()
} }
} }
` `;
const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId)
loader.absPath = manifestPathNoScheme const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId);
loader.path = manifestPathNoScheme loader.absPath = manifestPathNoScheme;
loader.path = manifestPathNoScheme;
} }
function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) { function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
if (!manifest || !manifest.id || !manifest.name || !manifest.component) { if (!manifest || !manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: invalid manifest fields:", absPath) console.error("PluginService: invalid manifest fields:", absPath);
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, bad: true } knownManifests[absPath] = {
return mtime: mtimeEpochMs,
source: sourceTag,
bad: true
};
return;
} }
const dir = absPath.substring(0, absPath.lastIndexOf('/')) const dir = absPath.substring(0, absPath.lastIndexOf('/'));
let comp = manifest.component let comp = manifest.component;
if (comp.startsWith("./")) comp = comp.slice(2) if (comp.startsWith("./"))
let settings = manifest.settings comp = comp.slice(2);
if (settings && settings.startsWith("./")) settings = settings.slice(2) let settings = manifest.settings;
if (settings && settings.startsWith("./"))
settings = settings.slice(2);
const info = {} const info = {};
for (const k in manifest) info[k] = manifest[k] for (const k in manifest)
info[k] = manifest[k];
let perms = manifest.permissions let perms = manifest.permissions;
if (typeof perms === "string") { if (typeof perms === "string") {
perms = perms.split(/\s*,\s*/) perms = perms.split(/\s*,\s*/);
} }
if (!Array.isArray(perms)) { if (!Array.isArray(perms)) {
perms = [] perms = [];
} }
info.permissions = perms.map(p => String(p).trim()) info.permissions = perms.map(p => String(p).trim());
info.manifestPath = absPath info.manifestPath = absPath;
info.pluginDirectory = dir info.pluginDirectory = dir;
info.componentPath = dir + "/" + comp info.componentPath = dir + "/" + comp;
info.settingsPath = settings ? (dir + "/" + settings) : null info.settingsPath = settings ? (dir + "/" + settings) : null;
info.loaded = isPluginLoaded(manifest.id) info.loaded = isPluginLoaded(manifest.id);
info.type = manifest.type || "widget" info.type = manifest.type || "widget";
info.source = sourceTag info.source = sourceTag;
const existing = availablePlugins[manifest.id] const existing = availablePlugins[manifest.id];
const shouldReplace = const shouldReplace = (!existing) || (existing && existing.source === "system" && sourceTag === "user");
(!existing) ||
(existing && existing.source === "system" && sourceTag === "user")
if (shouldReplace) { if (shouldReplace) {
if (existing && existing.loaded && existing.source !== sourceTag) { if (existing && existing.loaded && existing.source !== sourceTag) {
unloadPlugin(manifest.id) unloadPlugin(manifest.id);
} }
const newMap = Object.assign({}, availablePlugins) const newMap = Object.assign({}, availablePlugins);
newMap[manifest.id] = info newMap[manifest.id] = info;
availablePlugins = newMap availablePlugins = newMap;
pathToPluginId[absPath] = manifest.id pathToPluginId[absPath] = manifest.id;
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag } knownManifests[absPath] = {
pluginListUpdated() mtime: mtimeEpochMs,
const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false) source: sourceTag
if (enabled && !info.loaded) loadPlugin(manifest.id) };
_updateAvailablePluginsList();
pluginListUpdated();
const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false);
if (enabled && !info.loaded)
loadPlugin(manifest.id);
} else { } else {
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, shadowedBy: existing.source } knownManifests[absPath] = {
pathToPluginId[absPath] = manifest.id mtime: mtimeEpochMs,
source: sourceTag,
shadowedBy: existing.source
};
pathToPluginId[absPath] = manifest.id;
} }
} }
function unregisterPluginByPath(absPath, pluginId) { function unregisterPluginByPath(absPath, pluginId) {
const current = availablePlugins[pluginId] const current = availablePlugins[pluginId];
if (current && current.manifestPath === absPath) { if (current && current.manifestPath === absPath) {
if (current.loaded) unloadPlugin(pluginId) if (current.loaded)
const newMap = Object.assign({}, availablePlugins) unloadPlugin(pluginId);
delete newMap[pluginId] const newMap = Object.assign({}, availablePlugins);
availablePlugins = newMap delete newMap[pluginId];
availablePlugins = newMap;
} }
} }
function loadPlugin(pluginId) { function loadPlugin(pluginId, bustCache) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (!plugin) { if (!plugin) {
console.error("PluginService: Plugin not found:", pluginId) console.error("PluginService: Plugin not found:", pluginId);
pluginLoadFailed(pluginId, "Plugin not found") pluginLoadFailed(pluginId, "Plugin not found");
return false return false;
} }
if (plugin.loaded) { if (plugin.loaded) {
return true return true;
} }
const isDaemon = plugin.type === "daemon" const isDaemon = plugin.type === "daemon";
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher")) const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
const map = isDaemon ? pluginDaemonComponents : isLauncher ? pluginLauncherComponents : pluginWidgetComponents
const prevInstance = pluginInstances[pluginId] const prevInstance = pluginInstances[pluginId];
if (prevInstance) { if (prevInstance) {
prevInstance.destroy() prevInstance.destroy();
const newInstances = Object.assign({}, pluginInstances) const newInstances = Object.assign({}, pluginInstances);
delete newInstances[pluginId] delete newInstances[pluginId];
pluginInstances = newInstances pluginInstances = newInstances;
} }
try { try {
const url = "file://" + plugin.componentPath let url = "file://" + plugin.componentPath;
const comp = Qt.createComponent(url, Component.PreferSynchronous) if (bustCache)
url += "?t=" + Date.now();
const comp = Qt.createComponent(url, Component.PreferSynchronous);
if (comp.status === Component.Error) { if (comp.status === Component.Error) {
console.error("PluginService: component error", pluginId, comp.errorString()) console.error("PluginService: component error", pluginId, comp.errorString());
pluginLoadFailed(pluginId, comp.errorString()) pluginLoadFailed(pluginId, comp.errorString());
return false return false;
} }
if (isDaemon) { if (isDaemon) {
const instance = comp.createObject(root, { "pluginId": pluginId }) const instance = comp.createObject(root, {
"pluginId": pluginId
});
if (!instance) { if (!instance) {
console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString()) console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString());
pluginLoadFailed(pluginId, comp.errorString()) pluginLoadFailed(pluginId, comp.errorString());
return false return false;
} }
const newInstances = Object.assign({}, pluginInstances) const newInstances = Object.assign({}, pluginInstances);
newInstances[pluginId] = instance newInstances[pluginId] = instance;
pluginInstances = newInstances pluginInstances = newInstances;
const newDaemons = Object.assign({}, pluginDaemonComponents) const newDaemons = Object.assign({}, pluginDaemonComponents);
newDaemons[pluginId] = comp newDaemons[pluginId] = comp;
pluginDaemonComponents = newDaemons pluginDaemonComponents = newDaemons;
} else if (isLauncher) { } else if (isLauncher) {
const newLaunchers = Object.assign({}, pluginLauncherComponents) const newLaunchers = Object.assign({}, pluginLauncherComponents);
newLaunchers[pluginId] = comp newLaunchers[pluginId] = comp;
pluginLauncherComponents = newLaunchers pluginLauncherComponents = newLaunchers;
} else { } else {
const newComponents = Object.assign({}, pluginWidgetComponents) const newComponents = Object.assign({}, pluginWidgetComponents);
newComponents[pluginId] = comp newComponents[pluginId] = comp;
pluginWidgetComponents = newComponents pluginWidgetComponents = newComponents;
} }
plugin.loaded = true plugin.loaded = true;
loadedPlugins[pluginId] = plugin const newLoaded = Object.assign({}, loadedPlugins);
newLoaded[pluginId] = plugin;
pluginLoaded(pluginId) loadedPlugins = newLoaded;
return true
pluginLoaded(pluginId);
return true;
} catch (e) { } catch (e) {
console.error("PluginService: Error loading plugin:", pluginId, e.message) console.error("PluginService: Error loading plugin:", pluginId, e.message);
pluginLoadFailed(pluginId, e.message) pluginLoadFailed(pluginId, e.message);
return false return false;
} }
} }
function unloadPlugin(pluginId) { function unloadPlugin(pluginId) {
const plugin = loadedPlugins[pluginId] const plugin = loadedPlugins[pluginId];
if (!plugin) { if (!plugin) {
console.warn("PluginService: Plugin not loaded:", pluginId) console.warn("PluginService: Plugin not loaded:", pluginId);
return false return false;
} }
try { try {
const isDaemon = plugin.type === "daemon" const isDaemon = plugin.type === "daemon";
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher")) const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
const instance = pluginInstances[pluginId] const instance = pluginInstances[pluginId];
if (instance) { if (instance) {
instance.destroy() instance.destroy();
const newInstances = Object.assign({}, pluginInstances) const newInstances = Object.assign({}, pluginInstances);
delete newInstances[pluginId] delete newInstances[pluginId];
pluginInstances = newInstances pluginInstances = newInstances;
} }
if (isDaemon && pluginDaemonComponents[pluginId]) { if (isDaemon && pluginDaemonComponents[pluginId]) {
const newDaemons = Object.assign({}, pluginDaemonComponents) const newDaemons = Object.assign({}, pluginDaemonComponents);
delete newDaemons[pluginId] delete newDaemons[pluginId];
pluginDaemonComponents = newDaemons pluginDaemonComponents = newDaemons;
} else if (isLauncher && pluginLauncherComponents[pluginId]) { } else if (isLauncher && pluginLauncherComponents[pluginId]) {
const newLaunchers = Object.assign({}, pluginLauncherComponents) const newLaunchers = Object.assign({}, pluginLauncherComponents);
delete newLaunchers[pluginId] delete newLaunchers[pluginId];
pluginLauncherComponents = newLaunchers pluginLauncherComponents = newLaunchers;
} else if (pluginWidgetComponents[pluginId]) { } else if (pluginWidgetComponents[pluginId]) {
const newComponents = Object.assign({}, pluginWidgetComponents) const newComponents = Object.assign({}, pluginWidgetComponents);
delete newComponents[pluginId] delete newComponents[pluginId];
pluginWidgetComponents = newComponents pluginWidgetComponents = newComponents;
} }
plugin.loaded = false plugin.loaded = false;
delete loadedPlugins[pluginId] const newLoaded = Object.assign({}, loadedPlugins);
delete newLoaded[pluginId];
pluginUnloaded(pluginId) loadedPlugins = newLoaded;
return true
pluginUnloaded(pluginId);
return true;
} catch (error) { } catch (error) {
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message) console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message);
return false return false;
} }
} }
function getWidgetComponents() { function getWidgetComponents() {
return pluginWidgetComponents return pluginWidgetComponents;
} }
function getDaemonComponents() { function getDaemonComponents() {
return pluginDaemonComponents return pluginDaemonComponents;
} }
function getAvailablePlugins() { function getAvailablePlugins() {
const result = [] return availablePluginsList;
}
function _updateAvailablePluginsList() {
const result = [];
for (const key in availablePlugins) { for (const key in availablePlugins) {
result.push(availablePlugins[key]) result.push(availablePlugins[key]);
} }
return result availablePluginsList = result;
} }
function getPluginVariants(pluginId) { function getPluginVariants(pluginId) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (!plugin) { if (!plugin) {
return [] return [];
} }
const variants = SettingsData.getPluginSetting(pluginId, "variants", []) const variants = SettingsData.getPluginSetting(pluginId, "variants", []);
return variants return variants;
} }
function getAllPluginVariants() { function getAllPluginVariants() {
const result = [] const result = [];
for (const pluginId in availablePlugins) { for (const pluginId in availablePlugins) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (plugin.type !== "widget") { if (plugin.type !== "widget") {
continue continue;
} }
const variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId);
if (variants.length === 0) { if (variants.length === 0) {
result.push({ result.push({
pluginId: pluginId, pluginId: pluginId,
@@ -379,10 +414,10 @@ Singleton {
icon: plugin.icon || "extension", icon: plugin.icon || "extension",
description: plugin.description || "Plugin widget", description: plugin.description || "Plugin widget",
loaded: plugin.loaded loaded: plugin.loaded
}) });
} else { } else {
for (let i = 0; i < variants.length; i++) { for (let i = 0; i < variants.length; i++) {
const variant = variants[i] const variant = variants[i];
result.push({ result.push({
pluginId: pluginId, pluginId: pluginId,
variantId: variant.id, variantId: variant.id,
@@ -391,244 +426,245 @@ Singleton {
icon: variant.icon || plugin.icon || "extension", icon: variant.icon || plugin.icon || "extension",
description: variant.description || plugin.description || "Plugin widget variant", description: variant.description || plugin.description || "Plugin widget variant",
loaded: plugin.loaded loaded: plugin.loaded
}) });
} }
} }
} }
return result return result;
} }
function createPluginVariant(pluginId, variantName, variantConfig) { function createPluginVariant(pluginId, variantName, variantConfig) {
const variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId);
const variantId = "variant_" + Date.now() const variantId = "variant_" + Date.now();
const newVariant = Object.assign({}, variantConfig, { const newVariant = Object.assign({}, variantConfig, {
id: variantId, id: variantId,
name: variantName name: variantName
}) });
variants.push(newVariant) variants.push(newVariant);
SettingsData.setPluginSetting(pluginId, "variants", variants) SettingsData.setPluginSetting(pluginId, "variants", variants);
pluginDataChanged(pluginId) pluginDataChanged(pluginId);
return variantId return variantId;
} }
function removePluginVariant(pluginId, variantId) { function removePluginVariant(pluginId, variantId) {
const variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId);
const newVariants = variants.filter(function(v) { return v.id !== variantId }) const newVariants = variants.filter(function (v) {
SettingsData.setPluginSetting(pluginId, "variants", newVariants) return v.id !== variantId;
});
SettingsData.setPluginSetting(pluginId, "variants", newVariants);
const fullId = pluginId + ":" + variantId const fullId = pluginId + ":" + variantId;
removeWidgetFromDankBar(fullId) removeWidgetFromDankBar(fullId);
pluginDataChanged(pluginId) pluginDataChanged(pluginId);
} }
function removeWidgetFromDankBar(widgetId) { function removeWidgetFromDankBar(widgetId) {
function filterWidget(widget) { function filterWidget(widget) {
const id = typeof widget === "string" ? widget : widget.id const id = typeof widget === "string" ? widget : widget.id;
return id !== widgetId return id !== widgetId;
} }
const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default") const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
if (!defaultBar) return if (!defaultBar)
return;
const leftWidgets = defaultBar.leftWidgets || [];
const centerWidgets = defaultBar.centerWidgets || [];
const rightWidgets = defaultBar.rightWidgets || [];
const leftWidgets = defaultBar.leftWidgets || [] const newLeft = leftWidgets.filter(filterWidget);
const centerWidgets = defaultBar.centerWidgets || [] const newCenter = centerWidgets.filter(filterWidget);
const rightWidgets = defaultBar.rightWidgets || [] const newRight = rightWidgets.filter(filterWidget);
const newLeft = leftWidgets.filter(filterWidget)
const newCenter = centerWidgets.filter(filterWidget)
const newRight = rightWidgets.filter(filterWidget)
if (newLeft.length !== leftWidgets.length) { if (newLeft.length !== leftWidgets.length) {
SettingsData.setDankBarLeftWidgets(newLeft) SettingsData.setDankBarLeftWidgets(newLeft);
} }
if (newCenter.length !== centerWidgets.length) { if (newCenter.length !== centerWidgets.length) {
SettingsData.setDankBarCenterWidgets(newCenter) SettingsData.setDankBarCenterWidgets(newCenter);
} }
if (newRight.length !== rightWidgets.length) { if (newRight.length !== rightWidgets.length) {
SettingsData.setDankBarRightWidgets(newRight) SettingsData.setDankBarRightWidgets(newRight);
} }
} }
function updatePluginVariant(pluginId, variantId, variantConfig) { function updatePluginVariant(pluginId, variantId, variantConfig) {
const variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId);
for (let i = 0; i < variants.length; i++) { for (let i = 0; i < variants.length; i++) {
if (variants[i].id === variantId) { if (variants[i].id === variantId) {
variants[i] = Object.assign({}, variants[i], variantConfig) variants[i] = Object.assign({}, variants[i], variantConfig);
break break;
} }
} }
SettingsData.setPluginSetting(pluginId, "variants", variants) SettingsData.setPluginSetting(pluginId, "variants", variants);
pluginDataChanged(pluginId) pluginDataChanged(pluginId);
} }
function getPluginVariantData(pluginId, variantId) { function getPluginVariantData(pluginId, variantId) {
const variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId);
for (let i = 0; i < variants.length; i++) { for (let i = 0; i < variants.length; i++) {
if (variants[i].id === variantId) { if (variants[i].id === variantId) {
return variants[i] return variants[i];
} }
} }
return null return null;
} }
function getLoadedPlugins() { function getLoadedPlugins() {
const result = [] const result = [];
for (const key in loadedPlugins) { for (const key in loadedPlugins) {
result.push(loadedPlugins[key]) result.push(loadedPlugins[key]);
} }
return result return result;
} }
function isPluginLoaded(pluginId) { function isPluginLoaded(pluginId) {
return loadedPlugins[pluginId] !== undefined return loadedPlugins[pluginId] !== undefined;
} }
function enablePlugin(pluginId) { function enablePlugin(pluginId) {
SettingsData.setPluginSetting(pluginId, "enabled", true) SettingsData.setPluginSetting(pluginId, "enabled", true);
return loadPlugin(pluginId) return loadPlugin(pluginId);
} }
function disablePlugin(pluginId) { function disablePlugin(pluginId) {
SettingsData.setPluginSetting(pluginId, "enabled", false) SettingsData.setPluginSetting(pluginId, "enabled", false);
return unloadPlugin(pluginId) return unloadPlugin(pluginId);
} }
function reloadPlugin(pluginId) { function reloadPlugin(pluginId) {
if (isPluginLoaded(pluginId)) { if (isPluginLoaded(pluginId))
unloadPlugin(pluginId) unloadPlugin(pluginId);
} return loadPlugin(pluginId, true);
return loadPlugin(pluginId)
} }
function savePluginData(pluginId, key, value) { function savePluginData(pluginId, key, value) {
SettingsData.setPluginSetting(pluginId, key, value) SettingsData.setPluginSetting(pluginId, key, value);
pluginDataChanged(pluginId) pluginDataChanged(pluginId);
return true return true;
} }
function loadPluginData(pluginId, key, defaultValue) { function loadPluginData(pluginId, key, defaultValue) {
return SettingsData.getPluginSetting(pluginId, key, defaultValue) return SettingsData.getPluginSetting(pluginId, key, defaultValue);
} }
function saveAllPluginSettings() { function saveAllPluginSettings() {
SettingsData.savePluginSettings() SettingsData.savePluginSettings();
} }
function scanPlugins() { function scanPlugins() {
resyncDebounce.restart() resyncDebounce.restart();
} }
function forceRescanPlugin(pluginId) { function forceRescanPlugin(pluginId) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (plugin && plugin.manifestPath) { if (plugin && plugin.manifestPath) {
const manifestPath = plugin.manifestPath const manifestPath = plugin.manifestPath;
const source = plugin.source || "user" const source = plugin.source || "user";
delete knownManifests[manifestPath] delete knownManifests[manifestPath];
const newMap = Object.assign({}, availablePlugins) const newMap = Object.assign({}, availablePlugins);
delete newMap[pluginId] delete newMap[pluginId];
availablePlugins = newMap availablePlugins = newMap;
loadPluginManifestFile(manifestPath, source, Date.now()) loadPluginManifestFile(manifestPath, source, Date.now());
} }
} }
function createPluginDirectory() { function createPluginDirectory() {
const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }") const mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }");
if (mkdirProcess.status === Component.Ready) { if (mkdirProcess.status === Component.Ready) {
const process = mkdirProcess.createObject(root) const process = mkdirProcess.createObject(root);
process.command = ["mkdir", "-p", pluginDirectory] process.command = ["mkdir", "-p", pluginDirectory];
process.exited.connect(function(exitCode) { process.exited.connect(function (exitCode) {
if (exitCode !== 0) { if (exitCode !== 0) {
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode) console.error("PluginService: Failed to create plugin directory, exit code:", exitCode);
} }
process.destroy() process.destroy();
}) });
process.running = true process.running = true;
return true return true;
} else { } else {
console.error("PluginService: Failed to create mkdir process") console.error("PluginService: Failed to create mkdir process");
return false return false;
} }
} }
// Launcher plugin helper functions // Launcher plugin helper functions
function getLauncherPlugins() { function getLauncherPlugins() {
const launchers = {} const launchers = {};
// Check plugins that have launcher components // Check plugins that have launcher components
for (const pluginId in pluginLauncherComponents) { for (const pluginId in pluginLauncherComponents) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (plugin && plugin.loaded) { if (plugin && plugin.loaded) {
launchers[pluginId] = plugin launchers[pluginId] = plugin;
} }
} }
return launchers return launchers;
} }
function getLauncherPlugin(pluginId) { function getLauncherPlugin(pluginId) {
const plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId];
if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) { if (plugin && plugin.loaded && pluginLauncherComponents[pluginId]) {
return plugin return plugin;
} }
return null return null;
} }
function getPluginTrigger(pluginId) { function getPluginTrigger(pluginId) {
const plugin = getLauncherPlugin(pluginId) const plugin = getLauncherPlugin(pluginId);
if (plugin) { if (plugin) {
// Check if noTrigger is set (always active mode) // Check if noTrigger is set (always active mode)
const noTrigger = SettingsData.getPluginSetting(pluginId, "noTrigger", false) const noTrigger = SettingsData.getPluginSetting(pluginId, "noTrigger", false);
if (noTrigger) { if (noTrigger) {
return "" return "";
} }
// Otherwise load the custom trigger, defaulting to plugin manifest trigger // Otherwise load the custom trigger, defaulting to plugin manifest trigger
const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!") const customTrigger = SettingsData.getPluginSetting(pluginId, "trigger", plugin.trigger || "!");
return customTrigger return customTrigger;
} }
return null return null;
} }
function getAllPluginTriggers() { function getAllPluginTriggers() {
const triggers = {} const triggers = {};
const launchers = getLauncherPlugins() const launchers = getLauncherPlugins();
for (const pluginId in launchers) { for (const pluginId in launchers) {
const trigger = getPluginTrigger(pluginId) const trigger = getPluginTrigger(pluginId);
if (trigger && trigger.trim() !== "") { if (trigger && trigger.trim() !== "") {
triggers[trigger] = pluginId triggers[trigger] = pluginId;
} }
} }
return triggers return triggers;
} }
function getPluginsWithEmptyTrigger() { function getPluginsWithEmptyTrigger() {
const plugins = [] const plugins = [];
const launchers = getLauncherPlugins() const launchers = getLauncherPlugins();
for (const pluginId in launchers) { for (const pluginId in launchers) {
const trigger = getPluginTrigger(pluginId) const trigger = getPluginTrigger(pluginId);
if (!trigger || trigger.trim() === "") { if (!trigger || trigger.trim() === "") {
plugins.push(pluginId) plugins.push(pluginId);
} }
} }
return plugins return plugins;
} }
function getGlobalVar(pluginId, varName, defaultValue) { function getGlobalVar(pluginId, varName, defaultValue) {
if (globalVars[pluginId] && varName in globalVars[pluginId]) { if (globalVars[pluginId] && varName in globalVars[pluginId]) {
return globalVars[pluginId][varName] return globalVars[pluginId][varName];
} }
return defaultValue return defaultValue;
} }
function setGlobalVar(pluginId, varName, value) { function setGlobalVar(pluginId, varName, value) {
const newGlobals = Object.assign({}, globalVars) const newGlobals = Object.assign({}, globalVars);
if (!newGlobals[pluginId]) { if (!newGlobals[pluginId]) {
newGlobals[pluginId] = {} newGlobals[pluginId] = {};
} }
newGlobals[pluginId] = Object.assign({}, newGlobals[pluginId]) newGlobals[pluginId] = Object.assign({}, newGlobals[pluginId]);
newGlobals[pluginId][varName] = value newGlobals[pluginId][varName] = value;
globalVars = newGlobals globalVars = newGlobals;
globalVarChanged(pluginId, varName) globalVarChanged(pluginId, varName);
} }
} }