- name
- dms-plugin-dev
- description
- Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on Quickshell. Supports widget (bar, dock + Control Center), daemon (background service), launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab), dashCard (dash overview card), and composite (multi-surface) plugins. 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.
- compatibility
- Designed for Claude Code (or similar products)
- metadata
- {"author":"DankMaterialShell","version":"1.2","domain":"qml-desktop-development","framework":"DankMaterialShell","languages":"qml, javascript"}
- allowed-tools
- Bash Read Write Edit
# DankMaterialShell Plugin Development
## Overview
DMS plugins extend the desktop shell with bar and dock widgets, background services, launcher
integrations, desktop widgets, and dash tabs and cards. 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/
**Plugin types:**
| Type | Purpose | Base Component | Bar/dock pills | CC integration |
|-------------|--------------------------------|----------------------------|----------------|----------------|
| `widget` | Bar or dock 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 |
| `dash` | Tab in the dash popout | `DashTabComponent` | No | No |
| `dashCard` | Card in the dash overview grid | `DashCardComponent` | 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 or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same
component), optionally opens a popout or an attached dock panel, 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.
- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.
- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.
- **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](references/plugin-manifest-reference.md) for the full schema.
**Minimal manifest:**
```json
{
"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:**
```json
{
"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):**
```json
{
"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"]
}
```
**Standalone dash tab or card:** same single file form, `"type": "dash"` or `"type": "dashCard"`
plus `"component"`. A tab and a card together (or with a bar widget) use `components` with the
keys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and
options. See [dash-plugin-guide.md](references/dash-plugin-guide.md).
**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
```qml
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](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.
### Launcher
```qml
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](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.
### Desktop
```qml
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](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.
### Daemon
```qml
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](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
### Dash Tab
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
import qs.Modules.DankDash
DashTabComponent {
id: root
property date now: new Date()
implicitHeight: DashMetrics.tabMinHeight
Timer {
interval: 1000
repeat: true
running: root.live
onTriggered: root.now = new Date()
}
StyledText {
anchors.centerIn: parent
text: root.now.toLocaleTimeString(I18n.locale())
color: Theme.surfaceText
}
}
```
### Dash Card
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
DashCardComponent {
id: root
tone: options.tone ?? ""
StyledText {
anchors.centerIn: parent
text: I18n.trFor("myPlugin", "Hello")
color: root.contentColor
}
}
```
Gate timers and animations on `live`; the dash content stays alive after the popout closes.
Cards must lay out for every size in their manifest range. Use the card colors (`accentColor`,
`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).
### 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
```
`pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.
## 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):
```qml
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:
```json
{
"startupCheck": "./StartupCheck.qml",
"dependencies": ["mytool"]
}
```
## Step 5: Add Settings (Optional)
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
```qml
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" },
Ver en GitHub