1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -04:00

plugins: add support for composite plugins

- single plugin can register multiple types - e.g. daemon, bar widget,
  desktop widget
This commit is contained in:
bbedward
2026-06-05 10:33:34 -04:00
parent d3c23ba737
commit bcb5617194
10 changed files with 859 additions and 64 deletions
@@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
layerNamespacePlugin: "composite-example"
property var enabledEmojis: pluginData.emojis || ["😊", "😢", "❤️"]
property int cycleInterval: pluginData.cycleInterval || 3000
property int maxBarEmojis: pluginData.maxBarEmojis || 3
property int currentIndex: 0
property var displayedEmojis: []
Timer {
interval: root.cycleInterval
running: true
repeat: true
onTriggered: {
if (root.enabledEmojis.length > 0) {
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length;
root.updateDisplayedEmojis();
}
}
}
function updateDisplayedEmojis() {
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length);
let emojis = [];
for (let i = 0; i < maxToShow; i++) {
const idx = (root.currentIndex + i) % root.enabledEmojis.length;
emojis.push(root.enabledEmojis[idx]);
}
root.displayedEmojis = emojis;
}
Component.onCompleted: {
updateDisplayedEmojis();
}
onEnabledEmojisChanged: updateDisplayedEmojis()
onMaxBarEmojisChanged: updateDisplayedEmojis()
horizontalBarPill: Component {
Row {
id: emojiRow
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
verticalBarPill: Component {
Column {
id: emojiColumn
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
popoutContent: Component {
PopoutComponent {
id: popoutColumn
headerText: "Emoji Picker"
detailsText: "Click an emoji to copy it to clipboard"
showCloseButton: true
property var allEmojis: ["😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥", "😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘"]
Item {
width: parent.width
implicitHeight: root.popoutHeight - popoutColumn.headerHeight - popoutColumn.detailsHeight - Theme.spacingXL
DankGridView {
id: emojiGrid
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 50) * 50
height: parent.height
clip: true
cellWidth: 50
cellHeight: 50
model: popoutColumn.allEmojis
delegate: StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["dms", "cl", "copy", modelData]);
ToastService.showInfo("Copied " + modelData + " to clipboard");
popoutColumn.closePopout();
}
}
}
}
}
}
}
popoutWidth: 400
popoutHeight: 500
}
@@ -0,0 +1,79 @@
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property string scriptPath: pluginData.scriptPath || ""
property var popoutService: null
Connections {
target: SessionData
function onWallpaperPathChanged() {
if (scriptPath) {
var scriptProcess = scriptProcessComponent.createObject(root, {
wallpaperPath: SessionData.wallpaperPath
});
scriptProcess.running = true;
}
}
}
Component {
id: scriptProcessComponent
Process {
property string wallpaperPath: ""
command: [scriptPath, wallpaperPath]
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
console.log("CompositeDaemon script output:", text.trim());
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
ToastService.showError("Wallpaper Change Script Error", text.trim());
}
}
}
onExited: exitCode => {
if (exitCode !== 0) {
ToastService.showError("Wallpaper Change Script Error", "Script exited with code: " + exitCode);
}
destroy();
}
}
}
IpcHandler {
target: "compositeExample"
function runHook(): string {
if (!root.scriptPath)
return "no script configured";
var scriptProcess = scriptProcessComponent.createObject(root, {
wallpaperPath: SessionData.wallpaperPath
});
scriptProcess.running = true;
return "ran hook";
}
}
Component.onCompleted: {
console.info("CompositeDaemon: Started monitoring wallpaper changes");
}
Component.onDestruction: {
console.info("CompositeDaemon: Stopped monitoring wallpaper changes");
}
}
@@ -0,0 +1,172 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
minWidth: 120
minHeight: 120
property bool showSeconds: pluginData.showSeconds ?? true
property bool showDate: pluginData.showDate ?? true
property string clockStyle: pluginData.clockStyle ?? "analog"
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
SystemClock {
id: systemClock
precision: root.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
}
Rectangle {
id: background
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.backgroundOpacity
}
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingM
sourceComponent: root.clockStyle === "digital" ? digitalClock : analogClock
}
Component {
id: analogClock
Item {
id: analogClockRoot
property real clockSize: Math.min(width, height) - (root.showDate ? 30 : 0)
Item {
id: clockFace
width: analogClockRoot.clockSize
height: analogClockRoot.clockSize
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
Repeater {
model: 12
Rectangle {
required property int index
property real markAngle: index * 30
property real markRadius: clockFace.width / 2 - 8
x: clockFace.width / 2 + markRadius * Math.sin(markAngle * Math.PI / 180) - width / 2
y: clockFace.height / 2 - markRadius * Math.cos(markAngle * Math.PI / 180) - height / 2
width: index % 3 === 0 ? 8 : 4
height: width
radius: width / 2
color: index % 3 === 0 ? Theme.primary : Theme.outlineVariant
}
}
Rectangle {
id: hourHand
property int hours: systemClock.date?.getHours() % 12 ?? 0
property int minutes: systemClock.date?.getMinutes() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 6
height: clockFace.height * 0.25
radius: 3
color: Theme.primary
antialiasing: true
transformOrigin: Item.Bottom
rotation: (hours + minutes / 60) * 30
}
Rectangle {
id: minuteHand
property int minutes: systemClock.date?.getMinutes() ?? 0
property int seconds: systemClock.date?.getSeconds() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 4
height: clockFace.height * 0.35
radius: 2
color: Theme.onSurface
antialiasing: true
transformOrigin: Item.Bottom
rotation: (minutes + seconds / 60) * 6
}
Rectangle {
id: secondHand
visible: root.showSeconds
property int seconds: systemClock.date?.getSeconds() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 2
height: clockFace.height * 0.4
radius: 1
color: Theme.error
antialiasing: true
transformOrigin: Item.Bottom
rotation: seconds * 6
}
Rectangle {
anchors.centerIn: parent
width: 10
height: 10
radius: 5
color: Theme.primary
}
}
Text {
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
}
Component {
id: digitalClock
Item {
id: digitalRoot
property real timeFontSize: Math.min(width * 0.16, height * (root.showDate ? 0.4 : 0.5))
property real dateFontSize: Math.max(Theme.fontSizeSmall, timeFontSize * 0.35)
Text {
id: timeText
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: root.showDate ? -digitalRoot.dateFontSize * 0.8 : 0
text: systemClock.date?.toLocaleTimeString(Qt.locale(), root.showSeconds ? "hh:mm:ss" : "hh:mm") ?? ""
font.pixelSize: digitalRoot.timeFontSize
font.weight: Font.Bold
font.family: "monospace"
color: Theme.primary
}
Text {
id: dateText
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: timeText.bottom
anchors.topMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
font.pixelSize: digitalRoot.dateFontSize
color: Theme.surfaceText
}
}
}
}
@@ -0,0 +1,145 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleComposite"
StyledText {
width: parent.width
text: "Bar Widget — Emoji Cycler"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
SelectionSetting {
settingKey: "emojiSet"
label: "Emoji Set"
description: "Choose which collection of emojis to cycle through"
options: [
{
label: "Happy & Sad",
value: "happySad"
},
{
label: "Hearts",
value: "hearts"
},
{
label: "Hand Gestures",
value: "hands"
},
{
label: "All Mixed",
value: "mixed"
}
]
defaultValue: "happySad"
onValueChanged: {
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
};
root.saveValue("emojis", sets[value] || sets["happySad"]);
}
Component.onCompleted: {
const currentSet = value || defaultValue;
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
};
root.saveValue("emojis", sets[currentSet] || sets["happySad"]);
}
}
SliderSetting {
settingKey: "cycleInterval"
label: "Cycle Speed"
description: "How quickly emojis rotate"
defaultValue: 3000
minimum: 500
maximum: 10000
unit: "ms"
leftIcon: "schedule"
}
SliderSetting {
settingKey: "maxBarEmojis"
label: "Max Bar Emojis"
description: "Maximum number of emojis to display in the bar at once"
defaultValue: 3
minimum: 1
maximum: 8
rightIcon: "emoji_emotions"
}
StyledText {
width: parent.width
text: "Desktop Widget — Clock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
SelectionSetting {
settingKey: "clockStyle"
label: "Clock Style"
options: [
{
label: "Analog",
value: "analog"
},
{
label: "Digital",
value: "digital"
}
]
defaultValue: "analog"
}
ToggleSetting {
settingKey: "showSeconds"
label: "Show Seconds"
defaultValue: true
}
ToggleSetting {
settingKey: "showDate"
label: "Show Date"
defaultValue: true
}
SliderSetting {
settingKey: "backgroundOpacity"
label: "Background Opacity"
defaultValue: 50
minimum: 0
maximum: 100
unit: "%"
}
StyledText {
width: parent.width
text: "Daemon — Wallpaper Hook"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StringSetting {
settingKey: "scriptPath"
label: "Script Path"
description: "Script executed when the wallpaper changes. The new wallpaper path is passed as the first argument."
placeholder: "/path/to/your/script.sh"
defaultValue: ""
}
}
@@ -0,0 +1,53 @@
# Composite Example
A single plugin that provides **all three surfaces at once** by combining three of
the standalone example plugins:
| Surface | Source example | File |
|---------|----------------|------|
| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |
| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |
| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |
It demonstrates the `components` manifest map, where each surface points at its own
QML file:
```json
"type": "composite",
"components": {
"daemon": "./CompositeDaemon.qml",
"widget": "./CompositeBarWidget.qml",
"desktop": "./CompositeDesktopWidget.qml"
}
```
All surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings
namespace (`exampleComposite`), so `pluginData` is the same for every surface.
## Surfaces
- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script
on change. Also registers an `IpcHandler` (`target: "compositeExample"`) exposing a
`runHook` call, so you can trigger the hook over IPC.
- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout
that copies to the clipboard.
- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.
## Usage
1. Copy this directory into `$CONFIGPATH/DankMaterialShell/plugins/`.
2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.
(Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they
do not auto-load, because they also carry a daemon.)
3. Add the bar widget via Settings → Appearance → DankBar Layout.
4. Place the desktop clock via Settings → Desktop Widgets.
## Notes
- The daemon surface is instantiated once and lives for as long as the plugin is
enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.
- Cross-surface runtime state (not needed here) is best shared via
`PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each
surface is a separate object.
- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only
understood by DMS 1.5.0 and later.
@@ -0,0 +1,21 @@
{
"id": "exampleComposite",
"name": "Composite Example",
"description": "One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock",
"version": "1.0.0",
"author": "DankMaterialShell",
"type": "composite",
"capabilities": ["daemon", "dankbar-widget", "desktop-widget", "clipboard"],
"icon": "extension",
"components": {
"daemon": "./CompositeDaemon.qml",
"widget": "./CompositeBarWidget.qml",
"desktop": "./CompositeDesktopWidget.qml"
},
"settings": "./CompositeSettings.qml",
"requires_dms": ">=1.5.0",
"permissions": [
"settings_read",
"settings_write"
]
}
+74
View File
@@ -1635,6 +1635,79 @@ See `PLUGINS/ExampleDesktopClock/` for a complete working example demonstrating:
- Responsive sizing - Responsive sizing
- Edit mode handling - Edit mode handling
## Composite Plugins
A single plugin can provide **multiple surfaces at once** — for example a background
daemon (for IPC / monitoring), a bar widget, and a desktop widget. Because each surface
has a different lifecycle (the daemon is instantiated once; bar and desktop widgets are
instantiated per bar/placement per screen), each surface is its own QML file.
### Plugin Type Configuration
Instead of a single `type` + `component`, declare a `components` map. Set `type` to
`composite` (any value works; `composite` is conventional):
```json
{
"id": "myComposite",
"name": "My Composite Plugin",
"description": "A daemon plus a bar widget plus a desktop widget",
"version": "1.0.0",
"author": "Your Name",
"type": "composite",
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
"components": {
"daemon": "./MyDaemon.qml",
"widget": "./MyBarWidget.qml",
"desktop": "./MyDesktopWidget.qml",
"launcher": "./MyLauncher.qml"
},
"trigger": "#",
"settings": "./MySettings.qml",
"requires_dms": ">=1.5.0",
"permissions": ["settings_read", "settings_write"]
}
```
### Surfaces
Provide any subset of these keys in `components`:
| Surface | Component contract | Notes |
|---------|--------------------|-------|
| `widget` | `PluginComponent` (bar pills + optional Control Center widget) | see [Widget Component](#widget-component) |
| `desktop` | `DesktopPluginComponent` (or an `Item` following the desktop contract) | see [Desktop Plugins](#desktop-plugins) |
| `daemon` | any `Item` exposing `pluginService` / `pluginId` | instantiated once; ideal for IPC handlers and background monitoring |
| `launcher` | launcher contract (`getItems` / `executeItem`) | requires `trigger` (or empty-trigger mode); see [Launcher Plugins](#launcher-plugins) |
Each surface is loaded independently into its own registry, so the same plugin can show
up in the bar **and** on the desktop **and** run a daemon simultaneously.
### Shared State
Each surface is a separate object, so share runtime state through:
- `PluginService.getGlobalVar(pluginId, name, default)` / `setGlobalVar(...)` — reactive,
in-process, namespaced per plugin (see [Plugin Global Variables](#plugin-global-variables)).
- The daemon instance — register `IpcHandler`s or expose data other surfaces read via
global vars.
- `savePluginData` / `loadPluginData` for persisted settings (all surfaces of a plugin
share one settings namespace, so one `settings` component configures them all).
### Settings, Enabling, and Backwards Compatibility
- Declare a single top-level `settings` component; it configures every surface.
- Composite plugins respect the **enable toggle** in Settings → Plugins (they are not
auto-loaded). A pure `desktop` plugin still auto-loads for backwards compatibility.
- The legacy single `type` + `component` form is unchanged and fully supported — it is
treated internally as a one-entry `components` map.
### Example Plugin
See `PLUGINS/ExampleCompositePlugin/` for a working composite that combines the
WallpaperWatcher daemon, the Emoji Cycler bar widget, and the Desktop Clock into one
plugin.
## Resources ## Resources
- **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation - **Plugin Schema**: `plugin-schema.json` - JSON Schema for validation
@@ -1644,6 +1717,7 @@ See `PLUGINS/ExampleDesktopClock/` for a complete working example demonstrating:
- [LauncherExample](./LauncherExample/) - [LauncherExample](./LauncherExample/)
- [Calculator](https://github.com/rochacbruno/DankCalculator) - [Calculator](https://github.com/rochacbruno/DankCalculator)
- [Desktop Clock](./ExampleDesktopClock/) - [Desktop Clock](./ExampleDesktopClock/)
- [Composite Example](./ExampleCompositePlugin/)
- **PluginService**: `Services/PluginService.qml` - **PluginService**: `Services/PluginService.qml`
- **Settings UI**: `Modules/Settings/PluginsTab.qml` - **Settings UI**: `Modules/Settings/PluginsTab.qml`
- **DankBar Integration**: `Modules/DankBar/DankBar.qml` - **DankBar Integration**: `Modules/DankBar/DankBar.qml`
+55 -5
View File
@@ -11,8 +11,7 @@
"version", "version",
"author", "author",
"type", "type",
"capabilities", "capabilities"
"component"
], ],
"properties": { "properties": {
"id": { "id": {
@@ -42,8 +41,8 @@
}, },
"type": { "type": {
"type": "string", "type": "string",
"description": "Plugin type", "description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
"enum": ["widget", "daemon", "launcher", "desktop"] "enum": ["widget", "daemon", "launcher", "desktop", "composite"]
}, },
"capabilities": { "capabilities": {
"type": "array", "type": "array",
@@ -55,9 +54,37 @@
}, },
"component": { "component": {
"type": "string", "type": "string",
"description": "Relative path to main QML component file", "description": "Relative path to main QML component file. Required unless 'components' is provided.",
"pattern": "^\\./.*\\.qml$" "pattern": "^\\./.*\\.qml$"
}, },
"components": {
"type": "object",
"description": "Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.",
"properties": {
"widget": {
"type": "string",
"description": "Bar/Control Center widget component (PluginComponent)",
"pattern": "^\\./.*\\.qml$"
},
"desktop": {
"type": "string",
"description": "Desktop widget component",
"pattern": "^\\./.*\\.qml$"
},
"daemon": {
"type": "string",
"description": "Background daemon component (instantiated once)",
"pattern": "^\\./.*\\.qml$"
},
"launcher": {
"type": "string",
"description": "Launcher provider component (requires 'trigger')",
"pattern": "^\\./.*\\.qml$"
}
},
"additionalProperties": false,
"minProperties": 1
},
"trigger": { "trigger": {
"type": "string", "type": "string",
"description": "Trigger string for launcher activation (required for launcher type)" "description": "Trigger string for launcher activation (required for launcher type)"
@@ -109,6 +136,29 @@
"then": { "then": {
"required": ["trigger"] "required": ["trigger"]
} }
},
{
"if": {
"required": ["components"],
"properties": {
"components": {
"required": ["launcher"]
}
}
},
"then": {
"required": ["trigger"]
}
},
{
"anyOf": [
{
"required": ["component"]
},
{
"required": ["components"]
}
]
} }
], ],
"additionalProperties": true "additionalProperties": true
@@ -21,8 +21,7 @@ Singleton {
Connections { Connections {
target: PluginService target: PluginService
function onPluginLoaded(pluginId) { function onPluginLoaded(pluginId) {
const plugin = PluginService.availablePlugins[pluginId]; if (PluginService.pluginDesktopComponents[pluginId] !== undefined)
if (plugin?.type === "desktop")
syncPluginWidgets(); syncPluginWidgets();
} }
function onPluginUnloaded(pluginId) { function onPluginUnloaded(pluginId) {
+108 -43
View File
@@ -1,7 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtCore
import QtQuick import QtQuick
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import Quickshell import Quickshell
@@ -202,8 +201,51 @@ Singleton {
} }
} }
readonly property var pluginSurfaceKeys: ["widget", "desktop", "daemon", "launcher"]
function _stripDotSlash(p) {
return p.startsWith("./") ? p.slice(2) : p;
}
function _deriveLegacySurface(type, capabilities) {
if (type === "daemon")
return "daemon";
if (type === "launcher" || (capabilities && capabilities.includes("launcher")))
return "launcher";
if (type === "desktop")
return "desktop";
return "widget";
}
function _resolveComponentPaths(manifest, dir) {
const paths = {};
if (manifest.components && typeof manifest.components === "object") {
for (const surface in manifest.components) {
if (!pluginSurfaceKeys.includes(surface)) {
log.warn("unknown plugin surface", surface, "in", dir);
continue;
}
const rel = manifest.components[surface];
if (!rel)
continue;
paths[surface] = dir + "/" + _stripDotSlash(rel);
}
return paths;
}
if (manifest.component) {
const surface = _deriveLegacySurface(manifest.type, manifest.capabilities);
paths[surface] = dir + "/" + _stripDotSlash(manifest.component);
}
return paths;
}
function pluginHasSurface(pluginId, surface) {
const plugin = availablePlugins[pluginId];
return !!(plugin && plugin.surfaces && plugin.surfaces.includes(surface));
}
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 && !manifest.components)) {
log.error("invalid manifest fields:", absPath); log.error("invalid manifest fields:", absPath);
knownManifests[absPath] = { knownManifests[absPath] = {
mtime: mtimeEpochMs, mtime: mtimeEpochMs,
@@ -214,13 +256,22 @@ Singleton {
} }
const dir = absPath.substring(0, absPath.lastIndexOf('/')); const dir = absPath.substring(0, absPath.lastIndexOf('/'));
let comp = manifest.component;
if (comp.startsWith("./"))
comp = comp.slice(2);
let settings = manifest.settings; let settings = manifest.settings;
if (settings && settings.startsWith("./")) if (settings && settings.startsWith("./"))
settings = settings.slice(2); settings = settings.slice(2);
const componentPaths = _resolveComponentPaths(manifest, dir);
const surfaces = Object.keys(componentPaths);
if (surfaces.length === 0) {
log.error("no valid component surfaces in manifest:", absPath);
knownManifests[absPath] = {
mtime: mtimeEpochMs,
source: sourceTag,
bad: true
};
return;
}
const info = {}; const info = {};
for (const k in manifest) for (const k in manifest)
info[k] = manifest[k]; info[k] = manifest[k];
@@ -236,10 +287,12 @@ Singleton {
info.manifestPath = absPath; info.manifestPath = absPath;
info.pluginDirectory = dir; info.pluginDirectory = dir;
info.componentPath = dir + "/" + comp; info.componentPaths = componentPaths;
info.surfaces = surfaces;
info.componentPath = componentPaths.widget || componentPaths[surfaces[0]];
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 || (manifest.components ? "composite" : "widget");
info.source = sourceTag; info.source = sourceTag;
info.requires_dms = manifest.requires_dms || null; info.requires_dms = manifest.requires_dms || null;
@@ -260,7 +313,8 @@ Singleton {
}; };
_updateAvailablePluginsList(); _updateAvailablePluginsList();
pluginListUpdated(); pluginListUpdated();
const enabled = info.type === "desktop" || SettingsData.getPluginSetting(manifest.id, "enabled", false); const isPureDesktop = surfaces.length === 1 && surfaces[0] === "desktop";
const enabled = isPureDesktop || SettingsData.getPluginSetting(manifest.id, "enabled", false);
if (enabled && !info.loaded) if (enabled && !info.loaded)
loadPlugin(manifest.id); loadPlugin(manifest.id);
} else { } else {
@@ -296,58 +350,69 @@ Singleton {
return true; return true;
} }
const isDaemon = plugin.type === "daemon"; const componentPaths = plugin.componentPaths || {};
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher")); const surfaces = Object.keys(componentPaths);
const isDesktop = plugin.type === "desktop"; if (surfaces.length === 0) {
log.error("Plugin has no component surfaces:", pluginId);
pluginLoadFailed(pluginId, "No component surfaces");
return false;
}
const prevInstance = pluginInstances[pluginId]; const newWidgets = Object.assign({}, pluginWidgetComponents);
const newDesktop = Object.assign({}, pluginDesktopComponents);
const newDaemons = Object.assign({}, pluginDaemonComponents);
const newLaunchers = Object.assign({}, pluginLauncherComponents);
const newInstances = Object.assign({}, pluginInstances);
const prevInstance = newInstances[pluginId];
if (prevInstance) { if (prevInstance) {
prevInstance.destroy(); prevInstance.destroy();
const newInstances = Object.assign({}, pluginInstances);
delete newInstances[pluginId]; delete newInstances[pluginId];
pluginInstances = newInstances;
} }
try { try {
let url = "file://" + plugin.componentPath; for (const surface of surfaces) {
let url = "file://" + componentPaths[surface];
if (bustCache) if (bustCache)
url += "?t=" + Date.now(); url += "?t=" + Date.now();
const comp = Qt.createComponent(url, Component.PreferSynchronous); const comp = Qt.createComponent(url, Component.PreferSynchronous);
if (comp.status === Component.Error) { if (comp.status === Component.Error) {
log.error("component error", pluginId, comp.errorString()); log.error("component error", pluginId, surface, comp.errorString());
pluginLoadFailed(pluginId, comp.errorString()); pluginLoadFailed(pluginId, comp.errorString());
return false; return false;
} }
if (isDaemon) { switch (surface) {
const newDaemons = Object.assign({}, pluginDaemonComponents); case "daemon":
newDaemons[pluginId] = comp; newDaemons[pluginId] = comp;
pluginDaemonComponents = newDaemons; break;
} else if (isLauncher) { case "desktop":
newDesktop[pluginId] = comp;
break;
case "launcher": {
const instance = comp.createObject(root, { const instance = comp.createObject(root, {
"pluginService": root "pluginService": root
}); });
if (!instance) { if (!instance) {
log.error("failed to instantiate plugin:", pluginId, comp.errorString()); log.error("failed to instantiate launcher surface:", pluginId, comp.errorString());
pluginLoadFailed(pluginId, comp.errorString()); pluginLoadFailed(pluginId, comp.errorString());
return false; return false;
} }
const newInstances = Object.assign({}, pluginInstances);
newInstances[pluginId] = instance; newInstances[pluginId] = instance;
pluginInstances = newInstances;
const newLaunchers = Object.assign({}, pluginLauncherComponents);
newLaunchers[pluginId] = comp; newLaunchers[pluginId] = comp;
pluginLauncherComponents = newLaunchers; break;
} else if (isDesktop) {
const newDesktop = Object.assign({}, pluginDesktopComponents);
newDesktop[pluginId] = comp;
pluginDesktopComponents = newDesktop;
} else {
const newComponents = Object.assign({}, pluginWidgetComponents);
newComponents[pluginId] = comp;
pluginWidgetComponents = newComponents;
} }
default:
newWidgets[pluginId] = comp;
break;
}
}
pluginWidgetComponents = newWidgets;
pluginDesktopComponents = newDesktop;
pluginDaemonComponents = newDaemons;
pluginLauncherComponents = newLaunchers;
pluginInstances = newInstances;
plugin.loaded = true; plugin.loaded = true;
const newLoaded = Object.assign({}, loadedPlugins); const newLoaded = Object.assign({}, loadedPlugins);
@@ -371,10 +436,6 @@ Singleton {
} }
try { try {
const isDaemon = plugin.type === "daemon";
const isLauncher = plugin.type === "launcher" || (plugin.capabilities && plugin.capabilities.includes("launcher"));
const isDesktop = plugin.type === "desktop";
const instance = pluginInstances[pluginId]; const instance = pluginInstances[pluginId];
if (instance) { if (instance) {
instance.destroy(); instance.destroy();
@@ -383,19 +444,22 @@ Singleton {
pluginInstances = newInstances; pluginInstances = newInstances;
} }
if (isDaemon && pluginDaemonComponents[pluginId]) { if (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]) { }
if (pluginLauncherComponents[pluginId]) {
const newLaunchers = Object.assign({}, pluginLauncherComponents); const newLaunchers = Object.assign({}, pluginLauncherComponents);
delete newLaunchers[pluginId]; delete newLaunchers[pluginId];
pluginLauncherComponents = newLaunchers; pluginLauncherComponents = newLaunchers;
} else if (isDesktop && pluginDesktopComponents[pluginId]) { }
if (pluginDesktopComponents[pluginId]) {
const newDesktop = Object.assign({}, pluginDesktopComponents); const newDesktop = Object.assign({}, pluginDesktopComponents);
delete newDesktop[pluginId]; delete newDesktop[pluginId];
pluginDesktopComponents = newDesktop; pluginDesktopComponents = newDesktop;
} else if (pluginWidgetComponents[pluginId]) { }
if (pluginWidgetComponents[pluginId]) {
const newComponents = Object.assign({}, pluginWidgetComponents); const newComponents = Object.assign({}, pluginWidgetComponents);
delete newComponents[pluginId]; delete newComponents[pluginId];
pluginWidgetComponents = newComponents; pluginWidgetComponents = newComponents;
@@ -452,7 +516,8 @@ Singleton {
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") { const hasWidgetSurface = plugin.surfaces ? plugin.surfaces.includes("widget") : (plugin.type === "widget");
if (!hasWidgetSurface) {
continue; continue;
} }
const variants = getPluginVariants(pluginId); const variants = getPluginVariants(pluginId);