Plugin System
Create widgets for DankBar and Control Center using dynamically-loaded QML components.
Overview
Plugins let you add custom widgets to DankBar and Control Center. They're discovered from ~/.config/DankMaterialShell/plugins/ and managed via PluginService.
Architecture
Core Components
-
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
-
PluginsTab (
Modules/Settings/PluginsTab.qml)- UI for managing available plugins
- Access plugin settings
-
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
-
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
- Supports any dankbar position (top/left/right/bottom)
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
└── *.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",
"permissions": [
"settings_read",
"settings_write"
]
}
Required Fields:
id: Unique plugin identifier (camelCase, no spaces)name: Human-readable plugin namecomponent: Relative path to widget QML file
Optional Fields:
description: Short description of plugin functionality (displayed in UI)version: Semantic version string (displayed in UI)author: Plugin creator name (displayed in UI)icon: Material Design icon name (displayed in UI)settings: Path to settings component (enables settings UI)permissions: Required capabilities (enforced by PluginSettings component)
Permissions:
The plugin system enforces permissions when settings are accessed:
settings_read: Required to read plugin settings (currently not enforced)settings_write: Required to use PluginSettings component and save settings
If your plugin includes a settings component but doesn't declare settings_write permission, users will see an error message instead of the settings UI.
Widget Component
The main widget component uses the PluginComponent wrapper which provides automatic property injection and bar integration:
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
// Define horizontal bar pill, for top and bottom DankBar positions (optional)
horizontalBarPill: Component {
StyledRect {
width: content.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: content
anchors.centerIn: parent
text: "Hello World"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
// Define vertical bar pill, for left and right DankBar positions (optional)
verticalBarPill: Component {
// Same as horizontal but optimized for vertical layout
}
// Define popout content, opens when clicking the bar pill (optional)
popoutContent: Component {
PopoutComponent {
headerText: "My Plugin"
detailsText: "Optional description text goes here"
showCloseButton: true
// Your popout content goes here
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Popout Content"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
}
}
}
}
// Popout dimensions (required if popoutContent is set)
popoutWidth: 400
popoutHeight: 300
}
PluginComponent Properties (automatically injected):
axis: Bar axis information (horizontal/vertical)section: Bar section ("left", "center", "right")parentScreen: Screen reference for multi-monitor supportwidgetThickness: Recommended widget size perpendicular to barbarThickness: Bar thickness parallel to edge
Component Options:
horizontalBarPill: Component shown in horizontal barsverticalBarPill: Component shown in vertical barspopoutContent: Optional popout window contentpopoutWidth: Popout window widthpopoutHeight: Popout window heightpillClickAction: Custom click handler function (overrides popout)
Control Center Integration
Add your plugin to Control Center by defining CC properties:
PluginComponent {
ccWidgetIcon: "toggle_on"
ccWidgetPrimaryText: "My Feature"
ccWidgetSecondaryText: isEnabled ? "Active" : "Inactive"
ccWidgetIsActive: isEnabled
onCcWidgetToggled: {
isEnabled = !isEnabled
if (pluginService) {
pluginService.savePluginData("myPlugin", "isEnabled", isEnabled)
}
}
ccDetailContent: Component {
Rectangle {
implicitHeight: 200
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
// Your detail UI here
}
}
horizontalBarPill: Component { /* ... */ }
}
CC Properties:
ccWidgetIcon: Material icon nameccWidgetPrimaryText: Main labelccWidgetSecondaryText: Subtitle/statusccWidgetIsActive: Active state stylingccDetailContent: Optional dropdown panel (use for CompoundPill)
Signals:
ccWidgetToggled(): Fired when icon clickedccWidgetExpanded(): Fired when expand area clicked (CompoundPill only)
Widget Sizing:
- 25% width → SmallToggleButton (icon only)
- 50% width → ToggleButton (no detail) or CompoundPill (with detail)
- Users can resize in edit mode
Custom Click Actions:
Override default popout with pillClickAction:
pillClickAction: () => {
Process.exec("bash", ["-c", "notify-send 'Clicked!'"])
}
// Or with position params: (x, y, width, section, screen)
pillClickAction: (x, y, width, section, screen) => {
popoutService?.toggleControlCenter(x, y, width, section, screen)
}
The PluginComponent automatically handles:
- Bar orientation detection
- Click handlers for popouts
- Proper positioning and anchoring
- Theme integration
PopoutComponent
PopoutComponent provides a consistent header/content layout for plugin popouts:
import qs.Modules.Plugins
PopoutComponent {
headerText: "Header Title" // Main header text (bold, large)
detailsText: "Description text" // Optional description (smaller, gray)
showCloseButton: true // Show X button in top-right
// Access to exposed properties for dynamic sizing
readonly property int headerHeight // Height of header area
readonly property int detailsHeight // Height of description area
// Your content here - use parent.width for full width
// Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing
DankGridView {
width: parent.width
height: parent.height
// ...
}
}
PopoutComponent Properties:
headerText: Main header text (optional, hidden if empty)detailsText: Description text below header (optional, hidden if empty)showCloseButton: Show close button in header (default: false)closePopout: Function to close popout (auto-injected by PluginPopout)headerHeight: Readonly height of header (0 if not visible)detailsHeight: Readonly height of description (0 if not visible)
The component automatically handles spacing and layout. Content children are rendered below the description with proper padding.
Settings Component
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "yourPlugin"
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your API key for accessing the service"
placeholder: "Enter API key..."
}
ToggleSetting {
settingKey: "notifications"
label: "Enable Notifications"
description: "Show desktop notifications for updates"
defaultValue: true
}
SelectionSetting {
settingKey: "updateInterval"
label: "Update Interval"
description: "How often to refresh data"
options: [
{label: "1 minute", value: "60"},
{label: "5 minutes", value: "300"},
{label: "15 minutes", value: "900"}
]
defaultValue: "300"
}
ListSetting {
id: itemList
settingKey: "items"
label: "Saved Items"
description: "List of configured items"
delegate: Component {
StyledRect {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: modelData.name
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
width: 60
height: 28
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: "Remove"
color: Theme.errorText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: removeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: itemList.removeItem(index)
}
}
}
}
}
}
Available Setting Components:
All settings automatically save on change and load on component creation.
How Default Values Work:
Each setting component has a defaultValue property that is used when no saved value exists. Define sensible defaults in your settings UI:
StringSetting {
settingKey: "apiKey"
defaultValue: "" // Empty string if no key saved
}
ToggleSetting {
settingKey: "enabled"
defaultValue: true // Enabled by default
}
ListSettingWithInput {
settingKey: "locations"
defaultValue: [] // Empty array if no locations saved
}
-
PluginSettings - Root wrapper for all plugin settings
pluginId: Your plugin ID (required)- Auto-handles storage and provides saveValue/loadValue to children
- Place all other setting components inside this wrapper
-
StringSetting - Text input field
settingKey: Storage key (required)label: Display label (required)description: Help text (optional)placeholder: Input placeholder (optional)defaultValue: Default value (optional, default:"")- Layout: Vertical stack (label, description, input field)
-
ToggleSetting - Boolean toggle switch
settingKey: Storage key (required)label: Display label (required)description: Help text (optional)defaultValue: Default boolean (optional, default:false)- Layout: Horizontal (label/description left, toggle right)
-
SelectionSetting - Dropdown menu
settingKey: Storage key (required)label: Display label (required)description: Help text (optional)options: Array of{label, value}objects or simple strings (required)defaultValue: Default value (optional, default:"")- Layout: Horizontal (label/description left, dropdown right)
- Stores the
valuefield, displays thelabelfield
-
ListSetting - Manage list of items (manual add/remove)
settingKey: Storage key (required)label: Display label (required)description: Help text (optional)defaultValue: Default array (optional, default:[])delegate: Custom item delegate Component (optional)addItem(item): Add item to listremoveItem(index): Remove item from list- Use when you need custom UI for adding items
-
ListSettingWithInput - Complete list management with built-in form
settingKey: Storage key (required)label: Display label (required)description: Help text (optional)defaultValue: Default array (optional, default:[])fields: Array of field definitions (required)id: Field ID in saved object (required)label: Column header text (required)placeholder: Input placeholder (optional)width: Column width in pixels (optional, default 200)required: Must have value to add (optional, default false)default: Default value if empty (optional)
- Automatically generates:
- Column headers from field labels
- Input fields with placeholders
- Add button with validation
- List display showing all field values
- Remove buttons for each item
- Best for collecting structured data (servers, locations, etc.)
Complete Settings Example:
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myPlugin"
StyledText {
width: parent.width
text: "General Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StringSetting {
settingKey: "apiKey"
label: "API Key"
description: "Your service API key"
placeholder: "sk-..."
defaultValue: ""
}
ToggleSetting {
settingKey: "enabled"
label: "Enable Feature"
description: "Turn this feature on or off"
defaultValue: true
}
SelectionSetting {
settingKey: "theme"
label: "Theme"
description: "Choose your preferred theme"
options: [
{label: "Dark", value: "dark"},
{label: "Light", value: "light"},
{label: "Auto", value: "auto"}
]
defaultValue: "dark"
}
ListSettingWithInput {
settingKey: "locations"
label: "Locations"
description: "Track multiple locations"
defaultValue: []
fields: [
{id: "name", label: "Name", placeholder: "Home", width: 150, required: true},
{id: "timezone", label: "Timezone", placeholder: "America/New_York", width: 200, required: true}
]
}
}
Key Benefits:
- Zero boilerplate - just define your settings
- Automatic persistence to
settings.json - Clean, consistent UI across all plugins
- No manual
pluginServicecalls needed - Proper layout and spacing handled automatically
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.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
horizontalBarPill: Component {
StyledRect {
width: textItem.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: textItem
anchors.centerIn: parent
text: "Hello World"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: textItem.implicitWidth + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
StyledText {
id: textItem
anchors.centerIn: parent
text: "Hello"
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
rotation: 90
}
}
}
}
Note: Use PluginComponent wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.
Step 4: Create Settings Component (Optional)
Create MySettings.qml:
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
pluginId: "myPlugin"
StyledText {
width: parent.width
text: "Configure your plugin settings"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StringSetting {
settingKey: "text"
label: "Display Text"
description: "Text shown in the bar widget"
placeholder: "Hello World"
defaultValue: "Hello World"
}
ToggleSetting {
settingKey: "showIcon"
label: "Show Icon"
description: "Display an icon next to the text"
defaultValue: true
}
}
Step 5: Enable Plugin
- Run the shell:
qs -p $CONFIGPATH/quickshell/dms/shell.qml - Open Settings (Ctrl+,)
- Navigate to Plugins tab
- Click "Scan for Plugins"
- Enable your plugin with the toggle switch
- Add the plugin to your DankBar configuration
Adding Plugin to DankBar
After enabling a plugin, add it to the bar:
- Open Settings → Appearance → DankBar Layout
- Add a new widget entry with your plugin ID
- Choose section (left, center, right)
- Save and reload
Or edit $CONFIGPATH/quickshell/dms/config.json:
{
"dankBarLeftWidgets": [
{"widgetId": "myPlugin", "enabled": true}
]
}
Best Practices
- Use Existing Widgets: Leverage
qs.Widgetscomponents (DankIcon, DankToggle, etc.) for consistency - Follow Theme: Use
Themesingleton for colors, spacing, and fonts - Data Persistence: Use PluginService data APIs instead of manual file operations
- Error Handling: Gracefully handle missing dependencies and invalid data
- Performance: Keep widgets lightweight, avoid long operations that block the UI loop
- Responsive Design: Adapt to
compactModeand different screen sizes - Documentation: Include README.md explaining plugin usage
- Versioning: Use semantic versioning for updates
- 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 operationsPluginsTab:- UI interactionsPluginsTab:- Settings loading and accordion interface
Common Issues
-
Plugin Not Detected
- Check plugin.json syntax (use
jqor JSON validator) - Verify directory is in
$CONFIGPATH/DankMaterialShell/plugins/ - Click "Scan for Plugins" in Settings
- Check plugin.json syntax (use
-
Widget Not Displaying
- Ensure plugin is enabled in Settings
- Add plugin ID to DankBar widget list
- Check widget width/height properties
-
Settings Not Loading
- Verify
settingspath in plugin.json - Check settings component for errors
- Ensure plugin is enabled and loaded
- Review PluginsTab console output for injection issues
- Verify
-
Data Not Persisting
- Confirm pluginService.savePluginData() calls (with injection)
- Check
$CONFIGPATH/DankMaterialShell/settings.jsonfor 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 (not currently enforced)settings_write: Required to use PluginSettings - write plugin configuration (enforced)process: Execute system commands (not currently enforced)network: Network access (not currently enforced)
Currently, only settings_write is enforced by the PluginSettings component.
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
Launcher Plugins
Launcher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.
Overview
Launcher plugins enable you to:
- Add custom items to the launcher/app drawer
- Use trigger strings for quick filtering (e.g.,
!,#,@) - Execute custom actions when items are selected
- Provide searchable, categorized content
- Integrate seamlessly with the existing launcher
Plugin Type Configuration
To create a launcher plugin, set the plugin type in plugin.json:
{
"id": "myLauncher",
"name": "My Launcher Plugin",
"type": "launcher",
"capabilities": ["launcher"],
"component": "./MyLauncher.qml",
"settings": "./MySettings.qml",
"permissions": ["settings_read", "settings_write"]
}
Launcher Component Contract
Create MyLauncher.qml with the following interface:
import QtQuick
import qs.Services
Item {
id: root
// Required properties
property var pluginService: null
property string trigger: "#"
// Required signals
signal itemsChanged()
// Required: Return array of launcher items
function getItems(query) {
return [
{
name: "Item Name",
icon: "icon_name",
comment: "Description",
action: "type:data",
categories: ["MyLauncher"]
}
]
}
// Required: Execute item action
function executeItem(item) {
const [type, data] = item.action.split(":", 2)
// Handle action based on type
}
Component.onCompleted: {
if (pluginService) {
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
}
}
}
Item Structure
Each item returned by getItems() must include:
name(string): Display name shown in launchericon(string): Material Design icon namecomment(string): Description/subtitle textaction(string): Action identifier intype:dataformatcategories(array): Array containing your plugin name
Trigger System
Triggers control when your plugin's items appear in the launcher:
Empty Trigger Mode (No trigger):
- Items always visible alongside regular apps
- Search includes your items automatically
- Configure by saving empty trigger:
trigger: ""
Custom Trigger Mode:
- Items only appear when trigger is typed
- Example: Type
#to show only your plugin's items - Type
# queryto search within your plugin - Configure any string:
#,!,@,!custom, etc.
Trigger Configuration in Settings
Provide a settings component with trigger configuration:
import QtQuick
import QtQuick.Controls
import qs.Widgets
FocusScope {
id: root
property var pluginService: null
Column {
spacing: 12
CheckBox {
id: noTriggerToggle
text: "No trigger (always show)"
checked: loadSettings("noTrigger", false)
onCheckedChanged: {
saveSettings("noTrigger", checked)
if (checked) {
saveSettings("trigger", "")
} else {
saveSettings("trigger", triggerField.text || "#")
}
}
}
DankTextField {
id: triggerField
visible: !noTriggerToggle.checked
text: loadSettings("trigger", "#")
placeholderText: "#"
onTextEdited: {
saveSettings("trigger", text || "#")
}
}
}
function saveSettings(key, value) {
if (pluginService) {
pluginService.savePluginData("myLauncher", key, value)
}
}
function loadSettings(key, defaultValue) {
if (pluginService) {
return pluginService.loadPluginData("myLauncher", key, defaultValue)
}
return defaultValue
}
}
Action Execution
Handle different action types in executeItem():
function executeItem(item) {
const actionParts = item.action.split(":")
const actionType = actionParts[0]
const actionData = actionParts.slice(1).join(":")
switch (actionType) {
case "toast":
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Plugin", actionData)
}
break
case "copy":
// Copy to clipboard
break
case "script":
// Execute command
break
default:
console.warn("Unknown action:", actionType)
}
}
Search and Filtering
The launcher automatically handles search when:
With empty trigger:
- Your items appear in all searches
- No prefix needed
With custom trigger:
- Type trigger alone: Shows all your items
- Type trigger + query: Filters your items by query
- The query parameter is passed to your
getItems(query)function
Example getItems() implementation:
function getItems(query) {
const allItems = [
{name: "Item 1", ...},
{name: "Item 2", ...},
{name: "Test Item", ...}
]
if (!query || query.length === 0) {
return allItems
}
const lowerQuery = query.toLowerCase()
return allItems.filter(item => {
return item.name.toLowerCase().includes(lowerQuery) ||
item.comment.toLowerCase().includes(lowerQuery)
})
}
Integration Flow
- User opens launcher
- If empty trigger: Your items appear alongside apps
- If custom trigger: User types trigger (e.g.,
#) - Launcher calls
getItems(query)on your plugin - Your items displayed with your plugin's category
- User selects item and presses Enter
- Launcher calls
executeItem(item)on your plugin
Best Practices
- Unique Triggers: Choose non-conflicting trigger strings
- Fast Response: Return results quickly from
getItems() - Clear Names: Use descriptive item names and comments
- Error Handling: Gracefully handle failures in
executeItem() - Cleanup: Destroy temporary objects after use
- Empty Trigger Support: Consider if your plugin benefits from always being visible
Example Plugin
See PLUGINS/LauncherExample/ for a complete working example demonstrating:
- Trigger configuration (including empty trigger mode)
- Multiple action types (toast, copy, script)
- Search/filtering implementation
- Settings integration
- Proper error handling
Resources
- Example Plugins:
- PluginService:
Services/PluginService.qml - Settings UI:
Modules/Settings/PluginsTab.qml - DankBar Integration:
Modules/DankBar/DankBar.qml - Launcher Integration:
Modules/AppDrawer/AppLauncher.qml - Theme Reference:
Common/Theme.qml - Widget Library:
Widgets/
Contributing
Share your plugins with the community:
- Create a public repository with your plugin
- Include comprehensive README.md
- Add example screenshots
- Document dependencies and permissions
For plugin system improvements, submit issues or PRs to the main DMS repository.