- 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
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) |
|
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:
idmust be camelCase, matching pattern^[a-zA-Z][a-zA-Z0-9]*$versionmust be semver (e.g.,1.0.0)- Provide either
component(single-surface) orcomponents(multi-surface), not both component/ component paths must start with./and end with.qmltype: "launcher"(or acomponentsobject with alauncherkey) requires atriggerfieldsettings_writepermission is required if the plugin has a settings componentdependenciesreplaces the deprecatedrequiresfield
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 |
pluginDatais 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
- Validate
plugin.jsonagainst the schema at assets/plugin-schema.json - Run the shell with verbose output:
qs -v -p $CONFIGPATH/quickshell/dms/shell.qml - Open Settings > Plugins > Scan for Plugins
- 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_writepermission is declared - Data not persisting: check pluginService injection and permissions
- Startup check failing: check
pluginService.pluginLoadErrorsor rundms ipc plugin-scan status <id>
Common Mistakes
- Missing
settings_writepermission - Settings UI shows error without it - Missing
property var popoutService: null- Must declare for injection to work - Missing vertical bar pill - Widget disappears when bar is on left/right edge
- Hardcoded colors - Use
Theme.*properties, not hex values - Using
globalThis.clipboard- Does not exist; useQuickshell.execDetached(["dms", "cl", "copy", text]) - Wrong Theme property names -
Theme.fontSizeSdoes not exist, useTheme.fontSizeSmall - Wrong import for Quickshell - Use
import Quickshell(notimport QtQuickfor execDetached) - Forgetting
categoriesin launcher items - Items won't display without it - Not handling null pluginService - Always use optional chaining or null checks
- Using
PluginComponentfor launchers - Launchers use plainItem, notPluginComponent - Using
requiresinstead ofdependencies-requiresis deprecated; usedependencies - Providing both
componentandcomponents- Use one or the other, not both - Missing
triggeron composite with launcher surface - Still required whencomponentshas alauncherkey
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:
- plugin-manifest-reference.md - Complete plugin.json field reference and JSON schema
- widget-plugin-guide.md - PluginComponent, bar pills, popouts, click actions, CC integration
- launcher-plugin-guide.md - getItems/executeItem, triggers, icon types, context menus, tile view
- desktop-plugin-guide.md - DesktopPluginComponent, sizing, edit mode, position persistence
- daemon-plugin-guide.md - Event-driven background services, process execution
- settings-components-reference.md - All 7 setting components with complete property lists
- theme-reference.md - Theme colors, spacing, fonts, radii, common patterns
- data-persistence-guide.md - pluginData, state API, global variables
- popout-service-reference.md - PopoutService API for controlling shell popouts and modals
- advanced-patterns.md - Variants, JS utilities, qmldir, IPC, multi-file plugins