mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
feat: Plugin System (#276)
* feat: Plugin System * fix: merge conflicts
This commit is contained in:
committed by
GitHub
parent
ab0759f441
commit
53983933dc
167
CLAUDE.md
167
CLAUDE.md
@@ -63,6 +63,9 @@ quickshell -p shell.qml
|
||||
# Or use the shorthand
|
||||
qs -p .
|
||||
|
||||
# Run with verbose output for debugging
|
||||
qs -v -p shell.qml
|
||||
|
||||
# Code formatting and linting
|
||||
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format a QML file (requires qmlfmt, do not use qmlformat)
|
||||
qmllint **/*.qml # Lint all QML files for syntax errors
|
||||
@@ -89,6 +92,7 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
│ ├── DisplayService.qml
|
||||
│ ├── NotificationService.qml
|
||||
│ ├── WeatherService.qml
|
||||
│ ├── PluginService.qml
|
||||
│ └── [14 more services]
|
||||
├── Modules/ # UI components (93 files)
|
||||
│ ├── TopBar/ # Panel components (13 files)
|
||||
@@ -104,15 +108,21 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
│ ├── SettingsModal.qml
|
||||
│ ├── ClipboardHistoryModal.qml
|
||||
│ ├── ProcessListModal.qml
|
||||
│ ├── PluginSettingsModal.qml
|
||||
│ └── [7 more modals]
|
||||
└── Widgets/ # Reusable UI controls (19 files)
|
||||
├── DankIcon.qml
|
||||
├── DankSlider.qml
|
||||
├── DankToggle.qml
|
||||
├── DankTabBar.qml
|
||||
├── DankGridView.qml
|
||||
├── DankListView.qml
|
||||
└── [13 more widgets]
|
||||
├── Widgets/ # Reusable UI controls (19 files)
|
||||
│ ├── DankIcon.qml
|
||||
│ ├── DankSlider.qml
|
||||
│ ├── DankToggle.qml
|
||||
│ ├── DankTabBar.qml
|
||||
│ ├── DankGridView.qml
|
||||
│ ├── DankListView.qml
|
||||
│ └── [13 more widgets]
|
||||
└── plugins/ # External plugins directory ($CONFIGPATH/DankMaterialShell/plugins/)
|
||||
└── PluginName/ # Example Plugin structure
|
||||
├── plugin.json # Plugin manifest
|
||||
├── PluginNameWidget.qml # Widget component
|
||||
└── PluginNameSettings.qml # Settings UI
|
||||
```
|
||||
|
||||
### Component Organization
|
||||
@@ -163,6 +173,12 @@ shell.qml # Main entry point (minimal orchestration)
|
||||
- **DankLocationSearch**: Location picker with search
|
||||
- **SystemLogo**: Animated system branding component
|
||||
|
||||
7. **Plugins/** - External plugin system (`$CONFIGPATH/DankMaterialShell/plugins/`)
|
||||
- **PluginService**: Discovers, loads, and manages plugin lifecycle
|
||||
- **Dynamic Loading**: Plugins loaded at runtime from external directory
|
||||
- **DankBar Integration**: Plugin widgets rendered alongside built-in widgets
|
||||
- **Settings System**: Per-plugin settings with persistence
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
1. **Singleton Services Pattern**:
|
||||
@@ -408,10 +424,10 @@ When modifying the shell:
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
|
||||
property bool featureAvailable: false
|
||||
property type currentValue: defaultValue
|
||||
|
||||
|
||||
function performAction(param) {
|
||||
// Implementation
|
||||
}
|
||||
@@ -422,7 +438,7 @@ When modifying the shell:
|
||||
```qml
|
||||
// In module files
|
||||
property alias serviceValue: NewService.currentValue
|
||||
|
||||
|
||||
SomeControl {
|
||||
visible: NewService.featureAvailable
|
||||
enabled: NewService.featureAvailable
|
||||
@@ -430,6 +446,134 @@ When modifying the shell:
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Plugins
|
||||
|
||||
Plugins are external, dynamically-loaded components that extend DankBar functionality. Plugins are stored in `~/.config/DankMaterialShell/plugins/` and have their settings isolated from core DMS settings.
|
||||
|
||||
1. **Create plugin directory**:
|
||||
```bash
|
||||
mkdir -p ~/.config/DankMaterialShell/plugins/YourPlugin
|
||||
```
|
||||
|
||||
2. **Create manifest** (`plugin.json`):
|
||||
```json
|
||||
{
|
||||
"id": "yourPlugin",
|
||||
"name": "Your Plugin",
|
||||
"description": "Widget description",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"icon": "extension",
|
||||
"component": "./YourWidget.qml",
|
||||
"settings": "./YourSettings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **Create widget component** (`YourWidget.qml`):
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool compactMode: false
|
||||
property string section: "center"
|
||||
property real widgetHeight: 30
|
||||
property var pluginService: null
|
||||
|
||||
width: content.implicitWidth + 16
|
||||
height: widgetHeight
|
||||
radius: 8
|
||||
color: "#20FFFFFF"
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
var data = pluginService.loadPluginData("yourPlugin", "key", defaultValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create settings component** (`YourSettings.qml`):
|
||||
```qml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
|
||||
implicitHeight: settingsColumn.implicitHeight
|
||||
height: implicitHeight
|
||||
|
||||
Column {
|
||||
id: settingsColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Your Plugin Settings"
|
||||
font.pixelSize: 18
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
// Your settings UI here
|
||||
}
|
||||
|
||||
function saveSettings(key, value) {
|
||||
if (pluginService) {
|
||||
pluginService.savePluginData("yourPlugin", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
function loadSettings(key, defaultValue) {
|
||||
if (pluginService) {
|
||||
return pluginService.loadPluginData("yourPlugin", key, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **Enable plugin**:
|
||||
- Open Settings → Plugins
|
||||
- Click "Scan for Plugins"
|
||||
- Toggle plugin to enable
|
||||
- Add plugin ID to DankBar widget list
|
||||
|
||||
**Plugin Directory Structure:**
|
||||
```
|
||||
~/.config/DankMaterialShell/
|
||||
├── settings.json # Core DMS settings + plugin settings
|
||||
│ └── pluginSettings: {
|
||||
│ └── yourPlugin: {
|
||||
│ ├── enabled: true,
|
||||
│ └── customData: {...}
|
||||
│ }
|
||||
│ }
|
||||
└── plugins/ # Plugin files directory
|
||||
└── YourPlugin/ # Plugin directory (matches manifest ID)
|
||||
├── plugin.json # Plugin manifest
|
||||
├── YourWidget.qml # Widget component
|
||||
└── YourSettings.qml # Settings UI (optional)
|
||||
```
|
||||
|
||||
**Key Plugin APIs:**
|
||||
- `pluginService.loadPluginData(pluginId, key, default)` - Load persistent data
|
||||
- `pluginService.savePluginData(pluginId, key, value)` - Save persistent data
|
||||
- `PluginService.enablePlugin(pluginId)` - Load plugin
|
||||
- `PluginService.disablePlugin(pluginId)` - Unload plugin
|
||||
|
||||
**Important Notes:**
|
||||
- Plugin settings are automatically injected by the PluginService via `item.pluginService = PluginService`
|
||||
- Settings are stored in the main settings.json but namespaced under `pluginSettings.{pluginId}`
|
||||
- Plugin directories must match the plugin ID in the manifest
|
||||
- Use the injected `pluginService` property in both widget and settings components
|
||||
|
||||
### Debugging Common Issues
|
||||
|
||||
1. **Import errors**: Check import paths
|
||||
@@ -454,6 +598,7 @@ When modifying the shell:
|
||||
- **Function Discovery**: Use grep/search tools to find existing utility functions before implementing new ones
|
||||
- **Modern QML Patterns**: Leverage new widgets like DankTextField, DankDropdown, CachingImage
|
||||
- **Structured Organization**: Follow the established Services/Modules/Widgets/Modals separation
|
||||
- **Plugin System**: For user extensions, create plugins instead of modifying core modules - see docs/PLUGINS.md
|
||||
|
||||
### Common Widget Patterns
|
||||
|
||||
|
||||
@@ -164,6 +164,8 @@ Singleton {
|
||||
|
||||
property bool _loading: false
|
||||
|
||||
property var pluginSettings: ({})
|
||||
|
||||
function getEffectiveTimeFormat() {
|
||||
if (use24HourClock) {
|
||||
return Locale.ShortFormat
|
||||
@@ -350,6 +352,7 @@ Singleton {
|
||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
|
||||
surfaceBase = settings.surfaceBase !== undefined ? settings.surfaceBase : "s"
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
|
||||
pluginSettings = settings.pluginSettings !== undefined ? settings.pluginSettings : ({})
|
||||
applyStoredTheme()
|
||||
detectAvailableIconThemes()
|
||||
detectQtTools()
|
||||
@@ -468,7 +471,8 @@ Singleton {
|
||||
"notificationTimeoutNormal": notificationTimeoutNormal,
|
||||
"notificationTimeoutCritical": notificationTimeoutCritical,
|
||||
"notificationPopupPosition": notificationPopupPosition,
|
||||
"screenPreferences": screenPreferences
|
||||
"screenPreferences": screenPreferences,
|
||||
"pluginSettings": pluginSettings
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
@@ -1229,6 +1233,33 @@ Singleton {
|
||||
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
|
||||
}
|
||||
|
||||
// Plugin settings functions
|
||||
function getPluginSetting(pluginId, key, defaultValue) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
return defaultValue
|
||||
}
|
||||
return pluginSettings[pluginId][key] !== undefined ? pluginSettings[pluginId][key] : defaultValue
|
||||
}
|
||||
|
||||
function setPluginSetting(pluginId, key, value) {
|
||||
if (!pluginSettings[pluginId]) {
|
||||
pluginSettings[pluginId] = {}
|
||||
}
|
||||
pluginSettings[pluginId][key] = value
|
||||
saveSettings()
|
||||
}
|
||||
|
||||
function removePluginSettings(pluginId) {
|
||||
if (pluginSettings[pluginId]) {
|
||||
delete pluginSettings[pluginId]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginSettingsForPlugin(pluginId) {
|
||||
return pluginSettings[pluginId] || {}
|
||||
}
|
||||
|
||||
function _shq(s) {
|
||||
return "'" + String(s).replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
@@ -154,13 +154,26 @@ Item {
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
id: pluginsLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 10
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: PluginsTab {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: aboutLoader
|
||||
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 11
|
||||
visible: active
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: AboutTab {
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ DankModal {
|
||||
|
||||
objectName: "settingsModal"
|
||||
width: 800
|
||||
height: 750
|
||||
height: 800
|
||||
visible: false
|
||||
onBackgroundClicked: () => {
|
||||
return hide();
|
||||
|
||||
@@ -38,6 +38,9 @@ Rectangle {
|
||||
}, {
|
||||
"text": "Power",
|
||||
"icon": "power_settings_new"
|
||||
}, {
|
||||
"text": "Plugins",
|
||||
"icon": "extension"
|
||||
}, {
|
||||
"text": "About",
|
||||
"icon": "info"
|
||||
|
||||
@@ -266,7 +266,8 @@ Item {
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
const componentMap = {
|
||||
// Build dynamic component map including plugins
|
||||
let baseMap = {
|
||||
"launcherButton": "launcherButtonComponent",
|
||||
"workspaceSwitcher": "workspaceSwitcherComponent",
|
||||
"focusedWindow": "focusedWindowComponent",
|
||||
@@ -296,8 +297,15 @@ Item {
|
||||
"systemUpdate": "systemUpdateComponent"
|
||||
}
|
||||
|
||||
const componentKey = componentMap[widgetId]
|
||||
return componentKey ? root.components[componentKey] : null
|
||||
// For built-in components, get from components property
|
||||
const componentKey = baseMap[widgetId]
|
||||
if (componentKey && root.components[componentKey]) {
|
||||
return root.components[componentKey]
|
||||
}
|
||||
|
||||
// For plugin components, get from PluginService
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return pluginMap[widgetId] || null
|
||||
}
|
||||
|
||||
height: parent.height
|
||||
@@ -337,6 +345,7 @@ Item {
|
||||
id: centerRepeater
|
||||
model: root.widgetsModel
|
||||
|
||||
|
||||
Loader {
|
||||
property string widgetId: model.widgetId
|
||||
property var widgetData: model
|
||||
@@ -364,6 +373,17 @@ Item {
|
||||
if (root.axis && "isVertical" in item) {
|
||||
item.isVertical = root.axis.isVertical
|
||||
}
|
||||
|
||||
// Inject PluginService for plugin widgets
|
||||
if (item.pluginService !== undefined) {
|
||||
console.log("CenterSection: Injecting PluginService into plugin widget:", model.widgetId)
|
||||
item.pluginService = PluginService
|
||||
if (item.loadTimezones) {
|
||||
console.log("CenterSection: Calling loadTimezones for widget:", model.widgetId)
|
||||
item.loadTimezones()
|
||||
}
|
||||
}
|
||||
|
||||
layoutTimer.restart()
|
||||
}
|
||||
|
||||
@@ -379,4 +399,27 @@ Item {
|
||||
layoutTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for plugin changes and refresh components
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId === pluginId) {
|
||||
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
// Force refresh of component lookups
|
||||
for (var i = 0; i < centerRepeater.count; i++) {
|
||||
var item = centerRepeater.itemAt(i)
|
||||
if (item && item.widgetId === pluginId) {
|
||||
item.sourceComponent = root.getWidgetComponent(pluginId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,24 @@ Item {
|
||||
Qt.callLater(() => Qt.callLater(forceWidgetRefresh))
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded(pluginId) {
|
||||
console.log("DankBar: Plugin loaded:", pluginId)
|
||||
// Force componentMap to update by triggering property change
|
||||
if (topBarContent) {
|
||||
topBarContent.updateComponentMap()
|
||||
}
|
||||
}
|
||||
function onPluginUnloaded(pluginId) {
|
||||
console.log("DankBar: Plugin unloaded:", pluginId)
|
||||
// Force componentMap to update by triggering property change
|
||||
if (topBarContent) {
|
||||
topBarContent.updateComponentMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function forceWidgetRefresh() {
|
||||
}
|
||||
|
||||
@@ -366,7 +384,13 @@ Item {
|
||||
anchors.bottomMargin: !barWindow.isVertical ? SettingsData.dankBarInnerPadding / 2 : Math.max(Theme.spacingXS, SettingsData.dankBarInnerPadding * 0.8)
|
||||
clip: true
|
||||
|
||||
readonly property int availableWidth: width
|
||||
property int componentMapRevision: 0
|
||||
|
||||
function updateComponentMap() {
|
||||
componentMapRevision++
|
||||
}
|
||||
|
||||
readonly property int availableWidth: width
|
||||
readonly property int launcherButtonWidth: 40
|
||||
readonly property int workspaceSwitcherWidth: 120
|
||||
readonly property int focusedAppMaxWidth: 456
|
||||
@@ -421,35 +445,44 @@ Item {
|
||||
return widgetVisibility[widgetId] ?? true
|
||||
}
|
||||
|
||||
readonly property var componentMap: ({
|
||||
"launcherButton": launcherButtonComponent,
|
||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||
"focusedWindow": focusedWindowComponent,
|
||||
"runningApps": runningAppsComponent,
|
||||
"clock": clockComponent,
|
||||
"music": mediaComponent,
|
||||
"weather": weatherComponent,
|
||||
"systemTray": systemTrayComponent,
|
||||
"privacyIndicator": privacyIndicatorComponent,
|
||||
"clipboard": clipboardComponent,
|
||||
"cpuUsage": cpuUsageComponent,
|
||||
"memUsage": memUsageComponent,
|
||||
"diskUsage": diskUsageComponent,
|
||||
"cpuTemp": cpuTempComponent,
|
||||
"gpuTemp": gpuTempComponent,
|
||||
"notificationButton": notificationButtonComponent,
|
||||
"battery": batteryComponent,
|
||||
"controlCenterButton": controlCenterButtonComponent,
|
||||
"idleInhibitor": idleInhibitorComponent,
|
||||
"spacer": spacerComponent,
|
||||
"separator": separatorComponent,
|
||||
"network_speed_monitor": networkComponent,
|
||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||
"vpn": vpnComponent,
|
||||
"notepadButton": notepadButtonComponent,
|
||||
"colorPicker": colorPickerComponent,
|
||||
"systemUpdate": systemUpdateComponent
|
||||
})
|
||||
readonly property var componentMap: {
|
||||
// This property depends on componentMapRevision to ensure it updates when plugins change
|
||||
componentMapRevision;
|
||||
|
||||
let baseMap = {
|
||||
"launcherButton": launcherButtonComponent,
|
||||
"workspaceSwitcher": workspaceSwitcherComponent,
|
||||
"focusedWindow": focusedWindowComponent,
|
||||
"runningApps": runningAppsComponent,
|
||||
"clock": clockComponent,
|
||||
"music": mediaComponent,
|
||||
"weather": weatherComponent,
|
||||
"systemTray": systemTrayComponent,
|
||||
"privacyIndicator": privacyIndicatorComponent,
|
||||
"clipboard": clipboardComponent,
|
||||
"cpuUsage": cpuUsageComponent,
|
||||
"memUsage": memUsageComponent,
|
||||
"diskUsage": diskUsageComponent,
|
||||
"cpuTemp": cpuTempComponent,
|
||||
"gpuTemp": gpuTempComponent,
|
||||
"notificationButton": notificationButtonComponent,
|
||||
"battery": batteryComponent,
|
||||
"controlCenterButton": controlCenterButtonComponent,
|
||||
"idleInhibitor": idleInhibitorComponent,
|
||||
"spacer": spacerComponent,
|
||||
"separator": separatorComponent,
|
||||
"network_speed_monitor": networkComponent,
|
||||
"keyboard_layout_name": keyboardLayoutNameComponent,
|
||||
"vpn": vpnComponent,
|
||||
"notepadButton": notepadButtonComponent,
|
||||
"colorPicker": colorPickerComponent,
|
||||
"systemUpdate": systemUpdateComponent
|
||||
}
|
||||
|
||||
// Merge with plugin widgets
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return Object.assign(baseMap, pluginMap)
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId) {
|
||||
return componentMap[widgetId] || null
|
||||
@@ -1016,6 +1049,7 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,21 @@ Loader {
|
||||
if (axis && "isVertical" in item) {
|
||||
item.isVertical = axis.isVertical
|
||||
}
|
||||
|
||||
// Inject PluginService for plugin widgets
|
||||
if (item.pluginService !== undefined) {
|
||||
console.log("WidgetHost: Injecting PluginService into plugin widget:", widgetId)
|
||||
item.pluginService = PluginService
|
||||
if (item.loadTimezones) {
|
||||
console.log("WidgetHost: Calling loadTimezones for widget:", widgetId)
|
||||
item.loadTimezones()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetComponent(widgetId, components) {
|
||||
// Build component map for built-in widgets
|
||||
const componentMap = {
|
||||
"launcherButton": components.launcherButtonComponent,
|
||||
"workspaceSwitcher": components.workspaceSwitcherComponent,
|
||||
@@ -67,7 +78,14 @@ Loader {
|
||||
"systemUpdate": components.systemUpdateComponent
|
||||
}
|
||||
|
||||
return componentMap[widgetId] || null
|
||||
// Check for built-in component first
|
||||
if (componentMap[widgetId]) {
|
||||
return componentMap[widgetId]
|
||||
}
|
||||
|
||||
// Check for plugin component
|
||||
let pluginMap = PluginService.getWidgetComponents()
|
||||
return pluginMap[widgetId] || null
|
||||
}
|
||||
|
||||
function getWidgetVisible(widgetId, dgopAvailable) {
|
||||
|
||||
@@ -8,7 +8,8 @@ import qs.Widgets
|
||||
Item {
|
||||
id: dankBarTab
|
||||
|
||||
property var baseWidgetDefinitions: [{
|
||||
property var baseWidgetDefinitions: {
|
||||
var coreWidgets = [{
|
||||
"id": "launcherButton",
|
||||
"text": "App Launcher",
|
||||
"description": "Quick access to application launcher",
|
||||
@@ -177,6 +178,22 @@ Item {
|
||||
"icon": "update",
|
||||
"enabled": SystemUpdateService.distributionSupported
|
||||
}]
|
||||
|
||||
// Add plugin widgets dynamically
|
||||
var loadedPlugins = PluginService.getLoadedPlugins()
|
||||
for (var i = 0; i < loadedPlugins.length; i++) {
|
||||
var plugin = loadedPlugins[i]
|
||||
coreWidgets.push({
|
||||
"id": plugin.id,
|
||||
"text": plugin.name,
|
||||
"description": plugin.description || "Plugin widget",
|
||||
"icon": plugin.icon || "extension",
|
||||
"enabled": true
|
||||
})
|
||||
}
|
||||
|
||||
return coreWidgets
|
||||
}
|
||||
property var defaultLeftWidgets: [{
|
||||
"id": "launcherButton",
|
||||
"enabled": true
|
||||
|
||||
552
Modules/Settings/PluginsTab.qml
Normal file
552
Modules/Settings/PluginsTab.qml
Normal file
@@ -0,0 +1,552 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: pluginsTab
|
||||
|
||||
property string expandedPluginId: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("PluginsTab: Component completed")
|
||||
console.log("PluginsTab: PluginService available:", typeof PluginService !== "undefined")
|
||||
if (typeof PluginService !== "undefined") {
|
||||
console.log("PluginsTab: Available plugins:", Object.keys(PluginService.availablePlugins).length)
|
||||
console.log("PluginsTab: Plugin directory:", PluginService.pluginDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Theme.spacingL
|
||||
clip: true
|
||||
contentHeight: mainColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// Header Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: headerColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: headerColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "extension"
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Plugin Management"
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "#FFFFFF"
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Manage and configure plugins for extending DMS functionality"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Button {
|
||||
text: "Scan for Plugins"
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? "#4A90E2" : "#3A3A3A"
|
||||
radius: Theme.cornerRadius
|
||||
border.color: "#666666"
|
||||
border.width: 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
PluginService.scanPlugins()
|
||||
ToastService.showInfo("Scanning for plugins...")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Create Plugin Directory"
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? "#4A90E2" : "#3A3A3A"
|
||||
radius: Theme.cornerRadius
|
||||
border.color: "#666666"
|
||||
border.width: 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: "#FFFFFF"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
PluginService.createPluginDirectory()
|
||||
ToastService.showInfo("Created plugin directory: " + PluginService.pluginDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin Directory Info
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: directoryColumn.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: directoryColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Plugin Directory"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: "#FFFFFF"
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
font.family: "monospace"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Place plugin directories here. Each plugin should have a plugin.json manifest file."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available Plugins Section
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: Math.max(200, availableColumn.implicitHeight + Theme.spacingL * 2)
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: availableColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Available Plugins"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: "#FFFFFF"
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(150, pluginListView.contentHeight)
|
||||
|
||||
ListView {
|
||||
id: pluginListView
|
||||
|
||||
anchors.fill: parent
|
||||
model: PluginService.getAvailablePlugins()
|
||||
spacing: Theme.spacingM
|
||||
clip: true
|
||||
|
||||
delegate: StyledRect {
|
||||
id: pluginDelegate
|
||||
width: pluginListView.width
|
||||
height: pluginItemColumn.implicitHeight + Theme.spacingM * 2 + settingsContainer.height
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
// Store plugin data in properties to avoid scope issues
|
||||
property var pluginData: modelData
|
||||
property string pluginId: pluginData ? pluginData.id : ""
|
||||
property string pluginName: pluginData ? (pluginData.name || pluginData.id) : ""
|
||||
property string pluginVersion: pluginData ? (pluginData.version || "1.0.0") : ""
|
||||
property string pluginAuthor: pluginData ? (pluginData.author || "Unknown") : ""
|
||||
property string pluginDescription: pluginData ? (pluginData.description || "") : ""
|
||||
property string pluginIcon: pluginData ? (pluginData.icon || "extension") : "extension"
|
||||
property string pluginSettingsPath: pluginData ? (pluginData.settingsPath || "") : ""
|
||||
property var pluginPermissions: pluginData ? (pluginData.permissions || []) : []
|
||||
property bool hasSettings: pluginData && pluginData.settings !== undefined && pluginData.settings !== ""
|
||||
property bool isExpanded: pluginsTab.expandedPluginId === pluginId
|
||||
|
||||
onIsExpandedChanged: {
|
||||
console.log("Plugin", pluginId, "isExpanded changed to:", isExpanded)
|
||||
}
|
||||
|
||||
color: pluginMouseArea.containsMouse ? Theme.surfacePressed : (isExpanded ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
|
||||
border.color: isExpanded ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: isExpanded ? 2 : 1
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pluginMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: pluginDelegate.isExpanded ? settingsContainer.height : 0
|
||||
hoverEnabled: true
|
||||
cursorShape: pluginDelegate.hasSettings ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
console.log("Plugin clicked:", pluginDelegate.pluginId, "hasSettings:", pluginDelegate.hasSettings, "isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||
if (pluginDelegate.hasSettings) {
|
||||
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||
console.log("Collapsing plugin:", pluginDelegate.pluginId)
|
||||
pluginsTab.expandedPluginId = ""
|
||||
} else {
|
||||
console.log("Expanding plugin:", pluginDelegate.pluginId)
|
||||
pluginsTab.expandedPluginId = pluginDelegate.pluginId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: pluginItemColumn
|
||||
width: parent.width
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: pluginDelegate.pluginIcon
|
||||
size: Theme.iconSize
|
||||
color: PluginService.isPluginLoaded(pluginDelegate.pluginId) ? Theme.primary : "#CCCCCC"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
width: parent.width - 250
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
width: parent.width
|
||||
|
||||
StyledText {
|
||||
text: pluginDelegate.pluginName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: "#FFFFFF"
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: pluginDelegate.hasSettings ? (pluginDelegate.isExpanded ? "expand_less" : "expand_more") : ""
|
||||
size: 16
|
||||
color: pluginDelegate.hasSettings ? Theme.primary : "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: pluginDelegate.hasSettings
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "v" + pluginDelegate.pluginVersion + " by " + pluginDelegate.pluginAuthor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 10
|
||||
height: 1
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
onToggled: (isChecked) => {
|
||||
if (isChecked) {
|
||||
if (PluginService.enablePlugin(pluginDelegate.pluginId)) {
|
||||
ToastService.showInfo("Plugin enabled: " + pluginDelegate.pluginName)
|
||||
} else {
|
||||
ToastService.showError("Failed to enable plugin: " + pluginDelegate.pluginName)
|
||||
checked = false
|
||||
}
|
||||
} else {
|
||||
if (PluginService.disablePlugin(pluginDelegate.pluginId)) {
|
||||
ToastService.showInfo("Plugin disabled: " + pluginDelegate.pluginName)
|
||||
if (pluginsTab.expandedPluginId === pluginDelegate.pluginId) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
} else {
|
||||
ToastService.showError("Failed to disable plugin: " + pluginDelegate.pluginName)
|
||||
checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: pluginDelegate.pluginDescription
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
wrapMode: Text.WordWrap
|
||||
visible: pluginDelegate.pluginDescription !== ""
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
visible: pluginDelegate.pluginPermissions && Array.isArray(pluginDelegate.pluginPermissions) && pluginDelegate.pluginPermissions.length > 0
|
||||
|
||||
Repeater {
|
||||
model: pluginDelegate.pluginPermissions
|
||||
|
||||
Rectangle {
|
||||
height: 20
|
||||
width: permissionText.implicitWidth + Theme.spacingXS * 2
|
||||
radius: 10
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
id: permissionText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings container
|
||||
Item {
|
||||
id: settingsContainer
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: pluginDelegate.isExpanded && pluginDelegate.hasSettings ? Math.min(500, settingsLoader.item ? settingsLoader.item.implicitHeight + Theme.spacingL * 2 : 200) : 0
|
||||
clip: true
|
||||
|
||||
onHeightChanged: {
|
||||
console.log("Settings container height changed:", height, "for plugin:", pluginDelegate.pluginId)
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceContainerHighest.r, Theme.surfaceContainerHighest.g, Theme.surfaceContainerHighest.b, 0.5)
|
||||
radius: Theme.cornerRadius
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
contentHeight: settingsLoader.height
|
||||
contentWidth: width
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: settingsLoader
|
||||
width: parent.width
|
||||
active: pluginDelegate.isExpanded && pluginDelegate.hasSettings && PluginService.isPluginLoaded(pluginDelegate.pluginId)
|
||||
asynchronous: false
|
||||
|
||||
onActiveChanged: {
|
||||
console.log("Settings loader active changed to:", active, "for plugin:", pluginDelegate.pluginId,
|
||||
"isExpanded:", pluginDelegate.isExpanded, "hasSettings:", pluginDelegate.hasSettings,
|
||||
"isLoaded:", PluginService.isPluginLoaded(pluginDelegate.pluginId))
|
||||
}
|
||||
|
||||
source: {
|
||||
if (active && pluginDelegate.pluginSettingsPath) {
|
||||
console.log("Loading plugin settings from:", pluginDelegate.pluginSettingsPath)
|
||||
var path = pluginDelegate.pluginSettingsPath
|
||||
if (!path.startsWith("file://")) {
|
||||
path = "file://" + path
|
||||
}
|
||||
return path
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
console.log("Settings loader status changed:", status, "for plugin:", pluginDelegate.pluginId)
|
||||
if (status === Loader.Error) {
|
||||
console.error("Failed to load plugin settings:", pluginDelegate.pluginSettingsPath)
|
||||
} else if (status === Loader.Ready) {
|
||||
console.log("Settings successfully loaded for plugin:", pluginDelegate.pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
console.log("Plugin settings loaded for:", pluginDelegate.pluginId)
|
||||
|
||||
// Make PluginService available to the loaded component
|
||||
if (typeof PluginService !== "undefined") {
|
||||
console.log("Making PluginService available to plugin settings")
|
||||
console.log("PluginService functions available:",
|
||||
"savePluginData" in PluginService,
|
||||
"loadPluginData" in PluginService)
|
||||
item.pluginService = PluginService
|
||||
console.log("PluginService assignment completed, item.pluginService:", item.pluginService !== null)
|
||||
} else {
|
||||
console.error("PluginService not available in PluginsTab context")
|
||||
}
|
||||
|
||||
// Connect to height changes for dynamic resizing
|
||||
if (item.implicitHeightChanged) {
|
||||
item.implicitHeightChanged.connect(function() {
|
||||
console.log("Plugin settings height changed:", item.implicitHeight)
|
||||
})
|
||||
}
|
||||
|
||||
// Force load timezones for WorldClock plugin
|
||||
if (item.loadTimezones) {
|
||||
console.log("Calling loadTimezones for WorldClock plugin")
|
||||
item.loadTimezones()
|
||||
}
|
||||
// Generic initialization for any plugin
|
||||
if (item.initializeSettings) {
|
||||
item.initializeSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: !PluginService.isPluginLoaded(pluginDelegate.pluginId) ?
|
||||
"Enable plugin to access settings" :
|
||||
(settingsLoader.status === Loader.Error ?
|
||||
"Failed to load settings" :
|
||||
"No configurable settings")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: "#CCCCCC"
|
||||
visible: pluginDelegate.isExpanded && (!settingsLoader.active || settingsLoader.status === Loader.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "No plugins found.\nPlace plugins in " + PluginService.pluginDirectory
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: "#CCCCCC"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: pluginListView.model.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connections to update plugin list when plugins are loaded/unloaded
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onPluginLoaded() {
|
||||
pluginListView.model = PluginService.getAvailablePlugins()
|
||||
}
|
||||
function onPluginUnloaded() {
|
||||
pluginListView.model = PluginService.getAvailablePlugins()
|
||||
// Close expanded plugin if it was unloaded
|
||||
if (pluginsTab.expandedPluginId !== "" && !PluginService.isPluginLoaded(pluginsTab.expandedPluginId)) {
|
||||
pluginsTab.expandedPluginId = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
299
Services/PluginService.qml
Normal file
299
Services/PluginService.qml
Normal file
@@ -0,0 +1,299 @@
|
||||
pragma Singleton
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var availablePlugins: ({})
|
||||
property var loadedPlugins: ({})
|
||||
property var pluginWidgetComponents: ({})
|
||||
property string pluginDirectory: {
|
||||
var configDir = StandardPaths.writableLocation(StandardPaths.ConfigLocation)
|
||||
var configDirStr = configDir.toString()
|
||||
if (configDirStr.startsWith("file://")) {
|
||||
configDirStr = configDirStr.substring(7)
|
||||
}
|
||||
return configDirStr + "/DankMaterialShell/plugins"
|
||||
}
|
||||
|
||||
signal pluginLoaded(string pluginId)
|
||||
signal pluginUnloaded(string pluginId)
|
||||
signal pluginLoadFailed(string pluginId, string error)
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(initializePlugins)
|
||||
}
|
||||
|
||||
function initializePlugins() {
|
||||
scanPlugins()
|
||||
}
|
||||
|
||||
property var lsProcess: Process {
|
||||
id: dirScanner
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var output = text.trim()
|
||||
if (output) {
|
||||
var directories = output.split('\n')
|
||||
for (var i = 0; i < directories.length; i++) {
|
||||
var dir = directories[i].trim()
|
||||
if (dir) {
|
||||
var manifestPath = pluginDirectory + "/" + dir + "/plugin.json"
|
||||
console.log("PluginService: Found plugin directory:", dir, "checking manifest at:", manifestPath)
|
||||
loadPluginManifest(manifestPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("PluginService: No directories found in plugin directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.error("PluginService: Failed to scan plugin directory, exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanPlugins() {
|
||||
lsProcess.command = ["find", pluginDirectory, "-maxdepth", "1", "-type", "d", "-not", "-path", pluginDirectory, "-exec", "basename", "{}", ";"]
|
||||
lsProcess.running = true
|
||||
}
|
||||
|
||||
property var manifestReaders: ({})
|
||||
|
||||
function loadPluginManifest(manifestPath) {
|
||||
console.log("PluginService: Loading manifest:", manifestPath)
|
||||
|
||||
// Create a unique key for this manifest reader
|
||||
var readerId = "reader_" + Date.now() + "_" + Math.random()
|
||||
|
||||
var catProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { stdout: StdioCollector { } }")
|
||||
if (catProcess.status === Component.Ready) {
|
||||
var process = catProcess.createObject(root)
|
||||
process.command = ["cat", manifestPath]
|
||||
process.stdout.streamFinished.connect(function() {
|
||||
try {
|
||||
console.log("PluginService: DEBUGGING parsing manifest, text length:", process.stdout.text.length)
|
||||
var manifest = JSON.parse(process.stdout.text.trim())
|
||||
console.log("PluginService: Successfully parsed manifest for plugin:", manifest.id)
|
||||
processManifest(manifest, manifestPath)
|
||||
} catch (e) {
|
||||
console.error("PluginService: Failed to parse manifest", manifestPath, ":", e.message)
|
||||
}
|
||||
process.destroy()
|
||||
delete manifestReaders[readerId]
|
||||
})
|
||||
process.exited.connect(function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.error("PluginService: Failed to read manifest file:", manifestPath, "exit code:", exitCode)
|
||||
process.destroy()
|
||||
delete manifestReaders[readerId]
|
||||
}
|
||||
})
|
||||
manifestReaders[readerId] = process
|
||||
process.running = true
|
||||
} else {
|
||||
console.error("PluginService: Failed to create manifest reader process")
|
||||
}
|
||||
}
|
||||
|
||||
function processManifest(manifest, manifestPath) {
|
||||
registerPlugin(manifest, manifestPath)
|
||||
|
||||
// Auto-load plugin if it's enabled in settings (default to enabled)
|
||||
var enabled = SettingsData.getPluginSetting(manifest.id, "enabled", true)
|
||||
if (enabled) {
|
||||
loadPlugin(manifest.id)
|
||||
}
|
||||
}
|
||||
|
||||
function registerPlugin(manifest, manifestPath) {
|
||||
console.log("PluginService: registerPlugin called with", manifest.id)
|
||||
if (!manifest.id || !manifest.name || !manifest.component) {
|
||||
console.error("PluginService: Invalid manifest, missing required fields:", manifestPath)
|
||||
return
|
||||
}
|
||||
|
||||
var pluginDir = manifestPath.substring(0, manifestPath.lastIndexOf('/'))
|
||||
|
||||
// Clean up relative paths by removing './' prefix
|
||||
var componentFile = manifest.component
|
||||
if (componentFile.startsWith('./')) {
|
||||
componentFile = componentFile.substring(2)
|
||||
}
|
||||
|
||||
var settingsFile = manifest.settings
|
||||
if (settingsFile && settingsFile.startsWith('./')) {
|
||||
settingsFile = settingsFile.substring(2)
|
||||
}
|
||||
|
||||
var pluginInfo = {}
|
||||
for (var key in manifest) {
|
||||
pluginInfo[key] = manifest[key]
|
||||
}
|
||||
pluginInfo.manifestPath = manifestPath
|
||||
pluginInfo.pluginDirectory = pluginDir
|
||||
pluginInfo.componentPath = pluginDir + '/' + componentFile
|
||||
pluginInfo.settingsPath = settingsFile ? pluginDir + '/' + settingsFile : null
|
||||
pluginInfo.loaded = false
|
||||
|
||||
availablePlugins[manifest.id] = pluginInfo
|
||||
console.log("PluginService: Registered plugin:", manifest.id, "-", manifest.name)
|
||||
console.log("PluginService: Component path:", pluginInfo.componentPath)
|
||||
}
|
||||
|
||||
function loadPlugin(pluginId) {
|
||||
console.log("PluginService: loadPlugin called for", pluginId)
|
||||
var plugin = availablePlugins[pluginId]
|
||||
if (!plugin) {
|
||||
console.error("PluginService: Plugin not found:", pluginId)
|
||||
pluginLoadFailed(pluginId, "Plugin not found")
|
||||
return false
|
||||
}
|
||||
|
||||
if (plugin.loaded) {
|
||||
console.log("PluginService: Plugin already loaded:", pluginId)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
// Create the widget component
|
||||
var componentUrl = "file://" + plugin.componentPath
|
||||
console.log("PluginService: Loading component from:", componentUrl)
|
||||
|
||||
var component = Qt.createComponent(componentUrl)
|
||||
if (component.status === Component.Error) {
|
||||
console.error("PluginService: Failed to create component for plugin:", pluginId, "Error:", component.errorString())
|
||||
pluginLoadFailed(pluginId, component.errorString())
|
||||
return false
|
||||
}
|
||||
|
||||
pluginWidgetComponents[pluginId] = component
|
||||
plugin.loaded = true
|
||||
loadedPlugins[pluginId] = plugin
|
||||
|
||||
console.log("PluginService: Successfully loaded plugin:", pluginId)
|
||||
pluginLoaded(pluginId)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error("PluginService: Error loading plugin:", pluginId, "Error:", error.message)
|
||||
pluginLoadFailed(pluginId, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function unloadPlugin(pluginId) {
|
||||
var plugin = loadedPlugins[pluginId]
|
||||
if (!plugin) {
|
||||
console.warn("PluginService: Plugin not loaded:", pluginId)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove from component map
|
||||
delete pluginWidgetComponents[pluginId]
|
||||
|
||||
// Mark as unloaded
|
||||
plugin.loaded = false
|
||||
delete loadedPlugins[pluginId]
|
||||
|
||||
console.log("PluginService: Successfully unloaded plugin:", pluginId)
|
||||
pluginUnloaded(pluginId)
|
||||
return true
|
||||
|
||||
} catch (error) {
|
||||
console.error("PluginService: Error unloading plugin:", pluginId, "Error:", error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetComponents() {
|
||||
return pluginWidgetComponents
|
||||
}
|
||||
|
||||
function getAvailablePlugins() {
|
||||
var result = []
|
||||
for (var key in availablePlugins) {
|
||||
result.push(availablePlugins[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getLoadedPlugins() {
|
||||
var result = []
|
||||
for (var key in loadedPlugins) {
|
||||
result.push(loadedPlugins[key])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function isPluginLoaded(pluginId) {
|
||||
return loadedPlugins[pluginId] !== undefined
|
||||
}
|
||||
|
||||
function enablePlugin(pluginId) {
|
||||
console.log("PluginService: Enabling plugin:", pluginId)
|
||||
SettingsData.setPluginSetting(pluginId, "enabled", true)
|
||||
return loadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function disablePlugin(pluginId) {
|
||||
console.log("PluginService: Disabling plugin:", pluginId)
|
||||
SettingsData.setPluginSetting(pluginId, "enabled", false)
|
||||
return unloadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function reloadPlugin(pluginId) {
|
||||
if (isPluginLoaded(pluginId)) {
|
||||
unloadPlugin(pluginId)
|
||||
}
|
||||
return loadPlugin(pluginId)
|
||||
}
|
||||
|
||||
function savePluginData(pluginId, key, value) {
|
||||
console.log("PluginService: Saving plugin data:", pluginId, key, JSON.stringify(value))
|
||||
SettingsData.setPluginSetting(pluginId, key, value)
|
||||
console.log("PluginService: Data saved successfully")
|
||||
return true
|
||||
}
|
||||
|
||||
function loadPluginData(pluginId, key, defaultValue) {
|
||||
console.log("PluginService: Loading plugin data:", pluginId, key)
|
||||
var value = SettingsData.getPluginSetting(pluginId, key, defaultValue)
|
||||
console.log("PluginService: Loaded key:", key, "value:", JSON.stringify(value))
|
||||
return value
|
||||
}
|
||||
|
||||
function createPluginDirectory() {
|
||||
console.log("PluginService: Creating plugin directory:", pluginDirectory)
|
||||
var mkdirProcess = Qt.createComponent("data:text/plain,import Quickshell.Io; Process { }")
|
||||
if (mkdirProcess.status === Component.Ready) {
|
||||
var process = mkdirProcess.createObject(root)
|
||||
process.command = ["mkdir", "-p", pluginDirectory]
|
||||
process.exited.connect(function(exitCode) {
|
||||
if (exitCode === 0) {
|
||||
console.log("PluginService: Successfully created plugin directory")
|
||||
} else {
|
||||
console.error("PluginService: Failed to create plugin directory, exit code:", exitCode)
|
||||
}
|
||||
process.destroy()
|
||||
})
|
||||
process.running = true
|
||||
return true
|
||||
} else {
|
||||
console.error("PluginService: Failed to create mkdir process")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
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