1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00
This commit is contained in:
bbedward
2025-10-01 23:37:03 -04:00
parent 0ca12d275c
commit 554ef16e49
12 changed files with 556 additions and 60 deletions

View File

@@ -0,0 +1,97 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginSettings {
id: root
pluginId: "exampleEmojiPlugin"
// Header section to explain what this plugin does
StyledText {
width: parent.width
text: "Emoji Cycler Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
width: parent.width
text: "Configure which emojis appear in your bar, how quickly they cycle, and how many show at once."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
// Dropdown to select which emoji set to use
SelectionSetting {
settingKey: "emojiSet"
label: "Emoji Set"
description: "Choose which collection of emojis to cycle through"
options: [
{label: "Happy & Sad", value: "happySad"},
{label: "Hearts", value: "hearts"},
{label: "Hand Gestures", value: "hands"},
{label: "All Mixed", value: "mixed"}
]
defaultValue: "happySad"
// Update the actual emoji array when selection changes
onValueChanged: {
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const newEmojis = sets[value] || sets["happySad"]
root.saveValue("emojis", newEmojis)
}
Component.onCompleted: {
// Initialize the emojis array on first load
const currentSet = value || defaultValue
const sets = {
"happySad": ["😊", "😢", "😂", "😭", "😍", "😡"],
"hearts": ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍"],
"hands": ["👍", "👎", "👊", "✌️", "🤘", "👌", "✋", "🤚"],
"mixed": ["😊", "❤️", "👍", "🎉", "🔥", "✨", "🌟", "💯"]
}
const emojis = sets[currentSet] || sets["happySad"]
root.saveValue("emojis", emojis)
}
}
// Slider to control how fast emojis cycle (in milliseconds)
SliderSetting {
settingKey: "cycleInterval"
label: "Cycle Speed"
description: "How quickly emojis rotate (in seconds)"
defaultValue: 3000
minimum: 500
maximum: 10000
unit: "ms"
leftIcon: "schedule"
}
// Slider to control max emojis shown in the bar
SliderSetting {
settingKey: "maxBarEmojis"
label: "Max Bar Emojis"
description: "Maximum number of emojis to display in the bar at once"
defaultValue: 3
minimum: 1
maximum: 8
unit: ""
rightIcon: "emoji_emotions"
}
StyledText {
width: parent.width
text: "💡 Tip: Click the emoji widget in your bar to open the emoji picker and copy any emoji to your clipboard!"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}

View File

@@ -0,0 +1,185 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
property var pluginService: null
// Load settings from PluginService
property var enabledEmojis: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "emojis", ["😊", "😢", "❤️"]) : ["😊", "😢", "❤️"]
property int cycleInterval: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "cycleInterval", 3000) : 3000
property int maxBarEmojis: pluginService ? pluginService.loadPluginData("exampleEmojiPlugin", "maxBarEmojis", 3) : 3
// Current state for cycling through emojis
property int currentIndex: 0
property var displayedEmojis: []
// Timer to cycle through emojis at the configured interval
Timer {
interval: root.cycleInterval
running: true
repeat: true
onTriggered: {
if (root.enabledEmojis.length > 0) {
root.currentIndex = (root.currentIndex + 1) % root.enabledEmojis.length
root.updateDisplayedEmojis()
}
}
}
// Update the emojis shown in the bar when settings or index changes
function updateDisplayedEmojis() {
const maxToShow = Math.min(root.maxBarEmojis, root.enabledEmojis.length)
let emojis = []
for (let i = 0; i < maxToShow; i++) {
const idx = (root.currentIndex + i) % root.enabledEmojis.length
emojis.push(root.enabledEmojis[idx])
}
root.displayedEmojis = emojis
}
Component.onCompleted: {
updateDisplayedEmojis()
}
onEnabledEmojisChanged: updateDisplayedEmojis()
onMaxBarEmojisChanged: updateDisplayedEmojis()
horizontalBarPill: Component {
StyledRect {
width: emojiRow.implicitWidth + Theme.spacingM * 2
height: parent.widgetThickness
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Row {
id: emojiRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
}
verticalBarPill: Component {
StyledRect {
width: parent.widgetThickness
height: emojiColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
id: emojiColumn
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: root.displayedEmojis
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeMedium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
popoutContent: Component {
Item {
width: parent.width
height: parent.height
// A grid of 120+ emojis for the user to pick from
property var allEmojis: [
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
"😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
"😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
"🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
"😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
"🤧", "🥵", "🥶", "😶‍🌫️", "😵", "😵‍💫", "🤯", "🤠", "🥳", "😎",
"🤓", "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳",
"🥺", "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖",
"😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬",
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔",
"❤️‍🔥", "❤️‍🩹", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟",
"👍", "👎", "👊", "✊", "🤛", "🤜", "🤞", "✌️", "🤟", "🤘",
"👌", "🤌", "🤏", "👈", "👉", "👆", "👇", "☝️", "✋", "🤚"
]
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "Click an emoji to copy it!"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
DankFlickable {
width: parent.width
height: parent.height - parent.spacing - 30
contentWidth: emojiGrid.width
contentHeight: emojiGrid.height
clip: true
Grid {
id: emojiGrid
width: parent.width - Theme.spacingM
columns: 8
spacing: Theme.spacingS
Repeater {
model: allEmojis
StyledRect {
width: 45
height: 45
radius: Theme.cornerRadius
color: emojiMouseArea.containsMouse ? Theme.surfaceContainerHighest : Theme.surfaceContainerHigh
border.width: 0
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeXLarge
}
MouseArea {
id: emojiMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["sh", "-c", "echo -n '" + modelData + "' | wl-copy"])
ToastService.show("Copied " + modelData + " to clipboard", 2000)
root.closePopout()
}
}
}
}
}
}
}
}
}
popoutWidth: 400
popoutHeight: 500
}

