From 43603f56afccd60a87b964a1759526d9b677160f Mon Sep 17 00:00:00 2001 From: louzt Date: Mon, 8 Jun 2026 02:25:34 -0600 Subject: [PATCH] feat(plugins): expose IPC handlers for runtime plugin discovery Follow-up to #1659. That issue landed hot-reload for settings.json via FileView.watchChanges + a 1ms Timer to skirt the JSON parse race. It does not cover plugin discovery in runtime: adding a new plugin directory to ~/.config/DankMaterialShell/plugins/ while the shell is running is not consistently picked up by the existing FolderListModel watcher in PluginService.qml, and there is no IPC handle for forcing a rescan from outside the shell. Adds an IpcHandler on PluginService with five small functions: - scan(): wraps existing scanPlugins(), returns count snapshot - rescan(pluginId): wraps existing forceRescanPlugin(id), validates id - reload(pluginId): wraps existing reloadPlugin(id), validates id - list(): newline-joined id\tloaded\ttype\tname for every known plugin - status(pluginId): loaded\ttype\terror for one plugin Scope intentionally small: no file-watcher changes, no new daemons, no schema additions. Target string "plugins" does not collide with any existing target in DMSShellIPC.qml. Validation: - qs ipc --pid call plugins list returns one row per known plugin - qs ipc --pid call plugins scan returns SCAN_TRIGGERED with count - qs ipc --pid call plugins rescan returns RESCAN_TRIGGERED - Empty-arg paths return ERROR strings instead of throwing - git merge-tree against origin/master is clean --- quickshell/Services/PluginService.qml | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index e921bb04..7794c92e 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -966,4 +966,65 @@ Singleton { } return result; } + + IpcHandler { + target: "plugins" + + // Re-runs the discovery pass over both user and system plugin + // directories. Useful when a plugin directory was added or removed + // at runtime and the FolderListModel watcher did not pick the + // change up. + function scan(): string { + root.scanPlugins(); + return `SCAN_TRIGGERED: ${Object.keys(root.availablePlugins).length} known before debounce`; + } + + // Re-reads a single plugin's manifest without restarting the + // shell. Picks up edits to plugin.json made after the plugin was + // already loaded once. + function rescan(pluginId: string): string { + if (!pluginId) + return "ERROR: rescan requires a pluginId"; + const plugin = root.availablePlugins[pluginId]; + if (!plugin) + return `ERROR: unknown pluginId '${pluginId}' (try 'list' first)`; + root.forceRescanPlugin(pluginId); + return `RESCAN_TRIGGERED: ${pluginId}`; + } + + // Unloads and reloads a plugin in place. Lets a developer iterate + // on a plugin's QML without restarting the shell. + function reload(pluginId: string): string { + if (!pluginId) + return "ERROR: reload requires a pluginId"; + if (!(pluginId in root.availablePlugins)) + return `ERROR: unknown pluginId '${pluginId}'`; + root.reloadPlugin(pluginId); + return `RELOAD_TRIGGERED: ${pluginId}`; + } + + // Returns one `\t\t\t` line per known + // plugin. Format is intentionally simple for easy parsing by + // external shell scripts or CLI management tools. + function list(): string { + const lines = []; + for (const id in root.availablePlugins) { + const p = root.availablePlugins[id]; + lines.push(`${id}\t${p.loaded ? "loaded" : "unloaded"}\t${p.type || "unknown"}\t${p.name || ""}`); + } + return lines.join("\n"); + } + + // Returns `loaded|unloaded\ttype\terror` for a single plugin. Used + // by wrappers that want to poll after a `scan` or `reload`. + function status(pluginId: string): string { + if (!pluginId) + return "ERROR: status requires a pluginId"; + const plugin = root.availablePlugins[pluginId]; + if (!plugin) + return `unknown\t\t`; + const err = root.pluginLoadErrors[pluginId] || ""; + return `${plugin.loaded ? "loaded" : "unloaded"}\t${plugin.type || ""}\t${err}`; + } + } }