1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-12 23:32:50 -04:00
Files
DankMaterialShell/.agents/skills/dms-plugin-dev/references/advanced-patterns.md
T

6.9 KiB

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

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

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

.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

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

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

import "." as Local

Item {
    Component.onCompleted: {
        Local.MyService.setData("key", "value")
    }
}

Inline Component Declarations

Reusable sub-components defined inline:

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

.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:

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:

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:

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

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:

// 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