Development
██████╗ ███████╗██╗ ██╗ ██╔══██╗██╔════╝██║ ██║ ██║ ██║█████╗ ██║ ██║ ██║ ██║██╔══╝ ╚██╗ ██╔╝ ██████╔╝███████╗ ╚████╔╝ ╚═════╝ ╚══════╝ ╚═══╝
Build and ship plugins for DankMaterialShell. This guide covers the actual patterns and components you'll use, with real working examples from the plugin library.
Quick Start
1. Create Plugin Directory
mkdir -p ~/.config/DankMaterialShell/plugins/MyPlugin
cd ~/.config/DankMaterialShell/plugins/MyPlugin
2. Create Manifest
Save this as plugin.json:
{
"id": "myPlugin",
"name": "My Plugin",
"description": "What this plugin does",
"version": "1.0.0",
"author": "Your Name",
"icon": "widgets",
"type": "widget",
"component": "./MyWidget.qml",
"settings": "./MySettings.qml",
"permissions": ["settings_read", "settings_write"]
}
3. Create Widget Component
Save this as MyWidget.qml:
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property string displayText: pluginData.displayText || "Hello"
horizontalBarPill: Component {
Row {
spacing: Theme.spacingS
DankIcon {
name: "widgets"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.displayText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
DankIcon {
name: "widgets"
size: Theme.iconSize
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: root.displayText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
4. Create Settings Component
Save this as MySettings.qml:
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
PluginSettings {
id: root
pluginId: "myPlugin"
StyledText {
width: parent.width
text: "My Plugin Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Configure your plugin here"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StringSetting {
settingKey: "displayText"
label: "Display Text"
description: "Text shown in the bar"
placeholder: "Enter text"
defaultValue: "Hello"
}
}
5. Load It
- Open DMS Settings → Plugins
- Click "Scan for Plugins"
- Toggle your plugin on
- Add to DankBar widget list
- Restart shell:
dms restart
You now have a working plugin.
Widget Plugins
Widget plugins show up in DankBar or the Control Center. They use PluginComponent as the base.
DankBar Widget
Here's a real color display widget:
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property color customColor: pluginData.customColor || Theme.primary
horizontalBarPill: Component {
Row {
spacing: Theme.spacingS
Rectangle {
width: 20
height: 20
radius: 4
color: root.customColor
border.color: Theme.outlineStrong
border.width: 1
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.customColor.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
Rectangle {
width: 20
height: 20
radius: 4
color: root.customColor
border.color: Theme.outlineStrong
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: root.customColor.toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
The widget pulls customColor from pluginData, which automatically syncs with your settings. No manual loading needed.
Widget with Popout
Add a popout menu that opens when you click the widget.
To add a layer namespace to your plugin, just add layerNamespacePlugin: "<namespace for your plugin>" like below.
Make sure to only type what you want the namespace to be and to not add a prefix (like dms: or dms-plugin: for example) since the shell will add dms-plugin: as a prefix automatically.
For example, the namespace of the plugin below will be dms-plugin:emoji-launcher.
While you don't have to add a layer namespace to you widget (it will fallback to dms-plugin:plugin), it's prefered to do so.
As of right now, layer namespace only work with popout widget plugins.
PluginComponent {
id: root
layerNamespacePlugin: "emoji-launcher"
property var displayedEmojis: ["😊", "😢", "❤️"]
horizontalBarPill: Component {
Row {
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
verticalBarPill: Component {
Column {
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
popoutContent: Component {
PopoutComponent {
id: popoutColumn
headerText: "Emoji Picker"
detailsText: "Click an emoji to copy it"
showCloseButton: true
property var allEmojis: [
"😀", "😃", "😄", "😁", "😆", "🤣",
"❤️", "🧡", "💛", "💚", "💙", "💜"
]
Item {
width: parent.width
implicitHeight: root.popoutHeight - popoutColumn.headerHeight -
popoutColumn.detailsHeight - Theme.spacingXL
DankGridView {
anchors.fill: parent
cellWidth: 50
cellHeight: 50
model: popoutColumn.allEmojis
delegate: StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouse.containsMouse ?
Theme.surfaceContainerHighest :
Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["sh", "-c",
"echo -n '" + modelData + "' | wl-copy"])
ToastService.showInfo("Copied " + modelData)
popoutColumn.closePopout()
}
}
}
}
}
}
}
popoutWidth: 400
popoutHeight: 500
}
The PopoutComponent helper gives you consistent header/footer and a closePopout() function.
Control Center Widget
Add a toggle to the Control Center:
PluginComponent {
id: root
property bool isEnabled: pluginData.isEnabled || false
property int clickCount: pluginData.clickCount || 0
ccWidgetIcon: isEnabled ? "toggle_on" : "toggle_off"
ccWidgetPrimaryText: "Example Toggle"
ccWidgetSecondaryText: isEnabled ? `Active • ${clickCount} clicks` : "Inactive"
ccWidgetIsActive: isEnabled
onCcWidgetToggled: {
isEnabled = !isEnabled
clickCount += 1
if (pluginService) {
pluginService.savePluginData(pluginId, "isEnabled", isEnabled)
pluginService.savePluginData(pluginId, "clickCount", clickCount)
}
ToastService.showInfo(isEnabled ? "Enabled" : "Disabled")
}
horizontalBarPill: Component {
Row {
DankIcon {
name: root.isEnabled ? "toggle_on" : "toggle_off"
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
text: `${root.clickCount} clicks`
color: Theme.surfaceText
}
}
}
verticalBarPill: Component {
Column {
DankIcon {
name: root.isEnabled ? "toggle_on" : "toggle_off"
color: root.isEnabled ? Theme.primary : Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: `${root.clickCount}`
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Set ccWidgetIcon, ccWidgetPrimaryText, ccWidgetSecondaryText, and ccWidgetIsActive. Handle onCcWidgetToggled for toggle clicks.
Daemon Plugins
Daemon plugins run in the background without UI. They monitor events, automate tasks, or provide services.
Here's a daemon that runs a script whenever the wallpaper changes:
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Modules.Plugins
PluginComponent {
id: root
property string scriptPath: pluginData.scriptPath || ""
Connections {
target: SessionData
function onWallpaperPathChanged() {
if (scriptPath && scriptPath !== "") {
var process = scriptProcessComponent.createObject(root, {
wallpaperPath: SessionData.wallpaperPath
})
process.running = true
}
}
}
Component {
id: scriptProcessComponent
Process {
property string wallpaperPath: ""
command: [scriptPath, wallpaperPath]
stdout: SplitParser {
onRead: line => console.log("Script:", line)
}
stderr: SplitParser {
onRead: line => {
if (line.trim()) {
ToastService.showError("Script error", line)
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
ToastService.showError("Script failed", "Exit code: " + exitCode)
}
destroy()
}
}
}
Component.onCompleted: {
console.info("Wallpaper watcher daemon started")
}
}
Daemon manifest uses "type": "daemon":
{
"id": "wallpaperWatcher",
"type": "daemon",
"component": "./WallpaperWatcher.qml"
}
Desktop Plugins
Desktop plugins are not yet released. This documents the upcoming API.
Desktop plugins render directly on the desktop background layer using Wayland's wlr-layer-shell protocol. Users can freely position and resize them.
Basic Desktop Widget
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
// Size constraints
minWidth: 150
minHeight: 100
// Access saved settings via pluginData
property string displayText: pluginData.displayText ?? "Hello"
property real bgOpacity: (pluginData.backgroundOpacity ?? 80) / 100
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, root.bgOpacity)
Text {
anchors.centerIn: parent
text: root.displayText
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
}
}
}
Desktop manifest uses "type": "desktop":
{
"id": "myDesktopWidget",
"name": "My Desktop Widget",
"description": "A custom desktop widget",
"version": "1.0.0",
"author": "Your Name",
"type": "desktop",
"capabilities": ["desktop-widget"],
"component": "./MyWidget.qml",
"icon": "widgets",
"settings": "./MySettings.qml",
"requires_dms": ">=1.2.0",
"permissions": ["settings_read", "settings_write"]
}
DesktopPluginComponent Properties
Auto-injected (don't declare these):
| Property | Type | Description |
|---|---|---|
pluginService | var | Reference to PluginService for data persistence |
pluginId | string | Your plugin's unique identifier |
widgetWidth | real | Current widget width |
widgetHeight | real | Current widget height |
pluginData | var | Object containing all saved plugin settings |
Optional (define on your component):
| Property | Type | Default | Description |
|---|---|---|---|
minWidth | real | 100 | Minimum allowed width |
minHeight | real | 100 | Minimum allowed height |
defaultWidth | real | 200 | Initial width for new widgets |
defaultHeight | real | 200 | Initial height for new widgets |
forceSquare | bool | false | Constrain to square aspect ratio |
Helper functions:
// Read a specific setting with default value
function getData(key, defaultValue)
// Write a setting (triggers pluginDataChanged signal)
function setData(key, value)
User Interaction
Desktop widgets support:
| Action | Trigger | Description |
|---|---|---|
| Move | Right-click + drag anywhere | Repositions the widget |
| Resize | Right-click + drag bottom-right corner | Resizes within min/max bounds |
Responsive Layout
Adapt to widget dimensions:
GridLayout {
columns: {
if (root.widgetWidth < 200) return 1
if (root.widgetWidth < 400) return 2
return 3
}
}
Dynamic Size Constraints
DesktopPluginComponent {
id: root
property bool showAllTiles: pluginData.showAllTiles ?? true
minWidth: showAllTiles ? 200 : 100
minHeight: {
if (tileCount === 0) return 60
if (tileCount === 1) return 80
return 120 + (tileCount - 2) * 40
}
}
Time-Based Updates
Use SystemClock for efficient time updates:
import Quickshell
DesktopPluginComponent {
id: root
SystemClock {
id: clock
precision: SystemClock.Seconds // or Minutes
onDateChanged: updateDisplay()
}
function updateDisplay() {
// Update widget content
}
}
Canvas/Graph Performance
For graphing widgets:
Canvas {
id: graph
renderStrategy: Canvas.Cooperative
property var history: []
onHistoryChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.reset()
// Draw graph...
}
}
Complete Example: Desktop Clock
A clock widget with analog and digital modes, demonstrating dynamic component loading and responsive sizing.
// DesktopClock.qml
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
DesktopPluginComponent {
id: root
minWidth: 120
minHeight: 120
property bool showSeconds: pluginData.showSeconds ?? true
property bool showDate: pluginData.showDate ?? true
property string clockStyle: pluginData.clockStyle ?? "analog"
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 50) / 100
SystemClock {
id: systemClock
precision: root.showSeconds ? SystemClock.Seconds : SystemClock.Minutes
}
Rectangle {
id: background
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.backgroundOpacity
}
Loader {
anchors.fill: parent
anchors.margins: Theme.spacingM
sourceComponent: root.clockStyle === "digital" ? digitalClock : analogClock
}
Component {
id: analogClock
Item {
id: analogClockRoot
property real clockSize: Math.min(width, height) - (root.showDate ? 30 : 0)
Item {
id: clockFace
width: analogClockRoot.clockSize
height: analogClockRoot.clockSize
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
// Hour markers
Repeater {
model: 12
Rectangle {
required property int index
property real markAngle: index * 30
property real markRadius: clockFace.width / 2 - 8
x: clockFace.width / 2 + markRadius * Math.sin(markAngle * Math.PI / 180) - width / 2
y: clockFace.height / 2 - markRadius * Math.cos(markAngle * Math.PI / 180) - height / 2
width: index % 3 === 0 ? 8 : 4
height: width
radius: width / 2
color: index % 3 === 0 ? Theme.primary : Theme.outlineVariant
}
}
// Hour hand
Rectangle {
id: hourHand
property int hours: systemClock.date?.getHours() % 12 ?? 0
property int minutes: systemClock.date?.getMinutes() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 6
height: clockFace.height * 0.25
radius: 3
color: Theme.primary
antialiasing: true
transformOrigin: Item.Bottom
rotation: (hours + minutes / 60) * 30
}
// Minute hand
Rectangle {
id: minuteHand
property int minutes: systemClock.date?.getMinutes() ?? 0
property int seconds: systemClock.date?.getSeconds() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 4
height: clockFace.height * 0.35
radius: 2
color: Theme.onSurface
antialiasing: true
transformOrigin: Item.Bottom
rotation: (minutes + seconds / 60) * 6
}
// Second hand
Rectangle {
id: secondHand
visible: root.showSeconds
property int seconds: systemClock.date?.getSeconds() ?? 0
x: clockFace.width / 2 - width / 2
y: clockFace.height / 2 - height + 4
width: 2
height: clockFace.height * 0.4
radius: 1
color: Theme.error
antialiasing: true
transformOrigin: Item.Bottom
rotation: seconds * 6
}
// Center dot
Rectangle {
anchors.centerIn: parent
width: 10
height: 10
radius: 5
color: Theme.primary
}
}
Text {
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
}
}
Component {
id: digitalClock
Item {
id: digitalRoot
property real timeFontSize: Math.min(width * 0.16, height * (root.showDate ? 0.4 : 0.5))
property real dateFontSize: Math.max(Theme.fontSizeSmall, timeFontSize * 0.35)
Text {
id: timeText
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: root.showDate ? -digitalRoot.dateFontSize * 0.8 : 0
text: systemClock.date?.toLocaleTimeString(Qt.locale(), root.showSeconds ? "hh:mm:ss" : "hh:mm") ?? ""
font.pixelSize: digitalRoot.timeFontSize
font.weight: Font.Bold
font.family: "monospace"
color: Theme.primary
}
Text {
id: dateText
visible: root.showDate
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: timeText.bottom
anchors.topMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
font.pixelSize: digitalRoot.dateFontSize
color: Theme.surfaceText
}
}
}
}
// DesktopClockSettings.qml
import QtQuick
import qs.Common
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleDesktopClock"
SelectionSetting {
settingKey: "clockStyle"
label: I18n.tr("Clock Style")
options: [
{ label: I18n.tr("Analog"), value: "analog" },
{ label: I18n.tr("Digital"), value: "digital" }
]
defaultValue: "analog"
}
ToggleSetting {
settingKey: "showSeconds"
label: I18n.tr("Show Seconds")
defaultValue: true
}
ToggleSetting {
settingKey: "showDate"
label: I18n.tr("Show Date")
defaultValue: true
}
SliderSetting {
settingKey: "backgroundOpacity"
label: I18n.tr("Background Opacity")
defaultValue: 50
minimum: 0
maximum: 100
unit: "%"
}
}
Desktop Plugin Best Practices
- Use Theme singleton - Never hardcode colors, spacing, or font sizes
- Set appropriate minWidth/minHeight - Prevent unusable widget sizes
- Handle null data - Use
??operator for allpluginDataaccess - Optimize Canvas redraws - Use
renderStrategy: Canvas.Cooperative - Responsive layouts - Adapt to widget dimensions dynamically
- Transparency - Provide opacity controls so wallpaper shows through
Launcher Plugins
Launcher plugins add items to the Spotlight search. They're a bit different - they use a plain Item instead of PluginComponent.
import QtQuick
import Quickshell
import qs.Services
Item {
id: root
property var pluginService: null
property string trigger: "#"
signal itemsChanged()
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("emojiLauncher", "trigger", "#")
}
}
function getItems(query) {
const emojis = [
{
name: "Smile",
icon: "unicode:😊",
comment: "Smiling face",
action: "copy:😊",
categories: ["Emoji"]
},
{
name: "Heart",
icon: "unicode:❤️",
comment: "Red heart",
action: "copy:❤️",
categories: ["Emoji"]
}
]
if (!query || query.length === 0) {
return emojis
}
const lowerQuery = query.toLowerCase()
return emojis.filter(item =>
item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
)
}
function executeItem(item) {
if (!item || !item.action) return
const [actionType, ...rest] = item.action.split(":")
const actionData = rest.join(":")
switch (actionType) {
case "copy":
Quickshell.execDetached(["sh", "-c",
"echo -n '" + actionData + "' | wl-copy"])
ToastService.showInfo("Copied to clipboard")
break
case "toast":
ToastService.showInfo(item.name, actionData)
break
}
}
onTriggerChanged: {
if (pluginService) {
pluginService.savePluginData("emojiLauncher", "trigger", trigger)
}
}
}
Launcher manifest needs "type": "launcher" and a "trigger":
{
"id": "emojiLauncher",
"type": "launcher",
"trigger": "#",
"component": "./EmojiLauncher.qml"
}
Icon formats:
- Material Design:
"material:icon_name"or just"icon_name" - Unicode/Emoji:
"unicode:🚀"
Action formats:
- Copy to clipboard:
"copy:text" - Show toast:
"toast:message" - Run script:
"script:command args"
Plugin Settings
Use PluginSettings as the base and drop in setting components. They handle all the loading and saving automatically.
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
PluginSettings {
id: root
pluginId: "colorDemo"
StyledText {
width: parent.width
text: "Color Demo Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Pick colors for your widget"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
ColorSetting {
settingKey: "customColor"
label: "Widget Color"
description: "Color shown in the bar"
defaultValue: Theme.primary
}
SliderSetting {
settingKey: "updateInterval"
label: "Update Speed"
description: "How often to refresh"
defaultValue: 60
minimum: 10
maximum: 300
unit: "sec"
}
ToggleSetting {
settingKey: "showInBar"
label: "Show in Bar"
description: "Display widget in DankBar"
defaultValue: true
}
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your service API key"
placeholder: "Enter key"
defaultValue: ""
}
SelectionSetting {
settingKey: "theme"
label: "Theme"
description: "Widget appearance"
options: [
{label: "Light", value: "light"},
{label: "Dark", value: "dark"},
{label: "Auto", value: "auto"}
]
defaultValue: "dark"
}
}
The setting components available:
ColorSetting- Opens color picker modalSliderSetting- Numeric sliderToggleSetting- Boolean switchStringSetting- Text inputSelectionSetting- Dropdown menu
Access settings in your widget via pluginData:
property color customColor: pluginData.customColor || Theme.primary
property int updateInterval: pluginData.updateInterval || 60
property bool showInBar: pluginData.showInBar !== undefined ? pluginData.showInBar : true
property string apiKey: pluginData.apiKey || ""
property string theme: pluginData.theme || "dark"
Common Patterns
Auto-injected Properties
PluginComponent automatically provides these properties - don't declare them yourself:
pluginData- Reactive settings objectpluginService- Service for manual data operationspluginId- Your plugin's IDaxis- Bar axis infosection- "left", "center", or "right"parentScreen- Screen referencewidgetThickness- Widget height/widthbarThickness- Bar height/widthiconSize- Recommended icon size for the current bar contextvariants- Variant instances
Use iconSize for consistent icon sizing that adapts to bar orientation and user preferences:
DankIcon {
name: "settings"
size: root.iconSize
}
Saving Data Manually
Most of the time pluginData handles everything, but if you need to save manually:
if (pluginService) {
pluginService.savePluginData(pluginId, "key", value)
}
Showing Notifications
ToastService.showInfo("Title", "Message")
ToastService.showError("Title", "Error message")
Copying to Clipboard
Quickshell.execDetached(["sh", "-c", "echo -n 'text' | wl-copy"])
Timers
PluginComponent {
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
// Do something every second
}
}
}
Plugin Manifest Reference
Required Fields
{
"id": "pluginId",
"name": "Plugin Name",
"description": "What it does",
"version": "1.0.0",
"author": "Your Name",
"type": "widget",
"component": "./Widget.qml"
}
Optional Fields
{
"icon": "material_icon",
"settings": "./Settings.qml",
"trigger": "#",
"permissions": ["settings_read", "settings_write"],
"requires_dms": ">=0.1.18",
"requires": ["tool1", "tool2"]
}
Plugin Types
"widget"- DankBar or Control Center widget"daemon"- Background service"launcher"- Spotlight extension"desktop"- Desktop layer widget (DMS 1.2+)
Permissions
"settings_read"- Read plugin settings"settings_write"- Write plugin settings"process"- Execute system commands"network"- Network access
Testing
- Enable plugin: Settings → Plugins → Scan → Toggle on
- Add to bar: Settings → DankBar → Add widget
- Check console: Look for errors in shell output
- Hot-reload your plugin:
dms ipc call plugins reload myPlugin - Check settings file:
~/.config/DankMaterialShell/settings.json
Hot Reloading
During development you can reload plugins without restarting the shell:
# Reload your plugin after making changes
dms ipc call plugins reload myPlugin
# Check if it's running
dms ipc call plugins status myPlugin
# List all plugins and their state
dms ipc call plugins list
This is way faster than dms restart when you're iterating on your code.
Publishing
- Create GitHub repo
- Include
plugin.json, README, screenshots - Tag releases:
git tag v1.0.0 && git push --tags - Submit to registry: dms-plugin-registry
Examples
Check the PLUGINS/ directory in the DMS repo for real examples:
- ColorDemoPlugin - Color picker integration
- ExampleEmojiPlugin - Popout with grid view
- ControlCenterExample - Control Center toggle
- LauncherExample - Spotlight extension
- WallpaperWatcherDaemon - Background event watcher
Built-in desktop widgets (DMS 1.2+):
- DesktopClockWidget - Digital/analog clock with date display and style options
- SystemMonitorWidget - CPU, memory, network, disk, GPU tiles with real-time graphs
Clone them and experiment.