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:
PluginComponent {
id: root
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"
}
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/widthvariants- Variant instances
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
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
- Reload shell:
Ctrl+Shift+Rordms restart - Check settings file:
~/.config/DankMaterialShell/settings.json
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
Clone them and experiment.