1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-13 15:52:46 -04:00
Files
DankMaterialShell/.agents/skills/dms-plugin-dev/SKILL.md
T

16 KiB

name, description, compatibility, metadata, allowed-tools
name description compatibility metadata allowed-tools
dms-plugin-dev Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background service), launcher (search + actions), and desktop (draggable desktop widgets). Covers manifest creation, QML component development, settings UI, data persistence, theme integration, PopoutService usage, and external command execution. Use when the user wants to create, modify, or debug a DMS plugin, or asks about the DMS plugin API. Designed for Claude Code (or similar products)
author version domain framework languages
DankMaterialShell 1.0 qml-desktop-development DankMaterialShell qml, javascript
Bash Read Write Edit

DankMaterialShell Plugin Development

Overview

DMS plugins extend the desktop shell with custom widgets, background services, launcher integrations, and desktop widgets. Plugins are QML components discovered from ~/.config/DankMaterialShell/plugins/.

Minimum plugin structure:

~/.config/DankMaterialShell/plugins/YourPlugin/
  plugin.json        # Required: manifest with metadata
  YourComponent.qml  # Required: main QML component
  YourSettings.qml   # Optional: settings UI
  *.js               # Optional: JavaScript utilities

Plugin registry: Community plugins are available at https://plugins.danklinux.com/

Four plugin types:

Type Purpose Base Component Bar pills CC integration
widget Bar widget + popout PluginComponent Yes Yes
daemon Background service PluginComponent (no UI) No Optional
launcher Searchable items in launcher Item No No
desktop Draggable desktop widget DesktopPluginComponent No No

Step 1: Determine Plugin Type

Choose the type based on what the plugin does:

  • Shows in the bar? - Use widget. Displays a pill in DankBar, optionally opens a popout, optionally integrates with Control Center.
  • Runs in background only? - Use daemon. No visible UI, reacts to events (wallpaper changes, notifications, battery level, etc.).
  • Provides searchable/actionable items? - Use launcher. Items appear in the DMS launcher with trigger-based filtering (e.g., type = for calculator, : for emoji).
  • Shows on the desktop background? - Use desktop. Draggable, resizable widget on the desktop layer.

Step 2: Create the Manifest

Create plugin.json in your plugin directory. See plugin-manifest-reference.md for the full schema.

Minimal manifest:

{
    "id": "yourPlugin",
    "name": "Your Plugin Name",
    "description": "Brief description of what your plugin does",
    "version": "1.0.0",
    "author": "Your Name",
    "type": "widget",
    "capabilities": ["your-capability"],
    "component": "./YourWidget.qml"
}

With settings and permissions:

{
    "id": "yourPlugin",
    "name": "Your Plugin Name",
    "description": "Brief description",
    "version": "1.0.0",
    "author": "Your Name",
    "type": "widget",
    "capabilities": ["your-capability"],
    "component": "./YourWidget.qml",
    "icon": "extension",
    "settings": "./Settings.qml",
    "requires_dms": ">=0.1.0",
    "permissions": ["settings_read", "settings_write"]
}

Key rules:

  • id must be camelCase, matching pattern ^[a-zA-Z][a-zA-Z0-9]*$
  • version must be semver (e.g., 1.0.0)
  • component must start with ./ and end with .qml
  • type: "launcher" requires a trigger field
  • settings_write permission is required if the plugin has a settings component

Step 3: Create the Main Component

Widget

import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins

PluginComponent {
    property var popoutService: null

    horizontalBarPill: Component {
        StyledRect {
            width: label.implicitWidth + Theme.spacingM * 2
            height: parent.widgetThickness
            radius: Theme.cornerRadius
            color: Theme.surfaceContainerHigh

            StyledText {
                id: label
                anchors.centerIn: parent
                text: "Hello"
                color: Theme.surfaceText
                font.pixelSize: Theme.fontSizeMedium
            }
        }
    }

    verticalBarPill: Component {
        StyledRect {
            width: parent.widgetThickness
            height: label.implicitHeight + Theme.spacingM * 2
            radius: Theme.cornerRadius
            color: Theme.surfaceContainerHigh

            StyledText {
                id: label
                anchors.centerIn: parent
                text: "Hi"
                color: Theme.surfaceText
                font.pixelSize: Theme.fontSizeSmall
                rotation: 90
            }
        }
    }
}

See widget-plugin-guide.md for popouts, CC integration, and advanced features.

Launcher

import QtQuick
import qs.Services

