1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 07:42:46 -04:00

add dms-plugin-dev agent skill for plugin development (#2394)

This commit is contained in:
Bruno Rocha
2026-05-12 14:26:38 +01:00
committed by GitHub
parent c878ffb7f9
commit 9b68fc8213
25 changed files with 3491 additions and 0 deletions
@@ -0,0 +1,320 @@
# Advanced Patterns
Patterns observed in production DMS plugins that go beyond the basics.
## Plugin Variants
Create multiple widget instances from a single plugin definition. Each variant has its own configuration.
### Manifest
No special manifest changes needed - the variant system is built into PluginComponent.
### Widget with Variant Support
```qml
PluginComponent {
property string variantId: ""
property var variantData: ({})
property string displayText: variantData?.text || "Default"
horizontalBarPill: Component {
StyledRect {
width: label.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
StyledText {
id: label
anchors.centerIn: parent
text: root.displayText
}
}
}
}
```
Widget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
### Settings with Variant Management
```qml
PluginSettings {
pluginId: "exampleVariants"
// Variant creation UI
DankButton {
text: "Add New Instance"
onClicked: {
var id = "variant_" + Date.now()
root.createVariant(id, { name: "New Instance", text: "Hello" })
}
}
// Per-variant configuration
Repeater {
model: root.variants
delegate: Column {
StringSetting {
settingKey: modelData.id + "_text"
label: modelData.name || modelData.id
}
}
}
}
```
## JavaScript Utility Files
For complex logic, split into `.js` files:
### utils.js
```javascript
.pragma library
function formatDuration(ms) {
if (ms < 60000) return "just now"
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"
return Math.floor(ms / 3600000) + "h ago"
}
function parseResponse(json) {
try {
return JSON.parse(json)
} catch (e) {
return null
}
}
```
### Using in QML
```qml
import "utils.js" as Utils
Item {
StyledText {
text: Utils.formatDuration(Date.now() - timestamp)
}
}
```
The `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.
## qmldir for Singleton Services
For plugins with internal singleton services:
### qmldir
```
singleton MyService 1.0 MyService.qml
```
### MyService.qml
```qml
pragma Singleton
import QtQuick
QtObject {
property var cache: ({})
function getData(key) {
return cache[key] || null
}
function setData(key, value) {
cache[key] = value
}
}
```
### Using the singleton
```qml
import "." as Local
Item {
Component.onCompleted: {
Local.MyService.setData("key", "value")
}
}
```
## Inline Component Declarations
Reusable sub-components defined inline:
```qml
Item {
component StatusBadge: Rectangle {
property string label: ""
property color badgeColor: Theme.primary
width: badgeText.implicitWidth + Theme.spacingM * 2
height: 24
radius: 12
color: badgeColor
StyledText {
id: badgeText
anchors.centerIn: parent
text: label
color: Theme.onPrimary
font.pixelSize: Theme.fontSizeSmall
}
}
Row {
spacing: Theme.spacingS
StatusBadge { label: "Running"; badgeColor: Theme.success }
StatusBadge { label: "Stopped"; badgeColor: Theme.error }
}
}
```
## Multi-Provider Adapter Pattern
For plugins supporting multiple backends (AI providers, API services):
### apiAdapters.js
```javascript
.pragma library
function createAdapter(provider) {
switch (provider) {
case "openai": return {
url: "https://api.openai.com/v1/chat/completions",
headers: (key) => ({ "Authorization": "Bearer " + key }),
formatRequest: (messages) => JSON.stringify({ model: "gpt-4", messages: messages }),
parseResponse: (text) => JSON.parse(text).choices[0].message.content
}
case "anthropic": return {
url: "https://api.anthropic.com/v1/messages",
headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
formatRequest: (messages) => JSON.stringify({ model: "claude-sonnet-4-20250514", messages: messages }),
parseResponse: (text) => JSON.parse(text).content[0].text
}
default: return null
}
}
```
## IPC Integration
For plugins that respond to keyboard shortcuts or external commands:
```qml
PluginComponent {
Connections {
target: DMSIpc
function onCommandReceived(command, args) {
if (command === "myPlugin.toggle") {
doToggle()
} else if (command === "myPlugin.next") {
goNext()
}
}
}
}
```
External trigger: `dms ipc call myPlugin.toggle`
## Networking with Quickshell.Networking
For API calls using the built-in networking module:
```qml
import Quickshell.Networking
Item {
NetworkRequest {
id: request
url: "https://api.example.com/data"
method: "GET"
onResponseReceived: (response) => {
const data = JSON.parse(response.body)
processData(data)
}
onErrorOccurred: (error) => {
console.error("Network error:", error)
}
}
function fetchData() {
request.send()
}
}
```
## Toast Notifications
Show user feedback:
```qml
import qs.Services
// Info toast
ToastService?.showInfo("Operation completed")
// With title
ToastService?.showInfo("Plugin Name", "Data refreshed successfully")
```
Always use optional chaining since ToastService may not be available in all contexts.
## Clipboard Operations
```qml
import Quickshell
function copyToClipboard(text) {
Quickshell.execDetached(["dms", "cl", "copy", text])
ToastService?.showInfo("Copied to clipboard")
}
```
Do NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.
## Multi-File Plugin Architecture
Large plugins can be split across multiple files:
```
MyPlugin/
plugin.json
Main.qml # Main widget component
Settings.qml # Settings UI
DetailView.qml # Popout detail view
utils.js # Utility functions
apiAdapter.js # API adapter layer
qmldir # Optional: singleton registrations
```
Import sibling files:
```qml
// In Main.qml
import "." as Local
Item {
Loader {
source: "DetailView.qml"
}
}
```
## Performance Tips
1. Use `Proc.runCommand` with appropriate debounce for external commands
2. Pre-cache images and thumbnails for image-heavy plugins
3. Limit concurrent network requests
4. Use `Timer` with reasonable intervals (don't poll faster than needed)
5. Lazy-load heavy content (use `Loader` for complex popout content)
6. Avoid blocking the UI thread with synchronous operations