1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/docs/PLUGINS.md
Bruno Cesar Rocha 53983933dc feat: Plugin System (#276)
* feat: Plugin System

* fix: merge conflicts
2025-10-01 11:28:10 -04:00

12 KiB

Plugin System

The DMS shell includes an experimental plugin system that allows extending functionality through self-contained, dynamically-loaded QML components.

Overview

The plugin system enables developers to create custom widgets that can be displayed in the DankBar alongside built-in widgets. Plugins are discovered, loaded, and managed through the PluginService, providing a clean separation between core shell functionality and user extensions.

Architecture

Core Components

  1. PluginService (Services/PluginService.qml)

    • Singleton service managing plugin lifecycle
    • Discovers plugins from $CONFIGPATH/DankMaterialShell/plugins/
    • Handles loading, unloading, and state management
    • Provides data persistence for plugin settings
  2. PluginsTab (Modules/Settings/PluginsTab.qml)

    • UI for managing available plugins
    • Access plugin settings
  3. PluginsTab Settings (Modules/Settings/PluginsTab.qml)

    • Accordion-style plugin configuration interface
    • Dynamically loads plugin settings components inline
    • Provides consistent settings interface with proper focus handling
  4. DankBar Integration (Modules/DankBar/DankBar.qml)

    • Renders plugin widgets in the bar
    • Merges plugin components with built-in widgets
    • Supports left, center, and right sections

Plugin Structure

Each plugin must be a directory in $CONFIGPATH/DankMaterialShell/plugins/ containing:

$CONFIGPATH/DankMaterialShell/plugins/YourPlugin/
├── plugin.json          # Required: Plugin manifest
├── YourWidget.qml       # Required: Widget component
├── YourSettings.qml     # Optional: Settings UI
├── qmldir               # Optional: QML module definition
└── *.js                 # Optional: JavaScript utilities

Plugin Manifest (plugin.json)

The manifest file defines plugin metadata and configuration:

{
    "id": "yourPlugin",
    "name": "Your Plugin Name",
    "description": "Brief description of what your plugin does",
    "version": "1.0.0",
    "author": "Your Name",
    "icon": "material_icon_name",
    "component": "./YourWidget.qml",
    "settings": "./YourSettings.qml",
    "dependencies": {
        "libraryName": {
            "url": "https://cdn.example.com/library.js",
            "optional": true
        }
    },
    "settings_schema": {
        "settingKey": {
            "type": "string|number|boolean|array|object",
            "default": "defaultValue"
        }
    },
    "permissions": [
        "settings_read",
        "settings_write"
    ]
}

Required Fields:

  • id: Unique plugin identifier (camelCase, no spaces)
  • name: Human-readable plugin name
  • component: Relative path to widget QML file

Optional Fields:

  • description: Short description of plugin functionality
  • version: Semantic version string
  • author: Plugin creator name
  • icon: Material Design icon name
  • settings: Path to settings component
  • dependencies: External JS libraries
  • settings_schema: Configuration schema
  • permissions: Required capabilities

Widget Component

The main widget component is displayed in the DankBar. It receives several properties from the shell:

import QtQuick

Rectangle {
    id: root

    // Standard properties provided by DankBar
    property bool compactMode: false
    property string section: "center"       // "left", "center", or "right"
    property var popupTarget: null
    property var parentScreen: null
    property real barHeight: 48
    property real widgetHeight: 30

    // Widget dimensions
    width: content.implicitWidth + horizontalPadding * 2
    height: widgetHeight

    // PluginService is injected by PluginsTab when loading settings
    property var pluginService

    // Access plugin data
    Component.onCompleted: {
        if (pluginService) {
            var savedData = pluginService.loadPluginData("yourPlugin", "dataKey", defaultValue)
        }
    }

    // Save plugin data
    function saveData(key, value) {
        if (pluginService) {
            pluginService.savePluginData("yourPlugin", key, value)
        }
    }
}

Important Properties:

  • compactMode: Whether the bar is in compact display mode
  • section: Which bar section the widget is in
  • barHeight: Height of the entire bar
  • widgetHeight: Recommended widget height
  • parentScreen: Reference to the screen object

Settings Component

Optional settings UI loaded inline in the PluginsTab accordion interface:

import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets

Column {
    id: root

    // PluginService is injected by PluginsTab
    property var pluginService

    spacing: Theme.spacingM

    DankTextField {
        id: settingInput
        width: parent.width
        label: "Setting Label"
        text: pluginService ? pluginService.loadPluginData("yourPlugin", "settingKey", "default") : ""
        onTextChanged: {
            if (pluginService) {
                pluginService.savePluginData("yourPlugin", "settingKey", text)
            }
        }
    }

    DankToggle {
        checked: pluginService ? pluginService.loadPluginData("yourPlugin", "enabled", true) : false
        onToggled: {
            if (pluginService) {
                pluginService.savePluginData("yourPlugin", "enabled", checked)
            }
        }
    }
}

PluginService API

Properties

PluginService.pluginDirectory: string
// Path to plugins directory ($CONFIGPATH/DankMaterialShell/plugins)

PluginService.availablePlugins: object
// Map of all discovered plugins {pluginId: pluginInfo}

PluginService.loadedPlugins: object
// Map of currently loaded plugins {pluginId: pluginInfo}

PluginService.pluginWidgetComponents: object
// Map of loaded widget components {pluginId: Component}

Functions

// Plugin Management
PluginService.loadPlugin(pluginId: string): bool
PluginService.unloadPlugin(pluginId: string): bool
PluginService.reloadPlugin(pluginId: string): bool
PluginService.enablePlugin(pluginId: string): bool
PluginService.disablePlugin(pluginId: string): bool

// Plugin Discovery
PluginService.scanPlugins(): void
PluginService.getAvailablePlugins(): array
PluginService.getLoadedPlugins(): array
PluginService.isPluginLoaded(pluginId: string): bool
PluginService.getWidgetComponents(): object

// Data Persistence
PluginService.savePluginData(pluginId: string, key: string, value: any): bool
PluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any

Signals

PluginService.pluginLoaded(pluginId: string)
PluginService.pluginUnloaded(pluginId: string)
PluginService.pluginLoadFailed(pluginId: string, error: string)

Creating a Plugin

Step 1: Create Plugin Directory

mkdir -p $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
cd $CONFIGPATH/DankMaterialShell/plugins/MyPlugin

Step 2: Create Manifest

Create plugin.json:

{
    "id": "myPlugin",
    "name": "My Plugin",
    "description": "A sample plugin",
    "version": "1.0.0",
    "author": "Your Name",
    "icon": "extension",
    "component": "./MyWidget.qml",
    "settings": "./MySettings.qml",
    "permissions": ["settings_read", "settings_write"]
}

Step 3: Create Widget Component

Create MyWidget.qml:

import QtQuick
import qs.Services

Rectangle {
    id: root

    property bool compactMode: false
    property string section: "center"
    property real widgetHeight: 30
    property string displayText: "Hello World"

    width: textItem.implicitWidth + 16
    height: widgetHeight
    radius: 8
    color: "#20FFFFFF"

    Component.onCompleted: {
        displayText = PluginService.loadPluginData("myPlugin", "text", "Hello World")
    }

    Text {
        id: textItem
        anchors.centerIn: parent
        text: root.displayText
        color: "#FFFFFF"
        font.pixelSize: 13
    }

    MouseArea {
        anchors.fill: parent
        onClicked: console.log("Plugin clicked!")
    }
}

Step 4: Create Settings Component (Optional)

Create MySettings.qml:

import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets

Column {
    // PluginService is injected by PluginsTab
    property var pluginService

    spacing: Theme.spacingM

    StyledText {
        text: "Plugin Settings"
        font.pixelSize: Theme.fontSizeLarge
        font.weight: Font.Medium
    }

    DankTextField {
        width: parent.width
        label: "Display Text"
        text: pluginService ? pluginService.loadPluginData("myPlugin", "text", "Hello World") : ""
        onTextChanged: {
            if (pluginService) {
                pluginService.savePluginData("myPlugin", "text", text)
            }
        }
    }
}

Step 5: Enable Plugin

  1. Run the shell: qs -p $CONFIGPATH/quickshell/dms/shell.qml
  2. Open Settings (Ctrl+,)
  3. Navigate to Plugins tab
  4. Click "Scan for Plugins"
  5. Enable your plugin with the toggle switch
  6. Add the plugin to your DankBar configuration

Adding Plugin to DankBar

After enabling a plugin, add it to the bar:

  1. Open Settings → Appearance → DankBar Layout
  2. Add a new widget entry with your plugin ID
  3. Choose section (left, center, right)
  4. Save and reload

Or edit $CONFIGPATH/quickshell/dms/config.json:

{
    "dankBarLeftWidgets": [
        {"widgetId": "myPlugin", "enabled": true}
    ]
}

Best Practices

  1. Use Existing Widgets: Leverage qs.Widgets components (DankIcon, DankToggle, etc.) for consistency
  2. Follow Theme: Use Theme singleton for colors, spacing, and fonts
  3. Data Persistence: Use PluginService data APIs instead of manual file operations
  4. Error Handling: Gracefully handle missing dependencies and invalid data
  5. Performance: Keep widgets lightweight, avoid expensive operations
  6. Responsive Design: Adapt to compactMode and different screen sizes
  7. Clean Code: Follow QML code conventions from CLAUDE.md
  8. Documentation: Include README.md explaining plugin usage
  9. Versioning: Use semantic versioning for updates
  10. Dependencies: Document external library requirements

Debugging

Console Logging

View plugin logs:

qs -v -p $CONFIGPATH/quickshell/dms/shell.qml

Look for lines prefixed with:

  • PluginService: - Service operations
  • PluginsTab: - UI interactions
  • PluginsTab: - Settings loading and accordion interface

Common Issues

  1. Plugin Not Detected

    • Check plugin.json syntax (use jq or JSON validator)
    • Verify directory is in $CONFIGPATH/DankMaterialShell/plugins/
    • Click "Scan for Plugins" in Settings
  2. Widget Not Displaying

    • Ensure plugin is enabled in Settings
    • Add plugin ID to DankBar widget list
    • Check widget width/height properties
  3. Settings Not Loading

    • Verify settings path in plugin.json
    • Check settings component for errors
    • Ensure plugin is enabled and loaded
    • Review PluginsTab console output for injection issues
  4. Data Not Persisting

    • Confirm pluginService.savePluginData() calls (with injection)
    • Check $CONFIGPATH/DankMaterialShell/settings.json for pluginSettings data
    • Verify plugin has settings permissions
    • Ensure PluginService was properly injected into settings component

Security Considerations

Plugins run with full QML runtime access. Only install plugins from trusted sources.

Permissions System:

  • settings_read: Read plugin configuration
  • settings_write: Write plugin configuration
  • process: Execute system commands
  • network: Network access

Future versions may enforce permission restrictions.

API Stability

The plugin API is currently experimental. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.

Roadmap:

  • Plugin marketplace/repository
  • Sandboxed plugin execution
  • Enhanced permission system
  • Plugin update notifications
  • Inter-plugin communication

Resources

  • Example Plugin: https://github.com/rochacbruno/WorldClock
  • PluginService: Services/PluginService.qml
  • Settings UI: Modules/Settings/PluginsTab.qml
  • DankBar Integration: Modules/DankBar/DankBar.qml
  • Theme Reference: Common/Theme.qml
  • Widget Library: Widgets/

Contributing

Share your plugins with the community:

  1. Create a public repository with your plugin
  2. Include comprehensive README.md
  3. Add example screenshots
  4. Document dependencies and permissions

For plugin system improvements, submit issues or PRs to the main DMS repository.