| name | dms-plugin-dev |
| description | > Use when this capability is needed. |
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
- Validate
plugin.json against 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
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
- Missing
settings_write permission - 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; use Quickshell.execDetached(["dms", "cl", "copy", text])
- Wrong Theme property names -
Theme.fontSizeS does not exist, use Theme.fontSizeSmall
- Wrong import for Quickshell - Use
import Quickshell (not import QtQuick for execDetached)
- Forgetting
categories in launcher items - Items won't display without it
- Not handling null pluginService - Always use optional chaining or null checks
- 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:
- 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
Source: AvengeMedia/DankMaterialShell — distributed by TomeVault.