1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 00:12:50 -05:00

Re-do plugin scanning

This commit is contained in:
bbedward
2025-10-09 13:22:33 -04:00
parent e32622ac48
commit 9de5e3253e
3 changed files with 222 additions and 214 deletions

View File

@@ -38,6 +38,10 @@ Singleton {
return stringify(path).replace("file://", "") return stringify(path).replace("file://", "")
} }
function toFileUrl(path: string): string {
return path.startsWith("file://") ? path : "file://" + path
}
function mkdir(path: url): void { function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)]) Quickshell.execDetached(["mkdir", "-p", strip(path)])
} }

View File

@@ -600,6 +600,9 @@ FocusScope {
pluginsTab.expandedPluginId = "" pluginsTab.expandedPluginId = ""
} }
} }
function onPluginListUpdated() {
refreshPluginList()
}
} }
Connections { Connections {

View File

@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick import QtQuick
import Qt.labs.folderlistmodel
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
@@ -24,197 +25,192 @@ Singleton {
return configDirStr + "/DankMaterialShell/plugins" return configDirStr + "/DankMaterialShell/plugins"
} }
property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins" property string systemPluginDirectory: "/etc/xdg/quickshell/dms-plugins"
property var pluginDirectories: [pluginDirectory, systemPluginDirectory]
property var knownManifests: ({})
property var pathToPluginId: ({})
property var pluginInstances: ({})
signal pluginLoaded(string pluginId) signal pluginLoaded(string pluginId)
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()
Timer {
id: resyncDebounce
interval: 120
repeat: false
onTriggered: resyncAll()
}
Component.onCompleted: { Component.onCompleted: {
Qt.callLater(initializePlugins) userWatcher.folder = Paths.toFileUrl(root.pluginDirectory)
systemWatcher.folder = Paths.toFileUrl(root.systemPluginDirectory)
Qt.callLater(resyncAll)
} }
function initializePlugins() { FolderListModel {
scanPlugins() id: userWatcher
showDirs: true
showFiles: false
showDotAndDotDot: false
onCountChanged: resyncDebounce.restart()
onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
} }
property int currentScanIndex: 0 FolderListModel {
property var scanResults: [] id: systemWatcher
property var foundPlugins: ({}) showDirs: true
showFiles: false
showDotAndDotDot: false
property var lsProcess: Process { onCountChanged: resyncDebounce.restart()
id: dirScanner onStatusChanged: if (status === FolderListModel.Ready) resyncDebounce.restart()
}
stdout: StdioCollector { function snapshotModel(model, sourceTag) {
onStreamFinished: { const out = []
var output = text.trim() const n = model.count
var currentDir = pluginDirectories[currentScanIndex] for (let i = 0; i < n; i++) {
if (output) { const dirPath = model.get(i, "filePath")
var directories = output.split('\n') const manifestPath = dirPath + "/plugin.json"
for (var i = 0; i < directories.length; i++) { out.push({ path: manifestPath, source: sourceTag })
var dir = directories[i].trim() }
if (dir) { return out
var manifestPath = currentDir + "/" + dir + "/plugin.json" }
loadPluginManifest(manifestPath)
} function resyncAll() {
} const userList = snapshotModel(userWatcher, "user")
} const sysList = snapshotModel(systemWatcher, "system")
const seenPaths = {}
function consider(entry) {
const key = entry.path
seenPaths[key] = true
const prev = knownManifests[key]
if (!prev) {
loadPluginManifestFile(entry.path, entry.source, Date.now())
} }
} }
for (let i=0;i<userList.length;i++) consider(userList[i])
for (let i=0;i<sysList.length;i++) consider(sysList[i])
onExited: function(exitCode) { const removed = []
currentScanIndex++ for (const path in knownManifests) {
if (currentScanIndex < pluginDirectories.length) { if (!seenPaths[path]) removed.push(path)
scanNextDirectory()
} else {
currentScanIndex = 0
cleanupRemovedPlugins()
}
} }
} if (removed.length) {
removed.forEach(function(path) {
function scanPlugins() { const pid = pathToPluginId[path]
currentScanIndex = 0 if (pid) {
foundPlugins = {} unregisterPluginByPath(path, pid)
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
} }
delete knownManifests[path]
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }") delete pathToPluginId[path]
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 pluginListUpdated()
checker.running = true
} else {
console.error("PluginService: Failed to create file check process")
} }
} }
function processManifest(manifest, manifestPath) { function loadPluginManifestFile(manifestPathNoScheme, sourceTag, mtimeEpochMs) {
registerPlugin(manifest, manifestPath) const manifestId = "m_" + Math.random().toString(36).slice(2)
const qml = `
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false) import QtQuick
if (enabled) { import Quickshell.Io
loadPlugin(manifest.id) FileView {
} id: fv
property string absPath: ""
onLoaded: {
try {
let raw = text()
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1)
const manifest = JSON.parse(raw)
root._onManifestParsed(absPath, manifest, "${sourceTag}", ${mtimeEpochMs})
} catch (e) {
console.error("PluginService: bad manifest", absPath, e.message)
knownManifests[absPath] = { mtime: ${mtimeEpochMs}, source: "${sourceTag}", bad: true }
}
fv.destroy()
}
onLoadFailed: (err) => {
console.warn("PluginService: manifest load failed", absPath, err)
fv.destroy()
}
}
`
const loader = Qt.createQmlObject(qml, root, "mf_" + manifestId)
loader.absPath = manifestPathNoScheme
loader.path = manifestPathNoScheme
} }
function registerPlugin(manifest, manifestPath) { function _onManifestParsed(absPath, manifest, sourceTag, mtimeEpochMs) {
if (!manifest.id || !manifest.name || !manifest.component) { if (!manifest || !manifest.id || !manifest.name || !manifest.component) {
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath) console.error("PluginService: invalid manifest fields:", absPath)
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, bad: true }
return return
} }
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/')) 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)
// Clean up relative paths by removing './' prefix const info = {}
var componentFile = manifest.component for (const k in manifest) info[k] = manifest[k]
if (componentFile.startsWith('./')) { info.manifestPath = absPath
componentFile = componentFile.substring(2) info.pluginDirectory = dir
info.componentPath = dir + "/" + comp
info.settingsPath = settings ? (dir + "/" + settings) : null
info.loaded = isPluginLoaded(manifest.id)
info.type = manifest.type || "widget"
info.source = sourceTag
const existing = availablePlugins[manifest.id]
const shouldReplace =
(!existing) ||
(existing && existing.source === "system" && sourceTag === "user")
if (shouldReplace) {
if (existing && existing.loaded && existing.source !== sourceTag) {
unloadPlugin(manifest.id)
}
const newMap = Object.assign({}, availablePlugins)
newMap[manifest.id] = info
availablePlugins = newMap
pathToPluginId[absPath] = manifest.id
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag }
pluginListUpdated()
const enabled = SettingsData.getPluginSetting(manifest.id, "enabled", false)
if (enabled && !info.loaded) loadPlugin(manifest.id)
} else {
knownManifests[absPath] = { mtime: mtimeEpochMs, source: sourceTag, shadowedBy: existing.source }
pathToPluginId[absPath] = manifest.id
} }
}
var settingsFile = manifest.settings function unregisterPluginByPath(absPath, pluginId) {
if (settingsFile && settingsFile.startsWith('./')) { const current = availablePlugins[pluginId]
settingsFile = settingsFile.substring(2) if (current && current.manifestPath === absPath) {
if (current.loaded) unloadPlugin(pluginId)
const newMap = Object.assign({}, availablePlugins)
delete newMap[pluginId]
availablePlugins = newMap
} }
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) { function hasPermission(pluginId, permission) {
var plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId]
if (!plugin) { if (!plugin) {
return false return false
} }
var permissions = plugin.permissions || [] const permissions = plugin.permissions || []
return permissions.indexOf(permission) !== -1 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) { function loadPlugin(pluginId) {
var 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")
@@ -225,48 +221,43 @@ Singleton {
return true return true
} }
var isDaemon = plugin.type === "daemon" const isDaemon = plugin.type === "daemon"
var componentMap = isDaemon ? pluginDaemonComponents : pluginWidgetComponents const map = isDaemon ? pluginDaemonComponents : pluginWidgetComponents
if (componentMap[pluginId]) { const prevInstance = pluginInstances[pluginId]
componentMap[pluginId]?.destroy() if (prevInstance) {
if (isDaemon) { prevInstance.destroy()
var newDaemons = Object.assign({}, pluginDaemonComponents) const newInstances = Object.assign({}, pluginInstances)
delete newDaemons[pluginId] delete newInstances[pluginId]
pluginDaemonComponents = newDaemons pluginInstances = newInstances
} else {
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId]
pluginWidgetComponents = newComponents
}
} }
try { try {
var componentUrl = "file://" + plugin.componentPath const url = "file://" + plugin.componentPath
var component = Qt.createComponent(componentUrl, Component.PreferSynchronous) const comp = Qt.createComponent(url, Component.PreferSynchronous)
if (comp.status === Component.Error) {
if (component.status === Component.Loading) { console.error("PluginService: component error", pluginId, comp.errorString())
component.statusChanged.connect(function() { pluginLoadFailed(pluginId, comp.errorString())
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 return false
} }
if (isDaemon) { if (isDaemon) {
var newDaemons = Object.assign({}, pluginDaemonComponents) const instance = comp.createObject(root, { "pluginId": pluginId })
newDaemons[pluginId] = component if (!instance) {
console.error("PluginService: failed to instantiate daemon:", pluginId, comp.errorString())
pluginLoadFailed(pluginId, comp.errorString())
return false
}
const newInstances = Object.assign({}, pluginInstances)
newInstances[pluginId] = instance
pluginInstances = newInstances
const newDaemons = Object.assign({}, pluginDaemonComponents)
newDaemons[pluginId] = comp
pluginDaemonComponents = newDaemons pluginDaemonComponents = newDaemons
} else { } else {
var newComponents = Object.assign({}, pluginWidgetComponents) const newComponents = Object.assign({}, pluginWidgetComponents)
newComponents[pluginId] = component newComponents[pluginId] = comp
pluginWidgetComponents = newComponents pluginWidgetComponents = newComponents
} }
@@ -276,31 +267,37 @@ Singleton {
pluginLoaded(pluginId) pluginLoaded(pluginId)
return true return true
} catch (error) { } catch (e) {
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message) console.error("PluginService: Error loading plugin:", pluginId, e.message)
pluginLoadFailed(pluginId, error.message) pluginLoadFailed(pluginId, e.message)
return false return false
} }
} }
function unloadPlugin(pluginId) { function unloadPlugin(pluginId) {
var 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 {
var isDaemon = plugin.type === "daemon" const isDaemon = plugin.type === "daemon"
const instance = pluginInstances[pluginId]
if (instance) {
instance.destroy()
const newInstances = Object.assign({}, pluginInstances)
delete newInstances[pluginId]
pluginInstances = newInstances
}
if (isDaemon && pluginDaemonComponents[pluginId]) { if (isDaemon && pluginDaemonComponents[pluginId]) {
pluginDaemonComponents[pluginId]?.destroy() const newDaemons = Object.assign({}, pluginDaemonComponents)
var newDaemons = Object.assign({}, pluginDaemonComponents)
delete newDaemons[pluginId] delete newDaemons[pluginId]
pluginDaemonComponents = newDaemons pluginDaemonComponents = newDaemons
} else if (pluginWidgetComponents[pluginId]) { } else if (pluginWidgetComponents[pluginId]) {
pluginWidgetComponents[pluginId]?.destroy() const newComponents = Object.assign({}, pluginWidgetComponents)
var newComponents = Object.assign({}, pluginWidgetComponents)
delete newComponents[pluginId] delete newComponents[pluginId]
pluginWidgetComponents = newComponents pluginWidgetComponents = newComponents
} }
@@ -326,30 +323,30 @@ Singleton {
} }
function getAvailablePlugins() { function getAvailablePlugins() {
var result = [] const result = []
for (var key in availablePlugins) { for (const key in availablePlugins) {
result.push(availablePlugins[key]) result.push(availablePlugins[key])
} }
return result return result
} }
function getPluginVariants(pluginId) { function getPluginVariants(pluginId) {
var plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId]
if (!plugin) { if (!plugin) {
return [] return []
} }
var variants = SettingsData.getPluginSetting(pluginId, "variants", []) const variants = SettingsData.getPluginSetting(pluginId, "variants", [])
return variants return variants
} }
function getAllPluginVariants() { function getAllPluginVariants() {
var result = [] const result = []
for (var pluginId in availablePlugins) { for (const pluginId in availablePlugins) {
var plugin = availablePlugins[pluginId] const plugin = availablePlugins[pluginId]
if (plugin.type !== "widget") { if (plugin.type !== "widget") {
continue continue
} }
var variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId)
if (variants.length === 0) { if (variants.length === 0) {
result.push({ result.push({
pluginId: pluginId, pluginId: pluginId,
@@ -361,8 +358,8 @@ Singleton {
loaded: plugin.loaded loaded: plugin.loaded
}) })
} else { } else {
for (var i = 0; i < variants.length; i++) { for (let i = 0; i < variants.length; i++) {
var variant = variants[i] const variant = variants[i]
result.push({ result.push({
pluginId: pluginId, pluginId: pluginId,
variantId: variant.id, variantId: variant.id,
@@ -379,9 +376,9 @@ Singleton {
} }
function createPluginVariant(pluginId, variantName, variantConfig) { function createPluginVariant(pluginId, variantName, variantConfig) {
var variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId)
var variantId = "variant_" + Date.now() const variantId = "variant_" + Date.now()
var newVariant = Object.assign({}, variantConfig, { const newVariant = Object.assign({}, variantConfig, {
id: variantId, id: variantId,
name: variantName name: variantName
}) })
@@ -392,15 +389,15 @@ Singleton {
} }
function removePluginVariant(pluginId, variantId) { function removePluginVariant(pluginId, variantId) {
var variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId)
var newVariants = variants.filter(function(v) { return v.id !== variantId }) const newVariants = variants.filter(function(v) { return v.id !== variantId })
SettingsData.setPluginSetting(pluginId, "variants", newVariants) SettingsData.setPluginSetting(pluginId, "variants", newVariants)
pluginDataChanged(pluginId) pluginDataChanged(pluginId)
} }
function updatePluginVariant(pluginId, variantId, variantConfig) { function updatePluginVariant(pluginId, variantId, variantConfig) {
var variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId)
for (var 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
@@ -411,8 +408,8 @@ Singleton {
} }
function getPluginVariantData(pluginId, variantId) { function getPluginVariantData(pluginId, variantId) {
var variants = getPluginVariants(pluginId) const variants = getPluginVariants(pluginId)
for (var 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]
} }
@@ -421,8 +418,8 @@ Singleton {
} }
function getLoadedPlugins() { function getLoadedPlugins() {
var result = [] const result = []
for (var key in loadedPlugins) { for (const key in loadedPlugins) {
result.push(loadedPlugins[key]) result.push(loadedPlugins[key])
} }
return result return result
@@ -463,10 +460,14 @@ Singleton {
SettingsData.savePluginSettings() SettingsData.savePluginSettings()
} }
function scanPlugins() {
resyncDebounce.restart()
}
function createPluginDirectory() { function createPluginDirectory() {
var 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) {
var 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) {