1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/Services/PluginService.qml
2025-10-07 08:22:21 -04:00

485 lines
17 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
property var availablePlugins: ({})
property var loadedPlugins: ({})
property var pluginWidgetComponents: ({})
property var pluginDaemonComponents: ({})
property string pluginDirectory: {
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
var configDirStr = configDir.toString()
if (configDirStr.startsWith("file://")) {
configDirStr = configDirStr.substring(7)
}
return configDirStr + "/DankMaterialShell/plugins"
}
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
signal pluginLoaded(string pluginId)
signal pluginUnloaded(string pluginId)
signal pluginLoadFailed(string pluginId, string error)
signal pluginDataChanged(string pluginId)
Component.onCompleted: {
Qt.callLater(initializePlugins)
}
function initializePlugins() {
scanPlugins()
}
property int currentScanIndex: 0
property var scanResults: []
property var foundPlugins: ({})
property var lsProcess: Process {
id: dirScanner
stdout: StdioCollector {
onStreamFinished: {
var output = text.trim()
var currentDir = pluginDirectories[currentScanIndex]
if (output) {
var directories = output.split('\n')
for (var i = 0; i < directories.length; i++) {
var dir = directories[i].trim()
if (dir) {
var manifestPath = currentDir + "/" + dir + "/plugin.json"
loadPluginManifest(manifestPath)
}
}
}
}
}
onExited: function(exitCode) {
currentScanIndex++
if (currentScanIndex < pluginDirectories.length) {
scanNextDirectory()
} else {
currentScanIndex = 0
cleanupRemovedPlugins()
}
}
}
function scanPlugins() {
currentScanIndex = 0
foundPlugins = {}
scanNextDirectory()
}
function scanNextDirectory() {
var dir = pluginDirectories[currentScanIndex]
lsProcess.command = ["find", "-L", dir, "-maxdepth", "1", "-type", "d", "-not", "-path", dir, "-exec", "basename", "{}", ";"]
lsProcess.running = true
}
property var manifestReaders: ({})
function loadPluginManifest(manifestPath) {
var readerId = "reader_" + Date.now() + "_" + Math.random()
var checkProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
if (checkProcess.status === Component.Ready) {
var checker = checkProcess.createObject(root)
checker.command = ["test", "-f", manifestPath]
checker.exited.connect(function(exitCode) {
if (exitCode !== 0) {
checker.destroy()
delete manifestReaders[readerId]
return
}
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
if (catProcess.status === Component.Ready) {
var process = catProcess.createObject(root)
process.command = ["cat", manifestPath]
process.stdout.streamFinished.connect(function() {
try {
var manifest = JSON.parse(process.stdout.text.trim())
processManifest(manifest, manifestPath)
} catch (e) {
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
}
process.destroy()
delete manifestReaders[readerId]
})
process.exited.connect(function(exitCode) {
if (exitCode !== 0) {
console.error("PluginService: Failed to read manifest file:", manifestPath, "exit code:", exitCode)
process.destroy()
delete manifestReaders[readerId]
}
})
manifestReaders[readerId] = process
process.running = true
} else {
console.error("PluginService: Failed to create manifest reader process")
}
checker.destroy()
})
manifestReaders[readerId] = checker
checker.running = true
} else {
console.error("PluginService: Failed to create file check process")
}
}
function processManifest(manifest, manifestPath) {
registerPlugin(manifest, manifestPath)
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false)
if (enabled) {
loadPlugin(manifest.id)
}
}
function registerPlugin(manifest, manifestPath) {
if (!manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
return
}
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'))
// Clean up relative paths by removing './' prefix
var componentFile = manifest.component
if (componentFile.startsWith('./')) {
componentFile = componentFile.substring(2)
}
var settingsFile = manifest.settings
if (settingsFile && settingsFile.startsWith('./')) {
settingsFile = settingsFile.substring(2)
}
var pluginInfo = {}
for (var key in manifest) {
pluginInfo[key] = manifest[key]
}
pluginInfo.manifestPath = manifestPath
pluginInfo.pluginDirectory = pluginDir
pluginInfo.componentPath = pluginDir + '/' + componentFile
pluginInfo.settingsPath = settingsFile ? pluginDir + '/' + settingsFile : null
pluginInfo.loaded = false
pluginInfo.type = manifest.type || "widget"
var newPlugins = Object.assign({}, availablePlugins)
newPlugins[manifest.id] = pluginInfo
availablePlugins = newPlugins
foundPlugins[manifest.id] = true
}
function hasPermission(pluginId, permission) {
var plugin = availablePlugins[pluginId]
if (!plugin) {
return false
}
var permissions = plugin.permissions || []
return permissions.indexOf(permission) !== -1
}
function cleanupRemovedPlugins() {
var pluginsToRemove = []
for (var pluginId in availablePlugins) {
if (!foundPlugins[pluginId]) {
pluginsToRemove.push(pluginId)
}
}
if (pluginsToRemove.length > 0) {
var newPlugins = Object.assign({}, availablePlugins)
for (var i = 0; i < pluginsToRemove.length; i++) {
var pluginId = pluginsToRemove[i]
if (isPluginLoaded(pluginId)) {
unloadPlugin(pluginId)
}
delete newPlugins[pluginId]
}
availablePlugins = newPlugins
}
}
function loadPlugin(pluginId) {
var plugin = availablePlugins[pluginId]
if (!plugin) {
console.error("PluginService: Plugin not found:", pluginId)
pluginLoadFailed(pluginId, "Plugin not found")
return false
}
if (plugin.loaded) {
return true
}
var isDaemon = plugin.type === "daemon"
var componentMap = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
if (componentMap[pluginId]) {
componentMap[pluginId]?.destroy()
if (isDaemon) {
var newDaemons = Object.assign({}, pluginDaemonComponents)
delete newDaemons[pluginId]
pluginDaemonComponents = newDaemons
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
}
try {
var componentUrl = "file://" + plugin.componentPath
var component = Qt.createComponent(componentUrl, Component.PreferSynchronous)
if (component.status === Component.Loading) {
component.statusChanged.connect(function() {
if (component.status === Component.Error) {
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
pluginLoadFailed(pluginId, component.errorString())
}
})
}
if (component.status === Component.Error) {
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
pluginLoadFailed(pluginId, component.errorString())
return false
}
if (isDaemon) {
var newDaemons = Object.assign({}, pluginDaemonComponents)
newDaemons[pluginId] = component
pluginDaemonComponents = newDaemons
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component
pluginWidgetComponents = newComponents
}
plugin.loaded = true
loadedPlugins[pluginId] = plugin
pluginLoaded(pluginId)
return true
} catch (error) {
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message)
pluginLoadFailed(pluginId, error.message)
return false
}
}
function unloadPlugin(pluginId) {
var plugin = loadedPlugins[pluginId]
if (!plugin) {
console.warn("PluginService: Plugin not loaded:", pluginId)
return false
}
try {
var isDaemon = plugin.type === "daemon"
if (isDaemon && pluginDaemonComponents[pluginId]) {
pluginDaemonComponents[pluginId]?.destroy()
var newDaemons = Object.assign({}, pluginDaemonComponents)
delete newDaemons[pluginId]
pluginDaemonComponents = newDaemons
} else if (pluginWidgetComponents[pluginId]) {
pluginWidgetComponents[pluginId]?.destroy()
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
plugin.loaded = false
delete loadedPlugins[pluginId]
pluginUnloaded(pluginId)
return true
} catch (error) {
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message)
return false
}
}
function getWidgetComponents() {
return pluginWidgetComponents
}
function getDaemonComponents() {
return pluginDaemonComponents
}
function getAvailablePlugins() {
var result = []
for (var key in availablePlugins) {
result.push(availablePlugins[key])
}
return result
}
function getPluginVariants(pluginId) {
var plugin = availablePlugins[pluginId]
if (!plugin) {
return []
}
var variants = SettingsData.getPluginSetting(pluginId, "variants", [])
return variants
}
function getAllPluginVariants() {
var result = []
for (var pluginId in availablePlugins) {
var plugin = availablePlugins[pluginId]
if (plugin.type !== "widget") {
continue
}
var 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 (var i = 0; i < variants.length; i++) {
var 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) {
var variants = getPluginVariants(pluginId)
var variantId = "variant_" + Date.now()
var newVariant = Object.assign({}, variantConfig, {
id: variantId,
name: variantName
})
variants.push(newVariant)
SettingsData.setPluginSetting(pluginId, "variants", variants)
pluginDataChanged(pluginId)
return variantId
}
function removePluginVariant(pluginId, variantId) {
var variants = getPluginVariants(pluginId)
var newVariants = variants.filter(function(v) { return v.id !== variantId })
SettingsData.setPluginSetting(pluginId, "variants", newVariants)
pluginDataChanged(pluginId)
}
function updatePluginVariant(pluginId, variantId, variantConfig) {
var variants = getPluginVariants(pluginId)
for (var 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) {
var variants = getPluginVariants(pluginId)
for (var i = 0; i < variants.length; i++) {
if (variants[i].id === variantId) {
return variants[i]
}
}
return null
}
function getLoadedPlugins() {
var result = []
for (var key in loadedPlugins) {
result.push(loadedPlugins[key])
}
return result
}
function isPluginLoaded(pluginId) {
return loadedPlugins[pluginId] !== undefined
}
function enablePlugin(pluginId) {
SettingsData.setPluginSetting(pluginId, "enabled", true)
return loadPlugin(pluginId)
}
function disablePlugin(pluginId) {
SettingsData.setPluginSetting(pluginId, "enabled", false)
return unloadPlugin(pluginId)
}
function reloadPlugin(pluginId) {
if (isPluginLoaded(pluginId)) {
unloadPlugin(pluginId)
}
return loadPlugin(pluginId)
}
function savePluginData(pluginId, key, value) {
SettingsData.setPluginSetting(pluginId, key, value)
pluginDataChanged(pluginId)
return true
}
function loadPluginData(pluginId, key, defaultValue) {
return SettingsData.getPluginSetting(pluginId, key, defaultValue)
}
function saveAllPluginSettings() {
SettingsData.savePluginSettings()
}
function createPluginDirectory() {
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
if (mkdirProcess.status === Component.Ready) {
var 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
}
}
}