1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -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"
}
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 bool hasUpdate: false
property bool isReloading: false
property var sharedTooltip: null
property string pluginId: pluginData ? pluginData.id : ""
property string pluginDirectoryName: {
@@ -28,6 +29,10 @@ StyledRect {
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
property bool isExpanded: expandedPluginId === pluginId
property bool isLoaded: {
PluginService.loadedPlugins;
return PluginService.loadedPlugins[pluginId] !== undefined;
}
width: parent.width
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
@@ -63,7 +68,7 @@ StyledRect {
DankIcon {
name: root.pluginIcon
size: Theme.iconSize
color: PluginService.isPluginLoaded(root.pluginId) ? Theme.primary : Theme.surfaceVariantText
color: root.isLoaded ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
@@ -111,7 +116,7 @@ StyledRect {
height: 28
radius: 14
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 {
anchors.centerIn: parent
@@ -131,27 +136,21 @@ StyledRect {
DMSService.update(currentPluginName, response => {
if (response.error) {
ToastService.showError("Update failed: " + response.error);
} else {
ToastService.showInfo("Plugin updated: " + currentPluginName);
PluginService.forceRescanPlugin(currentPluginId);
if (DMSService.apiVersion >= 8) {
DMSService.listInstalled();
}
return;
}
ToastService.showInfo("Plugin updated: " + currentPluginName);
PluginService.forceRescanPlugin(currentPluginId);
if (DMSService.apiVersion >= 8)
DMSService.listInstalled();
});
}
onEntered: {
tooltipLoader.active = true;
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Update Plugin"), p.x, p.y - 40, null);
}
if (root.sharedTooltip)
root.sharedTooltip.show(I18n.tr("Update Plugin"), parent, 0, 0, "top");
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
if (root.sharedTooltip)
root.sharedTooltip.hide();
}
}
}
@@ -180,27 +179,21 @@ StyledRect {
DMSService.uninstall(currentPluginName, response => {
if (response.error) {
ToastService.showError("Uninstall failed: " + response.error);
} else {
ToastService.showInfo("Plugin uninstalled: " + currentPluginName);
PluginService.scanPlugins();
if (root.isExpanded) {
root.expandedPluginId = "";
}
return;
}
ToastService.showInfo("Plugin uninstalled: " + currentPluginName);
PluginService.scanPlugins();
if (root.isExpanded)
root.expandedPluginId = "";
});
}
onEntered: {
tooltipLoader.active = true;
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Uninstall Plugin"), p.x, p.y - 40, null);
}
if (root.sharedTooltip)
root.sharedTooltip.show(I18n.tr("Uninstall Plugin"), parent, 0, 0, "top");
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
if (root.sharedTooltip)
root.sharedTooltip.hide();
}
}
}
@@ -210,7 +203,7 @@ StyledRect {
height: 28
radius: 14
color: reloadArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency) : "transparent"
visible: PluginService.isPluginLoaded(root.pluginId)
visible: root.isLoaded
DankIcon {
anchors.centerIn: parent
@@ -230,23 +223,18 @@ StyledRect {
root.isReloading = true;
if (PluginService.reloadPlugin(currentPluginId)) {
ToastService.showInfo("Plugin reloaded: " + currentPluginName);
} else {
ToastService.showError("Failed to reload plugin: " + currentPluginName);
root.isReloading = false;
return;
}
ToastService.showError("Failed to reload plugin: " + currentPluginName);
root.isReloading = false;
}
onEntered: {
tooltipLoader.active = true;
if (tooltipLoader.item) {
const p = mapToItem(null, width / 2, 0);
tooltipLoader.item.show(I18n.tr("Reload Plugin"), p.x, p.y - 40, null);
}
if (root.sharedTooltip)
root.sharedTooltip.show(I18n.tr("Reload Plugin"), parent, 0, 0, "top");
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide();
}
tooltipLoader.active = false;
if (root.sharedTooltip)
root.sharedTooltip.hide();
}
}
}
@@ -254,7 +242,7 @@ StyledRect {
DankToggle {
id: pluginToggle
anchors.verticalCenter: parent.verticalCenter
checked: PluginService.isPluginLoaded(root.pluginId)
checked: root.isLoaded
onToggled: isChecked => {
const currentPluginId = root.pluginId;
const currentPluginName = root.pluginName;
@@ -262,21 +250,18 @@ StyledRect {
if (isChecked) {
if (PluginService.enablePlugin(currentPluginId)) {
ToastService.showInfo("Plugin enabled: " + currentPluginName);
} else {
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;
return;
}
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
anchors.fill: parent
anchors.margins: Theme.spacingL
active: root.isExpanded && root.hasSettings && PluginService.isPluginLoaded(root.pluginId)
active: root.isExpanded && root.hasSettings && root.isLoaded
asynchronous: false
source: {
@@ -376,16 +361,10 @@ StyledRect {
StyledText {
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
color: Theme.surfaceVariantText
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 installedPluginsData: ({})
property bool isReloading: false
property alias sharedTooltip: sharedTooltip
focus: true
DankTooltipV2 {
id: sharedTooltip
}
DankFlickable {
anchors.fill: parent
clip: true
@@ -218,7 +223,7 @@ FocusScope {
Repeater {
id: pluginRepeater
model: PluginService.getAvailablePlugins()
model: PluginService.availablePluginsList
PluginListItem {
pluginData: modelData
@@ -229,6 +234,7 @@ FocusScope {
return pluginsTab.installedPluginsData[pluginId] || pluginsTab.installedPluginsData[pluginName] || false;
}
isReloading: pluginsTab.isReloading
sharedTooltip: pluginsTab.sharedTooltip
onExpandedPluginIdChanged: {
pluginsTab.expandedPluginId = expandedPluginId;
}
@@ -253,12 +259,7 @@ FocusScope {
}
function refreshPluginList() {
Qt.callLater(() => {
var plugins = PluginService.getAvailablePlugins();
pluginRepeater.model = null;
pluginRepeater.model = plugins;
pluginsTab.isRefreshingPlugins = false;
});
pluginsTab.isRefreshingPlugins = false;
}
Connections {

View File

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