Item {
    id: root

    property var pluginService: null
    property string trigger: "#"

    signal itemsChanged()

    function getItems(query) {
        const items = [
            { name: "Item One", icon: "material:star", comment: "Description",
              action: "toast:Hello!", categories: ["MyPlugin"] }
        ]
        if (!query) return items
        const q = query.toLowerCase()
        return items.filter(i => i.name.toLowerCase().includes(q))
    }

    function executeItem(item) {
        const [type, ...rest] = item.action.split(":")
        const data = rest.join(":")
        if (type === "toast") ToastService?.showInfo(data)
        else if (type === "copy") Quickshell.execDetached(["dms", "cl", "copy", data])
    }
}

See launcher-plugin-guide.md for triggers, icon types, context menus, and image tiles.

Desktop

import QtQuick
import qs.Common

Item {
    id: root

    property var pluginService: null
    property string pluginId: ""
    property bool editMode: false
    property real widgetWidth: 200
    property real widgetHeight: 200
    property real minWidth: 150
    property real minHeight: 150

    Rectangle {
        anchors.fill: parent
        radius: Theme.cornerRadius
        color: Theme.surfaceContainer
        opacity: 0.85
        border.color: root.editMode ? Theme.primary : "transparent"
        border.width: root.editMode ? 2 : 0

        Text {
            anchors.centerIn: parent
            text: "Desktop Widget"
            color: Theme.surfaceText
        }
    }
}

See desktop-plugin-guide.md for sizing, persistence, and edit mode.

Daemon

import QtQuick
import qs.Common
import qs.Services
import qs.Modules.Plugins

PluginComponent {
    property var popoutService: null

    Connections {
        target: SessionData
        function onSomeSignal() {
            console.log("Event received")
        }
    }
}

See daemon-plugin-guide.md for event-driven patterns and process execution.

Step 4: Add Settings (Optional)

Wrap settings in PluginSettings with your pluginId. All settings auto-save and auto-load.

import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins

PluginSettings {
    pluginId: "yourPlugin"

    StringSetting {
        settingKey: "apiKey"
        label: "API Key"
        description: "Your API key"
        placeholder: "sk-..."
    }

    ToggleSetting {
        settingKey: "enabled"
        label: "Enable Feature"
        defaultValue: true
    }

    SelectionSetting {
        settingKey: "interval"
        label: "Refresh Interval"
        options: [
            { label: "1 min", value: "60" },
            { label: "5 min", value: "300" }
        ]
        defaultValue: "300"
    }
}

Available setting components: StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.

See settings-components-reference.md for full property lists.

Important: Your plugin must declare "permissions": ["settings_write"] in plugin.json, or the settings UI will show an error.

Step 5: Use Data Persistence

Three tiers of persistence:

API Persisted Use case
pluginService.savePluginData(id, key, val) / loadPluginData(id, key, default) Yes (settings.json) User preferences, config
pluginService.savePluginState(id, key, val) / loadPluginState(id, key, default) Yes (separate state file) Runtime state, history, cache
PluginGlobalVar { varName; defaultValue; value; set() } No (runtime only) Cross-instance shared state
  • pluginData is a reactive property on PluginComponent, auto-loaded from settings
  • React to settings changes with Connections { target: pluginService; function onPluginDataChanged(id) { ... } }
  • Global vars sync across all instances (multi-monitor, multiple bar sections)

See data-persistence-guide.md for details and examples.

Step 6: Theme Integration

Always use Theme.* properties from qs.Common - never hardcode colors or sizes.

Essential properties:

  • Colors: Theme.surfaceContainerHigh, Theme.surfaceText, Theme.primary, Theme.onPrimary
  • Fonts: Theme.fontSizeSmall (12), Theme.fontSizeMedium (14), Theme.fontSizeLarge (16), Theme.fontSizeXLarge (20)
  • Spacing: Theme.spacingXS, Theme.spacingS, Theme.spacingM, Theme.spacingL, Theme.spacingXL
  • Radius: Theme.cornerRadius, Theme.cornerRadiusSmall, Theme.cornerRadiusLarge
  • Icons: Theme.iconSizeSmall (16), Theme.iconSize (24), Theme.iconSizeLarge (32)

Common widgets from qs.Widgets: StyledText, StyledRect, DankIcon, DankButton, DankToggle, DankTextField, DankSlider, DankGridView, CachingImage.

See theme-reference.md for the complete property list.

Step 7: Add Popout Content (Widgets Only)

