mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-26 21:15:18 -04:00
skill(dms-plugin-dev): update for composite plugins, startupCheck, IPC discovery (#2699)
- Add composite plugin type with multi-surface components field - Add startupCheck manifest field and dependency gating docs - Add IPC runtime plugin discovery commands (plugin-scan target) - Add getPluginPath() to data persistence reference - Document bar reveal visibility optimization for widget plugins - Sync plugin-schema.json with source, add dependencies field - Bump skill version to 1.1
This commit is contained in:
@@ -2,15 +2,16 @@
|
|||||||
name: dms-plugin-dev
|
name: dms-plugin-dev
|
||||||
description: >
|
description: >
|
||||||
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
||||||
Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background
|
Quickshell. Supports five plugin types: widget (bar + Control Center), daemon (background
|
||||||
service), launcher (search + actions), and desktop (draggable desktop widgets). Covers
|
service), launcher (search + actions), desktop (draggable desktop widgets), and composite
|
||||||
manifest creation, QML component development, settings UI, data persistence, theme
|
(multi-surface). Covers manifest creation, QML component development, startup checks,
|
||||||
integration, PopoutService usage, and external command execution. Use when the user wants
|
settings UI, data persistence, theme integration, PopoutService usage, IPC runtime
|
||||||
to create, modify, or debug a DMS plugin, or asks about the DMS plugin API.
|
discovery, and external command execution. Use when the user wants to create, modify,
|
||||||
|
or debug a DMS plugin, or asks about the DMS plugin API.
|
||||||
compatibility: Designed for Claude Code (or similar products)
|
compatibility: Designed for Claude Code (or similar products)
|
||||||
metadata:
|
metadata:
|
||||||
author: DankMaterialShell
|
author: DankMaterialShell
|
||||||
version: "1.0"
|
version: "1.1"
|
||||||
domain: qml-desktop-development
|
domain: qml-desktop-development
|
||||||
framework: DankMaterialShell
|
framework: DankMaterialShell
|
||||||
languages: qml, javascript
|
languages: qml, javascript
|
||||||
@@ -37,14 +38,15 @@ integrations, and desktop widgets. Plugins are QML components discovered from
|
|||||||
|
|
||||||
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
||||||
|
|
||||||
**Four plugin types:**
|
**Five plugin types:**
|
||||||
|
|
||||||
| Type | Purpose | Base Component | Bar pills | CC integration |
|
| Type | Purpose | Base Component | Bar pills | CC integration |
|
||||||
|------------|--------------------------------|----------------------------|-----------|----------------|
|
|-------------|--------------------------------|----------------------------|-----------|----------------|
|
||||||
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
||||||
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
||||||
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
||||||
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
||||||
|
| `composite` | Multi-surface plugin | One component per surface | Optional | Optional |
|
||||||
|
|
||||||
## Step 1: Determine Plugin Type
|
## Step 1: Determine Plugin Type
|
||||||
|
|
||||||
@@ -58,6 +60,9 @@ Choose the type based on what the plugin does:
|
|||||||
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
||||||
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
||||||
desktop layer.
|
desktop layer.
|
||||||
|
- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination
|
||||||
|
of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own
|
||||||
|
QML component file.
|
||||||
|
|
||||||
## Step 2: Create the Manifest
|
## Step 2: Create the Manifest
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**With settings and permissions:**
|
**With settings, startup check, and permissions:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -92,7 +97,31 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
"component": "./YourWidget.qml",
|
"component": "./YourWidget.qml",
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"settings": "./Settings.qml",
|
"settings": "./Settings.qml",
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
"requires_dms": ">=0.1.0",
|
"requires_dms": ">=0.1.0",
|
||||||
|
"dependencies": ["mytool"],
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Composite plugin (multi-surface):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myComposite",
|
||||||
|
"name": "My Composite Plugin",
|
||||||
|
"description": "Daemon + widget + desktop from one plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
|
||||||
|
"icon": "extension",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./MyDaemon.qml",
|
||||||
|
"widget": "./MyBarWidget.qml",
|
||||||
|
"desktop": "./MyDesktopWidget.qml"
|
||||||
|
},
|
||||||
|
"settings": "./Settings.qml",
|
||||||
"permissions": ["settings_read", "settings_write"]
|
"permissions": ["settings_read", "settings_write"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -100,9 +129,11 @@ Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md
|
|||||||
**Key rules:**
|
**Key rules:**
|
||||||
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
||||||
- `version` must be semver (e.g., `1.0.0`)
|
- `version` must be semver (e.g., `1.0.0`)
|
||||||
- `component` must start with `./` and end with `.qml`
|
- Provide either `component` (single-surface) or `components` (multi-surface), not both
|
||||||
- `type: "launcher"` requires a `trigger` field
|
- `component` / component paths must start with `./` and end with `.qml`
|
||||||
|
- `type: "launcher"` (or a `components` object with a `launcher` key) requires a `trigger` field
|
||||||
- `settings_write` permission is **required** if the plugin has a settings component
|
- `settings_write` permission is **required** if the plugin has a settings component
|
||||||
|
- `dependencies` replaces the deprecated `requires` field
|
||||||
|
|
||||||
## Step 3: Create the Main Component
|
## Step 3: Create the Main Component
|
||||||
|
|
||||||
@@ -249,7 +280,69 @@ PluginComponent {
|
|||||||
|
|
||||||
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
||||||
|
|
||||||
## Step 4: Add Settings (Optional)
|
### Composite
|
||||||
|
|
||||||
|
For composite plugins, create a separate QML file per surface. Each surface uses the same
|
||||||
|
base component as the corresponding single-surface type (PluginComponent for widget/daemon,
|
||||||
|
Item for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.
|
||||||
|
|
||||||
|
```
|
||||||
|
MyCompositePlugin/
|
||||||
|
plugin.json
|
||||||
|
MyBarWidget.qml # PluginComponent (widget surface)
|
||||||
|
MyDaemon.qml # PluginComponent (daemon surface)
|
||||||
|
MyDesktopWidget.qml # Item with desktop widget properties
|
||||||
|
Settings.qml # Shared settings for all surfaces
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `pluginService.pluginHasSurface(pluginId, "widget")` to check whether a specific surface
|
||||||
|
is registered for a plugin at runtime.
|
||||||
|
|
||||||
|
## Step 4: Add Startup Check (Optional)
|
||||||
|
|
||||||
|
Gate plugin activation on dependency checks by providing a `startupCheck` component. This
|
||||||
|
runs before the plugin loads and blocks activation if a required tool or condition is missing.
|
||||||
|
|
||||||
|
Create a `StartupCheck.qml` (non-visual QtObject):
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
function check(done) {
|
||||||
|
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
done(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done({
|
||||||
|
"title": I18n.tr("mytool is required"),
|
||||||
|
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `done` callback accepts:
|
||||||
|
- `null` - allow activation
|
||||||
|
- A string - block with a short error message
|
||||||
|
- `{ title, details }` - block with a title and expandable details body
|
||||||
|
|
||||||
|
A synchronous variant (no `done` parameter, return the result directly) is also supported.
|
||||||
|
|
||||||
|
Failed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
|
||||||
|
|
||||||
|
Add to your manifest:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
|
"dependencies": ["mytool"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Add Settings (Optional)
|
||||||
|
|
||||||
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
||||||
|
|
||||||
@@ -293,7 +386,7 @@ See [settings-components-reference.md](references/settings-components-reference.
|
|||||||
|
|
||||||
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
||||||
|
|
||||||
## Step 5: Use Data Persistence
|
## Step 6: Use Data Persistence
|
||||||
|
|
||||||
Three tiers of persistence:
|
Three tiers of persistence:
|
||||||
|
|
||||||
@@ -302,6 +395,7 @@ Three tiers of persistence:
|
|||||||
| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |
|
| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |
|
||||||
| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |
|
| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |
|
||||||
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
||||||
|
| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |
|
||||||
|
|
||||||
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
||||||
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
||||||
@@ -309,7 +403,7 @@ Three tiers of persistence:
|
|||||||
|
|
||||||
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
||||||
|
|
||||||
## Step 6: Theme Integration
|
## Step 7: Theme Integration
|
||||||
|
|
||||||
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
||||||
|
|
||||||
@@ -324,7 +418,7 @@ Always use `Theme.*` properties from `qs.Common` - never hardcode colors or size
|
|||||||
|
|
||||||
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
||||||
|
|
||||||
## Step 7: Add Popout Content (Widgets Only)
|
## Step 8: Add Popout Content (Widgets Only)
|
||||||
|
|
||||||
Add a popout that opens when the bar pill is clicked:
|
Add a popout that opens when the bar pill is clicked:
|
||||||
|
|
||||||
@@ -360,7 +454,7 @@ PluginComponent {
|
|||||||
|
|
||||||
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
||||||
|
|
||||||
## Step 8: Control Center Integration (Widgets Only)
|
## Step 9: Control Center Integration (Widgets Only)
|
||||||
|
|
||||||
Add your widget to the Control Center grid:
|
Add your widget to the Control Center grid:
|
||||||
|
|
||||||
@@ -389,7 +483,7 @@ PluginComponent {
|
|||||||
|
|
||||||
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
||||||
|
|
||||||
## Step 9: External Commands and Clipboard
|
## Step 10: External Commands and Clipboard
|
||||||
|
|
||||||
**Run commands and capture output:**
|
**Run commands and capture output:**
|
||||||
|
|
||||||
@@ -420,18 +514,33 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|||||||
|
|
||||||
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
||||||
|
|
||||||
## Step 10: Validate and Test
|
## Step 11: Validate and Test
|
||||||
|
|
||||||
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
||||||
2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`
|
2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`
|
||||||
3. Open Settings > Plugins > Scan for Plugins
|
3. Open Settings > Plugins > Scan for Plugins
|
||||||
4. Enable your plugin and add it to the DankBar layout
|
4. Enable your plugin and add it to the DankBar layout
|
||||||
|
|
||||||
|
**Runtime plugin discovery via IPC:**
|
||||||
|
|
||||||
|
Plugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dms ipc plugin-scan scan # Trigger a full rescan of all plugin directories
|
||||||
|
dms ipc plugin-scan rescan <id> # Force rescan of a specific plugin
|
||||||
|
dms ipc plugin-scan reload <id> # Force reload of a loaded plugin
|
||||||
|
dms ipc plugin-scan list # List all known plugins (TSV: id, loaded, type, name)
|
||||||
|
dms ipc plugin-scan status <id> # Get status of a specific plugin (TSV: loaded, type, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugin IDs are validated against `^[a-zA-Z0-9_\-:]{1,64}$`.
|
||||||
|
|
||||||
**Common issues:**
|
**Common issues:**
|
||||||
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
||||||
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
||||||
- Settings error: verify `settings_write` permission is declared
|
- Settings error: verify `settings_write` permission is declared
|
||||||
- Data not persisting: check pluginService injection and permissions
|
- Data not persisting: check pluginService injection and permissions
|
||||||
|
- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`
|
||||||
|
|
||||||
## Common Mistakes
|
## Common Mistakes
|
||||||
|
|
||||||
@@ -445,6 +554,9 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|||||||
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
||||||
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
||||||
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
||||||
|
11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`
|
||||||
|
12. **Providing both `component` and `components`** - Use one or the other, not both
|
||||||
|
13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key
|
||||||
|
|
||||||
## Quick Reference: Imports
|
## Quick Reference: Imports
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,7 @@
|
|||||||
"version",
|
"version",
|
||||||
"author",
|
"author",
|
||||||
"type",
|
"type",
|
||||||
"capabilities",
|
"capabilities"
|
||||||
"component"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
@@ -42,8 +41,8 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plugin type",
|
"description": "Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.",
|
||||||
"enum": ["widget", "daemon", "launcher", "desktop"]
|
"enum": ["widget", "daemon", "launcher", "desktop", "composite"]
|
||||||
},
|
},
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -55,9 +54,37 @@
|
|||||||
},
|
},
|
||||||
"component": {
|
"component": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Relative path to main QML component file",
|
"description": "Relative path to main QML component file. Required unless 'components' is provided.",
|
||||||
"pattern": "^\\./.*\\.qml$"
|
"pattern": "^\\./.*\\.qml$"
|
||||||
},
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.",
|
||||||
|
"properties": {
|
||||||
|
"widget": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Bar/Control Center widget component (PluginComponent)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"desktop": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Desktop widget component",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"daemon": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Background daemon component (instantiated once)",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Launcher provider component (requires 'trigger')",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"minProperties": 1
|
||||||
|
},
|
||||||
"trigger": {
|
"trigger": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
"description": "Trigger string for launcher activation (required for launcher type)"
|
||||||
@@ -71,14 +98,26 @@
|
|||||||
"description": "Path to settings component QML file",
|
"description": "Path to settings component QML file",
|
||||||
"pattern": "^\\./.*\\.qml$"
|
"pattern": "^\\./.*\\.qml$"
|
||||||
},
|
},
|
||||||
|
"startupCheck": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.",
|
||||||
|
"pattern": "^\\./.*\\.qml$"
|
||||||
|
},
|
||||||
"requires_dms": {
|
"requires_dms": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
||||||
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Array of required system tools/dependencies (registry metadata)",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"requires": {
|
"requires": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Array of required system tools/dependencies",
|
"description": "Deprecated alias for 'dependencies'.",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
@@ -109,6 +148,29 @@
|
|||||||
"then": {
|
"then": {
|
||||||
"required": ["trigger"]
|
"required": ["trigger"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["components"],
|
||||||
|
"properties": {
|
||||||
|
"components": {
|
||||||
|
"required": ["launcher"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["trigger"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": ["component"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": ["components"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ function increment() {
|
|||||||
| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |
|
| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |
|
||||||
| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |
|
| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |
|
||||||
|
|
||||||
|
## Plugin Path
|
||||||
|
|
||||||
|
Retrieve a plugin's installation directory at runtime:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
var dir = pluginService.getPluginPath(pluginId)
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns the absolute path to the plugin's directory (e.g., `~/.config/DankMaterialShell/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
1. **pluginData is reactive** - bindings update automatically when data changes
|
1. **pluginData is reactive** - bindings update automatically when data changes
|
||||||
|
|||||||
@@ -9,15 +9,22 @@
|
|||||||
| `description` | string | Short description (shown in UI) | Non-empty |
|
| `description` | string | Short description (shown in UI) | Non-empty |
|
||||||
| `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` |
|
| `version` | string | Semantic version | Pattern `^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$` |
|
||||||
| `author` | string | Creator name or email | Non-empty |
|
| `author` | string | Creator name or email | Non-empty |
|
||||||
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop` |
|
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `composite` |
|
||||||
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
||||||
| `component` | string | Path to main QML file | Must start with `./`, end with `.qml` |
|
|
||||||
|
One of `component` or `components` is required (not both):
|
||||||
|
|
||||||
|
| Field | Type | Description | Validation |
|
||||||
|
|-------|------|-------------|------------|
|
||||||
|
| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |
|
||||||
|
| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher` |
|
||||||
|
|
||||||
## Conditional Requirements
|
## Conditional Requirements
|
||||||
|
|
||||||
| Condition | Required Field | Description |
|
| Condition | Required Field | Description |
|
||||||
|-----------|---------------|-------------|
|
|-----------|---------------|-------------|
|
||||||
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
||||||
|
| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |
|
||||||
|
|
||||||
## Optional Fields
|
## Optional Fields
|
||||||
|
|
||||||
@@ -25,8 +32,10 @@
|
|||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
||||||
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
||||||
|
| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |
|
||||||
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
||||||
| `requires` | array | System tool dependencies (e.g., `["curl", "jq"]`) |
|
| `dependencies` | array | System tool dependencies (e.g., `["curl", "jq"]`). Registry metadata. |
|
||||||
|
| `requires` | array | Deprecated alias for `dependencies` |
|
||||||
| `permissions` | array | Required permissions |
|
| `permissions` | array | Required permissions |
|
||||||
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
||||||
|
|
||||||
@@ -53,6 +62,65 @@ Capabilities are free-form strings that describe what the plugin does. Common va
|
|||||||
- `ai` - AI/LLM integration
|
- `ai` - AI/LLM integration
|
||||||
- `slideout` - uses slideout panel
|
- `slideout` - uses slideout panel
|
||||||
|
|
||||||
|
## Startup Check
|
||||||
|
|
||||||
|
The `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
function check(done) {
|
||||||
|
Proc.runCommand("myPlugin.depCheck", ["sh", "-c", "command -v mytool"], (stdout, exitCode) => {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
done(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done({
|
||||||
|
"title": I18n.tr("mytool is required"),
|
||||||
|
"details": I18n.tr("Install 'mytool' and re-enable this plugin.")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `done` callback accepts:
|
||||||
|
- `null` - allow activation
|
||||||
|
- A string - block with a short error message
|
||||||
|
- `{ title, details }` - block with a title and expandable details
|
||||||
|
|
||||||
|
A synchronous variant (no `done` parameter, return the result directly) is also supported.
|
||||||
|
|
||||||
|
Failed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.
|
||||||
|
|
||||||
|
## Components (Composite Plugins)
|
||||||
|
|
||||||
|
The `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "myComposite",
|
||||||
|
"name": "My Composite Plugin",
|
||||||
|
"description": "Daemon + widget + desktop from one plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Developer Name",
|
||||||
|
"type": "composite",
|
||||||
|
"capabilities": ["daemon", "dankbar-widget", "desktop-widget"],
|
||||||
|
"icon": "extension",
|
||||||
|
"components": {
|
||||||
|
"daemon": "./MyDaemon.qml",
|
||||||
|
"widget": "./MyBarWidget.qml",
|
||||||
|
"desktop": "./MyDesktopWidget.qml"
|
||||||
|
},
|
||||||
|
"settings": "./Settings.qml",
|
||||||
|
"permissions": ["settings_read", "settings_write"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid surface keys: `widget`, `desktop`, `daemon`, `launcher`. Provide any subset. Each surface is loaded independently in the appropriate registry.
|
||||||
|
|
||||||
## Complete Example
|
## Complete Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -67,8 +135,9 @@ Capabilities are free-form strings that describe what the plugin does. Common va
|
|||||||
"component": "./MyWidget.qml",
|
"component": "./MyWidget.qml",
|
||||||
"icon": "extension",
|
"icon": "extension",
|
||||||
"settings": "./Settings.qml",
|
"settings": "./Settings.qml",
|
||||||
|
"startupCheck": "./StartupCheck.qml",
|
||||||
"requires_dms": ">=0.1.18",
|
"requires_dms": ">=0.1.18",
|
||||||
"requires": ["curl", "jq"],
|
"dependencies": ["curl", "jq"],
|
||||||
"permissions": ["settings_read", "settings_write", "process", "network"]
|
"permissions": ["settings_read", "settings_write", "process", "network"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -235,10 +235,12 @@ Conditionally show/hide the bar pill:
|
|||||||
```qml
|
```qml
|
||||||
PluginComponent {
|
PluginComponent {
|
||||||
visibilityCommand: "pgrep -x myapp"
|
visibilityCommand: "pgrep -x myapp"
|
||||||
visibilityInterval: 5 // seconds between checks; polling pauses while the bar is hidden
|
visibilityInterval: 5 // seconds between checks
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.
|
||||||
|
|
||||||
## Popout Namespace
|
## Popout Namespace
|
||||||
|
|
||||||
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
||||||
|
|||||||
Reference in New Issue
Block a user