View File

@@ -0,0 +1,56 @@
# Emoji Cycler Plugin
An example DankMaterialShell plugin that displays cycling emojis in your bar with an emoji picker popout.
## Features
- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar
- **Emoji Picker**: Click the widget to open a grid of 120+ emojis
- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard (uses `wl-copy`)
- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown
## Installation
1. Copy this directory to `~/.config/DankMaterialShell/plugins/ExampleEmojiPlugin`
2. Open DMS Settings → Plugins
3. Click "Scan for Plugins"
4. Enable "Emoji Cycler"
5. Add `exampleEmojiPlugin` to your DankBar widget list
## Settings
### Emoji Set
Choose from different emoji collections:
- **Happy & Sad**: Mix of emotional faces
- **Hearts**: Various colored hearts
- **Hand Gestures**: Thumbs up, peace signs, etc.
- **All Mixed**: A bit of everything
### Cycle Speed
Control how fast emojis rotate (500ms - 10000ms)
### Max Bar Emojis
How many emojis to display at once (1-8)
## Usage
**In the bar**: Watch emojis cycle through automatically
**Click the widget**: Opens emoji picker with 120+ emojis
**Click any emoji**: Copies it to clipboard and shows toast
## Requirements
- `wl-copy` (for clipboard support on Wayland)
## Example Code Highlights
This plugin demonstrates:
- Using `PluginComponent` for bar integration
- `SelectionSetting`, `SliderSetting` for configuration
- Timer-based animation
- Popout content with grid layout
- External command execution (`Quickshell.execDetached`)
- Toast notifications (`ToastService.show`)
- Dynamic settings loading/saving
Perfect template for creating your own DMS plugins!

View File

@@ -0,0 +1,14 @@
{
"id": "exampleEmojiPlugin",
"name": "Emoji Cycler",
"description": "Display cycling emojis in your bar with a handy emoji picker popout",
"version": "1.0.0",
"author": "DMS Plugin System",
"icon": "mood",
"component": "./EmojiWidget.qml",
"settings": "./EmojiSettings.qml",
"permissions": [
"settings_read",
"settings_write"
]
}

688
PLUGINS/README.md Normal file
View File

@@ -0,0 +1,688 @@
# 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:
```json
{
"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 name
- `component`: 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:
```qml
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
// Define horizontal bar pill (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 (optional)
verticalBarPill: Component {
// Same as horizontal but optimized for vertical layout
}
// Define popout content (optional)
popoutContent: Component {
Column {
width: parent.width
spacing: Theme.spacingM
padding: 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 support
- `widgetThickness`: Recommended widget size perpendicular to bar
- `barThickness`: Bar thickness parallel to edge
**Component Options:**
- `horizontalBarPill`: Component shown in horizontal bars
- `verticalBarPill`: Component shown in vertical bars
- `popoutContent`: Optional popout window content
- `popoutWidth`: Popout window width
- `popoutHeight`: Popout window height
The PluginComponent automatically handles:
- Bar orientation detection
- Click handlers for popouts
- Proper positioning and anchoring
- Theme integration
### Settings Component
Optional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:
```qml
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. No manual `pluginService.savePluginData()` calls needed!
**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:
```qml
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
}
```
1. **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
2. **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)
3. **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)
4. **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 `value` field, displays the `label` field
5. **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 list
- `removeItem(index)`: Remove item from list
- Use when you need custom UI for adding items
6. **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:**
```qml
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 `pluginService` calls needed
- Proper layout and spacing handled automatically
## PluginService API
### Properties
```qml
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
```qml
// 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
```qml
PluginService.pluginLoaded(pluginId: string)
PluginService.pluginUnloaded(pluginId: string)
PluginService.pluginLoadFailed(pluginId: string, error: string)
```
## Creating a Plugin
### Step 1: Create Plugin Directory
```bash
mkdir -p $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
cd $CONFIGPATH/DankMaterialShell/plugins/MyPlugin
```
### Step 2: Create Manifest
Create `plugin.json`:
```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`:
```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`:
```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
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`:
```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:
```bash
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 (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
## 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
4. Add example screenshots
5. Document dependencies and permissions
For plugin system improvements, submit issues or PRs to the main DMS repository.