Add a popout that opens when the bar pill is clicked:

PluginComponent {
    popoutWidth: 400
    popoutHeight: 300

    popoutContent: Component {
        PopoutComponent {
            headerText: "My Plugin"
            detailsText: "Optional subtitle"
            showCloseButton: true

            Column {
                width: parent.width
                spacing: Theme.spacingM

                StyledText {
                    text: "Content here"
                    color: Theme.surfaceText
                }
            }
        }
    }

    horizontalBarPill: Component { /* ... */ }
    verticalBarPill: Component { /* ... */ }
}

PopoutComponent properties: headerText, detailsText, showCloseButton, closePopout() (auto-injected), headerHeight (readonly), detailsHeight (readonly).

Calculate available content height: popoutHeight - headerHeight - detailsHeight - spacing

Step 8: Control Center Integration (Widgets Only)

Add your widget to the Control Center grid:

PluginComponent {
    ccWidgetIcon: "toggle_on"
    ccWidgetPrimaryText: "My Feature"
    ccWidgetSecondaryText: isActive ? "On" : "Off"
    ccWidgetIsActive: isActive

    onCcWidgetToggled: {
        isActive = !isActive
        pluginService?.savePluginData(pluginId, "active", isActive)
    }

    // Optional: expandable detail panel (for CompoundPill)
    ccDetailContent: Component {
        Rectangle {
            implicitHeight: 200
            color: Theme.surfaceContainerHigh
            radius: Theme.cornerRadius
        }
    }
}

CC sizing: 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).

Step 9: External Commands and Clipboard

Run commands and capture output:

import qs.Common

Proc.runCommand(
    "myPlugin.fetch",
    ["curl", "-s", "https://api.example.com/data"],
    (stdout, exitCode) => {
        if (exitCode === 0) processData(stdout)
    },
    500  // debounce ms
)

Fire-and-forget (clipboard, notifications):

import Quickshell

Quickshell.execDetached(["dms", "cl", "copy", textToCopy])

Long-running processes: Use the Process QML component from Quickshell.Io with StdioCollector.

Shell commands with pipes: ["sh", "-c", "ps aux | grep foo"]

Do NOT use globalThis.clipboard or browser JavaScript APIs - they don't exist in the QML runtime.

Step 10: Validate and Test

  1. Validate plugin.json against the schema at assets/plugin-schema.json
  2. Run the shell with verbose output: qs -v -p $CONFIGPATH/quickshell/dms/shell.qml
  3. Open Settings > Plugins > Scan for Plugins
  4. Enable your plugin and add it to the DankBar layout

Common issues:

  • Plugin not detected: check plugin.json syntax with jq . plugin.json
  • Widget not showing: ensure it's enabled AND added to a DankBar section
  • Settings error: verify settings_write permission is declared
  • Data not persisting: check pluginService injection and permissions

Common Mistakes

  1. Missing settings_write permission - Settings UI shows error without it
  2. Missing property var popoutService: null - Must declare for injection to work
  3. Missing vertical bar pill - Widget disappears when bar is on left/right edge
  4. Hardcoded colors - Use Theme.* properties, not hex values
  5. Using globalThis.clipboard - Does not exist; use Quickshell.execDetached(["dms", "cl", "copy", text])
  6. Wrong Theme property names - Theme.fontSizeS does not exist, use Theme.fontSizeSmall
  7. Wrong import for Quickshell - Use import Quickshell (not import QtQuick for execDetached)
  8. Forgetting categories in launcher items - Items won't display without it
  9. Not handling null pluginService - Always use optional chaining or null checks
  10. Using PluginComponent for launchers - Launchers use plain Item, not PluginComponent

Quick Reference: Imports

Widget / Daemon:

import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins

Launcher:

import QtQuick
import qs.Services

Desktop:

import QtQuick
import qs.Common

For clipboard/exec: import Quickshell For processes: import Quickshell.Io For networking: import Quickshell.Networking For toast notifications: access ToastService from qs.Services

Quick Reference: File Naming

  • Directory name: PascalCase (e.g., MyAwesomePlugin/)
  • Plugin ID: camelCase (e.g., myAwesomePlugin)
  • QML files: PascalCase (e.g., MyWidget.qml, Settings.qml)
  • Component paths in manifest: relative with ./ prefix (e.g., "./MyWidget.qml")
  • JS utility files: camelCase (e.g., utils.js, apiAdapter.js)

Reference Files

Load these on demand for detailed API documentation: