mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 15:29:43 -04:00
add dms-plugin-dev agent skill for plugin development (#2394)
This commit is contained in:
104
.agents/skills/README.md
Normal file
104
.agents/skills/README.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Agent Skills
|
||||
|
||||
This directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.
|
||||
|
||||
Each skill is a directory with a `SKILL.md` entrypoint, optional reference docs, scripts, and templates. Agents load skills progressively: metadata at startup, full instructions on activation, and supporting files on demand.
|
||||
|
||||
## Available Skills
|
||||
|
||||
| Skill | Description |
|
||||
|-------|-------------|
|
||||
| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for DankMaterialShell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |
|
||||
|
||||
## Installation
|
||||
|
||||
The `.agents/skills/` directory at the project root is the standard location defined by the agentskills.io spec. Many agents discover skills from this path automatically. Some agents use their own directory conventions and need a symlink or copy.
|
||||
|
||||
### Claude Code
|
||||
|
||||
Claude Code discovers skills from `.claude/skills/` (project-level) or `~/.claude/skills/` (personal). To make skills from `.agents/skills/` available, symlink them into the Claude Code skills directory:
|
||||
|
||||
**Project-level** (this repo only):
|
||||
|
||||
```bash
|
||||
mkdir -p .claude/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
**Personal** (all your projects):
|
||||
|
||||
```bash
|
||||
ln -s /path/to/DankMaterialShell/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
After linking, the skill appears in Claude Code's `/` menu as `/dms-plugin-dev`, and Claude loads it automatically when you ask about DMS plugin development.
|
||||
|
||||
See the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.
|
||||
|
||||
### Cursor
|
||||
|
||||
Cursor discovers skills from `.cursor/skills/` in the project root:
|
||||
|
||||
```bash
|
||||
mkdir -p .cursor/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
See [Cursor skills docs](https://cursor.com/docs/context/skills) for details.
|
||||
|
||||
### VS Code (Copilot)
|
||||
|
||||
VS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:
|
||||
|
||||
```bash
|
||||
mkdir -p .github/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
See [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.
|
||||
|
||||
### Gemini CLI
|
||||
|
||||
Gemini CLI discovers skills from `.gemini/skills/` in the project root:
|
||||
|
||||
```bash
|
||||
mkdir -p .gemini/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
See [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.
|
||||
|
||||
### OpenAI Codex
|
||||
|
||||
Codex discovers skills from `.codex/skills/` in the project root:
|
||||
|
||||
```bash
|
||||
mkdir -p .codex/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
See [Codex skills docs](https://developers.openai.com/codex/skills/) for details.
|
||||
|
||||
### Other Agents
|
||||
|
||||
The Agent Skills standard is supported by 30+ tools including Goose, Roo Code, JetBrains Junie, Amp, OpenCode, OpenHands, Kiro, and more. Most discover skills from a dot-directory at the project root (e.g., `.goose/skills/`, `.roo/skills/`). Some read `.agents/skills/` directly.
|
||||
|
||||
Check the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.
|
||||
|
||||
The general pattern is:
|
||||
|
||||
```bash
|
||||
mkdir -p .<agent>/skills
|
||||
ln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev
|
||||
```
|
||||
|
||||
## Adding New Skills
|
||||
|
||||
To add a new skill to this directory:
|
||||
|
||||
1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)
|
||||
2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions
|
||||
3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories
|
||||
4. Keep `SKILL.md` under 500 lines - move detailed content to reference files
|
||||
|
||||
See the [Agent Skills specification](https://agentskills.io/specification) for the full format.
|
||||
497
.agents/skills/dms-plugin-dev/SKILL.md
Normal file
497
.agents/skills/dms-plugin-dev/SKILL.md
Normal file
@@ -0,0 +1,497 @@
|
||||
---
|
||||
name: dms-plugin-dev
|
||||
description: >
|
||||
Develop plugins for DankMaterialShell (DMS), a QML-based Linux desktop shell built on
|
||||
Quickshell. Supports four plugin types: widget (bar + Control Center), daemon (background
|
||||
service), launcher (search + actions), and desktop (draggable desktop widgets). Covers
|
||||
manifest creation, QML component development, settings UI, data persistence, theme
|
||||
integration, PopoutService usage, 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)
|
||||
metadata:
|
||||
author: DankMaterialShell
|
||||
version: "1.0"
|
||||
domain: qml-desktop-development
|
||||
framework: DankMaterialShell
|
||||
languages: qml, javascript
|
||||
allowed-tools: Bash Read Write Edit
|
||||
---
|
||||
|
||||
# DankMaterialShell Plugin Development
|
||||
|
||||
## Overview
|
||||
|
||||
DMS plugins extend the desktop shell with custom widgets, background services, launcher
|
||||
integrations, and desktop widgets. Plugins are QML components discovered from
|
||||
`~/.config/DankMaterialShell/plugins/`.
|
||||
|
||||
**Minimum plugin structure:**
|
||||
|
||||
```
|
||||
~/.config/DankMaterialShell/plugins/YourPlugin/
|
||||
plugin.json # Required: manifest with metadata
|
||||
YourComponent.qml # Required: main QML component
|
||||
YourSettings.qml # Optional: settings UI
|
||||
*.js # Optional: JavaScript utilities
|
||||
```
|
||||
|
||||
**Plugin registry:** Community plugins are available at https://plugins.danklinux.com/
|
||||
|
||||
**Four plugin types:**
|
||||
|
||||
| Type | Purpose | Base Component | Bar pills | CC integration |
|
||||
|------------|--------------------------------|----------------------------|-----------|----------------|
|
||||
| `widget` | Bar widget + popout | `PluginComponent` | Yes | Yes |
|
||||
| `daemon` | Background service | `PluginComponent` (no UI) | No | Optional |
|
||||
| `launcher` | Searchable items in launcher | `Item` | No | No |
|
||||
| `desktop` | Draggable desktop widget | `DesktopPluginComponent` | No | No |
|
||||
|
||||
## Step 1: Determine Plugin Type
|
||||
|
||||
Choose the type based on what the plugin does:
|
||||
|
||||
- **Shows in the bar?** - Use `widget`. Displays a pill in DankBar, optionally opens a popout,
|
||||
optionally integrates with Control Center.
|
||||
- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper
|
||||
changes, notifications, battery level, etc.).
|
||||
- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher
|
||||
with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).
|
||||
- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the
|
||||
desktop layer.
|
||||
|
||||
## Step 2: Create the Manifest
|
||||
|
||||
Create `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.
|
||||
|
||||
**Minimal manifest:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "yourPlugin",
|
||||
"name": "Your Plugin Name",
|
||||
"description": "Brief description of what your plugin does",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "widget",
|
||||
"capabilities": ["your-capability"],
|
||||
"component": "./YourWidget.qml"
|
||||
}
|
||||
```
|
||||
|
||||
**With settings and permissions:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "yourPlugin",
|
||||
"name": "Your Plugin Name",
|
||||
"description": "Brief description",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "widget",
|
||||
"capabilities": ["your-capability"],
|
||||
"component": "./YourWidget.qml",
|
||||
"icon": "extension",
|
||||
"settings": "./Settings.qml",
|
||||
"requires_dms": ">=0.1.0",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules:**
|
||||
- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`
|
||||
- `version` must be semver (e.g., `1.0.0`)
|
||||
- `component` must start with `./` and end with `.qml`
|
||||
- `type: "launcher"` requires a `trigger` field
|
||||
- `settings_write` permission is **required** if the plugin has a settings component
|
||||
|
||||
## Step 3: Create the Main Component
|
||||
|
||||
### Widget
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
property var popoutService: null
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: label.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: "Hello"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: label.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: "Hi"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.
|
||||
|
||||
### Launcher
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
signal itemsChanged()
|
||||
|
||||
function getItems(query) {
|
||||
const items = [
|
||||
{ name: "Item One", icon: "material:star", comment: "Description",
|
||||
action: "toast:Hello!", categories: ["MyPlugin"] }
|
||||
]
|
||||
if (!query) return items
|
||||
const q = query.toLowerCase()
|
||||
return items.filter(i => i.name.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
function executeItem(item) {
|
||||
const [type, ...rest] = item.action.split(":")
|
||||
const data = rest.join(":")
|
||||
if (type === "toast") ToastService?.showInfo(data)
|
||||
else if (type === "copy") Quickshell.execDetached(["dms", "cl", "copy", data])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.
|
||||
|
||||
### Desktop
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string pluginId: ""
|
||||
property bool editMode: false
|
||||
property real widgetWidth: 200
|
||||
property real widgetHeight: 200
|
||||
property real minWidth: 150
|
||||
property real minHeight: 150
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: 0.85
|
||||
border.color: root.editMode ? Theme.primary : "transparent"
|
||||
border.width: root.editMode ? 2 : 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Desktop Widget"
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.
|
||||
|
||||
### Daemon
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
property var popoutService: null
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onSomeSignal() {
|
||||
console.log("Event received")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.
|
||||
|
||||
## Step 4: Add Settings (Optional)
|
||||
|
||||
Wrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "yourPlugin"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "apiKey"
|
||||
label: "API Key"
|
||||
description: "Your API key"
|
||||
placeholder: "sk-..."
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "enabled"
|
||||
label: "Enable Feature"
|
||||
defaultValue: true
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "interval"
|
||||
label: "Refresh Interval"
|
||||
options: [
|
||||
{ label: "1 min", value: "60" },
|
||||
{ label: "5 min", value: "300" }
|
||||
]
|
||||
defaultValue: "300"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.
|
||||
|
||||
See [settings-components-reference.md](references/settings-components-reference.md) for full property lists.
|
||||
|
||||
**Important:** Your plugin must declare `"permissions": ["settings_write"]` in plugin.json, or the settings UI will show an error.
|
||||
|
||||
## Step 5: Use Data Persistence
|
||||
|
||||
Three tiers of persistence:
|
||||
|
||||
| API | Persisted | Use case |
|
||||
|-----|-----------|----------|
|
||||
| `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 |
|
||||
| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |
|
||||
|
||||
- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings
|
||||
- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`
|
||||
- Global vars sync across all instances (multi-monitor, multiple bar sections)
|
||||
|
||||
See [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.
|
||||
|
||||
## Step 6: Theme Integration
|
||||
|
||||
Always use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.
|
||||
|
||||
**Essential properties:**
|
||||
- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`
|
||||
- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)
|
||||
- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`
|
||||
- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`
|
||||
- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)
|
||||
|
||||
**Common widgets from `qs.Widgets`:** `StyledText`, `StyledRect`, `DankIcon`, `DankButton`, `DankToggle`, `DankTextField`, `DankSlider`, `DankGridView`, `CachingImage`.
|
||||
|
||||
See [theme-reference.md](references/theme-reference.md) for the complete property list.
|
||||
|
||||
## Step 7: Add Popout Content (Widgets Only)
|
||||
|
||||
Add a popout that opens when the bar pill is clicked:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
popoutWidth: 400
|
||||
popoutHeight: 300
|
||||
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
headerText: "My Plugin"
|
||||
detailsText: "Optional subtitle"
|
||||
showCloseButton: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Content here"
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
horizontalBarPill: Component { /* ... */ }
|
||||
verticalBarPill: Component { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
**PopoutComponent properties:** `headerText`, `detailsText`, `showCloseButton`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).
|
||||
|
||||
Calculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
||||
|
||||
## Step 8: Control Center Integration (Widgets Only)
|
||||
|
||||
Add your widget to the Control Center grid:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
ccWidgetIcon: "toggle_on"
|
||||
ccWidgetPrimaryText: "My Feature"
|
||||
ccWidgetSecondaryText: isActive ? "On" : "Off"
|
||||
ccWidgetIsActive: isActive
|
||||
|
||||
onCcWidgetToggled: {
|
||||
isActive = !isActive
|
||||
pluginService?.savePluginData(pluginId, "active", isActive)
|
||||
}
|
||||
|
||||
// Optional: expandable detail panel (for CompoundPill)
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
implicitHeight: 200
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).
|
||||
|
||||
## Step 9: External Commands and Clipboard
|
||||
|
||||
**Run commands and capture output:**
|
||||
|
||||
```qml
|
||||
import qs.Common
|
||||
|
||||
Proc.runCommand(
|
||||
"myPlugin.fetch",
|
||||
["curl", "-s", "https://api.example.com/data"],
|
||||
(stdout, exitCode) => {
|
||||
if (exitCode === 0) processData(stdout)
|
||||
},
|
||||
500 // debounce ms
|
||||
)
|
||||
```
|
||||
|
||||
**Fire-and-forget (clipboard, notifications):**
|
||||
|
||||
```qml
|
||||
import Quickshell
|
||||
|
||||
Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
||||
```
|
||||
|
||||
**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.
|
||||
|
||||
**Shell commands with pipes:** `["sh", "-c", "ps aux | grep foo"]`
|
||||
|
||||
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
||||
|
||||
## Step 10: Validate and Test
|
||||
|
||||
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`
|
||||
3. Open Settings > Plugins > Scan for Plugins
|
||||
4. Enable your plugin and add it to the DankBar layout
|
||||
|
||||
**Common issues:**
|
||||
- Plugin not detected: check plugin.json syntax with `jq . plugin.json`
|
||||
- Widget not showing: ensure it's enabled AND added to a DankBar section
|
||||
- Settings error: verify `settings_write` permission is declared
|
||||
- Data not persisting: check pluginService injection and permissions
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
1. **Missing `settings_write` permission** - Settings UI shows error without it
|
||||
2. **Missing `property var popoutService: null`** - Must declare for injection to work
|
||||
3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge
|
||||
4. **Hardcoded colors** - Use `Theme.*` properties, not hex values
|
||||
5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached(["dms", "cl", "copy", text])`
|
||||
6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`
|
||||
7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)
|
||||
8. **Forgetting `categories` in launcher items** - Items won't display without it
|
||||
9. **Not handling null pluginService** - Always use optional chaining or null checks
|
||||
10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`
|
||||
|
||||
## Quick Reference: Imports
|
||||
|
||||
**Widget / Daemon:**
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
```
|
||||
|
||||
**Launcher:**
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
```
|
||||
|
||||
**Desktop:**
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
```
|
||||
|
||||
**For clipboard/exec:** `import Quickshell`
|
||||
**For processes:** `import Quickshell.Io`
|
||||
**For networking:** `import Quickshell.Networking`
|
||||
**For toast notifications:** access `ToastService` from `qs.Services`
|
||||
|
||||
## Quick Reference: File Naming
|
||||
|
||||
- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)
|
||||
- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)
|
||||
- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)
|
||||
- **Component paths in manifest:** relative with `./` prefix (e.g., `"./MyWidget.qml"`)
|
||||
- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)
|
||||
|
||||
## Reference Files
|
||||
|
||||
Load these on demand for detailed API documentation:
|
||||
|
||||
- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema
|
||||
- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar pills, popouts, click actions, CC integration
|
||||
- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view
|
||||
- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence
|
||||
- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution
|
||||
- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists
|
||||
- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns
|
||||
- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables
|
||||
- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals
|
||||
- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins
|
||||
115
.agents/skills/dms-plugin-dev/assets/plugin-schema.json
Normal file
115
.agents/skills/dms-plugin-dev/assets/plugin-schema.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://danklinux.com/schemas/plugin.json",
|
||||
"title": "DankMaterialShell Plugin Manifest",
|
||||
"description": "Schema for DankMaterialShell plugin.json manifest files",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"version",
|
||||
"author",
|
||||
"type",
|
||||
"capabilities",
|
||||
"component"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique plugin identifier (camelCase, no spaces)",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Human-readable plugin name",
|
||||
"minLength": 1
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Short description of plugin functionality",
|
||||
"minLength": 1
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Semantic version string (e.g., '1.0.0')",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$"
|
||||
},
|
||||
"author": {
|
||||
"type": "string",
|
||||
"description": "Plugin creator name or email",
|
||||
"minLength": 1
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Plugin type",
|
||||
"enum": ["widget", "daemon", "launcher", "desktop"]
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"description": "Array of plugin capabilities",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"component": {
|
||||
"type": "string",
|
||||
"description": "Relative path to main QML component file",
|
||||
"pattern": "^\\./.*\\.qml$"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"description": "Trigger string for launcher activation (required for launcher type)"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Material Design icon name"
|
||||
},
|
||||
"settings": {
|
||||
"type": "string",
|
||||
"description": "Path to settings component QML file",
|
||||
"pattern": "^\\./.*\\.qml$"
|
||||
},
|
||||
"requires_dms": {
|
||||
"type": "string",
|
||||
"description": "Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')",
|
||||
"pattern": "^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"requires": {
|
||||
"type": "array",
|
||||
"description": "Array of required system tools/dependencies",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"description": "Required capabilities",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"settings_read",
|
||||
"settings_write",
|
||||
"process",
|
||||
"network"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "launcher"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["trigger"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
property var popoutService: null
|
||||
|
||||
// TODO: Read configuration from settings
|
||||
property string configValue: pluginData?.configValue || ""
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
configValue = pluginService.loadPluginData(pluginId, "configValue", "")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Connect to the service events you need
|
||||
// Connections {
|
||||
// target: SessionData
|
||||
// function onWallpaperPathChanged() {
|
||||
// console.log("[MyDaemon] Wallpaper changed:", SessionData.wallpaperPath)
|
||||
// handleEvent(SessionData.wallpaperPath)
|
||||
// }
|
||||
// }
|
||||
|
||||
function handleEvent(data) {
|
||||
Proc.runCommand(
|
||||
"myDaemon.handle",
|
||||
["echo", "Event received:", data],
|
||||
(stdout, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("[MyDaemon] Output:", stdout)
|
||||
} else {
|
||||
console.error("[MyDaemon] Failed:", exitCode)
|
||||
ToastService?.showInfo("Daemon action failed")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("[MyDaemon] Started")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myDaemon"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "configValue"
|
||||
label: "Configuration"
|
||||
description: "Value used by the daemon"
|
||||
placeholder: "Enter value..."
|
||||
defaultValue: ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "myDaemon",
|
||||
"name": "My Daemon",
|
||||
"description": "A background service that reacts to events",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "daemon",
|
||||
"capabilities": ["background-service"],
|
||||
"component": "./Daemon.qml",
|
||||
"icon": "settings",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write", "process"]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myDesktopWidget"
|
||||
|
||||
SliderSetting {
|
||||
settingKey: "opacity"
|
||||
label: "Opacity"
|
||||
description: "Widget background opacity"
|
||||
defaultValue: 85
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string pluginId: ""
|
||||
property bool editMode: false
|
||||
property real widgetWidth: 200
|
||||
property real widgetHeight: 200
|
||||
property real minWidth: 150
|
||||
property real minHeight: 150
|
||||
|
||||
// TODO: Load settings reactively
|
||||
property real bgOpacity: {
|
||||
if (!pluginService) return 0.85
|
||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
||||
return val / 100
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
||||
bgOpacity = val / 100
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: root.bgOpacity
|
||||
border.color: root.editMode ? Theme.primary : "transparent"
|
||||
border.width: root.editMode ? 2 : 0
|
||||
|
||||
// TODO: Add your widget content here
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Desktop Widget"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "myDesktopWidget",
|
||||
"name": "My Desktop Widget",
|
||||
"description": "A custom desktop widget",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "desktop",
|
||||
"capabilities": ["desktop-widget"],
|
||||
"component": "./Widget.qml",
|
||||
"icon": "widgets",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
signal itemsChanged()
|
||||
|
||||
// TODO: Define your items
|
||||
property var allItems: [
|
||||
{
|
||||
name: "Example Item",
|
||||
icon: "material:star",
|
||||
comment: "An example launcher item",
|
||||
action: "toast:Hello from my launcher!",
|
||||
categories: ["MyLauncher"]
|
||||
}
|
||||
]
|
||||
|
||||
function getItems(query) {
|
||||
if (!query || query.length === 0) return allItems
|
||||
|
||||
var q = query.toLowerCase()
|
||||
return allItems.filter(function(item) {
|
||||
return item.name.toLowerCase().includes(q) ||
|
||||
item.comment.toLowerCase().includes(q)
|
||||
})
|
||||
}
|
||||
|
||||
function executeItem(item) {
|
||||
var actionParts = item.action.split(":")
|
||||
var actionType = actionParts[0]
|
||||
var actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "toast":
|
||||
if (typeof ToastService !== "undefined")
|
||||
ToastService.showInfo(actionData)
|
||||
break
|
||||
case "copy":
|
||||
Quickshell.execDetached(["dms", "cl", "copy", actionData])
|
||||
if (typeof ToastService !== "undefined")
|
||||
ToastService.showInfo("Copied to clipboard")
|
||||
break
|
||||
default:
|
||||
console.warn("Unknown action type:", actionType)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("myLauncher", "trigger", "#")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myLauncher"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "trigger"
|
||||
label: "Trigger"
|
||||
description: "Type this prefix in the launcher to activate the plugin"
|
||||
placeholder: "#"
|
||||
defaultValue: "#"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "noTrigger"
|
||||
label: "Always Visible"
|
||||
description: "Show items alongside regular apps without needing a trigger"
|
||||
defaultValue: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "myLauncher",
|
||||
"name": "My Launcher",
|
||||
"description": "Custom launcher plugin with searchable items",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "launcher",
|
||||
"capabilities": ["launcher"],
|
||||
"component": "./Launcher.qml",
|
||||
"trigger": "#",
|
||||
"icon": "search",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myWidget"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "text"
|
||||
label: "Display Text"
|
||||
description: "Text shown in the bar widget"
|
||||
placeholder: "Hello"
|
||||
defaultValue: "Hello"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "showIcon"
|
||||
label: "Show Icon"
|
||||
description: "Display an icon next to the text"
|
||||
defaultValue: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
property var popoutService: null
|
||||
|
||||
// TODO: Read settings reactively
|
||||
property string displayText: pluginData?.text || "Hello"
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
displayText = pluginService.loadPluginData(pluginId, "text", "Hello")
|
||||
}
|
||||
}
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: label.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.displayText
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: label.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.displayText
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
rotation: 90
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Uncomment and customize popout content
|
||||
// popoutWidth: 350
|
||||
// popoutHeight: 300
|
||||
// popoutContent: Component {
|
||||
// PopoutComponent {
|
||||
// headerText: "My Widget"
|
||||
// showCloseButton: true
|
||||
//
|
||||
// Column {
|
||||
// width: parent.width
|
||||
// spacing: Theme.spacingM
|
||||
//
|
||||
// StyledText {
|
||||
// text: "Popout content here"
|
||||
// color: Theme.surfaceText
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "myWidget",
|
||||
"name": "My Widget",
|
||||
"description": "A custom bar widget",
|
||||
"version": "1.0.0",
|
||||
"author": "Your Name",
|
||||
"type": "widget",
|
||||
"capabilities": ["dankbar-widget"],
|
||||
"component": "./Widget.qml",
|
||||
"icon": "extension",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
320
.agents/skills/dms-plugin-dev/references/advanced-patterns.md
Normal file
320
.agents/skills/dms-plugin-dev/references/advanced-patterns.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Advanced Patterns
|
||||
|
||||
Patterns observed in production DMS plugins that go beyond the basics.
|
||||
|
||||
## Plugin Variants
|
||||
|
||||
Create multiple widget instances from a single plugin definition. Each variant has its own configuration.
|
||||
|
||||
### Manifest
|
||||
|
||||
No special manifest changes needed - the variant system is built into PluginComponent.
|
||||
|
||||
### Widget with Variant Support
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property string variantId: ""
|
||||
property var variantData: ({})
|
||||
|
||||
property string displayText: variantData?.text || "Default"
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: label.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.displayText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Widget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)
|
||||
|
||||
### Settings with Variant Management
|
||||
|
||||
```qml
|
||||
PluginSettings {
|
||||
pluginId: "exampleVariants"
|
||||
|
||||
// Variant creation UI
|
||||
DankButton {
|
||||
text: "Add New Instance"
|
||||
onClicked: {
|
||||
var id = "variant_" + Date.now()
|
||||
root.createVariant(id, { name: "New Instance", text: "Hello" })
|
||||
}
|
||||
}
|
||||
|
||||
// Per-variant configuration
|
||||
Repeater {
|
||||
model: root.variants
|
||||
delegate: Column {
|
||||
StringSetting {
|
||||
settingKey: modelData.id + "_text"
|
||||
label: modelData.name || modelData.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript Utility Files
|
||||
|
||||
For complex logic, split into `.js` files:
|
||||
|
||||
### utils.js
|
||||
|
||||
```javascript
|
||||
.pragma library
|
||||
|
||||
function formatDuration(ms) {
|
||||
if (ms < 60000) return "just now"
|
||||
if (ms < 3600000) return Math.floor(ms / 60000) + "m ago"
|
||||
return Math.floor(ms / 3600000) + "h ago"
|
||||
}
|
||||
|
||||
function parseResponse(json) {
|
||||
try {
|
||||
return JSON.parse(json)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using in QML
|
||||
|
||||
```qml
|
||||
import "utils.js" as Utils
|
||||
|
||||
Item {
|
||||
StyledText {
|
||||
text: Utils.formatDuration(Date.now() - timestamp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.
|
||||
|
||||
## qmldir for Singleton Services
|
||||
|
||||
For plugins with internal singleton services:
|
||||
|
||||
### qmldir
|
||||
|
||||
```
|
||||
singleton MyService 1.0 MyService.qml
|
||||
```
|
||||
|
||||
### MyService.qml
|
||||
|
||||
```qml
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property var cache: ({})
|
||||
|
||||
function getData(key) {
|
||||
return cache[key] || null
|
||||
}
|
||||
|
||||
function setData(key, value) {
|
||||
cache[key] = value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using the singleton
|
||||
|
||||
```qml
|
||||
import "." as Local
|
||||
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
Local.MyService.setData("key", "value")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Inline Component Declarations
|
||||
|
||||
Reusable sub-components defined inline:
|
||||
|
||||
```qml
|
||||
Item {
|
||||
component StatusBadge: Rectangle {
|
||||
property string label: ""
|
||||
property color badgeColor: Theme.primary
|
||||
|
||||
width: badgeText.implicitWidth + Theme.spacingM * 2
|
||||
height: 24
|
||||
radius: 12
|
||||
color: badgeColor
|
||||
|
||||
StyledText {
|
||||
id: badgeText
|
||||
anchors.centerIn: parent
|
||||
text: label
|
||||
color: Theme.onPrimary
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
StatusBadge { label: "Running"; badgeColor: Theme.success }
|
||||
StatusBadge { label: "Stopped"; badgeColor: Theme.error }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Provider Adapter Pattern
|
||||
|
||||
For plugins supporting multiple backends (AI providers, API services):
|
||||
|
||||
### apiAdapters.js
|
||||
|
||||
```javascript
|
||||
.pragma library
|
||||
|
||||
function createAdapter(provider) {
|
||||
switch (provider) {
|
||||
case "openai": return {
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
headers: (key) => ({ "Authorization": "Bearer " + key }),
|
||||
formatRequest: (messages) => JSON.stringify({ model: "gpt-4", messages: messages }),
|
||||
parseResponse: (text) => JSON.parse(text).choices[0].message.content
|
||||
}
|
||||
case "anthropic": return {
|
||||
url: "https://api.anthropic.com/v1/messages",
|
||||
headers: (key) => ({ "x-api-key": key, "anthropic-version": "2023-06-01" }),
|
||||
formatRequest: (messages) => JSON.stringify({ model: "claude-sonnet-4-20250514", messages: messages }),
|
||||
parseResponse: (text) => JSON.parse(text).content[0].text
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## IPC Integration
|
||||
|
||||
For plugins that respond to keyboard shortcuts or external commands:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
Connections {
|
||||
target: DMSIpc
|
||||
function onCommandReceived(command, args) {
|
||||
if (command === "myPlugin.toggle") {
|
||||
doToggle()
|
||||
} else if (command === "myPlugin.next") {
|
||||
goNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
External trigger: `dms ipc call myPlugin.toggle`
|
||||
|
||||
## Networking with Quickshell.Networking
|
||||
|
||||
For API calls using the built-in networking module:
|
||||
|
||||
```qml
|
||||
import Quickshell.Networking
|
||||
|
||||
Item {
|
||||
NetworkRequest {
|
||||
id: request
|
||||
url: "https://api.example.com/data"
|
||||
method: "GET"
|
||||
|
||||
onResponseReceived: (response) => {
|
||||
const data = JSON.parse(response.body)
|
||||
processData(data)
|
||||
}
|
||||
|
||||
onErrorOccurred: (error) => {
|
||||
console.error("Network error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchData() {
|
||||
request.send()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Toast Notifications
|
||||
|
||||
Show user feedback:
|
||||
|
||||
```qml
|
||||
import qs.Services
|
||||
|
||||
// Info toast
|
||||
ToastService?.showInfo("Operation completed")
|
||||
|
||||
// With title
|
||||
ToastService?.showInfo("Plugin Name", "Data refreshed successfully")
|
||||
```
|
||||
|
||||
Always use optional chaining since ToastService may not be available in all contexts.
|
||||
|
||||
## Clipboard Operations
|
||||
|
||||
```qml
|
||||
import Quickshell
|
||||
|
||||
function copyToClipboard(text) {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", text])
|
||||
ToastService?.showInfo("Copied to clipboard")
|
||||
}
|
||||
```
|
||||
|
||||
Do NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.
|
||||
|
||||
## Multi-File Plugin Architecture
|
||||
|
||||
Large plugins can be split across multiple files:
|
||||
|
||||
```
|
||||
MyPlugin/
|
||||
plugin.json
|
||||
Main.qml # Main widget component
|
||||
Settings.qml # Settings UI
|
||||
DetailView.qml # Popout detail view
|
||||
utils.js # Utility functions
|
||||
apiAdapter.js # API adapter layer
|
||||
qmldir # Optional: singleton registrations
|
||||
```
|
||||
|
||||
Import sibling files:
|
||||
|
||||
```qml
|
||||
// In Main.qml
|
||||
import "." as Local
|
||||
|
||||
Item {
|
||||
Loader {
|
||||
source: "DetailView.qml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. Use `Proc.runCommand` with appropriate debounce for external commands
|
||||
2. Pre-cache images and thumbnails for image-heavy plugins
|
||||
3. Limit concurrent network requests
|
||||
4. Use `Timer` with reasonable intervals (don't poll faster than needed)
|
||||
5. Lazy-load heavy content (use `Loader` for complex popout content)
|
||||
6. Avoid blocking the UI thread with synchronous operations
|
||||
272
.agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md
Normal file
272
.agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md
Normal file
@@ -0,0 +1,272 @@
|
||||
# Daemon Plugin Guide
|
||||
|
||||
Daemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.
|
||||
|
||||
## Base Component
|
||||
|
||||
Daemons use `PluginComponent` with no bar pills:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
property var popoutService: null
|
||||
|
||||
// Event-driven logic goes here
|
||||
}
|
||||
```
|
||||
|
||||
## When to Use Daemons
|
||||
|
||||
- Monitor system events (wallpaper changes, battery level, notifications)
|
||||
- Run periodic background tasks (polling APIs, checking system state)
|
||||
- Execute scripts in response to events
|
||||
- Control shell UI via PopoutService based on conditions
|
||||
|
||||
## Event-Driven Pattern
|
||||
|
||||
Use `Connections` to react to service signals:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property var popoutService: null
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onWallpaperPathChanged() {
|
||||
console.log("Wallpaper changed to:", SessionData.wallpaperPath)
|
||||
runScript(SessionData.wallpaperPath)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: BatteryService
|
||||
function onPercentageChanged() {
|
||||
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
|
||||
popoutService?.openBattery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Services
|
||||
|
||||
Common services daemons can connect to:
|
||||
|
||||
| Service | Signals/Properties | Description |
|
||||
|---------|-------------------|-------------|
|
||||
| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |
|
||||
| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |
|
||||
| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |
|
||||
| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |
|
||||
|
||||
Import services from `qs.Services`.
|
||||
|
||||
## Process Execution
|
||||
|
||||
### Simple command with Proc
|
||||
|
||||
```qml
|
||||
import qs.Common
|
||||
|
||||
PluginComponent {
|
||||
function runScript(arg) {
|
||||
Proc.runCommand(
|
||||
"myDaemon.script",
|
||||
["bash", "-c", "echo 'Processing: " + arg + "'"],
|
||||
(stdout, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Script output:", stdout)
|
||||
} else {
|
||||
ToastService?.showInfo("Script failed: exit " + exitCode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Long-running process with Process component
|
||||
|
||||
```qml
|
||||
import Quickshell.Io
|
||||
|
||||
PluginComponent {
|
||||
property string scriptPath: ""
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
command: ["bash", scriptPath]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onTextReceived: (text) => {
|
||||
console.log("stdout:", text)
|
||||
}
|
||||
}
|
||||
|
||||
stderr: StdioCollector {
|
||||
onTextReceived: (text) => {
|
||||
console.error("stderr:", text)
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
ToastService?.showInfo("Process failed: exit " + exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startProcess() {
|
||||
if (scriptPath && !proc.running) {
|
||||
proc.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Timer-Based Polling
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
Timer {
|
||||
interval: 60000 // every minute
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: checkStatus()
|
||||
}
|
||||
|
||||
function checkStatus() {
|
||||
Proc.runCommand(
|
||||
"myDaemon.check",
|
||||
["sh", "-c", "systemctl is-active myservice"],
|
||||
(stdout, exitCode) => {
|
||||
const active = stdout.trim() === "active"
|
||||
PluginService.setGlobalVar("myDaemon", "serviceActive", active)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Daemons access PluginService directly (it's injected via PluginComponent):
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property string configuredScript: pluginData?.scriptPath || ""
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId === pluginId) {
|
||||
configuredScript = pluginService.loadPluginData(pluginId, "scriptPath", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PopoutService Usage
|
||||
|
||||
Daemons can control shell UI via the injected popoutService:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property var popoutService: null
|
||||
|
||||
function showAlert() {
|
||||
popoutService?.openNotificationCenter()
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
popoutService?.openSettings()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [popout-service-reference.md](popout-service-reference.md) for the full API.
|
||||
|
||||
## Complete Example
|
||||
|
||||
Based on the WallpaperWatcherDaemon:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
property var popoutService: null
|
||||
|
||||
property string scriptPath: pluginData?.scriptPath || ""
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId === pluginId) {
|
||||
scriptPath = pluginService.loadPluginData(pluginId, "scriptPath", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onWallpaperPathChanged() {
|
||||
if (scriptPath) {
|
||||
runWallpaperScript(SessionData.wallpaperPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runWallpaperScript(wallpaperPath) {
|
||||
console.log("[WallpaperWatcher] Running script:", scriptPath, wallpaperPath)
|
||||
|
||||
Proc.runCommand(
|
||||
"wallpaperWatcher.run",
|
||||
["bash", scriptPath, wallpaperPath],
|
||||
(stdout, exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("[WallpaperWatcher] Script output:", stdout)
|
||||
} else {
|
||||
console.error("[WallpaperWatcher] Script failed:", exitCode)
|
||||
ToastService?.showInfo("Wallpaper script failed")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("[WallpaperWatcher] Daemon started")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manifest Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "wallpaperWatcher",
|
||||
"name": "Wallpaper Watcher",
|
||||
"description": "Runs a script when the wallpaper changes",
|
||||
"version": "1.0.0",
|
||||
"author": "Developer",
|
||||
"type": "daemon",
|
||||
"capabilities": ["wallpaper-automation"],
|
||||
"component": "./WallpaperWatcher.qml",
|
||||
"icon": "wallpaper",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write", "process"]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
# Data Persistence Guide
|
||||
|
||||
DMS plugins have three tiers of data persistence, each suited for different use cases.
|
||||
|
||||
## Tier 1: Plugin Data (Settings)
|
||||
|
||||
Persisted to `settings.json`. Use for user preferences and configuration.
|
||||
|
||||
### Saving
|
||||
|
||||
```qml
|
||||
pluginService.savePluginData(pluginId, "key", value)
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
```qml
|
||||
var value = pluginService.loadPluginData(pluginId, "key", defaultValue)
|
||||
```
|
||||
|
||||
### Reactive Access via pluginData
|
||||
|
||||
`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property string displayText: pluginData?.text || "Default"
|
||||
property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true
|
||||
}
|
||||
```
|
||||
|
||||
### Reacting to Settings Changes
|
||||
|
||||
When settings are changed (e.g., from the settings UI), react with `Connections`:
|
||||
|
||||
```qml
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
|
||||
showIcon = pluginService.loadPluginData(pluginId, "showIcon", true)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tier 2: Plugin State
|
||||
|
||||
Persisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).
|
||||
|
||||
### Saving
|
||||
|
||||
```qml
|
||||
pluginService.savePluginState(pluginId, "key", value)
|
||||
```
|
||||
|
||||
### Loading
|
||||
|
||||
```qml
|
||||
var state = pluginService.loadPluginState(pluginId, "key", defaultValue)
|
||||
```
|
||||
|
||||
### Additional Methods
|
||||
|
||||
```qml
|
||||
pluginService.clearPluginState(pluginId)
|
||||
pluginService.removePluginStateKey(pluginId, "key")
|
||||
```
|
||||
|
||||
### Example: Persistent History
|
||||
|
||||
```qml
|
||||
Item {
|
||||
property var history: []
|
||||
|
||||
Component.onCompleted: {
|
||||
history = pluginService?.loadPluginState(pluginId, "history", []) || []
|
||||
}
|
||||
|
||||
function addToHistory(entry) {
|
||||
history.unshift({
|
||||
text: entry,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
if (history.length > 100) history = history.slice(0, 100)
|
||||
pluginService?.savePluginState(pluginId, "history", history)
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
history = []
|
||||
pluginService?.removePluginStateKey(pluginId, "history")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tier 3: Global Variables (Runtime Only)
|
||||
|
||||
NOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).
|
||||
|
||||
### Using PluginGlobalVar Component
|
||||
|
||||
```qml
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
PluginGlobalVar {
|
||||
id: globalCounter
|
||||
varName: "counter"
|
||||
defaultValue: 0
|
||||
}
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
// ...
|
||||
StyledText {
|
||||
text: "Count: " + globalCounter.value
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
onClicked: globalCounter.set(globalCounter.value + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PluginGlobalVar properties:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `varName` | string | Required: name of the global variable |
|
||||
| `defaultValue` | any | Optional: default if not set |
|
||||
| `value` | any | Readonly: current value |
|
||||
|
||||
**Methods:**
|
||||
- `set(newValue)` - update the value (triggers reactivity across all instances)
|
||||
|
||||
### Using PluginService API Directly
|
||||
|
||||
```qml
|
||||
import qs.Services
|
||||
|
||||
property int counter: PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
|
||||
Connections {
|
||||
target: PluginService
|
||||
function onGlobalVarChanged(pluginId, varName) {
|
||||
if (pluginId === "myPlugin" && varName === "counter") {
|
||||
counter = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function increment() {
|
||||
var current = PluginService.getGlobalVar("myPlugin", "counter", 0)
|
||||
PluginService.setGlobalVar("myPlugin", "counter", current + 1)
|
||||
}
|
||||
```
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Need | API | Persisted | Scope |
|
||||
|------|-----|-----------|-------|
|
||||
| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |
|
||||
| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |
|
||||
| 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 |
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **pluginData is reactive** - bindings update automatically when data changes
|
||||
2. **Global vars are NOT persistent** - they reset when the shell restarts
|
||||
3. **State vs Data** - data is for user-facing settings, state is for internal runtime data
|
||||
4. **Null safety** - always check `pluginService` is not null before calling methods
|
||||
5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin
|
||||
6. **Performance** - global vars are efficient for frequent updates; settings writes are batched
|
||||
240
.agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md
Normal file
240
.agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# Desktop Plugin Guide
|
||||
|
||||
Desktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.
|
||||
|
||||
## Base Component
|
||||
|
||||
Desktop widgets use a plain `Item` with injected properties:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string pluginId: ""
|
||||
property bool editMode: false
|
||||
property real widgetWidth: 200
|
||||
property real widgetHeight: 200
|
||||
property real minWidth: 150
|
||||
property real minHeight: 150
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: 0.85
|
||||
|
||||
// Your content here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Injected Properties
|
||||
|
||||
These are set automatically by the DesktopPluginWrapper:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `pluginService` | object | PluginService reference for data persistence |
|
||||
| `pluginId` | string | Plugin's unique identifier |
|
||||
| `editMode` | bool | `true` when user is dragging/resizing |
|
||||
| `widgetWidth` | real | Current widget container width |
|
||||
| `widgetHeight` | real | Current widget container height |
|
||||
|
||||
## Optional Properties
|
||||
|
||||
Define these on your root item to customize behavior:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `minWidth` | real | 100 | Minimum allowed width during resize |
|
||||
| `minHeight` | real | 100 | Minimum allowed height during resize |
|
||||
|
||||
## Position and Size Persistence
|
||||
|
||||
Position (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.
|
||||
|
||||
## Edit Mode
|
||||
|
||||
When `editMode` is true, the user is repositioning or resizing. Use this to:
|
||||
- Show visual indicators (borders, handles)
|
||||
- Disable interactive elements to prevent accidental actions
|
||||
- Display additional controls
|
||||
|
||||
```qml
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
border.color: root.editMode ? Theme.primary : "transparent"
|
||||
border.width: root.editMode ? 2 : 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: !root.editMode
|
||||
onClicked: doSomething()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Loading and Saving Data
|
||||
|
||||
Use the injected `pluginService` for data persistence:
|
||||
|
||||
```qml
|
||||
property string displayMode: {
|
||||
if (!pluginService) return "default"
|
||||
return pluginService.loadPluginData(pluginId, "displayMode", "default")
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
root.displayMode = pluginService.loadPluginData(pluginId, "displayMode", "default")
|
||||
}
|
||||
}
|
||||
|
||||
function saveMode(mode) {
|
||||
pluginService?.savePluginData(pluginId, "displayMode", mode)
|
||||
}
|
||||
```
|
||||
|
||||
## Settings Component
|
||||
|
||||
Desktop plugin settings use the same `PluginSettings` wrapper as other types:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myDesktopWidget"
|
||||
|
||||
SliderSetting {
|
||||
settingKey: "opacity"
|
||||
label: "Opacity"
|
||||
description: "Widget background opacity"
|
||||
defaultValue: 85
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
}
|
||||
|
||||
SelectionSetting {
|
||||
settingKey: "style"
|
||||
label: "Display Style"
|
||||
options: [
|
||||
{ label: "Compact", value: "compact" },
|
||||
{ label: "Expanded", value: "expanded" }
|
||||
]
|
||||
defaultValue: "compact"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## User Interaction
|
||||
|
||||
Desktop widgets support:
|
||||
1. **Drag** - click and drag anywhere (in edit mode)
|
||||
2. **Resize** - drag bottom-right corner handle (in edit mode)
|
||||
3. **Edit mode toggle** - via the desktop edit button
|
||||
|
||||
## Complete Example
|
||||
|
||||
Based on the ExampleDesktopClock pattern:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string pluginId: ""
|
||||
property bool editMode: false
|
||||
property real widgetWidth: 250
|
||||
property real widgetHeight: 250
|
||||
property real minWidth: 150
|
||||
property real minHeight: 150
|
||||
|
||||
property string clockStyle: {
|
||||
if (!pluginService) return "digital"
|
||||
return pluginService.loadPluginData(pluginId, "clockStyle", "digital")
|
||||
}
|
||||
|
||||
property real bgOpacity: {
|
||||
if (!pluginService) return 0.85
|
||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
||||
return val / 100
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId !== pluginId) return
|
||||
clockStyle = pluginService.loadPluginData(pluginId, "clockStyle", "digital")
|
||||
var val = pluginService.loadPluginData(pluginId, "opacity", 85)
|
||||
bgOpacity = val / 100
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: root.bgOpacity
|
||||
border.color: root.editMode ? Theme.primary : "transparent"
|
||||
border.width: root.editMode ? 2 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Qt.formatTime(new Date(), "hh:mm:ss")
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: root.widgetWidth * 0.15
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: Qt.formatDate(new Date(), "ddd, MMM d")
|
||||
color: Theme.onSurfaceVariant
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.widgetWidth = root.widgetWidth // force update
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Manifest Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myDesktopClock",
|
||||
"name": "Desktop Clock",
|
||||
"description": "Analog and digital clock for the desktop",
|
||||
"version": "1.0.0",
|
||||
"author": "Developer",
|
||||
"type": "desktop",
|
||||
"capabilities": ["desktop-widget"],
|
||||
"component": "./ClockWidget.qml",
|
||||
"icon": "schedule",
|
||||
"settings": "./Settings.qml",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,308 @@
|
||||
# Launcher Plugin Guide
|
||||
|
||||
Launcher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.
|
||||
|
||||
## Base Component
|
||||
|
||||
Launchers use a plain `Item` (not PluginComponent):
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string trigger: "#"
|
||||
|
||||
signal itemsChanged()
|
||||
|
||||
function getItems(query) {
|
||||
// Return array of items
|
||||
return []
|
||||
}
|
||||
|
||||
function executeItem(item) {
|
||||
// Handle item selection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Required Interface
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `pluginService` | property | Injected PluginService reference (declare as `null`) |
|
||||
| `trigger` | property | Trigger string for activation |
|
||||
| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |
|
||||
| `getItems(query)` | function | Return array of items matching query |
|
||||
| `executeItem(item)` | function | Handle item selection |
|
||||
|
||||
## Item Structure
|
||||
|
||||
Each item returned by `getItems()`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
name: "Item Display Name", // Required: shown in launcher
|
||||
icon: "material:star", // Optional: icon specification
|
||||
comment: "Description text", // Required: subtitle text
|
||||
action: "type:data", // Required: action identifier
|
||||
categories: ["MyPlugin"], // Required: array with plugin category
|
||||
imageUrl: "https://..." // Optional: image for tile view
|
||||
}
|
||||
```
|
||||
|
||||
## Icon Types
|
||||
|
||||
### 1. Material Design Icons
|
||||
|
||||
```javascript
|
||||
{ icon: "material:lightbulb" }
|
||||
{ icon: "material:terminal" }
|
||||
{ icon: "material:translate" }
|
||||
```
|
||||
|
||||
Uses the Material Symbols Rounded font.
|
||||
|
||||
### 2. Unicode / Emoji Icons
|
||||
|
||||
```javascript
|
||||
{ icon: "unicode:smile_face" }
|
||||
```
|
||||
|
||||
Rendered at 70-80% of icon size with theming.
|
||||
|
||||
### 3. Desktop Theme Icons
|
||||
|
||||
```javascript
|
||||
{ icon: "firefox" }
|
||||
{ icon: "folder" }
|
||||
```
|
||||
|
||||
Uses the user's installed icon theme.
|
||||
|
||||
### 4. No Icon
|
||||
|
||||
Omit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.
|
||||
|
||||
## Trigger System
|
||||
|
||||
**Custom trigger** (items only appear when trigger is typed):
|
||||
|
||||
```json
|
||||
{ "trigger": "#" }
|
||||
```
|
||||
|
||||
- Type `#` alone: shows all plugin items
|
||||
- Type `# query`: filters plugin items by query
|
||||
- The query string (without trigger) is passed to `getItems(query)`
|
||||
|
||||
**No trigger** (items always visible alongside regular apps):
|
||||
|
||||
```json
|
||||
{ "trigger": "" }
|
||||
```
|
||||
|
||||
Save empty trigger at runtime:
|
||||
```qml
|
||||
Component.onCompleted: {
|
||||
trigger = pluginService?.loadPluginData(pluginId, "trigger", "#") ?? "#"
|
||||
}
|
||||
```
|
||||
|
||||
## Action Execution
|
||||
|
||||
Parse action strings in `executeItem()`:
|
||||
|
||||
```qml
|
||||
function executeItem(item) {
|
||||
const actionParts = item.action.split(":")
|
||||
const actionType = actionParts[0]
|
||||
const actionData = actionParts.slice(1).join(":")
|
||||
|
||||
switch (actionType) {
|
||||
case "toast":
|
||||
ToastService?.showInfo(actionData)
|
||||
break
|
||||
case "copy":
|
||||
Quickshell.execDetached(["dms", "cl", "copy", actionData])
|
||||
ToastService?.showInfo("Copied to clipboard")
|
||||
break
|
||||
case "exec":
|
||||
Quickshell.execDetached(actionData.split(" "))
|
||||
break
|
||||
case "url":
|
||||
Quickshell.execDetached(["xdg-open", actionData])
|
||||
break
|
||||
default:
|
||||
console.warn("Unknown action type:", actionType)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Search / Filtering
|
||||
|
||||
The `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).
|
||||
|
||||
```qml
|
||||
function getItems(query) {
|
||||
const allItems = [
|
||||
{ name: "Calculator", icon: "material:calculate",
|
||||
comment: "Open calculator", action: "exec:gnome-calculator",
|
||||
categories: ["Tools"] },
|
||||
{ name: "Terminal", icon: "material:terminal",
|
||||
comment: "Open terminal", action: "exec:alacritty",
|
||||
categories: ["Tools"] }
|
||||
]
|
||||
|
||||
if (!query || query.length === 0) return allItems
|
||||
|
||||
const q = query.toLowerCase()
|
||||
return allItems.filter(item =>
|
||||
item.name.toLowerCase().includes(q) ||
|
||||
item.comment.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Context Menu Actions
|
||||
|
||||
Add right-click actions to launcher items:
|
||||
|
||||
```qml
|
||||
function getContextMenuActions(item) {
|
||||
return [
|
||||
{ name: "Copy", icon: "material:content_copy",
|
||||
action: "copy:" + item.name },
|
||||
{ name: "Open in Browser", icon: "material:open_in_new",
|
||||
action: "url:" + item.url }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Context menu actions use the same `executeItem()` handler.
|
||||
|
||||
## Image Tile View
|
||||
|
||||
For image-heavy launchers (GIF search, sticker pickers), use tile view:
|
||||
|
||||
In `plugin.json`:
|
||||
```json
|
||||
{
|
||||
"viewMode": "tile",
|
||||
"viewModeEnforced": true
|
||||
}
|
||||
```
|
||||
|
||||
In items:
|
||||
```javascript
|
||||
{
|
||||
name: "Image Title",
|
||||
imageUrl: "https://example.com/image.png",
|
||||
comment: "Description",
|
||||
action: "copy:https://example.com/image.png",
|
||||
categories: ["MyPlugin"]
|
||||
}
|
||||
```
|
||||
|
||||
## State Persistence
|
||||
|
||||
For plugins with persistent state (notes, history, favorites):
|
||||
|
||||
```qml
|
||||
property var notes: []
|
||||
|
||||
Component.onCompleted: {
|
||||
const saved = pluginService?.loadPluginState(pluginId, "notes", [])
|
||||
if (saved) notes = saved
|
||||
}
|
||||
|
||||
function addNote(text) {
|
||||
notes.push({ text: text, timestamp: Date.now() })
|
||||
pluginService?.savePluginState(pluginId, "notes", notes)
|
||||
itemsChanged()
|
||||
}
|
||||
```
|
||||
|
||||
Use `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.
|
||||
|
||||
## Settings for Trigger Configuration
|
||||
|
||||
Provide a PluginSettings component for trigger customization:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "myLauncher"
|
||||
|
||||
StringSetting {
|
||||
settingKey: "trigger"
|
||||
label: "Trigger"
|
||||
description: "Type this prefix to activate the launcher plugin"
|
||||
placeholder: "#"
|
||||
defaultValue: "#"
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "noTrigger"
|
||||
label: "Always Visible"
|
||||
description: "Show items alongside regular apps (no trigger needed)"
|
||||
defaultValue: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginService: null
|
||||
property string trigger: "!"
|
||||
|
||||
signal itemsChanged()
|
||||
|
||||
property var commands: [
|
||||
{ name: "Lock Screen", icon: "material:lock",
|
||||
comment: "Lock the session", action: "exec:loginctl lock-session" },
|
||||
{ name: "Screenshot", icon: "material:screenshot_monitor",
|
||||
comment: "Take a screenshot", action: "exec:grim" },
|
||||
{ name: "File Manager", icon: "material:folder",
|
||||
comment: "Open file manager", action: "exec:nautilus" }
|
||||
]
|
||||
|
||||
function getItems(query) {
|
||||
if (!query) return commands
|
||||
const q = query.toLowerCase()
|
||||
return commands.filter(c =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
c.comment.toLowerCase().includes(q)
|
||||
)
|
||||
}
|
||||
|
||||
function executeItem(item) {
|
||||
const [type, ...rest] = item.action.split(":")
|
||||
const data = rest.join(":")
|
||||
if (type === "exec") {
|
||||
Quickshell.execDetached(data.split(" "))
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (pluginService) {
|
||||
trigger = pluginService.loadPluginData("quickCommands", "trigger", "!")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Plugin Manifest Reference (plugin.json)
|
||||
|
||||
## Required Fields
|
||||
|
||||
| Field | Type | Description | Validation |
|
||||
|-------|------|-------------|------------|
|
||||
| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |
|
||||
| `name` | string | Human-readable name | 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.-]+)?$` |
|
||||
| `author` | string | Creator name or email | Non-empty |
|
||||
| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop` |
|
||||
| `capabilities` | array | Plugin capabilities | At least 1 string item |
|
||||
| `component` | string | Path to main QML file | Must start with `./`, end with `.qml` |
|
||||
|
||||
## Conditional Requirements
|
||||
|
||||
| Condition | Required Field | Description |
|
||||
|-----------|---------------|-------------|
|
||||
| `type: "launcher"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |
|
||||
|
||||
## Optional Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `icon` | string | Material Design icon name (displayed in plugin list UI) |
|
||||
| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |
|
||||
| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\|<=?\|=\|>\|<)\d+\.\d+\.\d+$` |
|
||||
| `requires` | array | System tool dependencies (e.g., `["curl", "jq"]`) |
|
||||
| `permissions` | array | Required permissions |
|
||||
| `trigger` | string | Launcher trigger string (required for launcher type) |
|
||||
|
||||
## Permissions
|
||||
|
||||
| Permission | Description | Enforced |
|
||||
|------------|-------------|----------|
|
||||
| `settings_read` | Read plugin configuration | No (not currently enforced) |
|
||||
| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |
|
||||
| `process` | Execute system commands | No (not currently enforced) |
|
||||
| `network` | Network access | No (not currently enforced) |
|
||||
|
||||
If your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Capabilities are free-form strings that describe what the plugin does. Common values:
|
||||
|
||||
- `dankbar-widget` - general bar widget
|
||||
- `control-center` - integrates with Control Center
|
||||
- `monitoring` - system/service monitoring
|
||||
- `launcher` - launcher search provider
|
||||
- `desktop-widget` - desktop background widget
|
||||
- `ai` - AI/LLM integration
|
||||
- `slideout` - uses slideout panel
|
||||
|
||||
## Complete Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myPlugin",
|
||||
"name": "My Plugin",
|
||||
"description": "A sample plugin demonstrating all fields",
|
||||
"version": "1.0.0",
|
||||
"author": "Developer Name",
|
||||
"type": "widget",
|
||||
"capabilities": ["dankbar-widget", "control-center"],
|
||||
"component": "./MyWidget.qml",
|
||||
"icon": "extension",
|
||||
"settings": "./Settings.qml",
|
||||
"requires_dms": ">=0.1.18",
|
||||
"requires": ["curl", "jq"],
|
||||
"permissions": ["settings_read", "settings_write", "process", "network"]
|
||||
}
|
||||
```
|
||||
|
||||
## Launcher Example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "myLauncher",
|
||||
"name": "My Launcher",
|
||||
"description": "Search and execute custom actions",
|
||||
"version": "1.0.0",
|
||||
"author": "Developer Name",
|
||||
"type": "launcher",
|
||||
"capabilities": ["launcher"],
|
||||
"component": "./MyLauncher.qml",
|
||||
"trigger": "#",
|
||||
"icon": "search",
|
||||
"settings": "./Settings.qml",
|
||||
"requires_dms": ">=0.1.18",
|
||||
"permissions": ["settings_read", "settings_write"]
|
||||
}
|
||||
```
|
||||
|
||||
## JSON Schema
|
||||
|
||||
The complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:
|
||||
|
||||
```bash
|
||||
# Using python
|
||||
python3 -c "
|
||||
import json, jsonschema
|
||||
schema = json.load(open('path/to/plugin-schema.json'))
|
||||
manifest = json.load(open('plugin.json'))
|
||||
jsonschema.validate(manifest, schema)
|
||||
print('Valid!')
|
||||
"
|
||||
|
||||
# Using jq (syntax check only)
|
||||
jq . plugin.json
|
||||
```
|
||||
|
||||
## Additional Properties
|
||||
|
||||
The schema allows additional properties (`"additionalProperties": true`), so plugins can include custom fields. Common custom fields seen in production plugins:
|
||||
|
||||
- `viewMode` - launcher display mode (`"tile"` for image grids)
|
||||
- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)
|
||||
@@ -0,0 +1,120 @@
|
||||
# PopoutService Reference
|
||||
|
||||
The `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.
|
||||
|
||||
## Setup
|
||||
|
||||
Declare the property in your component for injection to work:
|
||||
|
||||
```qml
|
||||
property var popoutService: null
|
||||
```
|
||||
|
||||
Without this declaration, injection fails with: `Cannot assign to non-existent property "popoutService"`
|
||||
|
||||
## Popouts (DankPopout-based)
|
||||
|
||||
| Component | Open | Close | Toggle |
|
||||
|-----------|------|-------|--------|
|
||||
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
|
||||
| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |
|
||||
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
|
||||
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
|
||||
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
|
||||
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
|
||||
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
|
||||
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
|
||||
|
||||
## Modals (DankModal-based)
|
||||
|
||||
| Modal | Show | Hide | Notes |
|
||||
|-------|------|------|-------|
|
||||
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
|
||||
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |
|
||||
| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |
|
||||
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
|
||||
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |
|
||||
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
|
||||
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
|
||||
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |
|
||||
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
|
||||
|
||||
## Slideouts
|
||||
|
||||
| Component | Open | Close | Toggle |
|
||||
|-----------|------|-------|--------|
|
||||
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Simple toggle
|
||||
|
||||
```qml
|
||||
MouseArea {
|
||||
onClicked: popoutService?.toggleControlCenter()
|
||||
}
|
||||
```
|
||||
|
||||
### Conditional popout
|
||||
|
||||
```qml
|
||||
Connections {
|
||||
target: BatteryService
|
||||
function onPercentageChanged() {
|
||||
if (BatteryService.percentage < 10 && !BatteryService.isCharging) {
|
||||
popoutService?.openBattery()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Context menu with multiple actions
|
||||
|
||||
```qml
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) contextMenu.popup()
|
||||
else popoutService?.toggleControlCenter()
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: contextMenu
|
||||
MenuItem { text: "Settings"; onClicked: popoutService?.openSettings() }
|
||||
MenuItem { text: "Notifications"; onClicked: popoutService?.toggleNotificationCenter() }
|
||||
MenuItem { text: "Power"; onClicked: popoutService?.openPowerMenu() }
|
||||
}
|
||||
```
|
||||
|
||||
### Position-aware toggle (from bar pill)
|
||||
|
||||
Some toggle functions accept position parameters for proper popout placement:
|
||||
|
||||
```qml
|
||||
pillClickAction: (x, y, width, section, screen) => {
|
||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use optional chaining** (`?.`) - the service may not be injected yet
|
||||
2. **Check feature availability** before opening feature-specific popouts:
|
||||
```qml
|
||||
if (BatteryService.batteryAvailable) {
|
||||
popoutService?.openBattery()
|
||||
}
|
||||
```
|
||||
3. **Lazy loading** - first access may activate lazy loaders; this is normal
|
||||
4. **Popouts are shared** - avoid opening conflicting popouts simultaneously
|
||||
5. **User intent** - only trigger popouts from user actions or critical system events
|
||||
6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters
|
||||
|
||||
## Injection Locations
|
||||
|
||||
The service is injected at these points:
|
||||
- `DMSShell.qml` - daemon plugins
|
||||
- `WidgetHost.qml` - widget plugins in left/right bar sections
|
||||
- `CenterSection.qml` - center bar widgets
|
||||
- `PluginsTab.qml` - settings components
|
||||
@@ -0,0 +1,273 @@
|
||||
# Settings Components Reference
|
||||
|
||||
All plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.
|
||||
|
||||
## PluginSettings Wrapper
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginSettings {
|
||||
pluginId: "yourPlugin" // Required: must match plugin.json id
|
||||
|
||||
// Setting components go here
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The plugin must declare `"permissions": ["settings_write"]` in plugin.json for the settings UI to render. Without it, users see an error.
|
||||
|
||||
**PluginSettings provides to children:**
|
||||
- `saveValue(key, value)` - save a setting value
|
||||
- `loadValue(key, defaultValue)` - load a setting value
|
||||
- `saveState(key, value)` - save plugin state (separate file)
|
||||
- `loadState(key, defaultValue)` - load plugin state
|
||||
- `clearState()` - clear all plugin state
|
||||
- Variant management functions (for variant plugins)
|
||||
|
||||
## StringSetting
|
||||
|
||||
Text input field.
|
||||
|
||||
```qml
|
||||
StringSetting {
|
||||
settingKey: "apiKey" // Required: storage key
|
||||
label: "API Key" // Required: display label
|
||||
description: "Your API key" // Optional: help text
|
||||
placeholder: "sk-..." // Optional: input placeholder
|
||||
defaultValue: "" // Optional: default (default: "")
|
||||
}
|
||||
```
|
||||
|
||||
**Layout:** Vertical stack - label, description, input field.
|
||||
|
||||
## ToggleSetting
|
||||
|
||||
Boolean toggle switch.
|
||||
|
||||
```qml
|
||||
ToggleSetting {
|
||||
settingKey: "notifications" // Required: storage key
|
||||
label: "Enable Notifications" // Required: display label
|
||||
description: "Show alerts" // Optional: help text
|
||||
defaultValue: true // Optional: default (default: false)
|
||||
}
|
||||
```
|
||||
|
||||
**Layout:** Horizontal - label/description on left, toggle on right.
|
||||
|
||||
## SelectionSetting
|
||||
|
||||
Dropdown menu.
|
||||
|
||||
```qml
|
||||
SelectionSetting {
|
||||
settingKey: "theme" // Required: storage key
|
||||
label: "Theme" // Required: display label
|
||||
description: "Color scheme" // Optional: help text
|
||||
options: [ // Required: array of options
|
||||
{ label: "Dark", value: "dark" },
|
||||
{ label: "Light", value: "light" },
|
||||
{ label: "Auto", value: "auto" }
|
||||
]
|
||||
defaultValue: "dark" // Optional: default value
|
||||
}
|
||||
```
|
||||
|
||||
Options can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.
|
||||
|
||||
**Layout:** Horizontal - label/description on left, dropdown on right.
|
||||
|
||||
**Reacting to changes:**
|
||||
```qml
|
||||
SelectionSetting {
|
||||
settingKey: "updateInterval"
|
||||
label: "Update Interval"
|
||||
options: [
|
||||
{ label: "1 minute", value: "60" },
|
||||
{ label: "5 minutes", value: "300" }
|
||||
]
|
||||
defaultValue: "300"
|
||||
onValueChanged: (newValue) => {
|
||||
console.log("Interval changed to:", newValue)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SliderSetting
|
||||
|
||||
Numeric slider with min/max.
|
||||
|
||||
```qml
|
||||
SliderSetting {
|
||||
settingKey: "opacity" // Required: storage key
|
||||
label: "Opacity" // Required: display label
|
||||
description: "Background" // Optional: help text
|
||||
defaultValue: 85 // Optional: default value
|
||||
minimum: 0 // Required: min value
|
||||
maximum: 100 // Required: max value
|
||||
unit: "%" // Optional: unit label shown after value
|
||||
leftIcon: "dark_mode" // Optional: Material icon on left
|
||||
rightIcon: "light_mode" // Optional: Material icon on right
|
||||
}
|
||||
```
|
||||
|
||||
## ColorSetting
|
||||
|
||||
Color picker.
|
||||
|
||||
```qml
|
||||
ColorSetting {
|
||||
settingKey: "accentColor" // Required: storage key
|
||||
label: "Accent Color" // Required: display label
|
||||
description: "Custom accent" // Optional: help text
|
||||
defaultValue: "#ff5722" // Optional: default hex color
|
||||
}
|
||||
```
|
||||
|
||||
Displays a color swatch that opens a color picker dialog.
|
||||
|
||||
## ListSetting
|
||||
|
||||
Manage a list of items with manual add/remove. Use when you need custom UI for adding items.
|
||||
|
||||
```qml
|
||||
ListSetting {
|
||||
id: itemList
|
||||
settingKey: "items" // Required: storage key
|
||||
label: "Saved Items" // Required: display label
|
||||
description: "Your items" // Optional: help text
|
||||
defaultValue: [] // Optional: default array
|
||||
delegate: Component { // Optional: custom item display
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.name
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
height: 28
|
||||
color: removeArea.containsMouse ? Theme.errorHover : Theme.error
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "Remove"
|
||||
color: Theme.errorText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: itemList.removeItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `addItem(item)` - add an item to the list
|
||||
- `removeItem(index)` - remove item at index
|
||||
|
||||
## ListSettingWithInput
|
||||
|
||||
Complete list management with built-in form. Best for collecting structured data.
|
||||
|
||||
```qml
|
||||
ListSettingWithInput {
|
||||
settingKey: "locations" // Required: storage key
|
||||
label: "Locations" // Required: display label
|
||||
description: "Track zones" // Optional: help text
|
||||
defaultValue: [] // Optional: default array
|
||||
fields: [ // Required: field definitions
|
||||
{
|
||||
id: "name", // Required: key in saved object
|
||||
label: "Name", // Required: column header
|
||||
placeholder: "Home", // Optional: input placeholder
|
||||
width: 150, // Optional: column width (default: 200)
|
||||
required: true // Optional: must have value to add
|
||||
},
|
||||
{
|
||||
id: "timezone",
|
||||
label: "Timezone",
|
||||
placeholder: "America/New_York",
|
||||
width: 200,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Automatically generates: column headers, input fields, add button with validation, list display, remove buttons.
|
||||
|
||||
## Mixing Custom UI with Settings
|
||||
|
||||
You can interleave regular QML elements with setting components:
|
||||
|
||||
```qml
|
||||
PluginSettings {
|
||||
pluginId: "myPlugin"
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "General Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StringSetting {
|
||||
settingKey: "name"
|
||||
label: "Display Name"
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: "Advanced Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
topPadding: Theme.spacingL
|
||||
}
|
||||
|
||||
ToggleSetting {
|
||||
settingKey: "debug"
|
||||
label: "Debug Mode"
|
||||
defaultValue: false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Default Values
|
||||
|
||||
Define sensible defaults in every setting component. The default is used when no saved value exists:
|
||||
|
||||
```qml
|
||||
StringSetting { settingKey: "text"; defaultValue: "Hello" }
|
||||
ToggleSetting { settingKey: "enabled"; defaultValue: true }
|
||||
SelectionSetting { settingKey: "mode"; defaultValue: "auto" }
|
||||
SliderSetting { settingKey: "opacity"; defaultValue: 85 }
|
||||
ColorSetting { settingKey: "color"; defaultValue: "#ff5722" }
|
||||
ListSetting { settingKey: "items"; defaultValue: [] }
|
||||
ListSettingWithInput { settingKey: "data"; defaultValue: [] }
|
||||
```
|
||||
216
.agents/skills/dms-plugin-dev/references/theme-reference.md
Normal file
216
.agents/skills/dms-plugin-dev/references/theme-reference.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Theme Property Reference
|
||||
|
||||
All theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.
|
||||
|
||||
## Font Sizes
|
||||
|
||||
```qml
|
||||
Theme.fontSizeSmall // 12px (scaled by SettingsData.fontScale)
|
||||
Theme.fontSizeMedium // 14px (scaled)
|
||||
Theme.fontSizeLarge // 16px (scaled)
|
||||
Theme.fontSizeXLarge // 20px (scaled)
|
||||
```
|
||||
|
||||
## Icon Sizes
|
||||
|
||||
```qml
|
||||
Theme.iconSizeSmall // 16px
|
||||
Theme.iconSize // 24px (default)
|
||||
Theme.iconSizeLarge // 32px
|
||||
```
|
||||
|
||||
## Spacing
|
||||
|
||||
```qml
|
||||
Theme.spacingXS // Extra small
|
||||
Theme.spacingS // Small
|
||||
Theme.spacingM // Medium
|
||||
Theme.spacingL // Large
|
||||
Theme.spacingXL // Extra large
|
||||
```
|
||||
|
||||
## Border Radius
|
||||
|
||||
```qml
|
||||
Theme.cornerRadius // Standard
|
||||
Theme.cornerRadiusSmall // Smaller
|
||||
Theme.cornerRadiusLarge // Larger
|
||||
```
|
||||
|
||||
## Surface Colors
|
||||
|
||||
```qml
|
||||
Theme.surface
|
||||
Theme.surfaceContainerLow
|
||||
Theme.surfaceContainer
|
||||
Theme.surfaceContainerHigh
|
||||
Theme.surfaceContainerHighest
|
||||
```
|
||||
|
||||
## Text Colors
|
||||
|
||||
```qml
|
||||
Theme.onSurface // Primary text on surface
|
||||
Theme.onSurfaceVariant // Secondary text on surface
|
||||
Theme.surfaceText // Alias for primary surface text
|
||||
Theme.surfaceVariantText // Alias for secondary surface text
|
||||
Theme.outline // Border/divider color
|
||||
```
|
||||
|
||||
## Semantic Colors
|
||||
|
||||
```qml
|
||||
Theme.primary
|
||||
Theme.onPrimary
|
||||
Theme.secondary
|
||||
Theme.onSecondary
|
||||
Theme.error
|
||||
Theme.errorHover
|
||||
Theme.errorText
|
||||
Theme.warning
|
||||
Theme.success
|
||||
```
|
||||
|
||||
## Special Functions
|
||||
|
||||
```qml
|
||||
Theme.popupBackground() // Popup background with proper opacity
|
||||
```
|
||||
|
||||
## Common Widget Patterns
|
||||
|
||||
### Icon with Text
|
||||
|
||||
```qml
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "icon_name"
|
||||
color: Theme.onSurface
|
||||
font.pixelSize: Theme.iconSize
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Label"
|
||||
color: Theme.onSurface
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Container with Border
|
||||
|
||||
```qml
|
||||
Rectangle {
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
}
|
||||
```
|
||||
|
||||
### Hover Effect
|
||||
|
||||
```qml
|
||||
Rectangle {
|
||||
id: container
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)
|
||||
onExited: container.color = Theme.surfaceContainerHigh
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Clickable Pill
|
||||
|
||||
```qml
|
||||
StyledRect {
|
||||
width: content.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse
|
||||
? Qt.lighter(Theme.surfaceContainerHigh, 1.1)
|
||||
: Theme.surfaceContainerHigh
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
Row {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "star"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Label"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Wrong property names** (these do NOT exist):
|
||||
```qml
|
||||
Theme.fontSizeS // Use Theme.fontSizeSmall
|
||||
Theme.iconSizeS // Use Theme.iconSizeSmall
|
||||
Theme.spacingSmall // Use Theme.spacingS
|
||||
Theme.borderRadius // Use Theme.cornerRadius
|
||||
```
|
||||
|
||||
**Hardcoded values** (do NOT do this):
|
||||
```qml
|
||||
color: "#1e1e1e" // Use Theme.surfaceContainerHigh
|
||||
color: "white" // Use Theme.surfaceText
|
||||
font.pixelSize: 14 // Use Theme.fontSizeMedium
|
||||
```
|
||||
|
||||
## Available Widgets from qs.Widgets
|
||||
|
||||
| Widget | Description |
|
||||
|--------|-------------|
|
||||
| `StyledText` | Themed text with proper color defaults |
|
||||
| `StyledRect` | Themed rectangle |
|
||||
| `DankIcon` | Material Symbols icon renderer |
|
||||
| `DankNFIcon` | Nerd Font icon renderer |
|
||||
| `DankButton` | Themed button |
|
||||
| `DankToggle` | Toggle switch |
|
||||
| `DankTextField` | Text input field |
|
||||
| `DankSlider` | Slider control |
|
||||
| `DankDropdown` | Dropdown menu |
|
||||
| `DankGridView` | Grid layout view |
|
||||
| `DankListView` | List layout view |
|
||||
| `DankFlickable` | Scrollable container |
|
||||
| `DankTabBar` | Tab bar navigation |
|
||||
| `DankCollapsibleSection` | Collapsible content section |
|
||||
| `DankTooltip` | Hover tooltip |
|
||||
| `DankNumberStepper` | Number +/- control |
|
||||
| `DankFilterChips` | Filter chip row |
|
||||
| `CachingImage` | Image with disk cache |
|
||||
| `NumericText` | Fixed-width numeric display |
|
||||
|
||||
## Checking All Properties
|
||||
|
||||
```bash
|
||||
grep "property" Common/Theme.qml
|
||||
```
|
||||
369
.agents/skills/dms-plugin-dev/references/widget-plugin-guide.md
Normal file
369
.agents/skills/dms-plugin-dev/references/widget-plugin-guide.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# Widget Plugin Guide
|
||||
|
||||
Widgets are bar plugins that display pills in DankBar, optionally open popouts, and can integrate with the Control Center.
|
||||
|
||||
## Base Component
|
||||
|
||||
Widgets use `PluginComponent` from `qs.Modules.Plugins`.
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
property var popoutService: null
|
||||
|
||||
horizontalBarPill: Component { /* ... */ }
|
||||
verticalBarPill: Component { /* ... */ }
|
||||
popoutContent: Component { /* ... */ }
|
||||
popoutWidth: 400
|
||||
popoutHeight: 300
|
||||
}
|
||||
```
|
||||
|
||||
## Injected Properties
|
||||
|
||||
These are automatically set by the plugin host:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `axis` | object | Bar axis info (horizontal/vertical) |
|
||||
| `section` | string | Bar section: `"left"`, `"center"`, or `"right"` |
|
||||
| `parentScreen` | object | Screen reference for multi-monitor |
|
||||
| `widgetThickness` | real | Widget size perpendicular to bar edge |
|
||||
| `barThickness` | real | Bar thickness parallel to edge |
|
||||
| `pluginId` | string | This plugin's ID |
|
||||
| `pluginService` | object | PluginService reference |
|
||||
| `pluginData` | object | Reactive plugin settings data |
|
||||
|
||||
## Bar Pills
|
||||
|
||||
Define `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).
|
||||
|
||||
### Horizontal Bar Pill
|
||||
|
||||
```qml
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: content.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Row {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "star"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Label"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vertical Bar Pill
|
||||
|
||||
```qml
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: content.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
Column {
|
||||
id: content
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "star"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.iconSizeSmall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.
|
||||
|
||||
## Popout Content
|
||||
|
||||
Open a popout window when the bar pill is clicked:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
popoutWidth: 400
|
||||
popoutHeight: 300
|
||||
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
headerText: "My Plugin"
|
||||
detailsText: "Optional subtitle"
|
||||
showCloseButton: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: "Content here"
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**PopoutComponent properties:**
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `headerText` | string | `""` | Main header (bold, large). Hidden if empty. |
|
||||
| `detailsText` | string | `""` | Subtitle below header. Hidden if empty. |
|
||||
| `showCloseButton` | bool | `false` | Show X button in top-right corner. |
|
||||
| `closePopout` | function | (injected) | Call to close the popout programmatically. |
|
||||
| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |
|
||||
| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |
|
||||
|
||||
**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`
|
||||
|
||||
## Custom Click Actions
|
||||
|
||||
Override the default popout behavior:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
// Simple no-args handler
|
||||
pillClickAction: () => {
|
||||
popoutService?.toggleControlCenter()
|
||||
}
|
||||
|
||||
// With position params (x, y, width, section, screen)
|
||||
pillClickAction: (x, y, width, section, screen) => {
|
||||
popoutService?.toggleControlCenter(x, y, width, section, screen)
|
||||
}
|
||||
|
||||
pillRightClickAction: () => {
|
||||
popoutService?.openSettings()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Control Center Integration
|
||||
|
||||
Add CC properties to show your widget in the Control Center grid:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
ccWidgetIcon: "toggle_on"
|
||||
ccWidgetPrimaryText: "Feature Name"
|
||||
ccWidgetSecondaryText: isActive ? "Active" : "Off"
|
||||
ccWidgetIsActive: isActive
|
||||
|
||||
onCcWidgetToggled: {
|
||||
isActive = !isActive
|
||||
pluginService?.savePluginData(pluginId, "active", isActive)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CC properties:**
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `ccWidgetIcon` | string | Material icon name |
|
||||
| `ccWidgetPrimaryText` | string | Main label |
|
||||
| `ccWidgetSecondaryText` | string | Subtitle / status text |
|
||||
| `ccWidgetIsActive` | bool | Active state (changes styling) |
|
||||
|
||||
**CC signals:**
|
||||
|
||||
| Signal | When fired |
|
||||
|--------|-----------|
|
||||
| `ccWidgetToggled()` | Icon area clicked |
|
||||
| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |
|
||||
|
||||
**CC sizing rules:**
|
||||
- 25% width - SmallToggleButton (icon only)
|
||||
- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)
|
||||
- Users can resize in CC edit mode
|
||||
|
||||
### Detail Content (CompoundPill)
|
||||
|
||||
Add an expandable panel below the CC widget:
|
||||
|
||||
```qml
|
||||
ccDetailContent: Component {
|
||||
Rectangle {
|
||||
implicitHeight: 200
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Detail UI here
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Visibility Control
|
||||
|
||||
Conditionally show/hide the bar pill:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
visibilityCommand: "pgrep -x myapp"
|
||||
visibilityInterval: 5000 // check every 5 seconds
|
||||
}
|
||||
```
|
||||
|
||||
## Popout Namespace
|
||||
|
||||
For plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
layerNamespacePlugin: true
|
||||
}
|
||||
```
|
||||
|
||||
## Reading Plugin Data
|
||||
|
||||
Access saved settings reactively via the injected `pluginData`:
|
||||
|
||||
```qml
|
||||
PluginComponent {
|
||||
property string displayText: pluginData?.text || "Default"
|
||||
|
||||
Connections {
|
||||
target: pluginService
|
||||
function onPluginDataChanged(changedId) {
|
||||
if (changedId === pluginId)
|
||||
displayText = pluginService.loadPluginData(pluginId, "text", "Default")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
Based on the ExampleEmojiPlugin pattern:
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Services
|
||||
import qs.Modules.Plugins
|
||||
|
||||
PluginComponent {
|
||||
id: root
|
||||
property var popoutService: null
|
||||
|
||||
property var emojis: ["star", "heart", "smile"]
|
||||
property int currentIndex: 0
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: currentIndex = (currentIndex + 1) % emojis.length
|
||||
}
|
||||
|
||||
popoutWidth: 350
|
||||
popoutHeight: 400
|
||||
|
||||
horizontalBarPill: Component {
|
||||
StyledRect {
|
||||
width: label.implicitWidth + Theme.spacingM * 2
|
||||
height: parent.widgetThickness
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.emojis[root.currentIndex]
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verticalBarPill: Component {
|
||||
StyledRect {
|
||||
width: parent.widgetThickness
|
||||
height: label.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.emojis[root.currentIndex]
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
popoutContent: Component {
|
||||
PopoutComponent {
|
||||
headerText: "Emoji Picker"
|
||||
showCloseButton: true
|
||||
|
||||
DankGridView {
|
||||
width: parent.width
|
||||
height: 300
|
||||
cellWidth: 50
|
||||
cellHeight: 50
|
||||
model: root.emojis
|
||||
|
||||
delegate: Rectangle {
|
||||
width: 48
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: 24
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", modelData])
|
||||
ToastService?.showInfo("Copied " + modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user