1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 04:09:15 -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"
]
}