mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
feat: Plugin System (#276)
* feat: Plugin System * fix: merge conflicts
This commit is contained in:
committed by
GitHub
parent
ab0759f441
commit
53983933dc
456
docs/PLUGINS.md
Normal file
456
docs/PLUGINS.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# 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",
|
||||
"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:
|
||||
|
||||
```qml
|
||||
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:
|
||||
|
||||
```qml
|
||||
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
|
||||
|
||||
```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.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`:
|
||||
|
||||
```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`:
|
||||
|
||||
```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
|
||||
- `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
|
||||
4. Add example screenshots
|
||||
5. Document dependencies and permissions
|
||||
|
||||
For plugin system improvements, submit issues or PRs to the main DMS repository.
|
||||
Reference in New Issue
Block a user