1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-25 20:45:19 -04:00
Files
DankMaterialShell/.agents/skills/dms-plugin-dev/SKILL.md
T
Bruno Rocha 92994061da skill(dms-plugin-dev): update for composite plugins, startupCheck, IPC discovery (#2699)
- Add composite plugin type with multi-surface components field
- Add startupCheck manifest field and dependency gating docs
- Add IPC runtime plugin discovery commands (plugin-scan target)
- Add getPluginPath() to data persistence reference
- Document bar reveal visibility optimization for widget plugins
- Sync plugin-schema.json with source, add dependencies field
- Bump skill version to 1.1
2026-06-25 11:53:07 -04:00

20 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 five plugin types: widget (bar + Control Center), daemon (background service), launcher (search + actions), desktop (draggable desktop widgets), and composite (multi-surface). Covers manifest creation, QML component development, startup checks, settings UI, data persistence, theme integration, PopoutService usage, IPC runtime discovery, 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.1 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/

Five 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
composite Multi-surface plugin One component per surface Optional Optional

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.
  • Needs multiple surfaces? - Use composite. A single plugin that registers any combination of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own QML component file.

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, startup check, 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",
    "startupCheck": "./StartupCheck.qml",
    "requires_dms": ">=0.1.0",
    "dependencies": ["mytool"],
    "permissions": ["settings_read", "settings_write"]
}

Composite plugin (multi-surface):

{
    "id": "myComposite",
    "name": "My Composite Plugin",
    "description": "Daemon + widget + desktop from one plugin",
    "version": "1.0.0",
    "author": "Your Name",
    "type": "composite",
    "capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
    "icon": "extension",
    "components": {
        "daemon": "./MyDaemon.qml",
        "widget": "./MyBarWidget.qml",
        "desktop": "./MyDesktopWidget.qml"
    },
    "settings": "./Settings.qml",
    "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)
  • Provide either component (single-surface) or components (multi-surface), not both
  • component / component paths must start with ./ and end with .qml
  • type: "launcher" (or a components object with a launcher key) requires a trigger field
  • settings_write permission is required if the plugin has a settings component
  • dependencies replaces the deprecated requires field

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.

Composite

For composite plugins, create a separate QML file per surface. Each surface uses the same base component as the corresponding single-surface type (PluginComponent for widget/daemon, Item for launcher, etc.). All surfaces share the same pluginId and pluginService.

MyCompositePlugin/
  plugin.json
  MyBarWidget.qml      # PluginComponent (widget surface)
  MyDaemon.qml         # PluginComponent (daemon surface)
  MyDesktopWidget.qml  # Item with desktop widget properties
  Settings.qml         # Shared settings for all surfaces

Use pluginService.pluginHasSurface(pluginId, "widget") to check whether a specific surface is registered for a plugin at runtime.

Step 4: Add Startup Check (Optional)

Gate plugin activation on dependency checks by providing a startupCheck component. This runs before the plugin loads and blocks activation if a required tool or condition is missing.

Create a StartupCheck.qml (non-visual QtObject):

import QtQuick
import qs.Common

QtObject {
    function check(done) {
        Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
            if (exitCode === 0) {
                done(null);
                return;
            }
            done({
                "title": I18n.tr("mytool is required"),
                "details": I18n.tr("Install 'mytool' and re-enable this plugin.")
            });
        });
    }
}

The done callback accepts:

  • null - allow activation
  • A string - block with a short error message
  • { title, details } - block with a title and expandable details body

A synchronous variant (no done parameter, return the result directly) is also supported.

Failed checks show a toast error and store the error in pluginService.pluginLoadErrors.

Add to your manifest:

{
    "startupCheck": "./StartupCheck.qml",
    "dependencies": ["mytool"]
}

Step 5: 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 6: 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
pluginService.getPluginPath(id) N/A Get the plugin's installation directory path
  • 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 7: 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 8: 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 9: 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 10: 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 11: 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

Runtime plugin discovery via IPC:

Plugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:

dms ipc plugin-scan scan          # Trigger a full rescan of all plugin directories
dms ipc plugin-scan rescan <id>   # Force rescan of a specific plugin
dms ipc plugin-scan reload <id>   # Force reload of a loaded plugin
dms ipc plugin-scan list          # List all known plugins (TSV: id, loaded, type, name)
dms ipc plugin-scan status <id>   # Get status of a specific plugin (TSV: loaded, type, error)

Plugin IDs are validated against ^[a-zA-Z0-9_\-:]{1,64}$.

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
  • Startup check failing: check pluginService.pluginLoadErrors or run dms ipc plugin-scan status <id>

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
  11. Using requires instead of dependencies - requires is deprecated; use dependencies
  12. Providing both component and components - Use one or the other, not both
  13. Missing trigger on composite with launcher surface - Still required when components has a launcher key

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: