Skip to main content

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

  1. Open DMS Settings → Plugins
  2. Click "Scan for Plugins"
  3. Toggle your plugin on
  4. Add to DankBar widget list
  5. 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.

warning

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

Upcoming in DMS 1.2

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):

PropertyTypeDescription
pluginServicevarReference to PluginService for data persistence
pluginIdstringYour plugin's unique identifier
widgetWidthrealCurrent widget width
widgetHeightrealCurrent widget height
pluginDatavarObject containing all saved plugin settings

Optional (define on your component):

PropertyTypeDefaultDescription
minWidthreal100Minimum allowed width
minHeightreal100Minimum allowed height
defaultWidthreal200Initial width for new widgets
defaultHeightreal200Initial height for new widgets
forceSquareboolfalseConstrain 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:

ActionTriggerDescription
MoveRight-click + drag anywhereRepositions the widget
ResizeRight-click + drag bottom-right cornerResizes 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

  1. Use Theme singleton - Never hardcode colors, spacing, or font sizes
  2. Set appropriate minWidth/minHeight - Prevent unusable widget sizes
  3. Handle null data - Use ?? operator for all pluginData access
  4. Optimize Canvas redraws - Use renderStrategy: Canvas.Cooperative
  5. Responsive layouts - Adapt to widget dimensions dynamically
  6. 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 modal
  • SliderSetting - Numeric slider
  • ToggleSetting - Boolean switch
  • StringSetting - Text input
  • SelectionSetting - 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 object
  • pluginService - Service for manual data operations
  • pluginId - Your plugin's ID
  • axis - Bar axis info
  • section - "left", "center", or "right"
  • parentScreen - Screen reference
  • widgetThickness - Widget height/width
  • barThickness - Bar height/width
  • iconSize - Recommended icon size for the current bar context
  • variants - 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

  1. Enable plugin: Settings → Plugins → Scan → Toggle on
  2. Add to bar: Settings → DankBar → Add widget
  3. Check console: Look for errors in shell output
  4. Hot-reload your plugin: dms ipc call plugins reload myPlugin
  5. 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

  1. Create GitHub repo
  2. Include plugin.json, README, screenshots
  3. Tag releases: git tag v1.0.0 && git push --tags
  4. 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.