{"owner":"AvengeMedia","repo":"DankMaterialShell","hasSkills":true,"totalSkillsCount":50,"totalTokensCount":48160,"categories":["anthropic-skill","plugin-manifest","root-instruction"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/README.md",".agents/skills/dms-plugin-dev/SKILL.md",".agents/skills/dms-plugin-dev/assets/plugin-schema.json",".agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json",".agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json",".agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json",".agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json",".agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json",".agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json",".agents/skills/dms-plugin-dev/references/advanced-patterns.md",".agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md",".agents/skills/dms-plugin-dev/references/dash-plugin-guide.md",".agents/skills/dms-plugin-dev/references/data-persistence-guide.md",".agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md",".agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md",".agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md",".agents/skills/dms-plugin-dev/references/popout-service-reference.md",".agents/skills/dms-plugin-dev/references/settings-components-reference.md",".agents/skills/dms-plugin-dev/references/theme-reference.md",".agents/skills/dms-plugin-dev/references/widget-plugin-guide.md","AGENTS.md","quickshell/PLUGINS/AttachedPanelExample/README.md","quickshell/PLUGINS/AttachedPanelExample/plugin.json","quickshell/PLUGINS/ColorDemoPlugin/plugin.json","quickshell/PLUGINS/ControlCenterDetailExample/plugin.json","quickshell/PLUGINS/ControlCenterExample/plugin.json","quickshell/PLUGINS/DashCardExample/README.md","quickshell/PLUGINS/DashCardExample/plugin.json","quickshell/PLUGINS/DashCounterExample/README.md","quickshell/PLUGINS/DashCounterExample/plugin.json","quickshell/PLUGINS/DashTabExample/README.md","quickshell/PLUGINS/DashTabExample/plugin.json","quickshell/PLUGINS/ExampleCompositePlugin/README.md","quickshell/PLUGINS/ExampleCompositePlugin/plugin.json","quickshell/PLUGINS/ExampleDesktopClock/plugin.json","quickshell/PLUGINS/ExampleEmojiPlugin/README.md","quickshell/PLUGINS/ExampleEmojiPlugin/plugin.json","quickshell/PLUGINS/ExampleStartupCheck/plugin.json","quickshell/PLUGINS/ExampleWithVariants/README.md","quickshell/PLUGINS/ExampleWithVariants/plugin.json","quickshell/PLUGINS/LauncherExample/README.md","quickshell/PLUGINS/LauncherExample/plugin.json","quickshell/PLUGINS/LauncherImageExample/README.md","quickshell/PLUGINS/LauncherImageExample/plugin.json","quickshell/PLUGINS/PopoutControlExample/README.md","quickshell/PLUGINS/PopoutControlExample/plugin.json","quickshell/PLUGINS/QuickNotesExample/plugin.json","quickshell/PLUGINS/README.md","quickshell/PLUGINS/WallpaperWatcherDaemon/README.md","quickshell/PLUGINS/WallpaperWatcherDaemon/plugin.json"],"skills":{".agents/skills/README.md":"# Agent Skills\n\nThis directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.\n\nEach 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.\n\n## Available Skills\n\n| Skill | Description |\n|-------|-------------|\n| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for Dux Shell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |\n\n## Installation\n\nThe `.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.\n\n### Claude Code\n\nClaude 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:\n\n**Project-level** (this repo only):\n\n```bash\nmkdir -p .claude/skills\nln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev\n```\n\n**Personal** (all your projects):\n\n```bash\nln -s /path/to/dms/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev\n```\n\nAfter 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.\n\nSee the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.\n\n### Cursor\n\nCursor discovers skills from `.cursor/skills/` in the project root:\n\n```bash\nmkdir -p .cursor/skills\nln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev\n```\n\nSee [Cursor skills docs](https://cursor.com/docs/context/skills) for details.\n\n### VS Code (Copilot)\n\nVS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:\n\n```bash\nmkdir -p .github/skills\nln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev\n```\n\nSee [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.\n\n### Gemini CLI\n\nGemini CLI discovers skills from `.gemini/skills/` in the project root:\n\n```bash\nmkdir -p .gemini/skills\nln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev\n```\n\nSee [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.\n\n### OpenAI Codex\n\nCodex discovers skills from `.codex/skills/` in the project root:\n\n```bash\nmkdir -p .codex/skills\nln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev\n```\n\nSee [Codex skills docs](https://developers.openai.com/codex/skills/) for details.\n\n### Other Agents\n\nThe 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.\n\nCheck the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.\n\nThe general pattern is:\n\n```bash\nmkdir -p .<agent>/skills\nln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev\n```\n\n## Adding New Skills\n\nTo add a new skill to this directory:\n\n1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)\n2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions\n3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories\n4. Keep `SKILL.md` under 500 lines - move detailed content to reference files\n\nSee the [Agent Skills specification](https://agentskills.io/specification) for the full format.\n",".agents/skills/dms-plugin-dev/SKILL.md":"---\nname: dms-plugin-dev\ndescription: >\n  Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on\n  Quickshell. Supports widget (bar, dock + Control Center), daemon (background service),\n  launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab),\n  dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest\n  creation, QML component development, startup checks,\n  settings UI, data persistence, theme integration, PopoutService usage, IPC runtime\n  discovery, and external command execution. Use when the user wants to create, modify,\n  or debug a DMS plugin, or asks about the DMS plugin API.\ncompatibility: Designed for Claude Code (or similar products)\nmetadata:\n  author: Dux Shell\n  version: \"1.2\"\n  domain: qml-desktop-development\n  framework: Dux Shell\n  languages: qml, javascript\nallowed-tools: Bash Read Write Edit\n---\n\n# Dux Shell Plugin Development\n\n## Overview\n\nDMS plugins extend the desktop shell with bar and dock widgets, background services, launcher\nintegrations, desktop widgets, and dash tabs and cards. Plugins are QML components discovered from\n`~/.config/dms/plugins/`.\n\n**Minimum plugin structure:**\n\n```\n~/.config/dms/plugins/YourPlugin/\n  plugin.json        # Required: manifest with metadata\n  YourComponent.qml  # Required: main QML component\n  YourSettings.qml   # Optional: settings UI\n  *.js               # Optional: JavaScript utilities\n```\n\n**Plugin registry:** Community plugins are available at https://plugins.duxlabs.dev/\n\n**Plugin types:**\n\n| Type        | Purpose                        | Base Component             | Bar/dock pills | CC integration |\n|-------------|--------------------------------|----------------------------|----------------|----------------|\n| `widget`    | Bar or dock widget + popout    | `PluginComponent`          | Yes            | Yes            |\n| `daemon`    | Background service             | `PluginComponent` (no UI)  | No             | Optional       |\n| `launcher`  | Searchable items in launcher   | `Item`                     | No             | No             |\n| `desktop`   | Draggable desktop widget       | `DesktopPluginComponent`   | No             | No             |\n| `dash`      | Tab in the dash popout         | `DashTabComponent`         | No             | No             |\n| `dashCard`  | Card in the dash overview grid | `DashCardComponent`        | No             | No             |\n| `composite` | Multi-surface plugin           | One component per surface  | Optional       | Optional       |\n\n## Step 1: Determine Plugin Type\n\nChoose the type based on what the plugin does:\n\n- **Shows in the bar or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same\n  component), optionally opens a popout or an attached dock panel, optionally integrates with\n  Control Center.\n- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper\n  changes, notifications, battery level, etc.).\n- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher\n  with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).\n- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the\n  desktop layer.\n- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.\n- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.\n- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination\n  of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own\n  QML component file.\n\n## Step 2: Create the Manifest\n\nCreate `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.\n\n**Minimal manifest:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\"\n}\n```\n\n**With settings, startup check, and permissions:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"dependencies\": [\"mytool\"],\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Composite plugin (multi-surface):**\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Standalone dash tab or card:** same single file form, `\"type\": \"dash\"` or `\"type\": \"dashCard\"`\nplus `\"component\"`. A tab and a card together (or with a bar widget) use `components` with the\nkeys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and\noptions. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n**Key rules:**\n- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`\n- `version` must be semver (e.g., `1.0.0`)\n- Provide either `component` (single-surface) or `components` (multi-surface), not both\n- `component` / component paths must start with `./` and end with `.qml`\n- `type: \"launcher\"` (or a `components` object with a `launcher` key) requires a `trigger` field\n- `settings_write` permission is **required** if the plugin has a settings component\n- `dependencies` replaces the deprecated `requires` field\n\n## Step 3: Create the Main Component\n\n### Widget\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hi\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\nSee [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.\n\n### Launcher\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        const items = [\n            { name: \"Item One\", icon: \"material:star\", comment: \"Description\",\n              action: \"toast:Hello!\", categories: [\"MyPlugin\"] }\n        ]\n        if (!query) return items\n        const q = query.toLowerCase()\n        return items.filter(i => i.name.toLowerCase().includes(q))\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"toast\") ToastService?.showInfo(data)\n        else if (type === \"copy\") Quickshell.execDetached([\"dms\", \"cl\", \"copy\", data])\n    }\n}\n```\n\nSee [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.\n\n### Desktop\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Text {\n            anchors.centerIn: parent\n            text: \"Desktop Widget\"\n            color: Theme.surfaceText\n        }\n    }\n}\n```\n\nSee [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.\n\n### Daemon\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onSomeSignal() {\n            console.log(\"Event received\")\n        }\n    }\n}\n```\n\nSee [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.\n\n### Dash Tab\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    property date now: new Date()\n\n    implicitHeight: DashMetrics.tabMinHeight\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.now = new Date()\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: root.now.toLocaleTimeString(I18n.locale())\n        color: Theme.surfaceText\n    }\n}\n```\n\n### Dash Card\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nDashCardComponent {\n    id: root\n\n    tone: options.tone ?? \"\"\n\n    StyledText {\n        anchors.centerIn: parent\n        text: I18n.trFor(\"myPlugin\", \"Hello\")\n        color: root.contentColor\n    }\n}\n```\n\nGate timers and animations on `live`; the dash content stays alive after the popout closes.\nCards must lay out for every size in their manifest range. Use the card colors (`accentColor`,\n`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n### Composite\n\nFor composite plugins, create a separate QML file per surface. Each surface uses the same\nbase component as the corresponding single-surface type (PluginComponent for widget/daemon,\nItem for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.\n\n```\nMyCompositePlugin/\n  plugin.json\n  MyBarWidget.qml      # PluginComponent (widget surface)\n  MyDaemon.qml         # PluginComponent (daemon surface)\n  MyDesktopWidget.qml  # Item with desktop widget properties\n  Settings.qml         # Shared settings for all surfaces\n```\n\n`pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.\n\n## Step 4: Add Startup Check (Optional)\n\nGate plugin activation on dependency checks by providing a `startupCheck` component. This\nruns before the plugin loads and blocks activation if a required tool or condition is missing.\n\nCreate a `StartupCheck.qml` (non-visual QtObject):\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details body\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\nAdd to your manifest:\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"mytool\"]\n}\n```\n\n## Step 5: Add Settings (Optional)\n\nWrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key\"\n        placeholder: \"sk-...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"interval\"\n        label: \"Refresh Interval\"\n        options: [\n            { label: \"1 min\", value: \"60\" },\n            { label: \"5 min\", value: \"300\" }\n        ]\n        defaultValue: \"300\"\n    }\n}\n```\n\n**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.\n\nSee [settings-components-reference.md](references/settings-components-reference.md) for full property lists.\n\n**Important:** Your plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json, or the settings UI will show an error.\n\n## Step 6: Use Data Persistence\n\nThree tiers of persistence:\n\n| API | Persisted | Use case |\n|-----|-----------|----------|\n| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |\n| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |\n| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |\n| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |\n\n- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings\n- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`\n- Global vars sync across all instances (multi-monitor, multiple bar sections)\n\nSee [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.\n\n## Step 7: Theme Integration\n\nAlways use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.\n\n**Essential properties:**\n- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`\n- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)\n- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`\n- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`\n- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)\n\n**Common widgets from `qs.DCommon.Widgets`:** `StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.\n\nSee [theme-reference.md](references/theme-reference.md) for the complete property list.\n\n## Step 8: Add Popout Content (Widgets Only)\n\nAdd a popout that opens when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n}\n```\n\n**PopoutComponent properties:** `headerText`, `detailsText`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).\n\nCalculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Step 9: Control Center Integration (Widgets Only)\n\nAdd your widget to the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isActive ? \"On\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n\n    // Optional: expandable detail panel (for CompoundPill)\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.surfaceContainerHigh\n            radius: Theme.cornerRadius\n        }\n    }\n}\n```\n\n**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).\n\n## Step 10: External Commands and Clipboard\n\n**Run commands and capture output:**\n\n```qml\nimport qs.Common\n\nProc.runCommand(\n    \"myPlugin.fetch\",\n    [\"curl\", \"-s\", \"https://api.example.com/data\"],\n    (stdout, exitCode) => {\n        if (exitCode === 0) processData(stdout)\n    },\n    500  // debounce ms\n)\n```\n\n**Fire-and-forget (clipboard, notifications):**\n\n```qml\nimport Quickshell\n\nQuickshell.execDetached([\"dms\", \"cl\", \"copy\", textToCopy])\n```\n\n**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.\n\n**Shell commands with pipes:** `[\"sh\", \"-c\", \"ps aux | grep foo\"]`\n\n**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.\n\n## Translations (Optional)\n\nShip per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:\n\n```\nMyPlugin/\n└── translations/\n    ├── es.json\n    └── zh_CN.json\n```\n\nFile names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.\n\nEach file maps the English term to its translation, using the term as its own context bucket:\n\n```json\n{\n  \"Cycle Speed\": {\n    \"Cycle Speed\": \"Velocidad de ciclo\"\n  }\n}\n```\n\nIn QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:\n\n```qml\ntext: I18n.trFor(\"myPlugin\", \"Cycle Speed\")\n```\n\nLookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.\n\n## Step 11: Validate and Test\n\n1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)\n2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`\n3. Open Settings > Plugins > Scan for Plugins\n4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)\n\n**Runtime plugin discovery via IPC:**\n\nPlugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:\n\n```bash\ndms ipc plugin-scan scan          # Trigger a full rescan of all plugin directories\ndms ipc plugin-scan rescan <id>   # Force rescan of a specific plugin\ndms ipc plugin-scan reload <id>   # Force reload of a loaded plugin\ndms ipc plugin-scan list          # List all known plugins (TSV: id, loaded, type, name)\ndms ipc plugin-scan status <id>   # Get status of a specific plugin (TSV: loaded, type, error)\n```\n\nPlugin IDs are validated against `^[a-zA-Z0-9_\\-:]{1,64}$`.\n\n**Common issues:**\n- Plugin not detected: check plugin.json syntax with `jq . plugin.json`\n- Widget not showing: ensure it's enabled AND added to a DankBar section\n- Settings error: verify `settings_write` permission is declared\n- Data not persisting: check pluginService injection and permissions\n- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`\n\n## Common Mistakes\n\n1. **Missing `settings_write` permission** - Settings UI shows error without it\n2. **Missing `property var popoutService: null`** - Must declare for injection to work\n3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge\n4. **Hardcoded colors** - Use `Theme.*` properties, not hex values\n5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])`\n6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`\n7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)\n8. **Forgetting `categories` in launcher items** - Items won't display without it\n9. **Not handling null pluginService** - Always use optional chaining or null checks\n10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`\n11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`\n12. **Providing both `component` and `components`** - Use one or the other, not both\n13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key\n14. **Using `\"type\": \"widget\"` for a dash tab** - Dash surfaces need `\"type\": \"dash\"` / `\"dashCard\"` or a `components` key\n15. **Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`\n16. **Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`\n17. **Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone\n\n## Quick Reference: Imports\n\n**Widget / Daemon:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n```\n\n**Launcher:**\n```qml\nimport QtQuick\nimport qs.Services\n```\n\n**Desktop:**\n```qml\nimport QtQuick\nimport qs.Common\n```\n\n**Dash tab / card:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash          // DashMetrics, DashWidgetGrid\nimport qs.Modules.DankDash.Overview // Card, for tiles inside a tab\n```\n\n**For clipboard/exec:** `import Quickshell`\n**For processes:** `import Quickshell.Io`\n**For networking:** `import Quickshell.Networking`\n**For toast notifications:** access `ToastService` from `qs.Services`\n\n## Quick Reference: File Naming\n\n- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)\n- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)\n- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)\n- **Component paths in manifest:** relative with `./` prefix (e.g., `\"./MyWidget.qml\"`)\n- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)\n\n## Reference Files\n\nLoad these on demand for detailed API documentation:\n\n- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema\n- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration\n- [dash-plugin-guide.md](references/dash-plugin-guide.md) - DashTabComponent, DashCardComponent, DashWidgetGrid, dash options, card sizing\n- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view\n- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence\n- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution\n- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists\n- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns\n- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables\n- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals\n- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins\n",".agents/skills/dms-plugin-dev/assets/plugin-schema.json":"{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://duxlabs.dev/schemas/plugin.json\",\n  \"title\": \"Dux Shell Plugin Manifest\",\n  \"description\": \"Schema for Dux Shell plugin.json manifest files\",\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"name\",\n    \"description\",\n    \"version\",\n    \"author\",\n    \"type\",\n    \"capabilities\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"type\": \"string\",\n      \"description\": \"Unique plugin identifier (camelCase, no spaces)\",\n      \"pattern\": \"^[a-zA-Z][a-zA-Z0-9]*$\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable plugin name\",\n      \"minLength\": 1\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"Short description of plugin functionality\",\n      \"minLength\": 1\n    },\n    \"version\": {\n      \"type\": \"string\",\n      \"description\": \"Semantic version string (e.g., '1.0.0')\",\n      \"pattern\": \"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-[a-zA-Z0-9.-]+)?(\\\\+[a-zA-Z0-9.-]+)?$\"\n    },\n    \"author\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin creator name or email\",\n      \"minLength\": 1\n    },\n    \"type\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.\",\n      \"enum\": [\"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", \"composite\"]\n    },\n    \"capabilities\": {\n      \"type\": \"array\",\n      \"description\": \"Array of plugin capabilities\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"minItems\": 1\n    },\n    \"component\": {\n      \"type\": \"string\",\n      \"description\": \"Relative path to main QML component file. Required unless 'components' is provided.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"components\": {\n      \"type\": \"object\",\n      \"description\": \"Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.\",\n      \"properties\": {\n        \"widget\": {\n          \"type\": \"string\",\n          \"description\": \"Bar/Control Center widget component (PluginComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"desktop\": {\n          \"type\": \"string\",\n          \"description\": \"Desktop widget component\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"daemon\": {\n          \"type\": \"string\",\n          \"description\": \"Background daemon component (instantiated once)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"launcher\": {\n          \"type\": \"string\",\n          \"description\": \"Launcher provider component (requires 'trigger')\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dash\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard tab component (DashTabComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dashCard\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard overview card component (DashCardComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"dash\": {\n      \"type\": \"object\",\n      \"description\": \"Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.\",\n      \"properties\": {\n        \"icon\": {\n          \"type\": \"string\",\n          \"description\": \"Material icon for the dashboard tab\"\n        },\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"Tab label\"\n        },\n        \"card\": {\n          \"type\": \"object\",\n          \"description\": \"Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).\",\n          \"properties\": {\n            \"icon\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"w\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"h\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"minW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"minH\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"maxW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"maxH\": { \"type\": \"integer\", \"minimum\": 1 }\n          },\n          \"additionalProperties\": false\n        },\n        \"options\": {\n          \"type\": \"array\",\n          \"description\": \"User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\"key\", \"type\"],\n            \"properties\": {\n              \"key\": { \"type\": \"string\", \"not\": { \"const\": \"widgets\" }, \"description\": \"Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts\" },\n              \"text\": { \"type\": \"string\", \"description\": \"Row label, translated through the plugin catalog\" },\n              \"description\": { \"type\": \"string\", \"description\": \"Secondary text under a toggle row, translated through the plugin catalog\" },\n              \"type\": { \"type\": \"string\", \"enum\": [\"toggle\", \"choice\", \"number\"] },\n              \"def\": { \"description\": \"Default value: boolean for toggle, choice value for choice, number for number\" },\n              \"choices\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"required\": [\"value\"],\n                  \"properties\": {\n                    \"value\": { \"type\": \"string\" },\n                    \"text\": { \"type\": \"string\" }\n                  },\n                  \"additionalProperties\": false\n                }\n              },\n              \"min\": { \"type\": \"number\" },\n              \"max\": { \"type\": \"number\" },\n              \"step\": { \"type\": \"number\", \"exclusiveMinimum\": 0 },\n              \"unit\": { \"type\": \"string\" }\n            },\n            \"additionalProperties\": false\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"trigger\": {\n      \"type\": \"string\",\n      \"description\": \"Trigger string for launcher activation (required for launcher type)\"\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Material Design icon name\"\n    },\n    \"settings\": {\n      \"type\": \"string\",\n      \"description\": \"Path to settings component QML file\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"startupCheck\": {\n      \"type\": \"string\",\n      \"description\": \"Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"requires_dms\": {\n      \"type\": \"string\",\n      \"description\": \"Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')\",\n      \"pattern\": \"^(>=?|<=?|=|>|<)\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\"\n    },\n    \"dependencies\": {\n      \"type\": \"array\",\n      \"description\": \"Array of required system tools/dependencies (registry metadata)\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"requires\": {\n      \"type\": \"array\",\n      \"description\": \"Deprecated alias for 'dependencies'.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"permissions\": {\n      \"type\": \"array\",\n      \"description\": \"Required capabilities\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"settings_read\",\n          \"settings_write\",\n          \"process\",\n          \"network\"\n        ]\n      }\n    }\n  },\n  \"allOf\": [\n    {\n      \"if\": {\n        \"properties\": {\n          \"type\": {\n            \"const\": \"launcher\"\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"if\": {\n        \"required\": [\"components\"],\n        \"properties\": {\n          \"components\": {\n            \"required\": [\"launcher\"]\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"anyOf\": [\n        {\n          \"required\": [\"component\"]\n        },\n        {\n          \"required\": [\"components\"]\n        }\n      ]\n    }\n  ],\n  \"additionalProperties\": true\n}\n",".agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json":"{\n    \"id\": \"myDaemon\",\n    \"name\": \"My Daemon\",\n    \"description\": \"A background service that reacts to events\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"background-service\"],\n    \"component\": \"./Daemon.qml\",\n    \"icon\": \"settings\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json":"{\n    \"id\": \"myDashCard\",\n    \"name\": \"My Dash Card\",\n    \"description\": \"A custom dash overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./Card.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"card\": { \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 3, \"maxH\": 2 },\n        \"options\": [\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n",".agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json":"{\n    \"id\": \"myDashTab\",\n    \"name\": \"My Dash Tab\",\n    \"description\": \"A custom dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./Tab.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"title\": \"My Tab\",\n        \"options\": [\n            { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n",".agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json":"{\n    \"id\": \"myDesktopWidget\",\n    \"name\": \"My Desktop Widget\",\n    \"description\": \"A custom desktop widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json":"{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Custom launcher plugin with searchable items\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./Launcher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json":"{\n    \"id\": \"myWidget\",\n    \"name\": \"My Widget\",\n    \"description\": \"A custom bar widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/references/advanced-patterns.md":"# Advanced Patterns\n\nPatterns observed in production DMS plugins that go beyond the basics.\n\n## Plugin Variants\n\nCreate multiple widget instances from a single plugin definition. Each variant has its own configuration.\n\n### Manifest\n\nNo special manifest changes needed - the variant system is built into PluginComponent.\n\n### Widget with Variant Support\n\n```qml\nPluginComponent {\n    property string variantId: \"\"\n    property var variantData: ({})\n\n    property string displayText: variantData?.text || \"Default\"\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.displayText\n            }\n        }\n    }\n}\n```\n\nWidget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Settings with Variant Management\n\n```qml\nPluginSettings {\n    pluginId: \"exampleVariants\"\n\n    // Variant creation UI\n    DButton {\n        text: \"Add New Instance\"\n        onClicked: {\n            var id = \"variant_\" + Date.now()\n            root.createVariant(id, { name: \"New Instance\", text: \"Hello\" })\n        }\n    }\n\n    // Per-variant configuration\n    Repeater {\n        model: root.variants\n        delegate: Column {\n            StringSetting {\n                settingKey: modelData.id + \"_text\"\n                label: modelData.name || modelData.id\n            }\n        }\n    }\n}\n```\n\n## JavaScript Utility Files\n\nFor complex logic, split into `.js` files:\n\n### utils.js\n\n```javascript\n.pragma library\n\nfunction formatDuration(ms) {\n    if (ms < 60000) return \"just now\"\n    if (ms < 3600000) return Math.floor(ms / 60000) + \"m ago\"\n    return Math.floor(ms / 3600000) + \"h ago\"\n}\n\nfunction parseResponse(json) {\n    try {\n        return JSON.parse(json)\n    } catch (e) {\n        return null\n    }\n}\n```\n\n### Using in QML\n\n```qml\nimport \"utils.js\" as Utils\n\nItem {\n    StyledText {\n        text: Utils.formatDuration(Date.now() - timestamp)\n    }\n}\n```\n\nThe `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.\n\n## qmldir for Singleton Services\n\nFor plugins with internal singleton services:\n\n### qmldir\n\n```\nsingleton MyService 1.0 MyService.qml\n```\n\n### MyService.qml\n\n```qml\npragma Singleton\nimport QtQuick\n\nQtObject {\n    property var cache: ({})\n\n    function getData(key) {\n        return cache[key] || null\n    }\n\n    function setData(key, value) {\n        cache[key] = value\n    }\n}\n```\n\n### Using the singleton\n\n```qml\nimport \".\" as Local\n\nItem {\n    Component.onCompleted: {\n        Local.MyService.setData(\"key\", \"value\")\n    }\n}\n```\n\n## Inline Component Declarations\n\nReusable sub-components defined inline:\n\n```qml\nItem {\n    component StatusBadge: Rectangle {\n        property string label: \"\"\n        property color badgeColor: Theme.primary\n\n        width: badgeText.implicitWidth + Theme.spacingM * 2\n        height: 24\n        radius: 12\n        color: badgeColor\n\n        StyledText {\n            id: badgeText\n            anchors.centerIn: parent\n            text: label\n            color: Theme.onPrimary\n            font.pixelSize: Theme.fontSizeSmall\n        }\n    }\n\n    Row {\n        spacing: Theme.spacingS\n        StatusBadge { label: \"Running\"; badgeColor: Theme.success }\n        StatusBadge { label: \"Stopped\"; badgeColor: Theme.error }\n    }\n}\n```\n\n## Multi-Provider Adapter Pattern\n\nFor plugins supporting multiple backends (AI providers, API services):\n\n### apiAdapters.js\n\n```javascript\n.pragma library\n\nfunction createAdapter(provider) {\n    switch (provider) {\n        case \"openai\": return {\n            url: \"https://api.openai.com/v1/chat/completions\",\n            headers: (key) => ({ \"Authorization\": \"Bearer \" + key }),\n            formatRequest: (messages) => JSON.stringify({ model: \"gpt-4\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).choices[0].message.content\n        }\n        case \"anthropic\": return {\n            url: \"https://api.anthropic.com/v1/messages\",\n            headers: (key) => ({ \"x-api-key\": key, \"anthropic-version\": \"2023-06-01\" }),\n            formatRequest: (messages) => JSON.stringify({ model: \"claude-sonnet-4-20250514\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).content[0].text\n        }\n        default: return null\n    }\n}\n```\n\n## IPC Integration\n\nFor plugins that respond to keyboard shortcuts or external commands:\n\n```qml\nPluginComponent {\n    Connections {\n        target: DMSIpc\n        function onCommandReceived(command, args) {\n            if (command === \"myPlugin.toggle\") {\n                doToggle()\n            } else if (command === \"myPlugin.next\") {\n                goNext()\n            }\n        }\n    }\n}\n```\n\nExternal trigger: `dms ipc call myPlugin.toggle`\n\n## Networking with Quickshell.Networking\n\nFor API calls using the built-in networking module:\n\n```qml\nimport Quickshell.Networking\n\nItem {\n    NetworkRequest {\n        id: request\n        url: \"https://api.example.com/data\"\n        method: \"GET\"\n\n        onResponseReceived: (response) => {\n            const data = JSON.parse(response.body)\n            processData(data)\n        }\n\n        onErrorOccurred: (error) => {\n            console.error(\"Network error:\", error)\n        }\n    }\n\n    function fetchData() {\n        request.send()\n    }\n}\n```\n\n## Toast Notifications\n\nShow user feedback:\n\n```qml\nimport qs.Services\n\n// Info toast\nToastService?.showInfo(\"Operation completed\")\n\n// With title\nToastService?.showInfo(\"Plugin Name\", \"Data refreshed successfully\")\n```\n\nAlways use optional chaining since ToastService may not be available in all contexts.\n\n## Clipboard Operations\n\n```qml\nimport Quickshell\n\nfunction copyToClipboard(text) {\n    Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    ToastService?.showInfo(\"Copied to clipboard\")\n}\n```\n\nDo NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.\n\n## Multi-File Plugin Architecture\n\nLarge plugins can be split across multiple files:\n\n```\nMyPlugin/\n  plugin.json\n  Main.qml           # Main widget component\n  Settings.qml       # Settings UI\n  DetailView.qml     # Popout detail view\n  utils.js            # Utility functions\n  apiAdapter.js       # API adapter layer\n  qmldir              # Optional: singleton registrations\n```\n\nImport sibling files:\n\n```qml\n// In Main.qml\nimport \".\" as Local\n\nItem {\n    Loader {\n        source: \"DetailView.qml\"\n    }\n}\n```\n\n## Performance Tips\n\n1. Use `Proc.runCommand` with appropriate debounce for external commands\n2. Pre-cache images and thumbnails for image-heavy plugins\n3. Limit concurrent network requests\n4. Use `Timer` with reasonable intervals (don't poll faster than needed)\n5. Lazy-load heavy content (use `Loader` for complex popout content)\n6. Avoid blocking the UI thread with synchronous operations\n",".agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md":"# Daemon Plugin Guide\n\nDaemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.\n\n## Base Component\n\nDaemons use `PluginComponent` with no bar pills:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    // Event-driven logic goes here\n}\n```\n\n## When to Use Daemons\n\n- Monitor system events (wallpaper changes, battery level, notifications)\n- Run periodic background tasks (polling APIs, checking system state)\n- Execute scripts in response to events\n- Control shell UI via PopoutService based on conditions\n\n## Event-Driven Pattern\n\nUse `Connections` to react to service signals:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            console.log(\"Wallpaper changed to:\", SessionData.wallpaperPath)\n            runScript(SessionData.wallpaperPath)\n        }\n    }\n\n    Connections {\n        target: BatteryService\n        function onPercentageChanged() {\n            if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n                popoutService?.openBattery()\n            }\n        }\n    }\n}\n```\n\n## Available Services\n\nCommon services daemons can connect to:\n\n| Service | Signals/Properties | Description |\n|---------|-------------------|-------------|\n| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |\n| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |\n| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |\n| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |\n\nImport services from `qs.Services`.\n\n## Process Execution\n\n### Simple command with Proc\n\n```qml\nimport qs.Common\n\nPluginComponent {\n    function runScript(arg) {\n        Proc.runCommand(\n            \"myDaemon.script\",\n            [\"bash\", \"-c\", \"echo 'Processing: \" + arg + \"'\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Script output:\", stdout)\n                } else {\n                    ToastService?.showInfo(\"Script failed: exit \" + exitCode)\n                }\n            }\n        )\n    }\n}\n```\n\n### Long-running process with Process component\n\n```qml\nimport Quickshell.Io\n\nPluginComponent {\n    property string scriptPath: \"\"\n\n    Process {\n        id: proc\n        command: [\"bash\", scriptPath]\n        running: false\n\n        stdout: StdioCollector {\n            onTextReceived: (text) => {\n                console.log(\"stdout:\", text)\n            }\n        }\n\n        stderr: StdioCollector {\n            onTextReceived: (text) => {\n                console.error(\"stderr:\", text)\n            }\n        }\n\n        onExited: (exitCode) => {\n            if (exitCode !== 0) {\n                ToastService?.showInfo(\"Process failed: exit \" + exitCode)\n            }\n        }\n    }\n\n    function startProcess() {\n        if (scriptPath && !proc.running) {\n            proc.running = true\n        }\n    }\n}\n```\n\n## Timer-Based Polling\n\n```qml\nPluginComponent {\n    Timer {\n        interval: 60000  // every minute\n        running: true\n        repeat: true\n        onTriggered: checkStatus()\n    }\n\n    function checkStatus() {\n        Proc.runCommand(\n            \"myDaemon.check\",\n            [\"sh\", \"-c\", \"systemctl is-active myservice\"],\n            (stdout, exitCode) => {\n                const active = stdout.trim() === \"active\"\n                PluginService.setGlobalVar(\"myDaemon\", \"serviceActive\", active)\n            }\n        )\n    }\n}\n```\n\n## Data Persistence\n\nDaemons access PluginService directly (it's injected via PluginComponent):\n\n```qml\nPluginComponent {\n    property string configuredScript: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                configuredScript = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n}\n```\n\n## PopoutService Usage\n\nDaemons can control shell UI via the injected popoutService:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    function showAlert() {\n        popoutService?.openNotificationCenter()\n    }\n\n    function openSettings() {\n        popoutService?.openSettings()\n    }\n}\n```\n\nSee [popout-service-reference.md](popout-service-reference.md) for the full API.\n\n## Complete Example\n\nBased on the WallpaperWatcherDaemon:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport Quickshell.Io\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property string scriptPath: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                scriptPath = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            if (scriptPath) {\n                runWallpaperScript(SessionData.wallpaperPath)\n            }\n        }\n    }\n\n    function runWallpaperScript(wallpaperPath) {\n        console.log(\"[WallpaperWatcher] Running script:\", scriptPath, wallpaperPath)\n\n        Proc.runCommand(\n            \"wallpaperWatcher.run\",\n            [\"bash\", scriptPath, wallpaperPath],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"[WallpaperWatcher] Script output:\", stdout)\n                } else {\n                    console.error(\"[WallpaperWatcher] Script failed:\", exitCode)\n                    ToastService?.showInfo(\"Wallpaper script failed\")\n                }\n            }\n        )\n    }\n\n    Component.onCompleted: {\n        console.log(\"[WallpaperWatcher] Daemon started\")\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"wallpaperWatcher\",\n    \"name\": \"Wallpaper Watcher\",\n    \"description\": \"Runs a script when the wallpaper changes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper-automation\"],\n    \"component\": \"./WallpaperWatcher.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n```\n",".agents/skills/dms-plugin-dev/references/dash-plugin-guide.md":"# Dash Plugin Guide\n\nThe dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).\n\nRequires DMS 1.7 or later: set `\"requires_dms\": \">=1.7.0\"`.\n\n## Picking the manifest form\n\n| Plugin provides | Manifest |\n|-----------------|----------|\n| Tab only | `\"type\": \"dash\"`, `\"component\": \"./Tab.qml\"` |\n| Card only | `\"type\": \"dashCard\"`, `\"component\": \"./Card.qml\"` |\n| Tab and card | `\"type\": \"composite\"`, `\"components\": { \"dash\": ..., \"dashCard\": ... }` |\n| Dash plus bar/dock widget, daemon, ... | `\"type\": \"composite\"` with every surface in `components` |\n\nAdd the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).\n\n## Rules that trip people up\n\n- Only enabled plugins appear in the dash. There is no \"disabled\" placeholder.\n- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).\n- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash(\"plugin_\" + pluginId, x, y, width, section, screen)`.\n- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.\n- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.\n- `dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.\n- `widgets` is a reserved option key.\n- Use `I18n.trFor(\"<plugin id literal>\", \"...\")` for every string.\n\n## Tab: DashTabComponent\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    focusTarget: button\n    implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)\n    menuActions: [\n        {\n            label: I18n.trFor(\"myPlugin\", \"Refresh\"),\n            iconName: \"refresh\",\n            action: () => root.refresh()\n        }\n    ]\n\n    function refresh() {\n    }\n\n    function restoreFocus() {\n        button.forceActiveFocus(Qt.OtherFocusReason);\n    }\n\n    function handleKeyEvent(event) {\n        if (event.key !== Qt.Key_R)\n            return false;\n        refresh();\n        return true;\n    }\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.refresh()\n    }\n\n    Column {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingM\n\n        StyledText {\n            text: root.options.compact ? \"\" : I18n.trFor(\"myPlugin\", \"Hello\")\n            color: Theme.surfaceText\n        }\n\n        DButton {\n            id: button\n            text: I18n.trFor(\"myPlugin\", \"Refresh\")\n            onClicked: root.refresh()\n        }\n    }\n}\n```\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (`dashVisible`, `requestTab(id)`, `editMode`), `editMode`, `live`. Also `targetScreen`, `active` and `rowBudget` when the tab declares them.\n\nBase provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`.\n\nOptional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`.\n\nFor tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview.\n\n### Editable widgets in a tab\n\nAssign a `DashWidgetGrid` (from `qs.Modules.DankDash`) to `widgetGrid` to get Edit, Add widget, Reset and Clear All:\n\n```qml\nDashTabComponent {\n    id: tab\n    widgetGrid: grid\n    implicitHeight: grid.implicitHeight\n    focusTarget: grid.focusTarget\n\n    DashWidgetGrid {\n        id: grid\n        width: parent.width\n        entryId: tab.entryId\n        live: tab.live\n        editMode: tab.editMode\n        definitions: [\n            { id: \"main\", text: I18n.trFor(\"myPlugin\", \"Main\"), icon: \"star\", component: mainWidget, w: 2, h: 2, minW: 1, minH: 1, maxW: 4, maxH: 3 }\n        ]\n    }\n\n    Component {\n        id: mainWidget\n        Card {}\n    }\n}\n```\n\nThe grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.\n\n## Card: DashCardComponent\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashCardComponent {\n    id: root\n\n    readonly property bool wide: width >= DashMetrics.gridRowUnit * 2\n    readonly property bool tall: height >= DashMetrics.gridRowUnit * 2\n\n    tone: options.tone ?? \"\"\n\n    SystemClock {\n        id: clock\n        enabled: root.live\n        precision: SystemClock.Minutes\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: clock.date.toLocaleTimeString(I18n.locale(), \"hh:mm\")\n        font.pixelSize: root.tall ? Theme.fontSizeDisplay : Theme.fontSizeXLarge\n        color: root.accentColor\n    }\n}\n```\n\nBase (`Card`) provides: `pad`, `title`, `tone` (`\"\"`, `\"primary\"`, `\"secondary\"`, `\"tertiary\"`), `containerColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor`, `chipColor`, `tinted`, `clickable`, `signal clicked`, `options`, `pluginData`, `getData`, `setData`.\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`; bound: `live` (overview visible), `interactive` (false in edit mode).\n\nClicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.\n\nUse the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.\n\n## Testing\n\n```bash\ndms ipc call plugins reload myPlugin\ndms ipc call dash open plugin_myPlugin\n```\n\nWorking examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).\n",".agents/skills/dms-plugin-dev/references/data-persistence-guide.md":"# Data Persistence Guide\n\nDMS plugins have three tiers of data persistence, each suited for different use cases.\n\n## Tier 1: Plugin Data (Settings)\n\nPersisted to `settings.json`. Use for user preferences and configuration.\n\n### Saving\n\n```qml\npluginService.savePluginData(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar value = pluginService.loadPluginData(pluginId, \"key\", defaultValue)\n```\n\n### Reactive Access via pluginData\n\n`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n    property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true\n}\n```\n\n### Reacting to Settings Changes\n\nWhen settings are changed (e.g., from the settings UI), react with `Connections`:\n\n```qml\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        showIcon = pluginService.loadPluginData(pluginId, \"showIcon\", true)\n    }\n}\n```\n\n## Tier 2: Plugin State\n\nPersisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).\n\n### Saving\n\n```qml\npluginService.savePluginState(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar state = pluginService.loadPluginState(pluginId, \"key\", defaultValue)\n```\n\n### Additional Methods\n\n```qml\npluginService.clearPluginState(pluginId)\npluginService.removePluginStateKey(pluginId, \"key\")\n```\n\n### Example: Persistent History\n\n```qml\nItem {\n    property var history: []\n\n    Component.onCompleted: {\n        history = pluginService?.loadPluginState(pluginId, \"history\", []) || []\n    }\n\n    function addToHistory(entry) {\n        history.unshift({\n            text: entry,\n            timestamp: Date.now()\n        })\n        if (history.length > 100) history = history.slice(0, 100)\n        pluginService?.savePluginState(pluginId, \"history\", history)\n    }\n\n    function clearHistory() {\n        history = []\n        pluginService?.removePluginStateKey(pluginId, \"history\")\n    }\n}\n```\n\n## Tier 3: Global Variables (Runtime Only)\n\nNOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).\n\n### Using PluginGlobalVar Component\n\n```qml\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            // ...\n            StyledText {\n                text: \"Count: \" + globalCounter.value\n            }\n\n            MouseArea {\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `varName` | string | Required: name of the global variable |\n| `defaultValue` | any | Optional: default if not set |\n| `value` | any | Readonly: current value |\n\n**Methods:**\n- `set(newValue)` - update the value (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\n```qml\nimport qs.Services\n\nproperty int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\nConnections {\n    target: PluginService\n    function onGlobalVarChanged(pluginId, varName) {\n        if (pluginId === \"myPlugin\" && varName === \"counter\") {\n            counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n        }\n    }\n}\n\nfunction increment() {\n    var current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n}\n```\n\n## Decision Matrix\n\n| Need | API | Persisted | Scope |\n|------|-----|-----------|-------|\n| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |\n| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |\n| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |\n| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |\n\n## Plugin Path\n\nRetrieve a plugin's installation directory at runtime:\n\n```qml\nvar dir = pluginService.getPluginPath(pluginId)\n```\n\nReturns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.\n\n## Important Notes\n\n1. **pluginData is reactive** - bindings update automatically when data changes\n2. **Global vars are NOT persistent** - they reset when the shell restarts\n3. **State vs Data** - data is for user-facing settings, state is for internal runtime data\n4. **Null safety** - always check `pluginService` is not null before calling methods\n5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin\n6. **Performance** - global vars are efficient for frequent updates; settings writes are batched\n",".agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md":"# Desktop Plugin Guide\n\nDesktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.\n\n## Base Component\n\nDesktop widgets use a plain `Item` with injected properties:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n\n        // Your content here\n    }\n}\n```\n\n## Injected Properties\n\nThese are set automatically by the DesktopPluginWrapper:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `pluginService` | object | PluginService reference for data persistence |\n| `pluginId` | string | Plugin's unique identifier |\n| `editMode` | bool | `true` when user is dragging/resizing |\n| `widgetWidth` | real | Current widget container width |\n| `widgetHeight` | real | Current widget container height |\n\n## Optional Properties\n\nDefine these on your root item to customize behavior:\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `minWidth` | real | 100 | Minimum allowed width during resize |\n| `minHeight` | real | 100 | Minimum allowed height during resize |\n\n## Position and Size Persistence\n\nPosition (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.\n\n## Edit Mode\n\nWhen `editMode` is true, the user is repositioning or resizing. Use this to:\n- Show visual indicators (borders, handles)\n- Disable interactive elements to prevent accidental actions\n- Display additional controls\n\n```qml\nRectangle {\n    anchors.fill: parent\n    border.color: root.editMode ? Theme.primary : \"transparent\"\n    border.width: root.editMode ? 2 : 0\n\n    MouseArea {\n        anchors.fill: parent\n        enabled: !root.editMode\n        onClicked: doSomething()\n    }\n}\n```\n\n## Loading and Saving Data\n\nUse the injected `pluginService` for data persistence:\n\n```qml\nproperty string displayMode: {\n    if (!pluginService) return \"default\"\n    return pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n}\n\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        root.displayMode = pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n    }\n}\n\nfunction saveMode(mode) {\n    pluginService?.savePluginData(pluginId, \"displayMode\", mode)\n}\n```\n\n## Settings Component\n\nDesktop plugin settings use the same `PluginSettings` wrapper as other types:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myDesktopWidget\"\n\n    SliderSetting {\n        settingKey: \"opacity\"\n        label: \"Opacity\"\n        description: \"Widget background opacity\"\n        defaultValue: 85\n        minimum: 10\n        maximum: 100\n        unit: \"%\"\n    }\n\n    SelectionSetting {\n        settingKey: \"style\"\n        label: \"Display Style\"\n        options: [\n            { label: \"Compact\", value: \"compact\" },\n            { label: \"Expanded\", value: \"expanded\" }\n        ]\n        defaultValue: \"compact\"\n    }\n}\n```\n\n## User Interaction\n\nDesktop widgets support:\n1. **Drag** - click and drag anywhere (in edit mode)\n2. **Resize** - drag bottom-right corner handle (in edit mode)\n3. **Edit mode toggle** - via the desktop edit button\n\n## Complete Example\n\nBased on the ExampleDesktopClock pattern:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 250\n    property real widgetHeight: 250\n    property real minWidth: 150\n    property real minHeight: 150\n\n    property string clockStyle: {\n        if (!pluginService) return \"digital\"\n        return pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n    }\n\n    property real bgOpacity: {\n        if (!pluginService) return 0.85\n        var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n        return val / 100\n    }\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId !== pluginId) return\n            clockStyle = pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n            var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n            bgOpacity = val / 100\n        }\n    }\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: root.bgOpacity\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Column {\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatTime(new Date(), \"hh:mm:ss\")\n                color: Theme.surfaceText\n                font.pixelSize: root.widgetWidth * 0.15\n                font.weight: Font.Bold\n            }\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatDate(new Date(), \"ddd, MMM d\")\n                color: Theme.onSurfaceVariant\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    Timer {\n        interval: 1000\n        running: true\n        repeat: true\n        onTriggered: root.widgetWidth = root.widgetWidth // force update\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"myDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"Analog and digital clock for the desktop\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./ClockWidget.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n",".agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md":"# Launcher Plugin Guide\n\nLauncher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.\n\n## Base Component\n\nLaunchers use a plain `Item` (not PluginComponent):\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        // Return array of items\n        return []\n    }\n\n    function executeItem(item) {\n        // Handle item selection\n    }\n}\n```\n\n## Required Interface\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `pluginService` | property | Injected PluginService reference (declare as `null`) |\n| `trigger` | property | Trigger string for activation |\n| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |\n| `getItems(query)` | function | Return array of items matching query |\n| `executeItem(item)` | function | Handle item selection |\n\n## Item Structure\n\nEach item returned by `getItems()`:\n\n```javascript\n{\n    name: \"Item Display Name\",       // Required: shown in launcher\n    icon: \"material:star\",           // Optional: icon specification\n    comment: \"Description text\",     // Required: subtitle text\n    action: \"type:data\",             // Required: action identifier\n    categories: [\"MyPlugin\"],        // Required: array with plugin category\n    imageUrl: \"https://...\"          // Optional: image for tile view\n}\n```\n\n## Icon Types\n\n### 1. Material Design Icons\n\n```javascript\n{ icon: \"material:lightbulb\" }\n{ icon: \"material:terminal\" }\n{ icon: \"material:translate\" }\n```\n\nUses the Material Symbols Rounded font.\n\n### 2. Unicode / Emoji Icons\n\n```javascript\n{ icon: \"unicode:smile_face\" }\n```\n\nRendered at 70-80% of icon size with theming.\n\n### 3. Desktop Theme Icons\n\n```javascript\n{ icon: \"firefox\" }\n{ icon: \"folder\" }\n```\n\nUses the user's installed icon theme.\n\n### 4. No Icon\n\nOmit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.\n\n## Trigger System\n\n**Custom trigger** (items only appear when trigger is typed):\n\n```json\n{ \"trigger\": \"#\" }\n```\n\n- Type `#` alone: shows all plugin items\n- Type `# query`: filters plugin items by query\n- The query string (without trigger) is passed to `getItems(query)`\n\n**No trigger** (items always visible alongside regular apps):\n\n```json\n{ \"trigger\": \"\" }\n```\n\nSave empty trigger at runtime:\n```qml\nComponent.onCompleted: {\n    trigger = pluginService?.loadPluginData(pluginId, \"trigger\", \"#\") ?? \"#\"\n}\n```\n\n## Action Execution\n\nParse action strings in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            ToastService?.showInfo(actionData)\n            break\n        case \"copy\":\n            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", actionData])\n            ToastService?.showInfo(\"Copied to clipboard\")\n            break\n        case \"exec\":\n            Quickshell.execDetached(actionData.split(\" \"))\n            break\n        case \"url\":\n            Quickshell.execDetached([\"xdg-open\", actionData])\n            break\n        default:\n            console.warn(\"Unknown action type:\", actionType)\n    }\n}\n```\n\n## Search / Filtering\n\nThe `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).\n\n```qml\nfunction getItems(query) {\n    const allItems = [\n        { name: \"Calculator\", icon: \"material:calculate\",\n          comment: \"Open calculator\", action: \"exec:gnome-calculator\",\n          categories: [\"Tools\"] },\n        { name: \"Terminal\", icon: \"material:terminal\",\n          comment: \"Open terminal\", action: \"exec:alacritty\",\n          categories: [\"Tools\"] }\n    ]\n\n    if (!query || query.length === 0) return allItems\n\n    const q = query.toLowerCase()\n    return allItems.filter(item =>\n        item.name.toLowerCase().includes(q) ||\n        item.comment.toLowerCase().includes(q)\n    )\n}\n```\n\n## Context Menu Actions\n\nAdd right-click actions to launcher items:\n\n```qml\nfunction getContextMenuActions(item) {\n    return [\n        { name: \"Copy\", icon: \"material:content_copy\",\n          action: \"copy:\" + item.name },\n        { name: \"Open in Browser\", icon: \"material:open_in_new\",\n          action: \"url:\" + item.url }\n    ]\n}\n```\n\nContext menu actions use the same `executeItem()` handler.\n\n## Image Tile View\n\nFor image-heavy launchers (GIF search, sticker pickers), use tile view:\n\nIn `plugin.json`:\n```json\n{\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true\n}\n```\n\nIn items:\n```javascript\n{\n    name: \"Image Title\",\n    imageUrl: \"https://example.com/image.png\",\n    comment: \"Description\",\n    action: \"copy:https://example.com/image.png\",\n    categories: [\"MyPlugin\"]\n}\n```\n\n## State Persistence\n\nFor plugins with persistent state (notes, history, favorites):\n\n```qml\nproperty var notes: []\n\nComponent.onCompleted: {\n    const saved = pluginService?.loadPluginState(pluginId, \"notes\", [])\n    if (saved) notes = saved\n}\n\nfunction addNote(text) {\n    notes.push({ text: text, timestamp: Date.now() })\n    pluginService?.savePluginState(pluginId, \"notes\", notes)\n    itemsChanged()\n}\n```\n\nUse `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.\n\n## Settings for Trigger Configuration\n\nProvide a PluginSettings component for trigger customization:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myLauncher\"\n\n    StringSetting {\n        settingKey: \"trigger\"\n        label: \"Trigger\"\n        description: \"Type this prefix to activate the launcher plugin\"\n        placeholder: \"#\"\n        defaultValue: \"#\"\n    }\n\n    ToggleSetting {\n        settingKey: \"noTrigger\"\n        label: \"Always Visible\"\n        description: \"Show items alongside regular apps (no trigger needed)\"\n        defaultValue: false\n    }\n}\n```\n\n## Complete Example\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"!\"\n\n    signal itemsChanged()\n\n    property var commands: [\n        { name: \"Lock Screen\", icon: \"material:lock\",\n          comment: \"Lock the session\", action: \"exec:loginctl lock-session\" },\n        { name: \"Screenshot\", icon: \"material:screenshot_monitor\",\n          comment: \"Take a screenshot\", action: \"exec:grim\" },\n        { name: \"File Manager\", icon: \"material:folder\",\n          comment: \"Open file manager\", action: \"exec:nautilus\" }\n    ]\n\n    function getItems(query) {\n        if (!query) return commands\n        const q = query.toLowerCase()\n        return commands.filter(c =>\n            c.name.toLowerCase().includes(q) ||\n            c.comment.toLowerCase().includes(q)\n        )\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"exec\") {\n            Quickshell.execDetached(data.split(\" \"))\n        }\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"quickCommands\", \"trigger\", \"!\")\n        }\n    }\n}\n```\n",".agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md":"# Plugin Manifest Reference (plugin.json)\n\n## Required Fields\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |\n| `name` | string | Human-readable name | Non-empty |\n| `description` | string | Short description (shown in UI) | Non-empty |\n| `version` | string | Semantic version | Pattern `^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$` |\n| `author` | string | Creator name or email | Non-empty |\n| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `dash`, `dashCard`, `composite` |\n| `capabilities` | array | Plugin capabilities | At least 1 string item |\n\nOne of `component` or `components` is required (not both):\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |\n| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |\n\n## Conditional Requirements\n\n| Condition | Required Field | Description |\n|-----------|---------------|-------------|\n| `type: \"launcher\"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |\n| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `icon` | string | Material Design icon name (displayed in plugin list UI) |\n| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |\n| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |\n| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\\|<=?\\|=\\|>\\|<)\\d+\\.\\d+\\.\\d+$` |\n| `dependencies` | array | System tool dependencies (e.g., `[\"curl\", \"jq\"]`). Registry metadata. |\n| `requires` | array | Deprecated alias for `dependencies` |\n| `permissions` | array | Required permissions |\n| `trigger` | string | Launcher trigger string (required for launcher type) |\n| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |\n\n## Permissions\n\n| Permission | Description | Enforced |\n|------------|-------------|----------|\n| `settings_read` | Read plugin configuration | No (not currently enforced) |\n| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |\n| `process` | Execute system commands | No (not currently enforced) |\n| `network` | Network access | No (not currently enforced) |\n\nIf your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.\n\n## Capabilities\n\nCapabilities are free-form strings that describe what the plugin does. Common values:\n\n- `dankbar-widget` - general bar widget\n- `control-center` - integrates with Control Center\n- `monitoring` - system/service monitoring\n- `launcher` - launcher search provider\n- `desktop-widget` - desktop background widget\n- `dash-tab` - dash tab\n- `dash-card` - dash overview card\n- `ai` - AI/LLM integration\n- `slideout` - uses slideout panel\n\n## Startup Check\n\nThe `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\n## Components (Composite Plugins)\n\nThe `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nValid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.\n\n## Dash Block\n\nPlugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.\n\n```json\n\"dash\": {\n    \"icon\": \"timer\",\n    \"title\": \"Stopwatch\",\n    \"card\": { \"title\": \"Progress\", \"icon\": \"hourglass_bottom\", \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 4, \"maxH\": 2 },\n    \"options\": [\n        { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false, \"description\": \"Hide the secondary line\" },\n        { \"key\": \"style\", \"text\": \"Style\", \"type\": \"choice\", \"def\": \"bars\", \"choices\": [{ \"value\": \"bars\", \"text\": \"Bars\" }, { \"value\": \"dots\", \"text\": \"Dots\" }] },\n        { \"key\": \"limit\", \"text\": \"Items\", \"type\": \"number\", \"def\": 5, \"min\": 1, \"max\": 20, \"step\": 1 }\n    ]\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |\n| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |\n| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |\n| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |\n| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |\n\n## Dash Examples\n\nStandalone tab and standalone card use the single file form:\n\n```json\n{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"dash\": { \"title\": \"Stopwatch\" },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\n```json\n{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"dash\": { \"card\": { \"w\": 2, \"h\": 1, \"maxW\": 4, \"maxH\": 2 } },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\nA tab plus a card, or dash surfaces plus a bar widget, use `components` with `\"type\": \"composite\"`.\n\n## Complete Example\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin demonstrating all fields\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\", \"control-center\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"dependencies\": [\"curl\", \"jq\"],\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\", \"network\"]\n}\n```\n\n## Launcher Example\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Search and execute custom actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n## JSON Schema\n\nThe complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:\n\n```bash\n# Using python\npython3 -c \"\nimport json, jsonschema\nschema = json.load(open('path/to/plugin-schema.json'))\nmanifest = json.load(open('plugin.json'))\njsonschema.validate(manifest, schema)\nprint('Valid!')\n\"\n\n# Using jq (syntax check only)\njq . plugin.json\n```\n\n## Additional Properties\n\nThe schema allows additional properties (`\"additionalProperties\": true`), so plugins can include custom fields. Common custom fields seen in production plugins:\n\n- `viewMode` - launcher display mode (`\"tile\"` for image grids)\n- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)\n",".agents/skills/dms-plugin-dev/references/popout-service-reference.md":"# PopoutService Reference\n\nThe `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.\n\n## Setup\n\nDeclare the property in your component for injection to work:\n\n```qml\nproperty var popoutService: null\n```\n\nWithout this declaration, injection fails with: `Cannot assign to non-existent property \"popoutService\"`\n\n## Popouts (DankPopout-based)\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |\n| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |\n| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |\n| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |\n| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |\n| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |\n| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |\n| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |\n\n## Modals (DankModal-based)\n\n| Modal | Show | Hide | Notes |\n|-------|------|------|-------|\n| Settings | `openSettings()` | `closeSettings()` | Full settings interface |\n| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |\n| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |\n| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |\n| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |\n| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |\n| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |\n| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |\n| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |\n\n## Slideouts\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |\n\n## Usage Examples\n\n### Simple toggle\n\n```qml\nMouseArea {\n    onClicked: popoutService?.toggleControlCenter()\n}\n```\n\n### Conditional popout\n\n```qml\nConnections {\n    target: BatteryService\n    function onPercentageChanged() {\n        if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n            popoutService?.openBattery()\n        }\n    }\n}\n```\n\n### Context menu with multiple actions\n\n```qml\nMouseArea {\n    acceptedButtons: Qt.LeftButton | Qt.RightButton\n    onClicked: (mouse) => {\n        if (mouse.button === Qt.RightButton) contextMenu.popup()\n        else popoutService?.toggleControlCenter()\n    }\n}\n\nMenu {\n    id: contextMenu\n    MenuItem { text: \"Settings\"; onClicked: popoutService?.openSettings() }\n    MenuItem { text: \"Notifications\"; onClicked: popoutService?.toggleNotificationCenter() }\n    MenuItem { text: \"Power\"; onClicked: popoutService?.openPowerMenu() }\n}\n```\n\n### Position-aware toggle (from bar pill)\n\nSome toggle functions accept position parameters for proper popout placement:\n\n```qml\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\n## Best Practices\n\n1. **Always use optional chaining** (`?.`) - the service may not be injected yet\n2. **Check feature availability** before opening feature-specific popouts:\n   ```qml\n   if (BatteryService.batteryAvailable) {\n       popoutService?.openBattery()\n   }\n   ```\n3. **Lazy loading** - first access may activate lazy loaders; this is normal\n4. **Popouts are shared** - avoid opening conflicting popouts simultaneously\n5. **User intent** - only trigger popouts from user actions or critical system events\n6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters\n\n## Injection Locations\n\nThe service is injected at these points:\n- `DMSShell.qml` - daemon plugins\n- `WidgetHost.qml` - widget plugins in left/right bar sections\n- `CenterSection.qml` - center bar widgets\n- `PluginsTab.qml` - settings components\n",".agents/skills/dms-plugin-dev/references/settings-components-reference.md":"# Settings Components Reference\n\nAll plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.\n\n## PluginSettings Wrapper\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"  // Required: must match plugin.json id\n\n    // Setting components go here\n}\n```\n\n**Important:** The plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json for the settings UI to render. Without it, users see an error.\n\n**PluginSettings provides to children:**\n- `saveValue(key, value)` - save a setting value\n- `loadValue(key, defaultValue)` - load a setting value\n- `saveState(key, value)` - save plugin state (separate file)\n- `loadState(key, defaultValue)` - load plugin state\n- `clearState()` - clear all plugin state\n- Variant management functions (for variant plugins)\n\n## StringSetting\n\nText input field.\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"        // Required: storage key\n    label: \"API Key\"            // Required: display label\n    description: \"Your API key\" // Optional: help text\n    placeholder: \"sk-...\"      // Optional: input placeholder\n    defaultValue: \"\"            // Optional: default (default: \"\")\n}\n```\n\n**Layout:** Vertical stack - label, description, input field.\n\n## ToggleSetting\n\nBoolean toggle switch.\n\n```qml\nToggleSetting {\n    settingKey: \"notifications\"    // Required: storage key\n    label: \"Enable Notifications\"  // Required: display label\n    description: \"Show alerts\"     // Optional: help text\n    defaultValue: true             // Optional: default (default: false)\n}\n```\n\n**Layout:** Horizontal - label/description on left, toggle on right.\n\n## SelectionSetting\n\nDropdown menu.\n\n```qml\nSelectionSetting {\n    settingKey: \"theme\"           // Required: storage key\n    label: \"Theme\"                // Required: display label\n    description: \"Color scheme\"   // Optional: help text\n    options: [                    // Required: array of options\n        { label: \"Dark\", value: \"dark\" },\n        { label: \"Light\", value: \"light\" },\n        { label: \"Auto\", value: \"auto\" }\n    ]\n    defaultValue: \"dark\"          // Optional: default value\n}\n```\n\nOptions can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.\n\n**Layout:** Horizontal - label/description on left, dropdown on right.\n\n**Reacting to changes:**\n```qml\nSelectionSetting {\n    settingKey: \"updateInterval\"\n    label: \"Update Interval\"\n    options: [\n        { label: \"1 minute\", value: \"60\" },\n        { label: \"5 minutes\", value: \"300\" }\n    ]\n    defaultValue: \"300\"\n    onValueChanged: (newValue) => {\n        console.log(\"Interval changed to:\", newValue)\n    }\n}\n```\n\n## SliderSetting\n\nNumeric slider with min/max.\n\n```qml\nSliderSetting {\n    settingKey: \"opacity\"      // Required: storage key\n    label: \"Opacity\"           // Required: display label\n    description: \"Background\"  // Optional: help text\n    defaultValue: 85           // Optional: default value\n    minimum: 0                 // Required: min value\n    maximum: 100               // Required: max value\n    unit: \"%\"                  // Optional: unit label shown after value\n    leftIcon: \"dark_mode\"      // Optional: Material icon on left\n    rightIcon: \"light_mode\"    // Optional: Material icon on right\n}\n```\n\n## ColorSetting\n\nColor picker.\n\n```qml\nColorSetting {\n    settingKey: \"accentColor\"      // Required: storage key\n    label: \"Accent Color\"          // Required: display label\n    description: \"Custom accent\"   // Optional: help text\n    defaultValue: \"#ff5722\"        // Optional: default hex color\n}\n```\n\nDisplays a color swatch that opens a color picker dialog.\n\n## ListSetting\n\nManage a list of items with manual add/remove. Use when you need custom UI for adding items.\n\n```qml\nListSetting {\n    id: itemList\n    settingKey: \"items\"           // Required: storage key\n    label: \"Saved Items\"          // Required: display label\n    description: \"Your items\"     // Optional: help text\n    defaultValue: []              // Optional: default array\n    delegate: Component {         // Optional: custom item display\n        StyledRect {\n            width: parent.width\n            height: 40\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                anchors.left: parent.left\n                anchors.leftMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                text: modelData.name\n                color: Theme.surfaceText\n            }\n\n            Rectangle {\n                anchors.right: parent.right\n                anchors.rightMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                width: 60\n                height: 28\n                color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                radius: Theme.cornerRadius\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: \"Remove\"\n                    color: Theme.errorText\n                    font.pixelSize: Theme.fontSizeSmall\n                    font.weight: Font.Medium\n                }\n\n                MouseArea {\n                    id: removeArea\n                    anchors.fill: parent\n                    hoverEnabled: true\n                    cursorShape: Qt.PointingHandCursor\n                    onClicked: itemList.removeItem(index)\n                }\n            }\n        }\n    }\n}\n```\n\n**Methods:**\n- `addItem(item)` - add an item to the list\n- `removeItem(index)` - remove item at index\n\n## ListSettingWithInput\n\nComplete list management with built-in form. Best for collecting structured data.\n\n```qml\nListSettingWithInput {\n    settingKey: \"locations\"        // Required: storage key\n    label: \"Locations\"             // Required: display label\n    description: \"Track zones\"     // Optional: help text\n    defaultValue: []               // Optional: default array\n    fields: [                      // Required: field definitions\n        {\n            id: \"name\",            // Required: key in saved object\n            label: \"Name\",         // Required: column header\n            placeholder: \"Home\",   // Optional: input placeholder\n            width: 150,            // Optional: column width (default: 200)\n            required: true         // Optional: must have value to add\n        },\n        {\n            id: \"timezone\",\n            label: \"Timezone\",\n            placeholder: \"America/New_York\",\n            width: 200,\n            required: true\n        }\n    ]\n}\n```\n\nAutomatically generates: column headers, input fields, add button with validation, list display, remove buttons.\n\n## Mixing Custom UI with Settings\n\nYou can interleave regular QML elements with setting components:\n\n```qml\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"name\"\n        label: \"Display Name\"\n    }\n\n    StyledText {\n        width: parent.width\n        text: \"Advanced Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n        topPadding: Theme.spacingL\n    }\n\n    ToggleSetting {\n        settingKey: \"debug\"\n        label: \"Debug Mode\"\n        defaultValue: false\n    }\n}\n```\n\n## Default Values\n\nDefine sensible defaults in every setting component. The default is used when no saved value exists:\n\n```qml\nStringSetting { settingKey: \"text\"; defaultValue: \"Hello\" }\nToggleSetting { settingKey: \"enabled\"; defaultValue: true }\nSelectionSetting { settingKey: \"mode\"; defaultValue: \"auto\" }\nSliderSetting { settingKey: \"opacity\"; defaultValue: 85 }\nColorSetting { settingKey: \"color\"; defaultValue: \"#ff5722\" }\nListSetting { settingKey: \"items\"; defaultValue: [] }\nListSettingWithInput { settingKey: \"data\"; defaultValue: [] }\n```\n",".agents/skills/dms-plugin-dev/references/theme-reference.md":"# Theme Property Reference\n\nAll theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.\n\n## Font Sizes\n\n```qml\nTheme.fontSizeSmall     // 12px (scaled by SettingsData.fontScale)\nTheme.fontSizeMedium    // 14px (scaled)\nTheme.fontSizeLarge     // 16px (scaled)\nTheme.fontSizeXLarge    // 20px (scaled)\n```\n\n## Icon Sizes\n\n```qml\nTheme.iconSizeSmall     // 16px\nTheme.iconSize          // 24px (default)\nTheme.iconSizeLarge     // 32px\n```\n\n## Spacing\n\n```qml\nTheme.spacingXS         // Extra small\nTheme.spacingS          // Small\nTheme.spacingM          // Medium\nTheme.spacingL          // Large\nTheme.spacingXL         // Extra large\n```\n\n## Border Radius\n\n```qml\nTheme.cornerRadius      // Standard\nTheme.cornerRadiusSmall // Smaller\nTheme.cornerRadiusLarge // Larger\n```\n\n## Surface Colors\n\n```qml\nTheme.surface\nTheme.surfaceContainerLow\nTheme.surfaceContainer\nTheme.surfaceContainerHigh\nTheme.surfaceContainerHighest\n```\n\n## Text Colors\n\n```qml\nTheme.onSurface          // Primary text on surface\nTheme.onSurfaceVariant   // Secondary text on surface\nTheme.surfaceText        // Alias for primary surface text\nTheme.surfaceVariantText // Alias for secondary surface text\nTheme.outline            // Border/divider color\n```\n\n## Semantic Colors\n\n```qml\nTheme.primary\nTheme.onPrimary\nTheme.secondary\nTheme.onSecondary\nTheme.error\nTheme.errorHover\nTheme.errorText\nTheme.warning\nTheme.success\n```\n\n## Special Functions\n\n```qml\nTheme.popupBackground()  // Popup background with proper opacity\n```\n\n## Common Widget Patterns\n\n### Icon with Text\n\n```qml\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nRow {\n    spacing: Theme.spacingS\n\n    DIcon {\n        name: \"icon_name\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.iconSize\n    }\n\n    StyledText {\n        text: \"Label\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.fontSizeMedium\n    }\n}\n```\n\n### Container with Border\n\n```qml\nRectangle {\n    color: Theme.surfaceContainerHigh\n    radius: Theme.cornerRadius\n    border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)\n    border.width: 1\n}\n```\n\n### Hover Effect\n\n```qml\nRectangle {\n    id: container\n    color: Theme.surfaceContainerHigh\n\n    MouseArea {\n        anchors.fill: parent\n        hoverEnabled: true\n        onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        onExited: container.color = Theme.surfaceContainerHigh\n    }\n}\n```\n\n### Clickable Pill\n\n```qml\nStyledRect {\n    width: content.implicitWidth + Theme.spacingM * 2\n    height: parent.widgetThickness\n    radius: Theme.cornerRadius\n    color: mouseArea.containsMouse\n        ? Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        : Theme.surfaceContainerHigh\n\n    MouseArea {\n        id: mouseArea\n        anchors.fill: parent\n        hoverEnabled: true\n        cursorShape: Qt.PointingHandCursor\n    }\n\n    Row {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingS\n\n        DIcon {\n            name: \"star\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.iconSize\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        StyledText {\n            text: \"Label\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.fontSizeMedium\n            anchors.verticalCenter: parent.verticalCenter\n        }\n    }\n}\n```\n\n## Common Mistakes\n\n**Wrong property names** (these do NOT exist):\n```qml\nTheme.fontSizeS        // Use Theme.fontSizeSmall\nTheme.iconSizeS        // Use Theme.iconSizeSmall\nTheme.spacingSmall     // Use Theme.spacingS\nTheme.borderRadius     // Use Theme.cornerRadius\n```\n\n**Hardcoded values** (do NOT do this):\n```qml\ncolor: \"#1e1e1e\"       // Use Theme.surfaceContainerHigh\ncolor: \"white\"         // Use Theme.surfaceText\nfont.pixelSize: 14     // Use Theme.fontSizeMedium\n```\n\n## Available Widgets from qs.Widgets\n\n| Widget | Description |\n|--------|-------------|\n| `StyledText` | Themed text with proper color defaults |\n| `StyledRect` | Themed rectangle |\n| `DIcon` | Material Symbols icon renderer |\n| `DankNFIcon` | Nerd Font icon renderer |\n| `DButton` | Themed button |\n| `DToggle` | Toggle switch |\n| `DTextField` | Text input field |\n| `DSlider` | Slider control |\n| `DDropdown` | Dropdown menu |\n| `DGridView` | Grid layout view |\n| `DListView` | List layout view |\n| `DFlickable` | Scrollable container |\n| `DTabBar` | Tab bar navigation |\n| `DankCollapsibleSection` | Collapsible content section |\n| `DankTooltip` | Hover tooltip |\n| `DankNumberStepper` | Number +/- control |\n| `DankFilterChips` | Filter chip row |\n| `CachingImage` | Image with disk cache |\n| `NumericText` | Fixed-width numeric display |\n\n## Checking All Properties\n\n```bash\ngrep \"property\" Common/Theme.qml\n```\n",".agents/skills/dms-plugin-dev/references/widget-plugin-guide.md":"# Widget Plugin Guide\n\nWidgets display pills in DankBar and in docks (same component, no extra code), optionally open popouts, and can integrate with the Control Center.\n\n## Base Component\n\nWidgets use `PluginComponent` from `qs.Modules.Plugins`.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n    popoutContent: Component { /* ... */ }\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n## Injected Properties\n\nThese are automatically set by the plugin host:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `axis` | object | Bar axis info (horizontal/vertical) |\n| `section` | string | Bar section: `\"left\"`, `\"center\"`, or `\"right\"` |\n| `parentScreen` | object | Screen reference for multi-monitor |\n| `widgetThickness` | real | Widget size perpendicular to bar edge |\n| `barThickness` | real | Bar thickness parallel to edge |\n| `pluginId` | string | This plugin's ID |\n| `pluginService` | object | PluginService reference |\n| `pluginData` | object | Reactive plugin settings data |\n| `surfaceContext` | object | The host surface: `kind` (`\"bar\"` or `\"dock\"`), `id`, `screen`, `isVertical`, `thickness`, `live`, `editMode`, `inlineExpansion` |\n| `widgetInstanceId` | string | This occurrence; the same widget can appear on several bars and docks |\n\n## Bar Pills\n\nDefine `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).\n\n### Horizontal Bar Pill\n\n```qml\nhorizontalBarPill: Component {\n    StyledRect {\n        width: content.implicitWidth + Theme.spacingM * 2\n        height: parent.widgetThickness\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Row {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSize\n                anchors.verticalCenter: parent.verticalCenter\n            }\n\n            StyledText {\n                text: \"Label\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n                anchors.verticalCenter: parent.verticalCenter\n            }\n        }\n    }\n}\n```\n\n### Vertical Bar Pill\n\n```qml\nverticalBarPill: Component {\n    StyledRect {\n        width: parent.widgetThickness\n        height: content.implicitHeight + Theme.spacingM * 2\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Column {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSizeSmall\n            }\n        }\n    }\n}\n```\n\n**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.\n\n## Popout Content\n\nOpen a popout window when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                    font.pixelSize: Theme.fontSizeMedium\n                }\n            }\n        }\n    }\n}\n```\n\n**PopoutComponent properties:**\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `headerText` | string | `\"\"` | Main header (bold, large). Hidden if empty. |\n| `detailsText` | string | `\"\"` | Subtitle below header. Hidden if empty. |\n| `closePopout` | function | (injected) | Call to close the popout programmatically. |\n| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |\n| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |\n\n**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Custom Click Actions\n\nOverride the default popout behavior:\n\n```qml\nPluginComponent {\n    // Simple no-args handler\n    pillClickAction: () => {\n        popoutService?.toggleControlCenter()\n    }\n\n    // With position params (x, y, width, section, screen)\n    pillClickAction: (x, y, width, section, screen) => {\n        popoutService?.toggleControlCenter(x, y, width, section, screen)\n    }\n\n    pillRightClickAction: () => {\n        popoutService?.openSettings()\n    }\n}\n```\n\n## Control Center Integration\n\nAdd CC properties to show your widget in the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"Feature Name\"\n    ccWidgetSecondaryText: isActive ? \"Active\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n}\n```\n\n**CC properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `ccWidgetIcon` | string | Material icon name |\n| `ccWidgetPrimaryText` | string | Main label |\n| `ccWidgetSecondaryText` | string | Subtitle / status text |\n| `ccWidgetIsActive` | bool | Active state (changes styling) |\n\n**CC signals:**\n\n| Signal | When fired |\n|--------|-----------|\n| `ccWidgetToggled()` | Icon area clicked |\n| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |\n\n**CC sizing rules:**\n- 25% width - SmallToggleButton (icon only)\n- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)\n- Users can resize in CC edit mode\n\n### Detail Content (CompoundPill)\n\nAdd an expandable panel below the CC widget:\n\n```qml\nccDetailContent: Component {\n    Rectangle {\n        implicitHeight: 200\n        color: Theme.surfaceContainerHigh\n        radius: Theme.cornerRadius\n\n        Column {\n            anchors.fill: parent\n            anchors.margins: Theme.spacingM\n            spacing: Theme.spacingS\n\n            // Detail UI here\n        }\n    }\n}\n```\n\n## Visibility Control\n\nConditionally show/hide the bar pill:\n\n```qml\nPluginComponent {\n    visibilityCommand: \"pgrep -x myapp\"\n    visibilityInterval: 5  // seconds between checks\n}\n```\n\n**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.\n\n## Docks\n\nUsers add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.\n\n- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.\n- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.\n- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.\n- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.\n\nOptional attached panel: set `attachedContent: Component { ... }`. When the dock's \"Open widgets\" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.\n\n```qml\nPluginComponent {\n    id: root\n\n    attachedContent: Component {\n        Column {\n            padding: Theme.spacingM\n            spacing: Theme.spacingM\n\n            DButton {\n                text: I18n.trFor(\"myPlugin\", \"Close\")\n                onClicked: root.surfaceContext.dismissExpansion()\n            }\n        }\n    }\n}\n```\n\nThe panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.\n\n## Popout Namespace\n\nFor plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:\n\n```qml\nPluginComponent {\n    layerNamespacePlugin: \"my-plugin\"\n}\n```\n\nThe shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.\n\n## Reading Plugin Data\n\nAccess saved settings reactively via the injected `pluginData`:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId)\n                displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        }\n    }\n}\n```\n\n## Complete Example\n\nBased on the ExampleEmojiPlugin pattern:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property var emojis: [\"star\", \"heart\", \"smile\"]\n    property int currentIndex: 0\n\n    Timer {\n        interval: 2000\n        running: true\n        repeat: true\n        onTriggered: currentIndex = (currentIndex + 1) % emojis.length\n    }\n\n    popoutWidth: 350\n    popoutHeight: 400\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeLarge\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"Emoji Picker\"\n\n            DGridView {\n                width: parent.width\n                height: 300\n                cellWidth: 50\n                cellHeight: 50\n                model: root.emojis\n\n                delegate: Rectangle {\n                    width: 48\n                    height: 48\n                    radius: Theme.cornerRadius\n                    color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : \"transparent\"\n\n                    Text {\n                        anchors.centerIn: parent\n                        text: modelData\n                        font.pixelSize: 24\n                    }\n\n                    MouseArea {\n                        id: mouseArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        onClicked: {\n                            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n                            ToastService?.showInfo(\"Copied \" + modelData)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n","AGENTS.md":"## What is this?\n\nDMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.\n\n## Repo Structure\n\n- core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.\n- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/\n- dux-qml-common/ - submodule of shared QML widgets\n- docs/, distro/ - IPC and theme docs, packaging\n\n## Rules\n\n- Keep it simple.\n- Do not edit code in response to a question.\n- Never commit, push, or open a PR unless asked.\n- Point out glaring issues we missed.\n- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.\n- `core` and `quickshell` share the socket protocol. Changing one means checking the other.\n- QML uses Theme tokens, never hardcoded colors, spacing, or constants.\n- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.\n- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.\n- Guard clauses, early returns.\n- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.\n- Do not edit generated code. Mocks come from mockery.\n- No library for a simple problem. Use a modern one for a complex, common problem.\n\n## Tests\n\n- A test must fail on the broken code. Otherwise do not write it.\n- No useless tests: \"main.go contains func main()\", defaults, \"it instantiates\", behavior the change didn't touch.\n- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.\n- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.\n\n## Connections vs bound properties\n\n- In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.\n- Source in the same file: put the handler on it.\n- Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.\n- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.\n- A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.\n- Singletons under Services/ and Common/ keep their `Connections`.\n\n## Commit Messages\n\n`area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.\n","quickshell/PLUGINS/AttachedPanelExample/README.md":"# Attached panel example\n\nA normal widget plugin that also sets `attachedContent`. Enable it in Settings → Plugins\nand add it to a bar, a dock, or both.\n\n- On a bar, a click opens the regular popout.\n- On a dock, what a click does is the dock's choice. With Settings → Dock → Apps & widgets\n  → Open widgets set to Inline, the dock grows and shows the attached panel next to the\n  apps. With Popout (the default) it opens the same popout the bar would.\n\nEscape or Close dismisses the panel and hands focus back to the widget. Opening another\nattached widget replaces the panel. No background process, no timer.\n\nSee [the surface widget contract](../SURFACE_WIDGETS.md).\n","quickshell/PLUGINS/AttachedPanelExample/plugin.json":"{\n    \"id\": \"attachedPanelExample\",\n    \"name\": \"Attached Panel Example\",\n    \"description\": \"A widget with an optional attached dock panel and a normal bar popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS contributors\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./AttachedPanelExample.qml\",\n    \"icon\": \"sticky_note_2\",\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/ColorDemoPlugin/plugin.json":"{\n    \"id\": \"colorDemo\",\n    \"name\": \"Color Demo\",\n    \"description\": \"Demonstrates color picker plugin setting\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"icon\": \"palette\",\n    \"type\": \"widget\",\n    \"component\": \"./ColorDemoWidget.qml\",\n    \"settings\": \"./ColorDemoSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/ControlCenterDetailExample/plugin.json":"{\n    \"id\": \"controlCenterDetailExample\",\n    \"name\": \"CC Detail Example\",\n    \"description\": \"Example plugin with Control Center detail dropdown\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./DetailExampleWidget.qml\",\n    \"icon\": \"settings\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/ControlCenterExample/plugin.json":"{\n    \"id\": \"controlCenterExample\",\n    \"name\": \"CC Toggle Example\",\n    \"description\": \"Example plugin with Control Center toggle widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./ControlCenterExampleWidget.qml\",\n    \"icon\": \"toggle_on\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/DashCardExample/README.md":"# Progress Example\n\nA standalone overview card: how much of today, this week, this month or this year has passed. The manifest uses the single file form, `\"type\": \"dashCard\"` plus `\"component\"`.\n\nWhat it shows:\n\n- `DashCardComponent` as the root. It has no tab, so it is not clickable and keyboard navigation skips it.\n- A layout for every size in its range (1x1 up to 4x2). `wide` and `tall` come from the card's pixel size against `DashMetrics.gridRowUnit`.\n- `live` gating. `SystemClock` is disabled while the dash is hidden.\n- `tone` and the card colors (`accentColor`, `contentColor`, `mutedColor`, `chipColor`), so the card follows the Surface, Primary, Secondary and Tertiary tones like the built-in cards.\n- `dash.options`: a choice for the period, a toggle for the time left and a choice for the tone. They show up in the card's options sheet in edit mode and under Settings → Dashboard.\n\nAdd it from the dash: open the overview, hover the selected page icon, click the pencil, then Add widget.\n","quickshell/PLUGINS/DashCardExample/plugin.json":"{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card showing how much of the day, week, month or year has passed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"icon\": \"hourglass_bottom\",\n    \"dash\": {\n        \"card\": {\n            \"title\": \"Progress\",\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 4,\n            \"maxH\": 2\n        },\n        \"options\": [\n            {\n                \"key\": \"period\",\n                \"text\": \"Period\",\n                \"type\": \"choice\",\n                \"def\": \"day\",\n                \"choices\": [\n                    { \"value\": \"day\", \"text\": \"Day\" },\n                    { \"value\": \"week\", \"text\": \"Week\" },\n                    { \"value\": \"month\", \"text\": \"Month\" },\n                    { \"value\": \"year\", \"text\": \"Year\" }\n                ]\n            },\n            {\n                \"key\": \"remaining\",\n                \"text\": \"Show time left\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" },\n                    { \"value\": \"secondary\", \"text\": \"Secondary\" },\n                    { \"value\": \"tertiary\", \"text\": \"Tertiary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/DashCounterExample/README.md":"# Counter Example\n\nA composite plugin with three surfaces that share one counter:\n\n- `components.dash` (`CounterTab.qml`): a dash tab built on `DashTabComponent`. It hosts a `DashWidgetGrid`, so the dash Edit, Add widget, Reset and Clear All actions work on it. The Reset widget starts in the Add menu (`enabled: false`). `+` and `-` change the count.\n- `components.dashCard` (`CounterCard.qml`): an overview card built on `DashCardComponent`. Clicking it opens the tab. Left and Right change the count while it has focus.\n- `components.widget` (`CounterWidget.qml`): a bar pill. Left click toggles the dash on the counter tab, right click adds one. The same file works in a dock with no extra code.\n\nThe count is stored with `setData(\"count\", value)` / `savePluginData` and read back through `pluginData.count`. Every surface reloads `pluginData` on `pluginDataChanged`, so they stay in sync.\n\nOpen the tab from a terminal with `dms ipc call dash open plugin_dashCounterExample`. If the tab is hidden from the tab bar it opens as a detail page with a back button.\n","quickshell/PLUGINS/DashCounterExample/plugin.json":"{\n    \"id\": \"dashCounterExample\",\n    \"name\": \"Counter Example\",\n    \"description\": \"One counter shared by a dash tab, an overview card and a bar or dock widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"dash-tab\", \"dash-card\", \"dankbar-widget\"],\n    \"icon\": \"counter_1\",\n    \"components\": {\n        \"dash\": \"./CounterTab.qml\",\n        \"dashCard\": \"./CounterCard.qml\",\n        \"widget\": \"./CounterWidget.qml\"\n    },\n    \"dash\": {\n        \"title\": \"Counter\",\n        \"card\": {\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 3,\n            \"maxH\": 2\n        }\n    },\n    \"requires_dms\": \">=1.7.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/DashTabExample/README.md":"# Stopwatch Example\n\nA standalone dash tab. The manifest uses the single file form, `\"type\": \"dash\"` plus `\"component\"`, the same way a bar widget uses `\"type\": \"widget\"`.\n\nWhat it shows:\n\n- `DashTabComponent` as the root, sized with `implicitHeight` so the dash grows when many laps are shown.\n- `live` gating. The display timer only runs while the tab is on screen and the stopwatch is running. Time is computed from stored timestamps, so the stopwatch keeps counting while the dash is closed or another tab is open.\n- The plugin state API (`loadPluginState` / `savePluginState`) for runtime data. Nothing goes into settings, so no permissions are needed.\n- `focusTarget` and `restoreFocus()`. Opening the tab focuses Start without a focus ring, so Space starts it right away.\n- `handleKeyEvent`: Space starts or pauses, L records a lap, R resets.\n- `menuActions`: Reset in the tab's edit-mode controls.\n- `dash.options`: a toggle for hundredths and a number for how many laps to list, read as `options.hundredths` and `options.laps`.\n\nOpen it with `dms ipc call dash open plugin_dashTabExample`.\n","quickshell/PLUGINS/DashTabExample/plugin.json":"{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab: a stopwatch with laps that keeps time while the dash is closed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"icon\": \"timer\",\n    \"dash\": {\n        \"title\": \"Stopwatch\",\n        \"options\": [\n            {\n                \"key\": \"hundredths\",\n                \"text\": \"Show hundredths\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"laps\",\n                \"text\": \"Laps shown\",\n                \"type\": \"number\",\n                \"def\": 5,\n                \"min\": 0,\n                \"max\": 10\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/ExampleCompositePlugin/README.md":"# Composite Example\n\nA single plugin that provides **all three surfaces at once** by combining three of\nthe standalone example plugins:\n\n| Surface | Source example | File |\n|---------|----------------|------|\n| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |\n| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |\n| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |\n\nIt demonstrates the `components` manifest map, where each surface points at its own\nQML file:\n\n```json\n\"type\": \"composite\",\n\"components\": {\n    \"daemon\":  \"./CompositeDaemon.qml\",\n    \"widget\":  \"./CompositeBarWidget.qml\",\n    \"desktop\": \"./CompositeDesktopWidget.qml\"\n}\n```\n\nAll surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings\nnamespace (`exampleComposite`), so `pluginData` is the same for every surface.\n\n## Surfaces\n\n- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script\n  on change. Also registers an `IpcHandler` (`target: \"compositeExample\"`) exposing a\n  `runHook` call, so you can trigger the hook over IPC.\n- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout\n  that copies to the clipboard.\n- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.\n\n## Usage\n\n1. Copy this directory into `$CONFIGPATH/dms/plugins/`.\n2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.\n   (Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they\n   do not auto-load, because they also carry a daemon.)\n3. Add the bar widget via Settings → Appearance → DankBar Layout.\n4. Place the desktop clock via Settings → Desktop Widgets.\n\n## Notes\n\n- The daemon surface is instantiated once and lives for as long as the plugin is\n  enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.\n- Cross-surface runtime state (not needed here) is best shared via\n  `PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each\n  surface is a separate object.\n- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only\n  understood by DMS 1.5.0 and later.\n","quickshell/PLUGINS/ExampleCompositePlugin/plugin.json":"{\n    \"id\": \"exampleComposite\",\n    \"name\": \"Composite Example\",\n    \"description\": \"One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\", \"clipboard\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./CompositeDaemon.qml\",\n        \"widget\": \"./CompositeBarWidget.qml\",\n        \"desktop\": \"./CompositeDesktopWidget.qml\"\n    },\n    \"settings\": \"./CompositeSettings.qml\",\n    \"requires_dms\": \">=1.5.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleDesktopClock/plugin.json":"{\n    \"id\": \"exampleDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"An example desktop widget displaying an analog clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\", \"clock\"],\n    \"component\": \"./DesktopClock.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./DesktopClockSettings.qml\",\n    \"requires_dms\": \">=1.2.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleEmojiPlugin/README.md":"# Emoji Cycler Plugin\n\nAn example dms plugin that displays cycling emojis in your bar with an emoji picker popout.\n\n## Features\n\n- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar\n- **Emoji Picker**: Click the widget to open a grid of 120+ emojis\n- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard\n- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown\n\n## Installation\n\n1. Copy this directory to `~/.config/dms/plugins/ExampleEmojiPlugin`\n2. Open DMS Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Emoji Cycler\"\n5. Add `exampleEmojiPlugin` to your DankBar widget list\n\n## Settings\n\n### Emoji Set\nChoose from different emoji collections:\n- **Happy & Sad**: Mix of emotional faces\n- **Hearts**: Various colored hearts\n- **Hand Gestures**: Thumbs up, peace signs, etc.\n- **All Mixed**: A bit of everything\n\n### Cycle Speed\nControl how fast emojis rotate (500ms - 10000ms)\n\n### Max Bar Emojis\nHow many emojis to display at once (1-8)\n\n## Usage\n\n**In the bar**: Watch emojis cycle through automatically\n**Click the widget**: Opens emoji picker with 120+ emojis\n**Click any emoji**: Copies it to clipboard and shows toast\n\n## Example Code Highlights\n\nThis plugin demonstrates:\n- Using `PluginComponent` for bar integration\n- `SelectionSetting`, `SliderSetting` for configuration\n- Plugin-local translations (`translations/` + `I18n.trFor`)\n- Timer-based animation\n- Popout content with grid layout\n- External command execution (`Quickshell.execDetached`)\n- Toast notifications (`ToastService.show`)\n- Dynamic settings loading/saving\n\nPerfect template for creating your own DMS plugins!\n","quickshell/PLUGINS/ExampleEmojiPlugin/plugin.json":"{\n    \"id\": \"exampleEmojiPlugin\",\n    \"name\": \"Emoji Cycler\",\n    \"description\": \"Display cycling emojis in your bar with a handy emoji picker popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"emoji-search\", \"clipboard\", \"dankbar-widget\"],\n    \"component\": \"./EmojiWidget.qml\",\n    \"icon\": \"mood\",\n    \"settings\": \"./EmojiSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleStartupCheck/plugin.json":"{\n    \"id\": \"exampleStartupCheck\",\n    \"name\": \"Startup Check Example\",\n    \"description\": \"Demonstrates startupCheck - blocks activation when the 'boregard' dependency is missing\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./StartupCheckWidget.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"icon\": \"verified_user\",\n    \"dependencies\": [\"boregard\"],\n    \"permissions\": [\"process\"]\n}\n","quickshell/PLUGINS/ExampleWithVariants/README.md":"# Example with Variants Plugin\n\nThis plugin demonstrates the dynamic variant system for Dux Shell plugins.\n\n## What are Variants?\n\nVariants allow a single plugin to create multiple widget instances with different configurations. Each variant appears as a separate widget option in the Bar Settings \"Add Widget\" menu.\n\n## How It Works\n\n### Plugin Architecture\n\n1. **Single Component**: The plugin defines one widget component (`VariantWidget.qml`)\n2. **Variant Data**: Each variant stores its configuration in plugin settings\n3. **Dynamic Creation**: Variants are created through the plugin's settings UI\n4. **Widget ID Format**: Variants use the format `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Widget Properties\n\nEach variant widget receives:\n- `pluginService`: Reference to PluginService\n- `pluginId`: The base plugin ID (e.g., \"exampleVariants\")\n- `variantId`: The specific variant ID (e.g., \"variant_1234567890\")\n- `variantData`: The variant's configuration object\n\n### Variant Configuration\n\nThis example stores:\n```javascript\n{\n    id: \"variant_1234567890\",\n    name: \"My Variant\",\n    text: \"Display Text\",\n    icon: \"star\",\n    color: \"#FF5722\"\n}\n```\n\n## Creating Your Own Variant Plugin\n\n### 1. Widget Component\n\nCreate a widget that accepts variant data:\n\n```qml\nRectangle {\n    property var pluginService: null\n    property string pluginId: \"\"\n    property string variantId: \"\"\n    property var variantData: null\n\n    // Use variantData for configuration\n    property string displayText: variantData?.text || \"Default\"\n}\n```\n\n### 2. Settings Component\n\nProvide UI to manage variants:\n\n```qml\nFocusScope {\n    property var pluginService: null\n\n    // Create variant\n    pluginService.createPluginVariant(pluginId, variantName, variantConfig)\n\n    // Remove variant\n    pluginService.removePluginVariant(pluginId, variantId)\n\n    // Update variant\n    pluginService.updatePluginVariant(pluginId, variantId, variantConfig)\n\n    // Get all variants\n    var variants = pluginService.getPluginVariants(pluginId)\n}\n```\n\n### 3. Plugin Manifest\n\nStandard plugin manifest - no special configuration needed:\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin\",\n    \"component\": \"./Widget.qml\",\n    \"settings\": \"./Settings.qml\"\n}\n```\n\n## Use Cases\n\n- **Script Runner**: Multiple variants running different scripts\n- **System Monitors**: Different monitoring targets (CPU, GPU, Network)\n- **Quick Launchers**: Different apps or commands per variant\n- **Custom Indicators**: Different data sources or APIs\n- **Time Zones**: Multiple clocks for different time zones\n\n## API Reference\n\n### PluginService Functions\n\n- `getPluginVariants(pluginId)`: Get all variants for a plugin\n- `getAllPluginVariants()`: Get all variants across all plugins\n- `createPluginVariant(pluginId, name, config)`: Create new variant\n- `removePluginVariant(pluginId, variantId)`: Delete variant\n- `updatePluginVariant(pluginId, variantId, config)`: Update variant\n- `getPluginVariantData(pluginId, variantId)`: Get specific variant data\n\n### Widget Properties\n\n- `pluginId`: Base plugin identifier\n- `variantId`: Variant identifier (null if no variant)\n- `variantData`: Variant configuration object\n- `pluginService`: PluginService reference\n","quickshell/PLUGINS/ExampleWithVariants/plugin.json":"{\n    \"id\": \"exampleVariants\",\n    \"name\": \"Example with Variants\",\n    \"description\": \"Demonstrates dynamic variant creation for plugins\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"multiple-usecases\", \"variants\"],\n    \"component\": \"./VariantWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./VariantSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/LauncherExample/README.md":"# LauncherExample Plugin\n\nA demonstration plugin that showcases the DMS launcher plugin system capabilities.\n\n## Purpose\n\nThis plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:\n\n- **Plugin Structure**: Proper manifest, launcher, and settings components\n- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)\n- **Item Management**: Providing searchable items to the launcher\n- **Action Execution**: Handling different types of actions (toast, copy, script)\n- **Settings Integration**: Configurable plugin settings with persistence\n\n## Features\n\n### Example Items\n- **Test Items 1-3**: Demonstrate toast notifications\n- **Copy Action**: Shows clipboard integration\n- **Script Action**: Demonstrates command execution\n\n### Trigger System\n- **Default Trigger**: `#` (configurable in settings)\n- **Empty Trigger Option**: Items can always be visible without needing a trigger\n- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)\n- **Search**: Type `# test` to search within plugin items\n\n### Action Types\n- `toast:message` - Shows toast notification\n- `copy:text` - Copies text to clipboard\n- `script:command` - Executes shell command (demo only)\n\n## File Structure\n\n```\nPLUGINS/LauncherExample/\n├── plugin.json                    # Plugin manifest\n├── LauncherExampleLauncher.qml    # Main launcher component\n├── LauncherExampleSettings.qml    # Settings interface\n└── README.md                      # This documentation\n```\n\n## Installation\n\n1. **Plugin Directory**: Copy to `~/.config/dms/plugins/LauncherExample`\n2. **Enable Plugin**: Settings → Plugins → Enable \"LauncherExample\"\n3. **Configure**: Set custom trigger in plugin settings if desired\n\n## Usage\n\n### With Trigger (Default)\n1. Open launcher (Ctrl+Space or launcher button)\n2. Type `#` to activate plugin trigger\n3. Browse available items or add search terms\n4. Press Enter to execute selected item\n\n### Without Trigger (Empty Trigger Mode)\n1. Enable \"No trigger (always show)\" in plugin settings\n2. Open launcher - plugin items are always visible\n3. Search works normally with plugin items included\n4. Press Enter to execute selected item\n\n### Search Examples\n- `#` - Show all plugin items (with trigger enabled)\n- `# test` - Show items matching \"test\"\n- `# copy` - Show items matching \"copy\"\n- `test` - Show all items matching \"test\" (with empty trigger enabled)\n\n## Developer Guide\n\n### Plugin Contract\n\n**Launcher Component Requirements**:\n```qml\n// Required properties\nproperty var pluginService: null\nproperty string trigger: \"#\"\n\n// Required signals\nsignal itemsChanged()\n\n// Required functions\nfunction getItems(query): array\nfunction executeItem(item): void\n\n// Optional functions (for Shift+Enter paste support)\nfunction getPasteText(item): string|null\nfunction getPasteArgs(item): array|null\n```\n\n**Item Structure**:\n```javascript\n{\n    name: \"Item Name\",           // Display name\n    icon: \"icon_name\",           // Icon (optional, see Icon Types below)\n    comment: \"Description\",      // Subtitle text\n    action: \"type:data\",         // Action to execute\n    categories: [\"PluginName\"]   // Category array\n}\n```\n\n**Icon Types**:\n\nThe `icon` field supports four formats:\n\n1. **Material Design Icons** - Use `material:` prefix:\n   ```javascript\n   icon: \"material:lightbulb\"  // Material Symbols Rounded font\n   ```\n   Examples: `material:star`, `material:favorite`, `material:settings`\n\n2. **Unicode/Emoji Icons** - Use `unicode:` prefix:\n   ```javascript\n   icon: \"unicode:🚀\"  // Unicode character or emoji\n   ```\n   Display any Unicode character or emoji as the icon. Examples: `unicode:😀`, `unicode:⚡`, `unicode:🎨`\n\n3. **Desktop Theme Icons** - Use icon name directly:\n   ```javascript\n   icon: \"firefox\"  // Uses system icon theme\n   ```\n   Examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n4. **No Icon** - Omit the `icon` field entirely:\n   ```javascript\n   {\n       name: \"😀  Grinning Face\",\n       // No icon field\n       comment: \"Copy emoji\",\n       action: \"copy:😀\",\n       categories: [\"MyPlugin\"]\n   }\n   ```\n   When icon is omitted, the launcher hides the icon area and displays only text\n\n**Action Format**: `type:data` where:\n- `type` - Action handler (toast, copy, script, etc.)\n- `data` - Action-specific data\n\n### Settings Integration\n```qml\n// Save setting\npluginService.savePluginData(\"pluginId\", \"key\", value)\n\n// Load setting\npluginService.loadPluginData(\"pluginId\", \"key\", defaultValue)\n```\n\n### Trigger Configuration\n\nThe trigger can be configured in two ways:\n\n1. **Empty Trigger** (No Trigger Mode):\n   - Check \"No trigger (always show)\" in settings\n   - Saves `trigger: \"\"` and `noTrigger: true`\n   - Items always appear in launcher alongside regular apps\n\n2. **Custom Trigger**:\n   - Enter any string (e.g., `#`, `!`, `@`, `!ex`)\n   - Uncheck \"No trigger\" checkbox\n   - Items only appear when trigger is typed\n\n### Manifest Structure\n```json\n{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nNote: The `trigger` field in the manifest is optional and serves as the default trigger value.\n\n## Extending This Plugin\n\n### Adding New Items\n```qml\nfunction getItems(query) {\n    return [\n        {\n            name: \"My Item\",\n            icon: \"custom_icon\",\n            comment: \"Does something cool\",\n            action: \"custom:action_data\",\n            categories: [\"LauncherExample\"]\n        }\n    ]\n}\n```\n\n### Adding New Actions\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"custom\":\n            handleCustomAction(actionData)\n            break\n    }\n}\n```\n\n### Custom Trigger Logic\n```qml\nComponent.onCompleted: {\n    if (pluginService) {\n        trigger = pluginService.loadPluginData(\"launcherExample\", \"trigger\", \"#\")\n    }\n}\n\nonTriggerChanged: {\n    if (pluginService) {\n        pluginService.savePluginData(\"launcherExample\", \"trigger\", trigger)\n    }\n}\n```\n\n## Best Practices\n\n1. **Unique Triggers**: Choose triggers that don't conflict with other plugins\n2. **Clear Descriptions**: Write helpful item comments\n3. **Error Handling**: Gracefully handle action failures\n4. **Performance**: Return results quickly in getItems()\n5. **Cleanup**: Destroy temporary objects in executeItem()\n6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode\n\n## Testing\n\nTest the plugin by:\n1. Installing and enabling in DMS\n2. Testing with trigger enabled\n3. Testing with empty trigger (no trigger mode)\n4. Trying each action type\n5. Testing search functionality\n6. Verifying settings persistence\n\nThis plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!\n","quickshell/PLUGINS/LauncherExample/plugin.json":"{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"description\": \"Example launcher plugin demonstrating the launcher plugin system\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"extension\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\", \"command-execution\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"trigger\": \"#\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/LauncherImageExample/README.md":"# LauncherImageExample\n\nExample launcher plugin demonstrating tile mode with URL-based images.\n\n## Features\n\n- **Tile Mode**: Uses `viewMode: \"tile\"` in plugin.json to display results as image tiles\n- **Enforced View Mode**: Uses `viewModeEnforced: true` to lock the view to tile mode (users cannot change it)\n- **URL Images**: Demonstrates using `imageUrl` property for remote images\n\n## Usage\n\n1. Open the launcher (DankLauncherV2)\n2. Type `img` to activate the plugin\n3. Browse Dux Shell screenshots in tile view\n\n## Plugin Configuration\n\n```json\n{\n  \"viewMode\": \"tile\",\n  \"viewModeEnforced\": true\n}\n```\n\n- `viewMode`: Sets the default view mode (\"list\", \"grid\", or \"tile\")\n- `viewModeEnforced`: When true, users cannot switch view modes for this plugin\n\n## Item Data Structure\n\nTo display images in tile mode, set `imageUrl` directly on the item:\n\n```javascript\n{\n    name: \"Image Title\",\n    icon: \"material:image\",\n    comment: \"Image description\",\n    categories: [\"Category\"],\n    imageUrl: \"https://example.com/image.png\"\n}\n```\n\nThe `imageUrl` property supports remote URLs or local files, use `file://` prefix for local files.\n","quickshell/PLUGINS/LauncherImageExample/plugin.json":"{\n    \"id\": \"launcherImageExample\",\n    \"name\": \"Image Gallery Example\",\n    \"description\": \"Example launcher plugin demonstrating tile mode with images\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"photo_library\",\n    \"type\": \"launcher\",\n    \"trigger\": \"img\",\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true,\n    \"component\": \"./LauncherImageExample.qml\",\n    \"permissions\": []\n}\n","quickshell/PLUGINS/PopoutControlExample/README.md":"# Popout Control Example Plugin\n\nThis example plugin demonstrates:\n- Using `PopoutService` to trigger positioned popouts and modals\n- Using `pillClickAction` with position parameters\n- Using `PluginSettings` with dropdown selection\n- Dynamic widget text based on settings\n\nThe `pillClickAction` receives position parameters `(x, y, width, section, screen)` which are passed to PopoutService functions to properly position popouts relative to the widget.\n\n## PopoutService API\n\nThe `PopoutService` is automatically injected into plugin widgets and daemons as `popoutService`. It provides access to all shell popouts and modals.\n\n### Available Popouts\n\n#### Control Center\n```qml\npopoutService.openControlCenter()\npopoutService.closeControlCenter()\npopoutService.toggleControlCenter()\n```\n\n#### Notification Center\n```qml\npopoutService.openNotificationCenter()\npopoutService.closeNotificationCenter()\npopoutService.toggleNotificationCenter()\n```\n\n#### App Drawer\n```qml\npopoutService.openAppDrawer()\npopoutService.closeAppDrawer()\npopoutService.toggleAppDrawer()\n```\n\n#### Process List (Popout)\n```qml\npopoutService.openProcessList()\npopoutService.closeProcessList()\npopoutService.toggleProcessList()\n```\n\n#### DankDash\n```qml\npopoutService.openDankDash(tabIndex)    // tabIndex: 0=Calendar, 1=Media, 2=Weather\npopoutService.closeDankDash()\npopoutService.toggleDankDash(tabIndex)\n```\n\n#### Battery Popout\n```qml\npopoutService.openBattery()\npopoutService.closeBattery()\npopoutService.toggleBattery()\n```\n\n#### VPN Popout\n```qml\npopoutService.openVpn()\npopoutService.closeVpn()\npopoutService.toggleVpn()\n```\n\n#### System Update Popout\n```qml\npopoutService.openSystemUpdate()\npopoutService.closeSystemUpdate()\npopoutService.toggleSystemUpdate()\n```\n\n### Available Modals\n\n#### Settings Modal\n```qml\npopoutService.openSettings()\npopoutService.closeSettings()\n```\n\n#### Clipboard History Modal\n```qml\npopoutService.openClipboardHistory()\npopoutService.closeClipboardHistory()\n```\n\n#### Launcher Modal\n```qml\npopoutService.openDankLauncherV2()\npopoutService.closeDankLauncherV2()\npopoutService.toggleDankLauncherV2()\n```\n\n#### Power Menu Modal\n```qml\npopoutService.openPowerMenu()\npopoutService.closePowerMenu()\npopoutService.togglePowerMenu()\n```\n\n#### Process List Modal (fullscreen)\n```qml\npopoutService.showProcessListModal()\npopoutService.hideProcessListModal()\npopoutService.toggleProcessListModal()\n```\n\n#### Color Picker Modal\n```qml\npopoutService.showColorPicker()\npopoutService.hideColorPicker()\n```\n\n#### Notification Modal\n```qml\npopoutService.showNotificationModal()\npopoutService.hideNotificationModal()\n```\n\n#### WiFi Password Modal\n```qml\npopoutService.showWifiPasswordModal()\npopoutService.hideWifiPasswordModal()\n```\n\n#### Network Info Modal\n```qml\npopoutService.showNetworkInfoModal()\npopoutService.hideNetworkInfoModal()\n```\n\n#### Notepad Slideout\n```qml\npopoutService.openNotepad()\npopoutService.closeNotepad()\npopoutService.toggleNotepad()\n```\n\n## Usage in Plugins\n\n### Widget Plugins\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\n\nRectangle {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            popoutService?.toggleControlCenter()\n        }\n    }\n}\n```\n\n### Daemon Plugins\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    Connections {\n        target: NotificationService\n        function onNotificationReceived() {\n            popoutService?.openNotificationCenter()\n        }\n    }\n}\n```\n\n**Important**: The `popoutService` property **must** be declared in your plugin component. Without it, you'll get errors like:\n```\nError: Cannot assign to non-existent property \"popoutService\"\n```\n\n## Example Use Cases\n\n1. **Custom Launcher**: Create a widget that opens the app drawer\n2. **Quick Settings**: Toggle control center from a custom button\n3. **Notification Manager**: Open notification center on new notifications\n4. **System Monitor**: Open process list on high CPU usage\n5. **Power Management**: Trigger power menu from a custom widget\n\n## Installation\n\n1. Copy the plugin directory to `~/.config/dms/plugins/`\n2. Open Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Popout Control Example\"\n5. Add `popoutControlExample` to your DankBar widget list\n\n## Notes\n\n- The `popoutService` property is automatically injected - no manual setup required\n- Always use optional chaining (`?.`) when calling methods to handle null cases\n- Popouts are lazily loaded - first access may activate the loader\n- Some popouts require specific system features to be available\n","quickshell/PLUGINS/PopoutControlExample/plugin.json":"{\n    \"id\": \"popoutControlExample\",\n    \"name\": \"Popout Control Example\",\n    \"description\": \"Example widget demonstrating PopoutService usage with pillClickAction\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./PopoutControlWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./PopoutControlSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/QuickNotesExample/plugin.json":"{\n    \"id\": \"quickNotesExample\",\n    \"name\": \"Quick Notes\",\n    \"description\": \"Example launcher plugin demonstrating the plugin state API for persistent data like history and notes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"icon\": \"sticky_note_2\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\"],\n    \"component\": \"./QuickNotesLauncher.qml\",\n    \"settings\": \"./QuickNotesSettings.qml\",\n    \"trigger\": \"n\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/README.md":"# Plugin System\n\nExtend DMS with dynamically loaded QML components: bar and dock widgets, Control Center tiles, dash tabs and overview cards, desktop widgets, launcher providers and background daemons.\n\n## Plugin Registry\n\nBrowse and discover community plugins at **https://plugins.duxlabs.dev/**\n\n## Overview\n\nPlugins are discovered from `~/.config/dms/plugins/` and managed by PluginService. One plugin can provide a single surface or several (see [Composite Plugins](#composite-plugins)).\n\n## Architecture\n\n### Core Components\n\n1. **PluginService** (`Services/PluginService.qml`)\n   - Singleton service managing plugin lifecycle\n   - Discovers plugins from `$CONFIGPATH/dms/plugins/`\n   - Handles loading, unloading, and state management\n   - Provides data persistence for plugin settings\n\n2. **Plugins hub** (`Modules/Settings/PluginsHubHeader.qml`, `Modules/Settings/PluginsManageTab.qml`)\n   - UI for managing available plugins and registries\n\n3. **Plugin settings page** (`Modules/Settings/PluginSettingsPage.qml`)\n   - Loads a plugin's settings component through `PluginSettingsHost`\n\n4. **Bar and dock integration** (`Modules/SurfaceWidgets/SurfaceWidgetFactory.qml`, `SurfaceWidgetHost.qml`)\n   - Renders plugin widgets in bars and docks with the same component\n   - Merges plugin components with built-in widgets\n   - Supports every bar and dock edge\n\n5. **Dash integration** (`Modules/DDash/DashRegistry.qml`)\n   - Lists built-in and plugin tabs and overview cards\n   - Hosts plugin tabs in `DashTabHost.qml` and cards in `Overview/DashCardSlot.qml`\n\nMany widgets are implemented in the shared [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) library and re-exported by DMS. Plugins should keep importing `qs.Common`, `qs.Services`, `qs.Widgets`, and `qs.Modules.Plugins` — these remain the supported plugin API and are unaffected by where a widget is implemented.\n\n## Plugin Structure\n\nEach plugin must be a directory in `$CONFIGPATH/dms/plugins/` containing:\n\n```\n$CONFIGPATH/dms/plugins/YourPlugin/\n├── plugin.json          # Required: Plugin manifest\n├── YourWidget.qml       # Required: Widget component\n├── YourSettings.qml     # Optional: Settings UI\n├── *.js                 # Optional: JavaScript utilities\n└── translations/        # Optional: per-locale translation files (see Translations)\n```\n\n### Plugin Manifest (plugin.json)\n\nThe manifest file defines plugin metadata and configuration.\n\n**JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema.\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"thing-my-plugin-does\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"material_icon_name\",\n    \"settings\": \"./YourSettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"requires\": [\"some-system-tool\"],\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n```\n\n**Required Fields:**\n- `id`: Unique plugin identifier (camelCase, no spaces)\n- `name`: Human-readable plugin name\n- `description`: Short description of plugin functionality (displayed in UI)\n- `version`: Semantic version string (e.g., \"1.0.0\")\n- `author`: Plugin creator name or email\n- `type`: Plugin type - \"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", or \"composite\" (with `components`)\n- `capabilities`: Array of plugin capabilities  (e.g., [\"dankbar-widget\"], [\"control-center\"], [\"monitoring\"])\n- `component`: Relative path to main QML component file, or `components` for a multi-surface plugin\n\n**Required for Launcher Type:**\n- `trigger`: Trigger string for launcher activation (e.g., \"=\", \"#\", \"!\")\n\n**Optional Fields:**\n- `icon`: Material Design icon name (displayed in UI)\n- `settings`: Path to settings component (enables settings UI)\n- `requires_dms`: Minimum DMS version requirement (e.g., \">=0.1.18\", \">0.1.0\")\n- `requires`: Array of required system tools/dependencies (e.g., [\"curl\", \"jq\"])\n- `permissions`: Required DMS permissions (e.g., [\"settings_read\", \"settings_write\"])\n\n**Permissions:**\n\nThe plugin system enforces permissions when settings are accessed:\n- `settings_read`: Required to read plugin settings (currently not enforced)\n- `settings_write`: **Required** to use PluginSettings component and save settings\n\nIf your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI.\n\n### Widget Component\n\nThe main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    // Define horizontal bar pill, for top and bottom DankBar positions (optional)\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    // Define vertical bar pill, for left and right DankBar positions (optional)\n    verticalBarPill: Component {\n        // Same as horizontal but optimized for vertical layout\n    }\n\n    // Define popout content, opens when clicking the bar pill (optional)\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional description text goes here\"\n\n            // Your popout content goes here\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Popout Content\"\n                    font.pixelSize: Theme.fontSizeLarge\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    // Popout dimensions (required if popoutContent is set)\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n**PluginComponent Properties (automatically injected):**\n- `axis`: Bar axis information (horizontal/vertical)\n- `section`: Bar section (\"left\", \"center\", \"right\")\n- `parentScreen`: Screen reference for multi-monitor support\n- `widgetThickness`: Recommended widget size perpendicular to bar\n- `barThickness`: Bar thickness parallel to edge\n\n**Component Options:**\n- `horizontalBarPill`: Component shown in horizontal bars\n- `verticalBarPill`: Component shown in vertical bars\n- `popoutContent`: Optional popout window content\n- `popoutWidth`: Popout window width\n- `popoutHeight`: Popout window height\n- `pillClickAction`: Custom click handler function (overrides popout)\n- `pillRightClickAction`: Custom right click handler function\n\n### Control Center Integration\n\nAdd your plugin to Control Center by defining CC properties:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isEnabled ? \"Active\" : \"Inactive\"\n    ccWidgetIsActive: isEnabled\n\n    onCcWidgetToggled: {\n        isEnabled = !isEnabled\n        if (pluginService) {\n            pluginService.savePluginData(\"myPlugin\", \"isEnabled\", isEnabled)\n        }\n    }\n\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.foregroundColor(Theme.cardSurface)\n            radius: Theme.cornerRadius\n            // Your detail UI here\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n}\n```\n\n**CC Properties:**\n- `ccWidgetIcon`: Material icon name\n- `ccWidgetPrimaryText`: Main label\n- `ccWidgetSecondaryText`: Subtitle/status\n- `ccWidgetIsActive`: Active state styling\n- `ccWidgetIsToggle`: Whether the icon toggles state; set false for action-only tiles\n- `ccExpandedContent`: Optional inline controls for larger tiles\n- `ccExpandedMinimumHeight`: Minimum height for inline controls, default `Theme.listItemHeight`\n- `ccFooterContent`: Accepted but ignored; the footer strip is gone and users shrink tiles to half-height pills instead\n- `ccDetailContent`: Optional detail page\n\n**Signals:**\n- `ccWidgetToggled()`: Fired when icon clicked\n- `ccWidgetExpanded()`: Fired when opening the detail page\n\n**Widget sizing:**\n\nThe grid uses square cells, with eight columns by default. A standard strip is\n4×1; a 2×2 tile is square. Users resize tiles and the panel in edit mode. Panel\nwidth and tile height are limited by the current screen, independently of each\nother. Saved spans are retained when displaying a tile on a smaller screen.\nThe standard tile adapts its icon and labels automatically. Larger tiles can load\n`ccExpandedContent` below the header when enough space is available.\n\nUse `CcTileContent` from `qs.Modules.ControlCenter.Widgets` for inline content.\nIts `tile` is supplied by the host. It exposes `columns`, `rows`, `live`,\n`contentColor`, and `subtitleColor`; `width` and `height` are the available space.\nContent is disabled in edit mode and destroyed when the Control Center closes or\nshrinks below the minimum size. Keep persistent state in the plugin instance.\n\n```qml\nccExpandedContent: Component {\n    CcTileContent {\n        DButton {\n            maximumWidth: parent.width\n            text: I18n.trFor(\"myPlugin\", \"Run\")\n            onClicked: root.runAction()\n        }\n    }\n}\n```\n\nExisting plugins need no changes to use the standard responsive tile. Custom\ninline controls must fit their allocated space and gate ongoing work on `live`.\n\n**Custom Click Actions:**\n\nOverride default popout with `pillClickAction` and `pillRightClickAction`:\n\n```qml\npillClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Clicked!'\"])\n}\n\n// Or with position params: (x, y, width, section, screen)\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n\npillRightClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Right clicked!'\"])\n}\n\npillRightClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\nThe PluginComponent automatically handles:\n- Bar orientation detection\n- Click handlers for popouts\n- Proper positioning and anchoring\n- Theme integration\n\n### PopoutComponent\n\nPopoutComponent provides a consistent header/content layout for plugin popouts:\n\n```qml\nimport qs.Modules.Plugins\nimport qs.DCommon.Widgets\n\nPopoutComponent {\n    headerText: \"Header Title\"        // Main header text (bold, large)\n    detailsText: \"Description text\"   // Optional description (smaller, gray)\n\n    // Access to exposed properties for dynamic sizing\n    readonly property int headerHeight    // Height of header area\n    readonly property int detailsHeight   // Height of description area\n\n    // Your content here - use parent.width for full width\n    // Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing\n    DGridView {\n        width: parent.width\n        height: parent.height\n        // ...\n    }\n}\n```\n\n**PopoutComponent Properties:**\n- `headerText`: Main header text (optional, hidden if empty)\n- `detailsText`: Description text below header (optional, hidden if empty)\n- `closePopout`: Function to close popout (auto-injected by PluginPopout)\n- `headerHeight`: Readonly height of header (0 if not visible)\n- `detailsHeight`: Readonly height of description (0 if not visible)\n\nThe component automatically handles spacing and layout. Content children are rendered below the description with proper padding.\n\n### Settings Component\n\nOptional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    id: root\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key for accessing the service\"\n        placeholder: \"Enter API key...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"notifications\"\n        label: \"Enable Notifications\"\n        description: \"Show desktop notifications for updates\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"updateInterval\"\n        label: \"Update Interval\"\n        description: \"How often to refresh data\"\n        options: [\n            {label: \"1 minute\", value: \"60\"},\n            {label: \"5 minutes\", value: \"300\"},\n            {label: \"15 minutes\", value: \"900\"}\n        ]\n        defaultValue: \"300\"\n    }\n\n    ListSetting {\n        id: itemList\n        settingKey: \"items\"\n        label: \"Saved Items\"\n        description: \"List of configured items\"\n        delegate: Component {\n            StyledRect {\n                width: parent.width\n                height: 40\n                radius: Theme.cornerRadius\n                color: Theme.chipSurface\n\n                StyledText {\n                    anchors.left: parent.left\n                    anchors.leftMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    text: modelData.name\n                    color: Theme.surfaceText\n                }\n\n                Rectangle {\n                    anchors.right: parent.right\n                    anchors.rightMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    width: 60\n                    height: 28\n                    color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                    radius: Theme.cornerRadius\n\n                    StyledText {\n                        anchors.centerIn: parent\n                        text: \"Remove\"\n                        color: Theme.errorText\n                        font.pixelSize: Theme.fontSizeSmall\n                        font.weight: Font.Medium\n                    }\n\n                    MouseArea {\n                        id: removeArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        cursorShape: Qt.PointingHandCursor\n                        onClicked: itemList.removeItem(index)\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n**Available Setting Components:**\n\nAll settings automatically save on change and load on component creation.\n\n**How Default Values Work:**\n\nEach setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI:\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"\n    defaultValue: \"\"  // Empty string if no key saved\n}\n\nToggleSetting {\n    settingKey: \"enabled\"\n    defaultValue: true  // Enabled by default\n}\n\nListSettingWithInput {\n    settingKey: \"locations\"\n    defaultValue: []  // Empty array if no locations saved\n}\n```\n\n1. **PluginSettings** - Root wrapper for all plugin settings\n   - `pluginId`: Your plugin ID (required)\n   - Auto-handles storage and provides saveValue/loadValue to children\n   - Place all other setting components inside this wrapper\n\n2. **StringSetting** - Text input field\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `placeholder`: Input placeholder (optional)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Vertical stack (label, description, input field)\n\n3. **ToggleSetting** - Boolean toggle switch\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default boolean (optional, default: `false`)\n   - Layout: Horizontal (label/description left, toggle right)\n\n4. **SelectionSetting** - Dropdown menu\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `options`: Array of `{label, value}` objects or simple strings (required)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Horizontal (label/description left, dropdown right)\n   - Stores the `value` field, displays the `label` field\n\n5. **ListSetting** - Manage list of items (manual add/remove)\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `delegate`: Custom item delegate Component (optional)\n   - `addItem(item)`: Add item to list\n   - `removeItem(index)`: Remove item from list\n   - Use when you need custom UI for adding items\n\n6. **ListSettingWithInput** - Complete list management with built-in form\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `fields`: Array of field definitions (required)\n     - `id`: Field ID in saved object (required)\n     - `label`: Column header text (required)\n     - `placeholder`: Input placeholder (optional)\n     - `width`: Column width in pixels (optional, default 200)\n     - `required`: Must have value to add (optional, default false)\n     - `default`: Default value if empty (optional)\n   - Automatically generates:\n     - Column headers from field labels\n     - Input fields with placeholders\n     - Add button with validation\n     - List display showing all field values\n     - Remove buttons for each item\n   - Best for collecting structured data (servers, locations, etc.)\n\n**Complete Settings Example:**\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your service API key\"\n        placeholder: \"sk-...\"\n        defaultValue: \"\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        description: \"Turn this feature on or off\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"theme\"\n        label: \"Theme\"\n        description: \"Choose your preferred theme\"\n        options: [\n            {label: \"Dark\", value: \"dark\"},\n            {label: \"Light\", value: \"light\"},\n            {label: \"Auto\", value: \"auto\"}\n        ]\n        defaultValue: \"dark\"\n    }\n\n    ListSettingWithInput {\n        settingKey: \"locations\"\n        label: \"Locations\"\n        description: \"Track multiple locations\"\n        defaultValue: []\n        fields: [\n            {id: \"name\", label: \"Name\", placeholder: \"Home\", width: 150, required: true},\n            {id: \"timezone\", label: \"Timezone\", placeholder: \"America/New_York\", width: 200, required: true}\n        ]\n    }\n}\n```\n\n**Key Benefits:**\n- Zero boilerplate - just define your settings\n- Automatic persistence to `settings.json`\n- Clean, consistent UI across all plugins\n- No manual `pluginService` calls needed\n- Proper layout and spacing handled automatically\n\n## Translations\n\nPlugins ship their own translations. Drop a `translations/` directory next to `plugin.json`, wrap your strings in `I18n.trFor()`, done — nothing needs to change in the DMS repo, and users get your translations just by installing the plugin.\n\n```\nYourPlugin/\n├── plugin.json\n├── YourWidget.qml\n└── translations/\n    ├── es.json\n    ├── fr.json\n    └── zh_CN.json\n```\n\nDMS loads the file matching the user's locale when it discovers the plugin, and re-reads it whenever the locale changes. No restart, no registration call.\n\n### Marking Strings\n\nWrap every user-facing string in `I18n.trFor()`, with your plugin id as the first argument:\n\n```qml\nimport qs.Common\n\nStyledText {\n    text: I18n.trFor(\"yourPlugin\", \"Cycle Speed\")\n}\n```\n\nPlaceholders work the same as anywhere else in DMS:\n\n```qml\nToastService.showInfo(I18n.trFor(\"yourPlugin\", \"Copied %1 to clipboard\").arg(item))\n```\n\nLookup order is: your plugin's translation file → the global DMS catalog → the English term itself. Strings DMS already translates (\"Settings\", \"Close\", etc.) resolve from the global catalog for free, and anything untranslated renders as your English text instead of breaking.\n\nTwo rules:\n\n- The plugin id must be a **literal string** that exactly matches `id` in your `plugin.json`. The string extraction tooling for central translation reads call sites — a variable there means your strings never get extracted.\n- `I18n.trFor` doesn't exist on DMS versions without plugin translation support, and calling a missing function in QML is a TypeError, not a silent no-op. Bump `requires_dms` when you adopt it.\n\nPlain `I18n.tr()` still works inside plugins, but it only checks the global catalog — it will never see your `translations/` files.\n\n### Translation Files\n\nOne JSON file per locale. Top-level keys are context buckets; unless you have a reason to do otherwise, use the English term as its own bucket:\n\n```json\n{\n    \"Cycle Speed\": {\n        \"Cycle Speed\": \"Velocidad de ciclo\"\n    },\n    \"Copied %1 to clipboard\": {\n        \"Copied %1 to clipboard\": \"%1 copiado al portapapeles\"\n    }\n}\n```\n\nThere is no `en.json` — the strings in your QML are the English source. Translate what you want, skip what you don't; missing terms fall through the lookup order above.\n\nFile names match what DMS ships in its own catalog (`es.json`, `pt.json`, `zh_CN.json`, ...), but you're not limited to that list — resolution is driven by the user's locale. For a user on `zh_CN`, DMS tries `zh_CN.json`, then `zh-CN.json`, then `zh.json`, and uses the first one that exists.\n\n### Testing\n\n1. Reload after editing a file: `dms ipc call plugins reload yourPlugin`\n2. Switch languages in Settings → Locale — plugin strings retranslate live along with the rest of the shell\n3. Broken JSON logs a `bad plugin translations` warning in shell output and falls back to English\n\n`ExampleEmojiPlugin` is a complete working example — `trFor` calls in the widget and settings, plus `es.json` and `fr.json`.\n\n### Central Translation via POEditor\n\nPlugins in the [plugin registry](https://github.com/duxlabs-dev/dux-plugin-registry) can apply to join the DankPlugins POEditor project, translated by the same community that translates DMS itself. Approved plugins get every `I18n.trFor()` string uploaded under a `<pluginId>:` context, so your \"Auto\" and another plugin's \"Auto\" are separate terms with separate translations. Finished translations come back to the plugin repo as PRs to `translations/`, in the file format above. See the registry's CONTRIBUTING guide for the application process.\n\nStrings you wrap in plain `I18n.tr()` are not uploaded. They resolve from the shell catalog only, which is the right choice for terms DMS already has (\"Cancel\", \"Settings\"). A term DMS does not have stays English, and the sync warns about it.\n\n## PluginService API\n\n### Properties\n\n```qml\nPluginService.pluginDirectory: string\n// Path to plugins directory ($CONFIGPATH/dms/plugins)\n\nPluginService.availablePlugins: object\n// Map of all discovered plugins {pluginId: pluginInfo}\n\nPluginService.loadedPlugins: object\n// Map of currently loaded plugins {pluginId: pluginInfo}\n\nPluginService.pluginWidgetComponents: object\n// Map of loaded widget components {pluginId: Component}\n```\n\n### Functions\n\n```qml\n// Plugin Management\nPluginService.loadPlugin(pluginId: string): bool\nPluginService.unloadPlugin(pluginId: string): bool\nPluginService.reloadPlugin(pluginId: string): bool\nPluginService.enablePlugin(pluginId: string, onResult?: (ok: bool, error: string) => void): bool\nPluginService.disablePlugin(pluginId: string): bool\n\n// Plugin Discovery\nPluginService.scanPlugins(): void\nPluginService.getAvailablePlugins(): array\nPluginService.getLoadedPlugins(): array\nPluginService.isPluginLoaded(pluginId: string): bool\nPluginService.getWidgetComponents(): object\n\n// Data Persistence\nPluginService.savePluginData(pluginId: string, key: string, value: any): bool\nPluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any\n\n// Global Variables - Shared state across all plugin instances\nPluginService.getGlobalVar(pluginId: string, varName: string, defaultValue: any): any\nPluginService.setGlobalVar(pluginId: string, varName: string, value: any): void\n```\n\n### Signals\n\n```qml\nPluginService.pluginLoaded(pluginId: string)\nPluginService.pluginUnloaded(pluginId: string)\nPluginService.pluginLoadFailed(pluginId: string, error: string)\nPluginService.globalVarChanged(pluginId: string, varName: string)\n```\n\n## Startup Check (Dependency Gate)\n\nA plugin may optionally gate activation behind a dependency check. Point the manifest's `startupCheck` field at a small, **non-visual** component (a `QtObject` - it must not render in the graphics scene):\n\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"boregard\"]\n}\n```\n\nThe component exposes a `check` function that runs before the plugin loads, both on manual enable and on auto-load at startup. Call `done(null)` to allow activation, or `done(error)` to block it. The error can be a short string (title only) or an object with an expandable `details` body for long-form instructions:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"which\", \"boregard\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null)\n                return\n            }\n            done({\n                title: I18n.tr(\"boregard is required\"),\n                details: I18n.tr(\"Install it from https://duxlabs.dev, then re-enable this plugin.\")\n            })\n        })\n    }\n}\n```\n\nA synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). A check that fails to load, has no `check` function, or throws blocks activation the same way, with its error as the message. The check also runs again on reload. Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`.\n\n## Plugin Global Variables\n\nPlugins can share state across multiple instances using global variables. This is useful when you have the same widget displayed on multiple monitors or multiple instances of the same widget on different bars.\n\n### Why Use Global Variables?\n\nUnlike regular properties which are scoped to each component instance, global variables are synchronized across all instances of your plugin. This enables:\n\n- **Multi-monitor consistency**: Same data displayed across all monitors\n- **Multi-instance widgets**: Multiple instances of the same widget sharing state\n- **Cross-component communication**: Share data between widget and settings components\n\n### Using PluginGlobalVar\n\nThe `PluginGlobalVar` helper component provides reactive global variable access:\n\n```qml\nimport QtQuick\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Count: \" + globalCounter.value\n                color: Theme.surfaceText\n            }\n\n            MouseArea {\n                anchors.fill: parent\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar Properties:**\n- `varName` (required): Name of the global variable\n- `defaultValue` (optional): Default value if not set\n- `value` (readonly): Current value of the global variable\n\n**PluginGlobalVar Methods:**\n- `set(newValue)`: Update the global variable (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\nFor more control, use the PluginService API directly:\n\n```qml\nimport QtQuick\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\n    Connections {\n        target: PluginService\n        function onGlobalVarChanged(pluginId, varName) {\n            if (pluginId === \"myPlugin\" && varName === \"counter\") {\n                counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n            }\n        }\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            MouseArea {\n                anchors.fill: parent\n                onClicked: {\n                    const current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n                    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n                }\n            }\n        }\n    }\n}\n```\n\n### Global Variables vs Settings\n\n**Global Variables** (`getGlobalVar`/`setGlobalVar`):\n- Runtime state only (not persisted to disk)\n- Synchronized across all plugin instances\n- Changes trigger `globalVarChanged` signal for reactivity\n- Use for: counters, current selection, temporary UI state\n\n**Settings** (`savePluginData`/`loadPluginData`):\n- Persisted to `settings.json` across sessions\n- Loaded once per plugin instance\n- Use for: user preferences, API keys, configuration\n\n### Important Notes\n\n1. **Reactivity**: Global variables are reactive - all instances update when a value changes\n2. **Namespacing**: Variables are namespaced by plugin ID to avoid conflicts\n3. **Type Safety**: Values can be any QML/JavaScript type (numbers, strings, objects, arrays)\n4. **Not Persistent**: Global variables are cleared when the shell restarts (use settings for persistence)\n5. **Performance**: Efficient for frequent updates - changes only trigger updates for the specific variable\n\n## Creating a Plugin\n\n### Step 1: Create Plugin Directory\n\n```bash\nmkdir -p $CONFIGPATH/dms/plugins/MyPlugin\ncd $CONFIGPATH/dms/plugins/MyPlugin\n```\n\n### Step 2: Create Manifest\n\nCreate `plugin.json`:\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"my-functionality\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n### Step 3: Create Widget Component\n\nCreate `MyWidget.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    horizontalBarPill: Component {\n        StyledRect {\n            width: textItem.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: textItem.implicitWidth + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\n**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.\n\n### Step 4: Create Settings Component (Optional)\n\nCreate `MySettings.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"Configure your plugin settings\"\n        font.pixelSize: Theme.fontSizeMedium\n        color: Theme.surfaceVariantText\n        wrapMode: Text.WordWrap\n    }\n\n    StringSetting {\n        settingKey: \"text\"\n        label: \"Display Text\"\n        description: \"Text shown in the bar widget\"\n        placeholder: \"Hello World\"\n        defaultValue: \"Hello World\"\n    }\n\n    ToggleSetting {\n        settingKey: \"showIcon\"\n        label: \"Show Icon\"\n        description: \"Display an icon next to the text\"\n        defaultValue: true\n    }\n}\n```\n\n### Step 5: Enable Plugin\n\n1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml`\n2. Open Settings (Ctrl+,)\n3. Navigate to Plugins tab\n4. Click \"Scan for Plugins\"\n5. Enable your plugin with the toggle switch\n6. Add the plugin to your DankBar configuration\n\n## Adding Plugin to DankBar\n\nAfter enabling a plugin, add it to the bar:\n\n1. Open Settings → Appearance → DankBar Layout\n2. Add a new widget entry with your plugin ID\n3. Choose section (left, center, right)\n4. Save and reload\n\nOr edit `$CONFIGPATH/quickshell/dms/config.json`:\n\n```json\n{\n    \"dankBarLeftWidgets\": [\n        {\"widgetId\": \"myPlugin\", \"enabled\": true}\n    ]\n}\n```\n\n## Best Practices\n\n1. **Use Existing Widgets**: Leverage `qs.DCommon.Widgets` components (DIcon, DToggle, etc.) for consistency\n2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts\n3. **Data Persistence**: Use PluginService data APIs instead of manual file operations\n4. **Error Handling**: Gracefully handle missing dependencies and invalid data\n5. **Performance**: Keep widgets lightweight, avoid long operations that block the UI loop\n6. **Responsive Design**: Adapt to `compactMode` and different screen sizes\n7. **Documentation**: Include README.md explaining plugin usage\n8. **Versioning**: Use semantic versioning for updates\n9. **Dependencies**: Document external library requirements\n\n## Clipboard Access\n\nPlugins that need to copy text to the clipboard should use the built-in `dms cl copy` command through Quickshell's `execDetached` function.\n\n### Correct Method\n\nImport Quickshell and use `execDetached` with `dms cl copy`:\n\n```qml\nimport QtQuick\nimport Quickshell\n\nItem {\n    function copyToClipboard(text) {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    }\n}\n```\n\n### Example Usage\n\nFrom the ExampleEmojiPlugin (EmojiWidget.qml):\n\n```qml\nMouseArea {\n    onClicked: {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n        ToastService.showInfo(\"Copied \" + modelData + \" to clipboard\")\n        popoutColumn.closePopout()\n    }\n}\n```\n\n### Important Notes\n\n1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime\n2. **Always** import `Quickshell` at the top of your QML file\n3. Consider showing a toast notification to confirm the copy action to users\n\n### Dependencies\n\nThis method uses the built-in DMS clipboard functionality which has native Wayland support.\n\n## Running External Commands\n\nPlugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup.\n\n### Correct Method\n\nImport the `Proc` singleton from `qs.Common` and use `runCommand`:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function fetchData() {\n        Proc.runCommand(\n            \"myPlugin.fetchData\",\n            [\"curl\", \"-s\", \"https://api.example.com/data\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Success:\", stdout)\n                    processData(stdout)\n                } else {\n                    console.error(\"Command failed with exit code:\", exitCode)\n                }\n            },\n            100\n        )\n    }\n}\n```\n\n### Function Signature\n\n```qml\nProc.runCommand(id, command, callback, debounceMs)\n```\n\n**Parameters:**\n- `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one\n- `command` (array): Command and arguments as an array (e.g., `[\"sh\", \"-c\", \"echo hello\"]`)\n- `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes\n  - `stdout` (string): Captured standard output from the command\n  - `exitCode` (number): Exit code of the process (0 typically means success)\n- `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified\n\n### Key Features\n\n1. **Automatic Cleanup**: Process objects are automatically destroyed after completion\n2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one\n3. **Output Capture**: Automatically captures stdout for processing\n4. **Error Handling**: Exit codes are passed to the callback for error detection\n\n### Example Usage\n\n#### Simple Command Execution\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function checkNetwork() {\n        Proc.runCommand(\n            \"myPlugin.ping\",\n            [\"ping\", \"-c\", \"1\", \"8.8.8.8\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Network is up\")\n                } else {\n                    console.log(\"Network is down\")\n                }\n            }\n        )\n    }\n}\n```\n\n#### Parsing Command Output\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    property var diskUsage: ({})\n\n    function updateDiskUsage() {\n        Proc.runCommand(\n            \"myPlugin.df\",\n            [\"df\", \"-h\", \"/home\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    const lines = output.trim().split(\"\\n\")\n                    if (lines.length > 1) {\n                        const parts = lines[1].split(/\\s+/)\n                        diskUsage = {\n                            total: parts[1],\n                            used: parts[2],\n                            available: parts[3],\n                            percent: parts[4]\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n```\n\n#### Shell Commands with Pipes\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function getTopProcess() {\n        Proc.runCommand(\n            \"myPlugin.topProcess\",\n            [\"sh\", \"-c\", \"ps aux | sort -nrk 3,3 | head -n 1\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Top process:\", output)\n                }\n            }\n        )\n    }\n}\n```\n\n#### Debouncing Rapid Updates\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nItem {\n    DTextField {\n        id: searchField\n        placeholderText: \"Search files...\"\n\n        onTextChanged: {\n            Proc.runCommand(\n                \"myPlugin.search\",\n                [\"find\", \"/home\", \"-name\", \"*\" + text + \"*\"],\n                (output, exitCode) => {\n                    if (exitCode === 0) {\n                        updateSearchResults(output)\n                    }\n                },\n                500\n            )\n        }\n    }\n}\n```\n\n### Important Notes\n\n1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `\"myPlugin.actionName\"`) to avoid conflicts\n2. **Debouncing**: Use appropriate debounce delays for your use case:\n   - Fast updates (50-100ms): System monitoring, real-time data\n   - User input (300-500ms): Search fields, text input processing\n   - Network requests (500-1000ms): API calls, web scraping\n3. **Error Handling**: Always check the exit code in your callback before processing output\n4. **Shell Commands**: Use `[\"sh\", \"-c\", \"command\"]` for complex shell commands with pipes or redirects\n5. **Security**: Sanitize user input before passing to commands to prevent command injection\n6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely\n\n### Comparison with Other Methods\n\n**Proc.runCommand** vs **Quickshell.execDetached**:\n- Use `Proc.runCommand` when you need to capture output or check exit codes\n- Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy)\n\n**Proc.runCommand** vs **Process component**:\n- Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup\n- Use `Process` component for long-running processes or when you need fine-grained control\n\n## Debugging\n\n### Console Logging\n\nView plugin logs:\n\n```bash\nqs -v -p $CONFIGPATH/quickshell/dms/shell.qml\n```\n\nLook for lines prefixed with:\n- `PluginService:` - Service operations\n- `PluginsTab:` - UI interactions\n- `PluginsTab:` - Settings loading and accordion interface\n\n### Common Issues\n\n1. **Plugin Not Detected**\n   - Check plugin.json syntax (use `jq` or JSON validator)\n   - Verify directory is in `$CONFIGPATH/dms/plugins/`\n   - Click \"Scan for Plugins\" in Settings\n\n2. **Widget Not Displaying**\n   - Ensure plugin is enabled in Settings\n   - Add plugin ID to DankBar widget list\n   - Check widget width/height properties\n\n3. **Settings Not Loading**\n   - Verify `settings` path in plugin.json\n   - Check settings component for errors\n   - Ensure plugin is enabled and loaded\n   - Review PluginsTab console output for injection issues\n\n4. **Data Not Persisting**\n   - Confirm pluginService.savePluginData() calls (with injection)\n   - Check `$CONFIGPATH/dms/settings.json` for pluginSettings data\n   - Verify plugin has settings permissions\n   - Ensure PluginService was properly injected into settings component\n\n## Security Considerations\n\nPlugins run with full QML runtime access. Only install plugins from trusted sources.\n\n**Permissions System:**\n- `settings_read`: Read plugin configuration (not currently enforced)\n- `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced)\n- `process`: Execute system commands (not currently enforced)\n- `network`: Network access (not currently enforced)\n\nCurrently, only `settings_write` is enforced by the PluginSettings component.\n\n## API Stability\n\nThe plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.\n\n**Roadmap:**\n- Plugin marketplace/repository\n- Sandboxed plugin execution\n- Enhanced permission system\n- Plugin update notifications\n- Inter-plugin communication\n\n## Launcher Plugins\n\nLauncher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.\n\n### Overview\n\nLauncher plugins enable you to:\n- Add custom items to the launcher/app drawer\n- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)\n- Execute custom actions when items are selected\n- Provide searchable, categorized content\n- Integrate seamlessly with the existing launcher\n\n### Plugin Type Configuration\n\nTo create a launcher plugin, set the plugin type in `plugin.json`:\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher Plugin\",\n    \"description\": \"A custom launcher plugin for quick actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"show-thing\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nLauncher components are instantiated the first time the launcher asks them for items, not at shell startup or launcher open, so a plugin not allowed without a trigger is created once its trigger is typed or the launcher is filtered to it. Put background work (timers, processes, IPC handlers) in a daemon surface.\n\nLauncher results use shared Expressive rows and tiles in every launcher style. The item and action contracts are unchanged. List rows use a second line when `comment` is present; image and window preview tiles remain supported. Plugins do not need to supply a visual delegate.\n\n### Launcher Component Contract\n\nCreate `MyLauncher.qml` with the following interface:\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    // Required properties\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    // Required signals\n    signal itemsChanged()\n\n    // Required: Return array of launcher items\n    function getItems(query) {\n        return [\n            {\n                name: \"Item Name\",\n                icon: \"icon_name\",\n                comment: \"Description\",\n                action: \"type:data\",\n                categories: [\"MyLauncher\"]\n            }\n        ]\n    }\n\n    // Required: Execute item action\n    function executeItem(item) {\n        const [type, data] = item.action.split(\":\", 2)\n        // Handle action based on type\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"myLauncher\", \"trigger\", \"#\")\n        }\n    }\n}\n```\n\n### Item Structure\n\nEach item returned by `getItems()` must include:\n\n- `name` (string): Display name shown in launcher\n- `icon` (string, optional): Icon specification (see Icon Types below)\n- `comment` (string): Description/subtitle text\n- `action` (string): Action identifier in `type:data` format\n- `categories` (array): Array containing your plugin name\n\n### Icon Types\n\nThe `icon` field supports four formats:\n\n**1. Material Design Icons** - Use the `material:` prefix:\n```javascript\n{\n    name: \"My Item\",\n    icon: \"material:lightbulb\",  // Material Symbols Rounded font\n    comment: \"Uses Material Design icon\",\n    action: \"toast:Hello!\",\n    categories: [\"MyPlugin\"]\n}\n```\nAvailable icons: Any icon from Material Symbols font (e.g., `lightbulb`, `star`, `favorite`, `settings`, `terminal`, `translate`, `sentiment_satisfied`)\n\n**2. Unicode/Emoji Icons** - Use the `unicode:` prefix:\n```javascript\n{\n    name: \"Grinning Face\",\n    icon: \"unicode:😀\",  // Unicode character or emoji\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nDisplay any Unicode character or emoji as the icon. The character is rendered at 70-80% of the icon size with proper theming. Perfect for emoji pickers, symbol selectors, or character libraries.\n\n**3. Desktop Theme Icons** - Use icon name directly:\n```javascript\n{\n    name: \"Firefox\",\n    icon: \"firefox\",  // Uses system icon theme\n    comment: \"Launches Firefox browser\",\n    action: \"exec:firefox\",\n    categories: [\"MyPlugin\"]\n}\n```\nUses the user's installed icon theme. Common examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n**4. No Icon** - Omit the `icon` field entirely:\n```javascript\n{\n    name: \"😀  Grinning Face\",\n    // No icon field - emoji/unicode in name displays without icon area\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nWhen `icon` is omitted, the launcher shows the first letter of the item name in a themed circle.\n\n### Trigger System\n\nTriggers control when your plugin's items appear in the launcher:\n\n**Empty Trigger Mode** (No trigger):\n- Items always visible alongside regular apps\n- Search includes your items automatically\n- Configure by saving empty trigger: `trigger: \"\"`\n\n**Custom Trigger Mode**:\n- Items only appear when trigger is typed\n- Example: Type `#` to show only your plugin's items\n- Type `# query` to search within your plugin\n- Configure any string: `#`, `!`, `@`, `!custom`, etc.\n\n### Trigger Configuration in Settings\n\nProvide a settings component with trigger configuration:\n\n```qml\nimport QtQuick\nimport QtQuick.Controls\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nFocusScope {\n    id: root\n\n    property var pluginService: null\n\n    Column {\n        spacing: 12\n\n        CheckBox {\n            id: noTriggerToggle\n            text: \"No trigger (always show)\"\n            checked: loadSettings(\"noTrigger\", false)\n\n            onCheckedChanged: {\n                saveSettings(\"noTrigger\", checked)\n                if (checked) {\n                    saveSettings(\"trigger\", \"\")\n                } else {\n                    saveSettings(\"trigger\", triggerField.text || \"#\")\n                }\n            }\n        }\n\n        DTextField {\n            id: triggerField\n            visible: !noTriggerToggle.checked\n            text: loadSettings(\"trigger\", \"#\")\n            placeholderText: \"#\"\n\n            onTextEdited: {\n                saveSettings(\"trigger\", text || \"#\")\n            }\n        }\n    }\n\n    function saveSettings(key, value) {\n        if (pluginService) {\n            pluginService.savePluginData(\"myLauncher\", key, value)\n        }\n    }\n\n    function loadSettings(key, defaultValue) {\n        if (pluginService) {\n            return pluginService.loadPluginData(\"myLauncher\", key, defaultValue)\n        }\n        return defaultValue\n    }\n}\n```\n\n### Action Execution\n\nHandle different action types in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            if (typeof ToastService !== \"undefined\") {\n                ToastService.showInfo(\"Plugin\", actionData)\n            }\n            break\n        case \"copy\":\n            // Copy to clipboard\n            break\n        case \"script\":\n            // Execute command\n         \n\n<!-- [TRUNCATED AT 50KB] -->","quickshell/PLUGINS/WallpaperWatcherDaemon/README.md":"# Wallpaper Watcher Daemon\n\nRun a script whenever your wallpaper changes.\n\n## What it does\n\nThis daemon monitors wallpaper changes and executes a script you specify. The new wallpaper path gets passed as the first argument to your script.\n\n## Setup\n\n1. Enable the plugin in Settings → Plugins\n2. Configure the script path in the plugin settings\n3. Make sure your script is executable (`chmod +x /path/to/script.sh`)\n\n## Example script\n\n```bash\n#!/bin/bash\necho \"New wallpaper: $1\"\n# Do something with the wallpaper path\n```\n\nSave this to a file, make it executable, and point the plugin to it.\n\n## Use cases\n\n- Generate color schemes from the new wallpaper\n- Update theme files based on wallpaper colors\n- Send notifications when wallpaper changes\n- Sync wallpaper info to other devices\n- Log wallpaper history\n\n## Notes\n\n- Script errors show up as toast notifications\n- Script output goes to console logs\n- The daemon runs invisibly in the background\n","quickshell/PLUGINS/WallpaperWatcherDaemon/plugin.json":"{\n    \"id\": \"wallpaperWatcherDaemon\",\n    \"name\": \"Wallpaper Watcher Daemon\",\n    \"description\": \"Background daemon that monitors wallpaper changes and logs them\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper\", \"monitoring\"],\n    \"component\": \"./WallpaperWatcherDaemon.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./WallpaperWatcherSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n"},"files":{".agents/skills/README.md":"# Agent Skills\n\nThis directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.\n\nEach 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.\n\n## Available Skills\n\n| Skill | Description |\n|-------|-------------|\n| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for Dux Shell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |\n\n## Installation\n\nThe `.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.\n\n### Claude Code\n\nClaude 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:\n\n**Project-level** (this repo only):\n\n```bash\nmkdir -p .claude/skills\nln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev\n```\n\n**Personal** (all your projects):\n\n```bash\nln -s /path/to/dms/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev\n```\n\nAfter 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.\n\nSee the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.\n\n### Cursor\n\nCursor discovers skills from `.cursor/skills/` in the project root:\n\n```bash\nmkdir -p .cursor/skills\nln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev\n```\n\nSee [Cursor skills docs](https://cursor.com/docs/context/skills) for details.\n\n### VS Code (Copilot)\n\nVS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:\n\n```bash\nmkdir -p .github/skills\nln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev\n```\n\nSee [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.\n\n### Gemini CLI\n\nGemini CLI discovers skills from `.gemini/skills/` in the project root:\n\n```bash\nmkdir -p .gemini/skills\nln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev\n```\n\nSee [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.\n\n### OpenAI Codex\n\nCodex discovers skills from `.codex/skills/` in the project root:\n\n```bash\nmkdir -p .codex/skills\nln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev\n```\n\nSee [Codex skills docs](https://developers.openai.com/codex/skills/) for details.\n\n### Other Agents\n\nThe 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.\n\nCheck the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.\n\nThe general pattern is:\n\n```bash\nmkdir -p .<agent>/skills\nln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev\n```\n\n## Adding New Skills\n\nTo add a new skill to this directory:\n\n1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)\n2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions\n3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories\n4. Keep `SKILL.md` under 500 lines - move detailed content to reference files\n\nSee the [Agent Skills specification](https://agentskills.io/specification) for the full format.\n",".agents/skills/dms-plugin-dev/SKILL.md":"---\nname: dms-plugin-dev\ndescription: >\n  Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on\n  Quickshell. Supports widget (bar, dock + Control Center), daemon (background service),\n  launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab),\n  dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest\n  creation, QML component development, startup checks,\n  settings UI, data persistence, theme integration, PopoutService usage, IPC runtime\n  discovery, and external command execution. Use when the user wants to create, modify,\n  or debug a DMS plugin, or asks about the DMS plugin API.\ncompatibility: Designed for Claude Code (or similar products)\nmetadata:\n  author: Dux Shell\n  version: \"1.2\"\n  domain: qml-desktop-development\n  framework: Dux Shell\n  languages: qml, javascript\nallowed-tools: Bash Read Write Edit\n---\n\n# Dux Shell Plugin Development\n\n## Overview\n\nDMS plugins extend the desktop shell with bar and dock widgets, background services, launcher\nintegrations, desktop widgets, and dash tabs and cards. Plugins are QML components discovered from\n`~/.config/dms/plugins/`.\n\n**Minimum plugin structure:**\n\n```\n~/.config/dms/plugins/YourPlugin/\n  plugin.json        # Required: manifest with metadata\n  YourComponent.qml  # Required: main QML component\n  YourSettings.qml   # Optional: settings UI\n  *.js               # Optional: JavaScript utilities\n```\n\n**Plugin registry:** Community plugins are available at https://plugins.duxlabs.dev/\n\n**Plugin types:**\n\n| Type        | Purpose                        | Base Component             | Bar/dock pills | CC integration |\n|-------------|--------------------------------|----------------------------|----------------|----------------|\n| `widget`    | Bar or dock widget + popout    | `PluginComponent`          | Yes            | Yes            |\n| `daemon`    | Background service             | `PluginComponent` (no UI)  | No             | Optional       |\n| `launcher`  | Searchable items in launcher   | `Item`                     | No             | No             |\n| `desktop`   | Draggable desktop widget       | `DesktopPluginComponent`   | No             | No             |\n| `dash`      | Tab in the dash popout         | `DashTabComponent`         | No             | No             |\n| `dashCard`  | Card in the dash overview grid | `DashCardComponent`        | No             | No             |\n| `composite` | Multi-surface plugin           | One component per surface  | Optional       | Optional       |\n\n## Step 1: Determine Plugin Type\n\nChoose the type based on what the plugin does:\n\n- **Shows in the bar or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same\n  component), optionally opens a popout or an attached dock panel, optionally integrates with\n  Control Center.\n- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper\n  changes, notifications, battery level, etc.).\n- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher\n  with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).\n- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the\n  desktop layer.\n- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.\n- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.\n- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination\n  of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own\n  QML component file.\n\n## Step 2: Create the Manifest\n\nCreate `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.\n\n**Minimal manifest:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\"\n}\n```\n\n**With settings, startup check, and permissions:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"dependencies\": [\"mytool\"],\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Composite plugin (multi-surface):**\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Standalone dash tab or card:** same single file form, `\"type\": \"dash\"` or `\"type\": \"dashCard\"`\nplus `\"component\"`. A tab and a card together (or with a bar widget) use `components` with the\nkeys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and\noptions. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n**Key rules:**\n- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`\n- `version` must be semver (e.g., `1.0.0`)\n- Provide either `component` (single-surface) or `components` (multi-surface), not both\n- `component` / component paths must start with `./` and end with `.qml`\n- `type: \"launcher\"` (or a `components` object with a `launcher` key) requires a `trigger` field\n- `settings_write` permission is **required** if the plugin has a settings component\n- `dependencies` replaces the deprecated `requires` field\n\n## Step 3: Create the Main Component\n\n### Widget\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hi\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\nSee [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.\n\n### Launcher\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        const items = [\n            { name: \"Item One\", icon: \"material:star\", comment: \"Description\",\n              action: \"toast:Hello!\", categories: [\"MyPlugin\"] }\n        ]\n        if (!query) return items\n        const q = query.toLowerCase()\n        return items.filter(i => i.name.toLowerCase().includes(q))\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"toast\") ToastService?.showInfo(data)\n        else if (type === \"copy\") Quickshell.execDetached([\"dms\", \"cl\", \"copy\", data])\n    }\n}\n```\n\nSee [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.\n\n### Desktop\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Text {\n            anchors.centerIn: parent\n            text: \"Desktop Widget\"\n            color: Theme.surfaceText\n        }\n    }\n}\n```\n\nSee [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.\n\n### Daemon\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onSomeSignal() {\n            console.log(\"Event received\")\n        }\n    }\n}\n```\n\nSee [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.\n\n### Dash Tab\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    property date now: new Date()\n\n    implicitHeight: DashMetrics.tabMinHeight\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.now = new Date()\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: root.now.toLocaleTimeString(I18n.locale())\n        color: Theme.surfaceText\n    }\n}\n```\n\n### Dash Card\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nDashCardComponent {\n    id: root\n\n    tone: options.tone ?? \"\"\n\n    StyledText {\n        anchors.centerIn: parent\n        text: I18n.trFor(\"myPlugin\", \"Hello\")\n        color: root.contentColor\n    }\n}\n```\n\nGate timers and animations on `live`; the dash content stays alive after the popout closes.\nCards must lay out for every size in their manifest range. Use the card colors (`accentColor`,\n`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n### Composite\n\nFor composite plugins, create a separate QML file per surface. Each surface uses the same\nbase component as the corresponding single-surface type (PluginComponent for widget/daemon,\nItem for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.\n\n```\nMyCompositePlugin/\n  plugin.json\n  MyBarWidget.qml      # PluginComponent (widget surface)\n  MyDaemon.qml         # PluginComponent (daemon surface)\n  MyDesktopWidget.qml  # Item with desktop widget properties\n  Settings.qml         # Shared settings for all surfaces\n```\n\n`pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.\n\n## Step 4: Add Startup Check (Optional)\n\nGate plugin activation on dependency checks by providing a `startupCheck` component. This\nruns before the plugin loads and blocks activation if a required tool or condition is missing.\n\nCreate a `StartupCheck.qml` (non-visual QtObject):\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details body\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\nAdd to your manifest:\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"mytool\"]\n}\n```\n\n## Step 5: Add Settings (Optional)\n\nWrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key\"\n        placeholder: \"sk-...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"interval\"\n        label: \"Refresh Interval\"\n        options: [\n            { label: \"1 min\", value: \"60\" },\n            { label: \"5 min\", value: \"300\" }\n        ]\n        defaultValue: \"300\"\n    }\n}\n```\n\n**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.\n\nSee [settings-components-reference.md](references/settings-components-reference.md) for full property lists.\n\n**Important:** Your plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json, or the settings UI will show an error.\n\n## Step 6: Use Data Persistence\n\nThree tiers of persistence:\n\n| API | Persisted | Use case |\n|-----|-----------|----------|\n| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |\n| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |\n| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |\n| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |\n\n- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings\n- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`\n- Global vars sync across all instances (multi-monitor, multiple bar sections)\n\nSee [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.\n\n## Step 7: Theme Integration\n\nAlways use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.\n\n**Essential properties:**\n- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`\n- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)\n- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`\n- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`\n- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)\n\n**Common widgets from `qs.DCommon.Widgets`:** `StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.\n\nSee [theme-reference.md](references/theme-reference.md) for the complete property list.\n\n## Step 8: Add Popout Content (Widgets Only)\n\nAdd a popout that opens when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n}\n```\n\n**PopoutComponent properties:** `headerText`, `detailsText`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).\n\nCalculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Step 9: Control Center Integration (Widgets Only)\n\nAdd your widget to the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isActive ? \"On\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n\n    // Optional: expandable detail panel (for CompoundPill)\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.surfaceContainerHigh\n            radius: Theme.cornerRadius\n        }\n    }\n}\n```\n\n**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).\n\n## Step 10: External Commands and Clipboard\n\n**Run commands and capture output:**\n\n```qml\nimport qs.Common\n\nProc.runCommand(\n    \"myPlugin.fetch\",\n    [\"curl\", \"-s\", \"https://api.example.com/data\"],\n    (stdout, exitCode) => {\n        if (exitCode === 0) processData(stdout)\n    },\n    500  // debounce ms\n)\n```\n\n**Fire-and-forget (clipboard, notifications):**\n\n```qml\nimport Quickshell\n\nQuickshell.execDetached([\"dms\", \"cl\", \"copy\", textToCopy])\n```\n\n**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.\n\n**Shell commands with pipes:** `[\"sh\", \"-c\", \"ps aux | grep foo\"]`\n\n**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.\n\n## Translations (Optional)\n\nShip per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:\n\n```\nMyPlugin/\n└── translations/\n    ├── es.json\n    └── zh_CN.json\n```\n\nFile names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.\n\nEach file maps the English term to its translation, using the term as its own context bucket:\n\n```json\n{\n  \"Cycle Speed\": {\n    \"Cycle Speed\": \"Velocidad de ciclo\"\n  }\n}\n```\n\nIn QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:\n\n```qml\ntext: I18n.trFor(\"myPlugin\", \"Cycle Speed\")\n```\n\nLookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.\n\n## Step 11: Validate and Test\n\n1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)\n2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`\n3. Open Settings > Plugins > Scan for Plugins\n4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)\n\n**Runtime plugin discovery via IPC:**\n\nPlugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:\n\n```bash\ndms ipc plugin-scan scan          # Trigger a full rescan of all plugin directories\ndms ipc plugin-scan rescan <id>   # Force rescan of a specific plugin\ndms ipc plugin-scan reload <id>   # Force reload of a loaded plugin\ndms ipc plugin-scan list          # List all known plugins (TSV: id, loaded, type, name)\ndms ipc plugin-scan status <id>   # Get status of a specific plugin (TSV: loaded, type, error)\n```\n\nPlugin IDs are validated against `^[a-zA-Z0-9_\\-:]{1,64}$`.\n\n**Common issues:**\n- Plugin not detected: check plugin.json syntax with `jq . plugin.json`\n- Widget not showing: ensure it's enabled AND added to a DankBar section\n- Settings error: verify `settings_write` permission is declared\n- Data not persisting: check pluginService injection and permissions\n- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`\n\n## Common Mistakes\n\n1. **Missing `settings_write` permission** - Settings UI shows error without it\n2. **Missing `property var popoutService: null`** - Must declare for injection to work\n3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge\n4. **Hardcoded colors** - Use `Theme.*` properties, not hex values\n5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])`\n6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`\n7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)\n8. **Forgetting `categories` in launcher items** - Items won't display without it\n9. **Not handling null pluginService** - Always use optional chaining or null checks\n10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`\n11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`\n12. **Providing both `component` and `components`** - Use one or the other, not both\n13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key\n14. **Using `\"type\": \"widget\"` for a dash tab** - Dash surfaces need `\"type\": \"dash\"` / `\"dashCard\"` or a `components` key\n15. **Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`\n16. **Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`\n17. **Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone\n\n## Quick Reference: Imports\n\n**Widget / Daemon:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n```\n\n**Launcher:**\n```qml\nimport QtQuick\nimport qs.Services\n```\n\n**Desktop:**\n```qml\nimport QtQuick\nimport qs.Common\n```\n\n**Dash tab / card:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash          // DashMetrics, DashWidgetGrid\nimport qs.Modules.DankDash.Overview // Card, for tiles inside a tab\n```\n\n**For clipboard/exec:** `import Quickshell`\n**For processes:** `import Quickshell.Io`\n**For networking:** `import Quickshell.Networking`\n**For toast notifications:** access `ToastService` from `qs.Services`\n\n## Quick Reference: File Naming\n\n- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)\n- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)\n- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)\n- **Component paths in manifest:** relative with `./` prefix (e.g., `\"./MyWidget.qml\"`)\n- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)\n\n## Reference Files\n\nLoad these on demand for detailed API documentation:\n\n- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema\n- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration\n- [dash-plugin-guide.md](references/dash-plugin-guide.md) - DashTabComponent, DashCardComponent, DashWidgetGrid, dash options, card sizing\n- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view\n- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence\n- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution\n- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists\n- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns\n- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables\n- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals\n- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins\n",".agents/skills/dms-plugin-dev/assets/plugin-schema.json":"{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://duxlabs.dev/schemas/plugin.json\",\n  \"title\": \"Dux Shell Plugin Manifest\",\n  \"description\": \"Schema for Dux Shell plugin.json manifest files\",\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"name\",\n    \"description\",\n    \"version\",\n    \"author\",\n    \"type\",\n    \"capabilities\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"type\": \"string\",\n      \"description\": \"Unique plugin identifier (camelCase, no spaces)\",\n      \"pattern\": \"^[a-zA-Z][a-zA-Z0-9]*$\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable plugin name\",\n      \"minLength\": 1\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"Short description of plugin functionality\",\n      \"minLength\": 1\n    },\n    \"version\": {\n      \"type\": \"string\",\n      \"description\": \"Semantic version string (e.g., '1.0.0')\",\n      \"pattern\": \"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-[a-zA-Z0-9.-]+)?(\\\\+[a-zA-Z0-9.-]+)?$\"\n    },\n    \"author\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin creator name or email\",\n      \"minLength\": 1\n    },\n    \"type\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.\",\n      \"enum\": [\"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", \"composite\"]\n    },\n    \"capabilities\": {\n      \"type\": \"array\",\n      \"description\": \"Array of plugin capabilities\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"minItems\": 1\n    },\n    \"component\": {\n      \"type\": \"string\",\n      \"description\": \"Relative path to main QML component file. Required unless 'components' is provided.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"components\": {\n      \"type\": \"object\",\n      \"description\": \"Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.\",\n      \"properties\": {\n        \"widget\": {\n          \"type\": \"string\",\n          \"description\": \"Bar/Control Center widget component (PluginComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"desktop\": {\n          \"type\": \"string\",\n          \"description\": \"Desktop widget component\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"daemon\": {\n          \"type\": \"string\",\n          \"description\": \"Background daemon component (instantiated once)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"launcher\": {\n          \"type\": \"string\",\n          \"description\": \"Launcher provider component (requires 'trigger')\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dash\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard tab component (DashTabComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dashCard\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard overview card component (DashCardComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"dash\": {\n      \"type\": \"object\",\n      \"description\": \"Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.\",\n      \"properties\": {\n        \"icon\": {\n          \"type\": \"string\",\n          \"description\": \"Material icon for the dashboard tab\"\n        },\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"Tab label\"\n        },\n        \"card\": {\n          \"type\": \"object\",\n          \"description\": \"Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).\",\n          \"properties\": {\n            \"icon\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"w\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"h\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"minW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"minH\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"maxW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"maxH\": { \"type\": \"integer\", \"minimum\": 1 }\n          },\n          \"additionalProperties\": false\n        },\n        \"options\": {\n          \"type\": \"array\",\n          \"description\": \"User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\"key\", \"type\"],\n            \"properties\": {\n              \"key\": { \"type\": \"string\", \"not\": { \"const\": \"widgets\" }, \"description\": \"Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts\" },\n              \"text\": { \"type\": \"string\", \"description\": \"Row label, translated through the plugin catalog\" },\n              \"description\": { \"type\": \"string\", \"description\": \"Secondary text under a toggle row, translated through the plugin catalog\" },\n              \"type\": { \"type\": \"string\", \"enum\": [\"toggle\", \"choice\", \"number\"] },\n              \"def\": { \"description\": \"Default value: boolean for toggle, choice value for choice, number for number\" },\n              \"choices\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"required\": [\"value\"],\n                  \"properties\": {\n                    \"value\": { \"type\": \"string\" },\n                    \"text\": { \"type\": \"string\" }\n                  },\n                  \"additionalProperties\": false\n                }\n              },\n              \"min\": { \"type\": \"number\" },\n              \"max\": { \"type\": \"number\" },\n              \"step\": { \"type\": \"number\", \"exclusiveMinimum\": 0 },\n              \"unit\": { \"type\": \"string\" }\n            },\n            \"additionalProperties\": false\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"trigger\": {\n      \"type\": \"string\",\n      \"description\": \"Trigger string for launcher activation (required for launcher type)\"\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Material Design icon name\"\n    },\n    \"settings\": {\n      \"type\": \"string\",\n      \"description\": \"Path to settings component QML file\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"startupCheck\": {\n      \"type\": \"string\",\n      \"description\": \"Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"requires_dms\": {\n      \"type\": \"string\",\n      \"description\": \"Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')\",\n      \"pattern\": \"^(>=?|<=?|=|>|<)\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\"\n    },\n    \"dependencies\": {\n      \"type\": \"array\",\n      \"description\": \"Array of required system tools/dependencies (registry metadata)\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"requires\": {\n      \"type\": \"array\",\n      \"description\": \"Deprecated alias for 'dependencies'.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"permissions\": {\n      \"type\": \"array\",\n      \"description\": \"Required capabilities\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"settings_read\",\n          \"settings_write\",\n          \"process\",\n          \"network\"\n        ]\n      }\n    }\n  },\n  \"allOf\": [\n    {\n      \"if\": {\n        \"properties\": {\n          \"type\": {\n            \"const\": \"launcher\"\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"if\": {\n        \"required\": [\"components\"],\n        \"properties\": {\n          \"components\": {\n            \"required\": [\"launcher\"]\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"anyOf\": [\n        {\n          \"required\": [\"component\"]\n        },\n        {\n          \"required\": [\"components\"]\n        }\n      ]\n    }\n  ],\n  \"additionalProperties\": true\n}\n",".agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json":"{\n    \"id\": \"myDaemon\",\n    \"name\": \"My Daemon\",\n    \"description\": \"A background service that reacts to events\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"background-service\"],\n    \"component\": \"./Daemon.qml\",\n    \"icon\": \"settings\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json":"{\n    \"id\": \"myDashCard\",\n    \"name\": \"My Dash Card\",\n    \"description\": \"A custom dash overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./Card.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"card\": { \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 3, \"maxH\": 2 },\n        \"options\": [\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n",".agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json":"{\n    \"id\": \"myDashTab\",\n    \"name\": \"My Dash Tab\",\n    \"description\": \"A custom dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./Tab.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"title\": \"My Tab\",\n        \"options\": [\n            { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n",".agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json":"{\n    \"id\": \"myDesktopWidget\",\n    \"name\": \"My Desktop Widget\",\n    \"description\": \"A custom desktop widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json":"{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Custom launcher plugin with searchable items\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./Launcher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json":"{\n    \"id\": \"myWidget\",\n    \"name\": \"My Widget\",\n    \"description\": \"A custom bar widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n",".agents/skills/dms-plugin-dev/references/advanced-patterns.md":"# Advanced Patterns\n\nPatterns observed in production DMS plugins that go beyond the basics.\n\n## Plugin Variants\n\nCreate multiple widget instances from a single plugin definition. Each variant has its own configuration.\n\n### Manifest\n\nNo special manifest changes needed - the variant system is built into PluginComponent.\n\n### Widget with Variant Support\n\n```qml\nPluginComponent {\n    property string variantId: \"\"\n    property var variantData: ({})\n\n    property string displayText: variantData?.text || \"Default\"\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.displayText\n            }\n        }\n    }\n}\n```\n\nWidget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Settings with Variant Management\n\n```qml\nPluginSettings {\n    pluginId: \"exampleVariants\"\n\n    // Variant creation UI\n    DButton {\n        text: \"Add New Instance\"\n        onClicked: {\n            var id = \"variant_\" + Date.now()\n            root.createVariant(id, { name: \"New Instance\", text: \"Hello\" })\n        }\n    }\n\n    // Per-variant configuration\n    Repeater {\n        model: root.variants\n        delegate: Column {\n            StringSetting {\n                settingKey: modelData.id + \"_text\"\n                label: modelData.name || modelData.id\n            }\n        }\n    }\n}\n```\n\n## JavaScript Utility Files\n\nFor complex logic, split into `.js` files:\n\n### utils.js\n\n```javascript\n.pragma library\n\nfunction formatDuration(ms) {\n    if (ms < 60000) return \"just now\"\n    if (ms < 3600000) return Math.floor(ms / 60000) + \"m ago\"\n    return Math.floor(ms / 3600000) + \"h ago\"\n}\n\nfunction parseResponse(json) {\n    try {\n        return JSON.parse(json)\n    } catch (e) {\n        return null\n    }\n}\n```\n\n### Using in QML\n\n```qml\nimport \"utils.js\" as Utils\n\nItem {\n    StyledText {\n        text: Utils.formatDuration(Date.now() - timestamp)\n    }\n}\n```\n\nThe `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.\n\n## qmldir for Singleton Services\n\nFor plugins with internal singleton services:\n\n### qmldir\n\n```\nsingleton MyService 1.0 MyService.qml\n```\n\n### MyService.qml\n\n```qml\npragma Singleton\nimport QtQuick\n\nQtObject {\n    property var cache: ({})\n\n    function getData(key) {\n        return cache[key] || null\n    }\n\n    function setData(key, value) {\n        cache[key] = value\n    }\n}\n```\n\n### Using the singleton\n\n```qml\nimport \".\" as Local\n\nItem {\n    Component.onCompleted: {\n        Local.MyService.setData(\"key\", \"value\")\n    }\n}\n```\n\n## Inline Component Declarations\n\nReusable sub-components defined inline:\n\n```qml\nItem {\n    component StatusBadge: Rectangle {\n        property string label: \"\"\n        property color badgeColor: Theme.primary\n\n        width: badgeText.implicitWidth + Theme.spacingM * 2\n        height: 24\n        radius: 12\n        color: badgeColor\n\n        StyledText {\n            id: badgeText\n            anchors.centerIn: parent\n            text: label\n            color: Theme.onPrimary\n            font.pixelSize: Theme.fontSizeSmall\n        }\n    }\n\n    Row {\n        spacing: Theme.spacingS\n        StatusBadge { label: \"Running\"; badgeColor: Theme.success }\n        StatusBadge { label: \"Stopped\"; badgeColor: Theme.error }\n    }\n}\n```\n\n## Multi-Provider Adapter Pattern\n\nFor plugins supporting multiple backends (AI providers, API services):\n\n### apiAdapters.js\n\n```javascript\n.pragma library\n\nfunction createAdapter(provider) {\n    switch (provider) {\n        case \"openai\": return {\n            url: \"https://api.openai.com/v1/chat/completions\",\n            headers: (key) => ({ \"Authorization\": \"Bearer \" + key }),\n            formatRequest: (messages) => JSON.stringify({ model: \"gpt-4\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).choices[0].message.content\n        }\n        case \"anthropic\": return {\n            url: \"https://api.anthropic.com/v1/messages\",\n            headers: (key) => ({ \"x-api-key\": key, \"anthropic-version\": \"2023-06-01\" }),\n            formatRequest: (messages) => JSON.stringify({ model: \"claude-sonnet-4-20250514\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).content[0].text\n        }\n        default: return null\n    }\n}\n```\n\n## IPC Integration\n\nFor plugins that respond to keyboard shortcuts or external commands:\n\n```qml\nPluginComponent {\n    Connections {\n        target: DMSIpc\n        function onCommandReceived(command, args) {\n            if (command === \"myPlugin.toggle\") {\n                doToggle()\n            } else if (command === \"myPlugin.next\") {\n                goNext()\n            }\n        }\n    }\n}\n```\n\nExternal trigger: `dms ipc call myPlugin.toggle`\n\n## Networking with Quickshell.Networking\n\nFor API calls using the built-in networking module:\n\n```qml\nimport Quickshell.Networking\n\nItem {\n    NetworkRequest {\n        id: request\n        url: \"https://api.example.com/data\"\n        method: \"GET\"\n\n        onResponseReceived: (response) => {\n            const data = JSON.parse(response.body)\n            processData(data)\n        }\n\n        onErrorOccurred: (error) => {\n            console.error(\"Network error:\", error)\n        }\n    }\n\n    function fetchData() {\n        request.send()\n    }\n}\n```\n\n## Toast Notifications\n\nShow user feedback:\n\n```qml\nimport qs.Services\n\n// Info toast\nToastService?.showInfo(\"Operation completed\")\n\n// With title\nToastService?.showInfo(\"Plugin Name\", \"Data refreshed successfully\")\n```\n\nAlways use optional chaining since ToastService may not be available in all contexts.\n\n## Clipboard Operations\n\n```qml\nimport Quickshell\n\nfunction copyToClipboard(text) {\n    Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    ToastService?.showInfo(\"Copied to clipboard\")\n}\n```\n\nDo NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.\n\n## Multi-File Plugin Architecture\n\nLarge plugins can be split across multiple files:\n\n```\nMyPlugin/\n  plugin.json\n  Main.qml           # Main widget component\n  Settings.qml       # Settings UI\n  DetailView.qml     # Popout detail view\n  utils.js            # Utility functions\n  apiAdapter.js       # API adapter layer\n  qmldir              # Optional: singleton registrations\n```\n\nImport sibling files:\n\n```qml\n// In Main.qml\nimport \".\" as Local\n\nItem {\n    Loader {\n        source: \"DetailView.qml\"\n    }\n}\n```\n\n## Performance Tips\n\n1. Use `Proc.runCommand` with appropriate debounce for external commands\n2. Pre-cache images and thumbnails for image-heavy plugins\n3. Limit concurrent network requests\n4. Use `Timer` with reasonable intervals (don't poll faster than needed)\n5. Lazy-load heavy content (use `Loader` for complex popout content)\n6. Avoid blocking the UI thread with synchronous operations\n",".agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md":"# Daemon Plugin Guide\n\nDaemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.\n\n## Base Component\n\nDaemons use `PluginComponent` with no bar pills:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    // Event-driven logic goes here\n}\n```\n\n## When to Use Daemons\n\n- Monitor system events (wallpaper changes, battery level, notifications)\n- Run periodic background tasks (polling APIs, checking system state)\n- Execute scripts in response to events\n- Control shell UI via PopoutService based on conditions\n\n## Event-Driven Pattern\n\nUse `Connections` to react to service signals:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            console.log(\"Wallpaper changed to:\", SessionData.wallpaperPath)\n            runScript(SessionData.wallpaperPath)\n        }\n    }\n\n    Connections {\n        target: BatteryService\n        function onPercentageChanged() {\n            if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n                popoutService?.openBattery()\n            }\n        }\n    }\n}\n```\n\n## Available Services\n\nCommon services daemons can connect to:\n\n| Service | Signals/Properties | Description |\n|---------|-------------------|-------------|\n| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |\n| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |\n| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |\n| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |\n\nImport services from `qs.Services`.\n\n## Process Execution\n\n### Simple command with Proc\n\n```qml\nimport qs.Common\n\nPluginComponent {\n    function runScript(arg) {\n        Proc.runCommand(\n            \"myDaemon.script\",\n            [\"bash\", \"-c\", \"echo 'Processing: \" + arg + \"'\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Script output:\", stdout)\n                } else {\n                    ToastService?.showInfo(\"Script failed: exit \" + exitCode)\n                }\n            }\n        )\n    }\n}\n```\n\n### Long-running process with Process component\n\n```qml\nimport Quickshell.Io\n\nPluginComponent {\n    property string scriptPath: \"\"\n\n    Process {\n        id: proc\n        command: [\"bash\", scriptPath]\n        running: false\n\n        stdout: StdioCollector {\n            onTextReceived: (text) => {\n                console.log(\"stdout:\", text)\n            }\n        }\n\n        stderr: StdioCollector {\n            onTextReceived: (text) => {\n                console.error(\"stderr:\", text)\n            }\n        }\n\n        onExited: (exitCode) => {\n            if (exitCode !== 0) {\n                ToastService?.showInfo(\"Process failed: exit \" + exitCode)\n            }\n        }\n    }\n\n    function startProcess() {\n        if (scriptPath && !proc.running) {\n            proc.running = true\n        }\n    }\n}\n```\n\n## Timer-Based Polling\n\n```qml\nPluginComponent {\n    Timer {\n        interval: 60000  // every minute\n        running: true\n        repeat: true\n        onTriggered: checkStatus()\n    }\n\n    function checkStatus() {\n        Proc.runCommand(\n            \"myDaemon.check\",\n            [\"sh\", \"-c\", \"systemctl is-active myservice\"],\n            (stdout, exitCode) => {\n                const active = stdout.trim() === \"active\"\n                PluginService.setGlobalVar(\"myDaemon\", \"serviceActive\", active)\n            }\n        )\n    }\n}\n```\n\n## Data Persistence\n\nDaemons access PluginService directly (it's injected via PluginComponent):\n\n```qml\nPluginComponent {\n    property string configuredScript: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                configuredScript = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n}\n```\n\n## PopoutService Usage\n\nDaemons can control shell UI via the injected popoutService:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    function showAlert() {\n        popoutService?.openNotificationCenter()\n    }\n\n    function openSettings() {\n        popoutService?.openSettings()\n    }\n}\n```\n\nSee [popout-service-reference.md](popout-service-reference.md) for the full API.\n\n## Complete Example\n\nBased on the WallpaperWatcherDaemon:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport Quickshell.Io\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property string scriptPath: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                scriptPath = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            if (scriptPath) {\n                runWallpaperScript(SessionData.wallpaperPath)\n            }\n        }\n    }\n\n    function runWallpaperScript(wallpaperPath) {\n        console.log(\"[WallpaperWatcher] Running script:\", scriptPath, wallpaperPath)\n\n        Proc.runCommand(\n            \"wallpaperWatcher.run\",\n            [\"bash\", scriptPath, wallpaperPath],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"[WallpaperWatcher] Script output:\", stdout)\n                } else {\n                    console.error(\"[WallpaperWatcher] Script failed:\", exitCode)\n                    ToastService?.showInfo(\"Wallpaper script failed\")\n                }\n            }\n        )\n    }\n\n    Component.onCompleted: {\n        console.log(\"[WallpaperWatcher] Daemon started\")\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"wallpaperWatcher\",\n    \"name\": \"Wallpaper Watcher\",\n    \"description\": \"Runs a script when the wallpaper changes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper-automation\"],\n    \"component\": \"./WallpaperWatcher.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n```\n",".agents/skills/dms-plugin-dev/references/dash-plugin-guide.md":"# Dash Plugin Guide\n\nThe dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).\n\nRequires DMS 1.7 or later: set `\"requires_dms\": \">=1.7.0\"`.\n\n## Picking the manifest form\n\n| Plugin provides | Manifest |\n|-----------------|----------|\n| Tab only | `\"type\": \"dash\"`, `\"component\": \"./Tab.qml\"` |\n| Card only | `\"type\": \"dashCard\"`, `\"component\": \"./Card.qml\"` |\n| Tab and card | `\"type\": \"composite\"`, `\"components\": { \"dash\": ..., \"dashCard\": ... }` |\n| Dash plus bar/dock widget, daemon, ... | `\"type\": \"composite\"` with every surface in `components` |\n\nAdd the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).\n\n## Rules that trip people up\n\n- Only enabled plugins appear in the dash. There is no \"disabled\" placeholder.\n- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).\n- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash(\"plugin_\" + pluginId, x, y, width, section, screen)`.\n- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.\n- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.\n- `dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.\n- `widgets` is a reserved option key.\n- Use `I18n.trFor(\"<plugin id literal>\", \"...\")` for every string.\n\n## Tab: DashTabComponent\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    focusTarget: button\n    implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)\n    menuActions: [\n        {\n            label: I18n.trFor(\"myPlugin\", \"Refresh\"),\n            iconName: \"refresh\",\n            action: () => root.refresh()\n        }\n    ]\n\n    function refresh() {\n    }\n\n    function restoreFocus() {\n        button.forceActiveFocus(Qt.OtherFocusReason);\n    }\n\n    function handleKeyEvent(event) {\n        if (event.key !== Qt.Key_R)\n            return false;\n        refresh();\n        return true;\n    }\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.refresh()\n    }\n\n    Column {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingM\n\n        StyledText {\n            text: root.options.compact ? \"\" : I18n.trFor(\"myPlugin\", \"Hello\")\n            color: Theme.surfaceText\n        }\n\n        DButton {\n            id: button\n            text: I18n.trFor(\"myPlugin\", \"Refresh\")\n            onClicked: root.refresh()\n        }\n    }\n}\n```\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (`dashVisible`, `requestTab(id)`, `editMode`), `editMode`, `live`. Also `targetScreen`, `active` and `rowBudget` when the tab declares them.\n\nBase provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`.\n\nOptional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`.\n\nFor tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview.\n\n### Editable widgets in a tab\n\nAssign a `DashWidgetGrid` (from `qs.Modules.DankDash`) to `widgetGrid` to get Edit, Add widget, Reset and Clear All:\n\n```qml\nDashTabComponent {\n    id: tab\n    widgetGrid: grid\n    implicitHeight: grid.implicitHeight\n    focusTarget: grid.focusTarget\n\n    DashWidgetGrid {\n        id: grid\n        width: parent.width\n        entryId: tab.entryId\n        live: tab.live\n        editMode: tab.editMode\n        definitions: [\n            { id: \"main\", text: I18n.trFor(\"myPlugin\", \"Main\"), icon: \"star\", component: mainWidget, w: 2, h: 2, minW: 1, minH: 1, maxW: 4, maxH: 3 }\n        ]\n    }\n\n    Component {\n        id: mainWidget\n        Card {}\n    }\n}\n```\n\nThe grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.\n\n## Card: DashCardComponent\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashCardComponent {\n    id: root\n\n    readonly property bool wide: width >= DashMetrics.gridRowUnit * 2\n    readonly property bool tall: height >= DashMetrics.gridRowUnit * 2\n\n    tone: options.tone ?? \"\"\n\n    SystemClock {\n        id: clock\n        enabled: root.live\n        precision: SystemClock.Minutes\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: clock.date.toLocaleTimeString(I18n.locale(), \"hh:mm\")\n        font.pixelSize: root.tall ? Theme.fontSizeDisplay : Theme.fontSizeXLarge\n        color: root.accentColor\n    }\n}\n```\n\nBase (`Card`) provides: `pad`, `title`, `tone` (`\"\"`, `\"primary\"`, `\"secondary\"`, `\"tertiary\"`), `containerColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor`, `chipColor`, `tinted`, `clickable`, `signal clicked`, `options`, `pluginData`, `getData`, `setData`.\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`; bound: `live` (overview visible), `interactive` (false in edit mode).\n\nClicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.\n\nUse the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.\n\n## Testing\n\n```bash\ndms ipc call plugins reload myPlugin\ndms ipc call dash open plugin_myPlugin\n```\n\nWorking examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).\n",".agents/skills/dms-plugin-dev/references/data-persistence-guide.md":"# Data Persistence Guide\n\nDMS plugins have three tiers of data persistence, each suited for different use cases.\n\n## Tier 1: Plugin Data (Settings)\n\nPersisted to `settings.json`. Use for user preferences and configuration.\n\n### Saving\n\n```qml\npluginService.savePluginData(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar value = pluginService.loadPluginData(pluginId, \"key\", defaultValue)\n```\n\n### Reactive Access via pluginData\n\n`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n    property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true\n}\n```\n\n### Reacting to Settings Changes\n\nWhen settings are changed (e.g., from the settings UI), react with `Connections`:\n\n```qml\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        showIcon = pluginService.loadPluginData(pluginId, \"showIcon\", true)\n    }\n}\n```\n\n## Tier 2: Plugin State\n\nPersisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).\n\n### Saving\n\n```qml\npluginService.savePluginState(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar state = pluginService.loadPluginState(pluginId, \"key\", defaultValue)\n```\n\n### Additional Methods\n\n```qml\npluginService.clearPluginState(pluginId)\npluginService.removePluginStateKey(pluginId, \"key\")\n```\n\n### Example: Persistent History\n\n```qml\nItem {\n    property var history: []\n\n    Component.onCompleted: {\n        history = pluginService?.loadPluginState(pluginId, \"history\", []) || []\n    }\n\n    function addToHistory(entry) {\n        history.unshift({\n            text: entry,\n            timestamp: Date.now()\n        })\n        if (history.length > 100) history = history.slice(0, 100)\n        pluginService?.savePluginState(pluginId, \"history\", history)\n    }\n\n    function clearHistory() {\n        history = []\n        pluginService?.removePluginStateKey(pluginId, \"history\")\n    }\n}\n```\n\n## Tier 3: Global Variables (Runtime Only)\n\nNOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).\n\n### Using PluginGlobalVar Component\n\n```qml\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            // ...\n            StyledText {\n                text: \"Count: \" + globalCounter.value\n            }\n\n            MouseArea {\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `varName` | string | Required: name of the global variable |\n| `defaultValue` | any | Optional: default if not set |\n| `value` | any | Readonly: current value |\n\n**Methods:**\n- `set(newValue)` - update the value (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\n```qml\nimport qs.Services\n\nproperty int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\nConnections {\n    target: PluginService\n    function onGlobalVarChanged(pluginId, varName) {\n        if (pluginId === \"myPlugin\" && varName === \"counter\") {\n            counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n        }\n    }\n}\n\nfunction increment() {\n    var current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n}\n```\n\n## Decision Matrix\n\n| Need | API | Persisted | Scope |\n|------|-----|-----------|-------|\n| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |\n| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |\n| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |\n| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |\n\n## Plugin Path\n\nRetrieve a plugin's installation directory at runtime:\n\n```qml\nvar dir = pluginService.getPluginPath(pluginId)\n```\n\nReturns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.\n\n## Important Notes\n\n1. **pluginData is reactive** - bindings update automatically when data changes\n2. **Global vars are NOT persistent** - they reset when the shell restarts\n3. **State vs Data** - data is for user-facing settings, state is for internal runtime data\n4. **Null safety** - always check `pluginService` is not null before calling methods\n5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin\n6. **Performance** - global vars are efficient for frequent updates; settings writes are batched\n",".agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md":"# Desktop Plugin Guide\n\nDesktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.\n\n## Base Component\n\nDesktop widgets use a plain `Item` with injected properties:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n\n        // Your content here\n    }\n}\n```\n\n## Injected Properties\n\nThese are set automatically by the DesktopPluginWrapper:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `pluginService` | object | PluginService reference for data persistence |\n| `pluginId` | string | Plugin's unique identifier |\n| `editMode` | bool | `true` when user is dragging/resizing |\n| `widgetWidth` | real | Current widget container width |\n| `widgetHeight` | real | Current widget container height |\n\n## Optional Properties\n\nDefine these on your root item to customize behavior:\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `minWidth` | real | 100 | Minimum allowed width during resize |\n| `minHeight` | real | 100 | Minimum allowed height during resize |\n\n## Position and Size Persistence\n\nPosition (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.\n\n## Edit Mode\n\nWhen `editMode` is true, the user is repositioning or resizing. Use this to:\n- Show visual indicators (borders, handles)\n- Disable interactive elements to prevent accidental actions\n- Display additional controls\n\n```qml\nRectangle {\n    anchors.fill: parent\n    border.color: root.editMode ? Theme.primary : \"transparent\"\n    border.width: root.editMode ? 2 : 0\n\n    MouseArea {\n        anchors.fill: parent\n        enabled: !root.editMode\n        onClicked: doSomething()\n    }\n}\n```\n\n## Loading and Saving Data\n\nUse the injected `pluginService` for data persistence:\n\n```qml\nproperty string displayMode: {\n    if (!pluginService) return \"default\"\n    return pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n}\n\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        root.displayMode = pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n    }\n}\n\nfunction saveMode(mode) {\n    pluginService?.savePluginData(pluginId, \"displayMode\", mode)\n}\n```\n\n## Settings Component\n\nDesktop plugin settings use the same `PluginSettings` wrapper as other types:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myDesktopWidget\"\n\n    SliderSetting {\n        settingKey: \"opacity\"\n        label: \"Opacity\"\n        description: \"Widget background opacity\"\n        defaultValue: 85\n        minimum: 10\n        maximum: 100\n        unit: \"%\"\n    }\n\n    SelectionSetting {\n        settingKey: \"style\"\n        label: \"Display Style\"\n        options: [\n            { label: \"Compact\", value: \"compact\" },\n            { label: \"Expanded\", value: \"expanded\" }\n        ]\n        defaultValue: \"compact\"\n    }\n}\n```\n\n## User Interaction\n\nDesktop widgets support:\n1. **Drag** - click and drag anywhere (in edit mode)\n2. **Resize** - drag bottom-right corner handle (in edit mode)\n3. **Edit mode toggle** - via the desktop edit button\n\n## Complete Example\n\nBased on the ExampleDesktopClock pattern:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 250\n    property real widgetHeight: 250\n    property real minWidth: 150\n    property real minHeight: 150\n\n    property string clockStyle: {\n        if (!pluginService) return \"digital\"\n        return pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n    }\n\n    property real bgOpacity: {\n        if (!pluginService) return 0.85\n        var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n        return val / 100\n    }\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId !== pluginId) return\n            clockStyle = pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n            var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n            bgOpacity = val / 100\n        }\n    }\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: root.bgOpacity\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Column {\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatTime(new Date(), \"hh:mm:ss\")\n                color: Theme.surfaceText\n                font.pixelSize: root.widgetWidth * 0.15\n                font.weight: Font.Bold\n            }\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatDate(new Date(), \"ddd, MMM d\")\n                color: Theme.onSurfaceVariant\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    Timer {\n        interval: 1000\n        running: true\n        repeat: true\n        onTriggered: root.widgetWidth = root.widgetWidth // force update\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"myDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"Analog and digital clock for the desktop\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./ClockWidget.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n",".agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md":"# Launcher Plugin Guide\n\nLauncher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.\n\n## Base Component\n\nLaunchers use a plain `Item` (not PluginComponent):\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        // Return array of items\n        return []\n    }\n\n    function executeItem(item) {\n        // Handle item selection\n    }\n}\n```\n\n## Required Interface\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `pluginService` | property | Injected PluginService reference (declare as `null`) |\n| `trigger` | property | Trigger string for activation |\n| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |\n| `getItems(query)` | function | Return array of items matching query |\n| `executeItem(item)` | function | Handle item selection |\n\n## Item Structure\n\nEach item returned by `getItems()`:\n\n```javascript\n{\n    name: \"Item Display Name\",       // Required: shown in launcher\n    icon: \"material:star\",           // Optional: icon specification\n    comment: \"Description text\",     // Required: subtitle text\n    action: \"type:data\",             // Required: action identifier\n    categories: [\"MyPlugin\"],        // Required: array with plugin category\n    imageUrl: \"https://...\"          // Optional: image for tile view\n}\n```\n\n## Icon Types\n\n### 1. Material Design Icons\n\n```javascript\n{ icon: \"material:lightbulb\" }\n{ icon: \"material:terminal\" }\n{ icon: \"material:translate\" }\n```\n\nUses the Material Symbols Rounded font.\n\n### 2. Unicode / Emoji Icons\n\n```javascript\n{ icon: \"unicode:smile_face\" }\n```\n\nRendered at 70-80% of icon size with theming.\n\n### 3. Desktop Theme Icons\n\n```javascript\n{ icon: \"firefox\" }\n{ icon: \"folder\" }\n```\n\nUses the user's installed icon theme.\n\n### 4. No Icon\n\nOmit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.\n\n## Trigger System\n\n**Custom trigger** (items only appear when trigger is typed):\n\n```json\n{ \"trigger\": \"#\" }\n```\n\n- Type `#` alone: shows all plugin items\n- Type `# query`: filters plugin items by query\n- The query string (without trigger) is passed to `getItems(query)`\n\n**No trigger** (items always visible alongside regular apps):\n\n```json\n{ \"trigger\": \"\" }\n```\n\nSave empty trigger at runtime:\n```qml\nComponent.onCompleted: {\n    trigger = pluginService?.loadPluginData(pluginId, \"trigger\", \"#\") ?? \"#\"\n}\n```\n\n## Action Execution\n\nParse action strings in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            ToastService?.showInfo(actionData)\n            break\n        case \"copy\":\n            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", actionData])\n            ToastService?.showInfo(\"Copied to clipboard\")\n            break\n        case \"exec\":\n            Quickshell.execDetached(actionData.split(\" \"))\n            break\n        case \"url\":\n            Quickshell.execDetached([\"xdg-open\", actionData])\n            break\n        default:\n            console.warn(\"Unknown action type:\", actionType)\n    }\n}\n```\n\n## Search / Filtering\n\nThe `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).\n\n```qml\nfunction getItems(query) {\n    const allItems = [\n        { name: \"Calculator\", icon: \"material:calculate\",\n          comment: \"Open calculator\", action: \"exec:gnome-calculator\",\n          categories: [\"Tools\"] },\n        { name: \"Terminal\", icon: \"material:terminal\",\n          comment: \"Open terminal\", action: \"exec:alacritty\",\n          categories: [\"Tools\"] }\n    ]\n\n    if (!query || query.length === 0) return allItems\n\n    const q = query.toLowerCase()\n    return allItems.filter(item =>\n        item.name.toLowerCase().includes(q) ||\n        item.comment.toLowerCase().includes(q)\n    )\n}\n```\n\n## Context Menu Actions\n\nAdd right-click actions to launcher items:\n\n```qml\nfunction getContextMenuActions(item) {\n    return [\n        { name: \"Copy\", icon: \"material:content_copy\",\n          action: \"copy:\" + item.name },\n        { name: \"Open in Browser\", icon: \"material:open_in_new\",\n          action: \"url:\" + item.url }\n    ]\n}\n```\n\nContext menu actions use the same `executeItem()` handler.\n\n## Image Tile View\n\nFor image-heavy launchers (GIF search, sticker pickers), use tile view:\n\nIn `plugin.json`:\n```json\n{\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true\n}\n```\n\nIn items:\n```javascript\n{\n    name: \"Image Title\",\n    imageUrl: \"https://example.com/image.png\",\n    comment: \"Description\",\n    action: \"copy:https://example.com/image.png\",\n    categories: [\"MyPlugin\"]\n}\n```\n\n## State Persistence\n\nFor plugins with persistent state (notes, history, favorites):\n\n```qml\nproperty var notes: []\n\nComponent.onCompleted: {\n    const saved = pluginService?.loadPluginState(pluginId, \"notes\", [])\n    if (saved) notes = saved\n}\n\nfunction addNote(text) {\n    notes.push({ text: text, timestamp: Date.now() })\n    pluginService?.savePluginState(pluginId, \"notes\", notes)\n    itemsChanged()\n}\n```\n\nUse `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.\n\n## Settings for Trigger Configuration\n\nProvide a PluginSettings component for trigger customization:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myLauncher\"\n\n    StringSetting {\n        settingKey: \"trigger\"\n        label: \"Trigger\"\n        description: \"Type this prefix to activate the launcher plugin\"\n        placeholder: \"#\"\n        defaultValue: \"#\"\n    }\n\n    ToggleSetting {\n        settingKey: \"noTrigger\"\n        label: \"Always Visible\"\n        description: \"Show items alongside regular apps (no trigger needed)\"\n        defaultValue: false\n    }\n}\n```\n\n## Complete Example\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"!\"\n\n    signal itemsChanged()\n\n    property var commands: [\n        { name: \"Lock Screen\", icon: \"material:lock\",\n          comment: \"Lock the session\", action: \"exec:loginctl lock-session\" },\n        { name: \"Screenshot\", icon: \"material:screenshot_monitor\",\n          comment: \"Take a screenshot\", action: \"exec:grim\" },\n        { name: \"File Manager\", icon: \"material:folder\",\n          comment: \"Open file manager\", action: \"exec:nautilus\" }\n    ]\n\n    function getItems(query) {\n        if (!query) return commands\n        const q = query.toLowerCase()\n        return commands.filter(c =>\n            c.name.toLowerCase().includes(q) ||\n            c.comment.toLowerCase().includes(q)\n        )\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"exec\") {\n            Quickshell.execDetached(data.split(\" \"))\n        }\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"quickCommands\", \"trigger\", \"!\")\n        }\n    }\n}\n```\n",".agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md":"# Plugin Manifest Reference (plugin.json)\n\n## Required Fields\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |\n| `name` | string | Human-readable name | Non-empty |\n| `description` | string | Short description (shown in UI) | Non-empty |\n| `version` | string | Semantic version | Pattern `^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$` |\n| `author` | string | Creator name or email | Non-empty |\n| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `dash`, `dashCard`, `composite` |\n| `capabilities` | array | Plugin capabilities | At least 1 string item |\n\nOne of `component` or `components` is required (not both):\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |\n| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |\n\n## Conditional Requirements\n\n| Condition | Required Field | Description |\n|-----------|---------------|-------------|\n| `type: \"launcher\"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |\n| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `icon` | string | Material Design icon name (displayed in plugin list UI) |\n| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |\n| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |\n| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\\|<=?\\|=\\|>\\|<)\\d+\\.\\d+\\.\\d+$` |\n| `dependencies` | array | System tool dependencies (e.g., `[\"curl\", \"jq\"]`). Registry metadata. |\n| `requires` | array | Deprecated alias for `dependencies` |\n| `permissions` | array | Required permissions |\n| `trigger` | string | Launcher trigger string (required for launcher type) |\n| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |\n\n## Permissions\n\n| Permission | Description | Enforced |\n|------------|-------------|----------|\n| `settings_read` | Read plugin configuration | No (not currently enforced) |\n| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |\n| `process` | Execute system commands | No (not currently enforced) |\n| `network` | Network access | No (not currently enforced) |\n\nIf your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.\n\n## Capabilities\n\nCapabilities are free-form strings that describe what the plugin does. Common values:\n\n- `dankbar-widget` - general bar widget\n- `control-center` - integrates with Control Center\n- `monitoring` - system/service monitoring\n- `launcher` - launcher search provider\n- `desktop-widget` - desktop background widget\n- `dash-tab` - dash tab\n- `dash-card` - dash overview card\n- `ai` - AI/LLM integration\n- `slideout` - uses slideout panel\n\n## Startup Check\n\nThe `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\n## Components (Composite Plugins)\n\nThe `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nValid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.\n\n## Dash Block\n\nPlugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.\n\n```json\n\"dash\": {\n    \"icon\": \"timer\",\n    \"title\": \"Stopwatch\",\n    \"card\": { \"title\": \"Progress\", \"icon\": \"hourglass_bottom\", \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 4, \"maxH\": 2 },\n    \"options\": [\n        { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false, \"description\": \"Hide the secondary line\" },\n        { \"key\": \"style\", \"text\": \"Style\", \"type\": \"choice\", \"def\": \"bars\", \"choices\": [{ \"value\": \"bars\", \"text\": \"Bars\" }, { \"value\": \"dots\", \"text\": \"Dots\" }] },\n        { \"key\": \"limit\", \"text\": \"Items\", \"type\": \"number\", \"def\": 5, \"min\": 1, \"max\": 20, \"step\": 1 }\n    ]\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |\n| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |\n| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |\n| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |\n| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |\n\n## Dash Examples\n\nStandalone tab and standalone card use the single file form:\n\n```json\n{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"dash\": { \"title\": \"Stopwatch\" },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\n```json\n{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"dash\": { \"card\": { \"w\": 2, \"h\": 1, \"maxW\": 4, \"maxH\": 2 } },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\nA tab plus a card, or dash surfaces plus a bar widget, use `components` with `\"type\": \"composite\"`.\n\n## Complete Example\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin demonstrating all fields\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\", \"control-center\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"dependencies\": [\"curl\", \"jq\"],\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\", \"network\"]\n}\n```\n\n## Launcher Example\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Search and execute custom actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n## JSON Schema\n\nThe complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:\n\n```bash\n# Using python\npython3 -c \"\nimport json, jsonschema\nschema = json.load(open('path/to/plugin-schema.json'))\nmanifest = json.load(open('plugin.json'))\njsonschema.validate(manifest, schema)\nprint('Valid!')\n\"\n\n# Using jq (syntax check only)\njq . plugin.json\n```\n\n## Additional Properties\n\nThe schema allows additional properties (`\"additionalProperties\": true`), so plugins can include custom fields. Common custom fields seen in production plugins:\n\n- `viewMode` - launcher display mode (`\"tile\"` for image grids)\n- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)\n",".agents/skills/dms-plugin-dev/references/popout-service-reference.md":"# PopoutService Reference\n\nThe `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.\n\n## Setup\n\nDeclare the property in your component for injection to work:\n\n```qml\nproperty var popoutService: null\n```\n\nWithout this declaration, injection fails with: `Cannot assign to non-existent property \"popoutService\"`\n\n## Popouts (DankPopout-based)\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |\n| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |\n| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |\n| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |\n| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |\n| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |\n| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |\n| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |\n\n## Modals (DankModal-based)\n\n| Modal | Show | Hide | Notes |\n|-------|------|------|-------|\n| Settings | `openSettings()` | `closeSettings()` | Full settings interface |\n| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |\n| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |\n| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |\n| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |\n| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |\n| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |\n| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |\n| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |\n\n## Slideouts\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |\n\n## Usage Examples\n\n### Simple toggle\n\n```qml\nMouseArea {\n    onClicked: popoutService?.toggleControlCenter()\n}\n```\n\n### Conditional popout\n\n```qml\nConnections {\n    target: BatteryService\n    function onPercentageChanged() {\n        if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n            popoutService?.openBattery()\n        }\n    }\n}\n```\n\n### Context menu with multiple actions\n\n```qml\nMouseArea {\n    acceptedButtons: Qt.LeftButton | Qt.RightButton\n    onClicked: (mouse) => {\n        if (mouse.button === Qt.RightButton) contextMenu.popup()\n        else popoutService?.toggleControlCenter()\n    }\n}\n\nMenu {\n    id: contextMenu\n    MenuItem { text: \"Settings\"; onClicked: popoutService?.openSettings() }\n    MenuItem { text: \"Notifications\"; onClicked: popoutService?.toggleNotificationCenter() }\n    MenuItem { text: \"Power\"; onClicked: popoutService?.openPowerMenu() }\n}\n```\n\n### Position-aware toggle (from bar pill)\n\nSome toggle functions accept position parameters for proper popout placement:\n\n```qml\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\n## Best Practices\n\n1. **Always use optional chaining** (`?.`) - the service may not be injected yet\n2. **Check feature availability** before opening feature-specific popouts:\n   ```qml\n   if (BatteryService.batteryAvailable) {\n       popoutService?.openBattery()\n   }\n   ```\n3. **Lazy loading** - first access may activate lazy loaders; this is normal\n4. **Popouts are shared** - avoid opening conflicting popouts simultaneously\n5. **User intent** - only trigger popouts from user actions or critical system events\n6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters\n\n## Injection Locations\n\nThe service is injected at these points:\n- `DMSShell.qml` - daemon plugins\n- `WidgetHost.qml` - widget plugins in left/right bar sections\n- `CenterSection.qml` - center bar widgets\n- `PluginsTab.qml` - settings components\n",".agents/skills/dms-plugin-dev/references/settings-components-reference.md":"# Settings Components Reference\n\nAll plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.\n\n## PluginSettings Wrapper\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"  // Required: must match plugin.json id\n\n    // Setting components go here\n}\n```\n\n**Important:** The plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json for the settings UI to render. Without it, users see an error.\n\n**PluginSettings provides to children:**\n- `saveValue(key, value)` - save a setting value\n- `loadValue(key, defaultValue)` - load a setting value\n- `saveState(key, value)` - save plugin state (separate file)\n- `loadState(key, defaultValue)` - load plugin state\n- `clearState()` - clear all plugin state\n- Variant management functions (for variant plugins)\n\n## StringSetting\n\nText input field.\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"        // Required: storage key\n    label: \"API Key\"            // Required: display label\n    description: \"Your API key\" // Optional: help text\n    placeholder: \"sk-...\"      // Optional: input placeholder\n    defaultValue: \"\"            // Optional: default (default: \"\")\n}\n```\n\n**Layout:** Vertical stack - label, description, input field.\n\n## ToggleSetting\n\nBoolean toggle switch.\n\n```qml\nToggleSetting {\n    settingKey: \"notifications\"    // Required: storage key\n    label: \"Enable Notifications\"  // Required: display label\n    description: \"Show alerts\"     // Optional: help text\n    defaultValue: true             // Optional: default (default: false)\n}\n```\n\n**Layout:** Horizontal - label/description on left, toggle on right.\n\n## SelectionSetting\n\nDropdown menu.\n\n```qml\nSelectionSetting {\n    settingKey: \"theme\"           // Required: storage key\n    label: \"Theme\"                // Required: display label\n    description: \"Color scheme\"   // Optional: help text\n    options: [                    // Required: array of options\n        { label: \"Dark\", value: \"dark\" },\n        { label: \"Light\", value: \"light\" },\n        { label: \"Auto\", value: \"auto\" }\n    ]\n    defaultValue: \"dark\"          // Optional: default value\n}\n```\n\nOptions can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.\n\n**Layout:** Horizontal - label/description on left, dropdown on right.\n\n**Reacting to changes:**\n```qml\nSelectionSetting {\n    settingKey: \"updateInterval\"\n    label: \"Update Interval\"\n    options: [\n        { label: \"1 minute\", value: \"60\" },\n        { label: \"5 minutes\", value: \"300\" }\n    ]\n    defaultValue: \"300\"\n    onValueChanged: (newValue) => {\n        console.log(\"Interval changed to:\", newValue)\n    }\n}\n```\n\n## SliderSetting\n\nNumeric slider with min/max.\n\n```qml\nSliderSetting {\n    settingKey: \"opacity\"      // Required: storage key\n    label: \"Opacity\"           // Required: display label\n    description: \"Background\"  // Optional: help text\n    defaultValue: 85           // Optional: default value\n    minimum: 0                 // Required: min value\n    maximum: 100               // Required: max value\n    unit: \"%\"                  // Optional: unit label shown after value\n    leftIcon: \"dark_mode\"      // Optional: Material icon on left\n    rightIcon: \"light_mode\"    // Optional: Material icon on right\n}\n```\n\n## ColorSetting\n\nColor picker.\n\n```qml\nColorSetting {\n    settingKey: \"accentColor\"      // Required: storage key\n    label: \"Accent Color\"          // Required: display label\n    description: \"Custom accent\"   // Optional: help text\n    defaultValue: \"#ff5722\"        // Optional: default hex color\n}\n```\n\nDisplays a color swatch that opens a color picker dialog.\n\n## ListSetting\n\nManage a list of items with manual add/remove. Use when you need custom UI for adding items.\n\n```qml\nListSetting {\n    id: itemList\n    settingKey: \"items\"           // Required: storage key\n    label: \"Saved Items\"          // Required: display label\n    description: \"Your items\"     // Optional: help text\n    defaultValue: []              // Optional: default array\n    delegate: Component {         // Optional: custom item display\n        StyledRect {\n            width: parent.width\n            height: 40\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                anchors.left: parent.left\n                anchors.leftMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                text: modelData.name\n                color: Theme.surfaceText\n            }\n\n            Rectangle {\n                anchors.right: parent.right\n                anchors.rightMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                width: 60\n                height: 28\n                color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                radius: Theme.cornerRadius\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: \"Remove\"\n                    color: Theme.errorText\n                    font.pixelSize: Theme.fontSizeSmall\n                    font.weight: Font.Medium\n                }\n\n                MouseArea {\n                    id: removeArea\n                    anchors.fill: parent\n                    hoverEnabled: true\n                    cursorShape: Qt.PointingHandCursor\n                    onClicked: itemList.removeItem(index)\n                }\n            }\n        }\n    }\n}\n```\n\n**Methods:**\n- `addItem(item)` - add an item to the list\n- `removeItem(index)` - remove item at index\n\n## ListSettingWithInput\n\nComplete list management with built-in form. Best for collecting structured data.\n\n```qml\nListSettingWithInput {\n    settingKey: \"locations\"        // Required: storage key\n    label: \"Locations\"             // Required: display label\n    description: \"Track zones\"     // Optional: help text\n    defaultValue: []               // Optional: default array\n    fields: [                      // Required: field definitions\n        {\n            id: \"name\",            // Required: key in saved object\n            label: \"Name\",         // Required: column header\n            placeholder: \"Home\",   // Optional: input placeholder\n            width: 150,            // Optional: column width (default: 200)\n            required: true         // Optional: must have value to add\n        },\n        {\n            id: \"timezone\",\n            label: \"Timezone\",\n            placeholder: \"America/New_York\",\n            width: 200,\n            required: true\n        }\n    ]\n}\n```\n\nAutomatically generates: column headers, input fields, add button with validation, list display, remove buttons.\n\n## Mixing Custom UI with Settings\n\nYou can interleave regular QML elements with setting components:\n\n```qml\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"name\"\n        label: \"Display Name\"\n    }\n\n    StyledText {\n        width: parent.width\n        text: \"Advanced Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n        topPadding: Theme.spacingL\n    }\n\n    ToggleSetting {\n        settingKey: \"debug\"\n        label: \"Debug Mode\"\n        defaultValue: false\n    }\n}\n```\n\n## Default Values\n\nDefine sensible defaults in every setting component. The default is used when no saved value exists:\n\n```qml\nStringSetting { settingKey: \"text\"; defaultValue: \"Hello\" }\nToggleSetting { settingKey: \"enabled\"; defaultValue: true }\nSelectionSetting { settingKey: \"mode\"; defaultValue: \"auto\" }\nSliderSetting { settingKey: \"opacity\"; defaultValue: 85 }\nColorSetting { settingKey: \"color\"; defaultValue: \"#ff5722\" }\nListSetting { settingKey: \"items\"; defaultValue: [] }\nListSettingWithInput { settingKey: \"data\"; defaultValue: [] }\n```\n",".agents/skills/dms-plugin-dev/references/theme-reference.md":"# Theme Property Reference\n\nAll theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.\n\n## Font Sizes\n\n```qml\nTheme.fontSizeSmall     // 12px (scaled by SettingsData.fontScale)\nTheme.fontSizeMedium    // 14px (scaled)\nTheme.fontSizeLarge     // 16px (scaled)\nTheme.fontSizeXLarge    // 20px (scaled)\n```\n\n## Icon Sizes\n\n```qml\nTheme.iconSizeSmall     // 16px\nTheme.iconSize          // 24px (default)\nTheme.iconSizeLarge     // 32px\n```\n\n## Spacing\n\n```qml\nTheme.spacingXS         // Extra small\nTheme.spacingS          // Small\nTheme.spacingM          // Medium\nTheme.spacingL          // Large\nTheme.spacingXL         // Extra large\n```\n\n## Border Radius\n\n```qml\nTheme.cornerRadius      // Standard\nTheme.cornerRadiusSmall // Smaller\nTheme.cornerRadiusLarge // Larger\n```\n\n## Surface Colors\n\n```qml\nTheme.surface\nTheme.surfaceContainerLow\nTheme.surfaceContainer\nTheme.surfaceContainerHigh\nTheme.surfaceContainerHighest\n```\n\n## Text Colors\n\n```qml\nTheme.onSurface          // Primary text on surface\nTheme.onSurfaceVariant   // Secondary text on surface\nTheme.surfaceText        // Alias for primary surface text\nTheme.surfaceVariantText // Alias for secondary surface text\nTheme.outline            // Border/divider color\n```\n\n## Semantic Colors\n\n```qml\nTheme.primary\nTheme.onPrimary\nTheme.secondary\nTheme.onSecondary\nTheme.error\nTheme.errorHover\nTheme.errorText\nTheme.warning\nTheme.success\n```\n\n## Special Functions\n\n```qml\nTheme.popupBackground()  // Popup background with proper opacity\n```\n\n## Common Widget Patterns\n\n### Icon with Text\n\n```qml\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nRow {\n    spacing: Theme.spacingS\n\n    DIcon {\n        name: \"icon_name\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.iconSize\n    }\n\n    StyledText {\n        text: \"Label\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.fontSizeMedium\n    }\n}\n```\n\n### Container with Border\n\n```qml\nRectangle {\n    color: Theme.surfaceContainerHigh\n    radius: Theme.cornerRadius\n    border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)\n    border.width: 1\n}\n```\n\n### Hover Effect\n\n```qml\nRectangle {\n    id: container\n    color: Theme.surfaceContainerHigh\n\n    MouseArea {\n        anchors.fill: parent\n        hoverEnabled: true\n        onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        onExited: container.color = Theme.surfaceContainerHigh\n    }\n}\n```\n\n### Clickable Pill\n\n```qml\nStyledRect {\n    width: content.implicitWidth + Theme.spacingM * 2\n    height: parent.widgetThickness\n    radius: Theme.cornerRadius\n    color: mouseArea.containsMouse\n        ? Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        : Theme.surfaceContainerHigh\n\n    MouseArea {\n        id: mouseArea\n        anchors.fill: parent\n        hoverEnabled: true\n        cursorShape: Qt.PointingHandCursor\n    }\n\n    Row {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingS\n\n        DIcon {\n            name: \"star\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.iconSize\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        StyledText {\n            text: \"Label\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.fontSizeMedium\n            anchors.verticalCenter: parent.verticalCenter\n        }\n    }\n}\n```\n\n## Common Mistakes\n\n**Wrong property names** (these do NOT exist):\n```qml\nTheme.fontSizeS        // Use Theme.fontSizeSmall\nTheme.iconSizeS        // Use Theme.iconSizeSmall\nTheme.spacingSmall     // Use Theme.spacingS\nTheme.borderRadius     // Use Theme.cornerRadius\n```\n\n**Hardcoded values** (do NOT do this):\n```qml\ncolor: \"#1e1e1e\"       // Use Theme.surfaceContainerHigh\ncolor: \"white\"         // Use Theme.surfaceText\nfont.pixelSize: 14     // Use Theme.fontSizeMedium\n```\n\n## Available Widgets from qs.Widgets\n\n| Widget | Description |\n|--------|-------------|\n| `StyledText` | Themed text with proper color defaults |\n| `StyledRect` | Themed rectangle |\n| `DIcon` | Material Symbols icon renderer |\n| `DankNFIcon` | Nerd Font icon renderer |\n| `DButton` | Themed button |\n| `DToggle` | Toggle switch |\n| `DTextField` | Text input field |\n| `DSlider` | Slider control |\n| `DDropdown` | Dropdown menu |\n| `DGridView` | Grid layout view |\n| `DListView` | List layout view |\n| `DFlickable` | Scrollable container |\n| `DTabBar` | Tab bar navigation |\n| `DankCollapsibleSection` | Collapsible content section |\n| `DankTooltip` | Hover tooltip |\n| `DankNumberStepper` | Number +/- control |\n| `DankFilterChips` | Filter chip row |\n| `CachingImage` | Image with disk cache |\n| `NumericText` | Fixed-width numeric display |\n\n## Checking All Properties\n\n```bash\ngrep \"property\" Common/Theme.qml\n```\n",".agents/skills/dms-plugin-dev/references/widget-plugin-guide.md":"# Widget Plugin Guide\n\nWidgets display pills in DankBar and in docks (same component, no extra code), optionally open popouts, and can integrate with the Control Center.\n\n## Base Component\n\nWidgets use `PluginComponent` from `qs.Modules.Plugins`.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n    popoutContent: Component { /* ... */ }\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n## Injected Properties\n\nThese are automatically set by the plugin host:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `axis` | object | Bar axis info (horizontal/vertical) |\n| `section` | string | Bar section: `\"left\"`, `\"center\"`, or `\"right\"` |\n| `parentScreen` | object | Screen reference for multi-monitor |\n| `widgetThickness` | real | Widget size perpendicular to bar edge |\n| `barThickness` | real | Bar thickness parallel to edge |\n| `pluginId` | string | This plugin's ID |\n| `pluginService` | object | PluginService reference |\n| `pluginData` | object | Reactive plugin settings data |\n| `surfaceContext` | object | The host surface: `kind` (`\"bar\"` or `\"dock\"`), `id`, `screen`, `isVertical`, `thickness`, `live`, `editMode`, `inlineExpansion` |\n| `widgetInstanceId` | string | This occurrence; the same widget can appear on several bars and docks |\n\n## Bar Pills\n\nDefine `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).\n\n### Horizontal Bar Pill\n\n```qml\nhorizontalBarPill: Component {\n    StyledRect {\n        width: content.implicitWidth + Theme.spacingM * 2\n        height: parent.widgetThickness\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Row {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSize\n                anchors.verticalCenter: parent.verticalCenter\n            }\n\n            StyledText {\n                text: \"Label\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n                anchors.verticalCenter: parent.verticalCenter\n            }\n        }\n    }\n}\n```\n\n### Vertical Bar Pill\n\n```qml\nverticalBarPill: Component {\n    StyledRect {\n        width: parent.widgetThickness\n        height: content.implicitHeight + Theme.spacingM * 2\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Column {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSizeSmall\n            }\n        }\n    }\n}\n```\n\n**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.\n\n## Popout Content\n\nOpen a popout window when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                    font.pixelSize: Theme.fontSizeMedium\n                }\n            }\n        }\n    }\n}\n```\n\n**PopoutComponent properties:**\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `headerText` | string | `\"\"` | Main header (bold, large). Hidden if empty. |\n| `detailsText` | string | `\"\"` | Subtitle below header. Hidden if empty. |\n| `closePopout` | function | (injected) | Call to close the popout programmatically. |\n| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |\n| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |\n\n**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Custom Click Actions\n\nOverride the default popout behavior:\n\n```qml\nPluginComponent {\n    // Simple no-args handler\n    pillClickAction: () => {\n        popoutService?.toggleControlCenter()\n    }\n\n    // With position params (x, y, width, section, screen)\n    pillClickAction: (x, y, width, section, screen) => {\n        popoutService?.toggleControlCenter(x, y, width, section, screen)\n    }\n\n    pillRightClickAction: () => {\n        popoutService?.openSettings()\n    }\n}\n```\n\n## Control Center Integration\n\nAdd CC properties to show your widget in the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"Feature Name\"\n    ccWidgetSecondaryText: isActive ? \"Active\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n}\n```\n\n**CC properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `ccWidgetIcon` | string | Material icon name |\n| `ccWidgetPrimaryText` | string | Main label |\n| `ccWidgetSecondaryText` | string | Subtitle / status text |\n| `ccWidgetIsActive` | bool | Active state (changes styling) |\n\n**CC signals:**\n\n| Signal | When fired |\n|--------|-----------|\n| `ccWidgetToggled()` | Icon area clicked |\n| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |\n\n**CC sizing rules:**\n- 25% width - SmallToggleButton (icon only)\n- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)\n- Users can resize in CC edit mode\n\n### Detail Content (CompoundPill)\n\nAdd an expandable panel below the CC widget:\n\n```qml\nccDetailContent: Component {\n    Rectangle {\n        implicitHeight: 200\n        color: Theme.surfaceContainerHigh\n        radius: Theme.cornerRadius\n\n        Column {\n            anchors.fill: parent\n            anchors.margins: Theme.spacingM\n            spacing: Theme.spacingS\n\n            // Detail UI here\n        }\n    }\n}\n```\n\n## Visibility Control\n\nConditionally show/hide the bar pill:\n\n```qml\nPluginComponent {\n    visibilityCommand: \"pgrep -x myapp\"\n    visibilityInterval: 5  // seconds between checks\n}\n```\n\n**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.\n\n## Docks\n\nUsers add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.\n\n- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.\n- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.\n- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.\n- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.\n\nOptional attached panel: set `attachedContent: Component { ... }`. When the dock's \"Open widgets\" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.\n\n```qml\nPluginComponent {\n    id: root\n\n    attachedContent: Component {\n        Column {\n            padding: Theme.spacingM\n            spacing: Theme.spacingM\n\n            DButton {\n                text: I18n.trFor(\"myPlugin\", \"Close\")\n                onClicked: root.surfaceContext.dismissExpansion()\n            }\n        }\n    }\n}\n```\n\nThe panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.\n\n## Popout Namespace\n\nFor plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:\n\n```qml\nPluginComponent {\n    layerNamespacePlugin: \"my-plugin\"\n}\n```\n\nThe shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.\n\n## Reading Plugin Data\n\nAccess saved settings reactively via the injected `pluginData`:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId)\n                displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        }\n    }\n}\n```\n\n## Complete Example\n\nBased on the ExampleEmojiPlugin pattern:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property var emojis: [\"star\", \"heart\", \"smile\"]\n    property int currentIndex: 0\n\n    Timer {\n        interval: 2000\n        running: true\n        repeat: true\n        onTriggered: currentIndex = (currentIndex + 1) % emojis.length\n    }\n\n    popoutWidth: 350\n    popoutHeight: 400\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeLarge\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"Emoji Picker\"\n\n            DGridView {\n                width: parent.width\n                height: 300\n                cellWidth: 50\n                cellHeight: 50\n                model: root.emojis\n\n                delegate: Rectangle {\n                    width: 48\n                    height: 48\n                    radius: Theme.cornerRadius\n                    color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : \"transparent\"\n\n                    Text {\n                        anchors.centerIn: parent\n                        text: modelData\n                        font.pixelSize: 24\n                    }\n\n                    MouseArea {\n                        id: mouseArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        onClicked: {\n                            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n                            ToastService?.showInfo(\"Copied \" + modelData)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n","AGENTS.md":"## What is this?\n\nDMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.\n\n## Repo Structure\n\n- core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.\n- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/\n- dux-qml-common/ - submodule of shared QML widgets\n- docs/, distro/ - IPC and theme docs, packaging\n\n## Rules\n\n- Keep it simple.\n- Do not edit code in response to a question.\n- Never commit, push, or open a PR unless asked.\n- Point out glaring issues we missed.\n- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.\n- `core` and `quickshell` share the socket protocol. Changing one means checking the other.\n- QML uses Theme tokens, never hardcoded colors, spacing, or constants.\n- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.\n- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.\n- Guard clauses, early returns.\n- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.\n- Do not edit generated code. Mocks come from mockery.\n- No library for a simple problem. Use a modern one for a complex, common problem.\n\n## Tests\n\n- A test must fail on the broken code. Otherwise do not write it.\n- No useless tests: \"main.go contains func main()\", defaults, \"it instantiates\", behavior the change didn't touch.\n- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.\n- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.\n\n## Connections vs bound properties\n\n- In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.\n- Source in the same file: put the handler on it.\n- Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.\n- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.\n- A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.\n- Singletons under Services/ and Common/ keep their `Connections`.\n\n## Commit Messages\n\n`area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.\n","quickshell/PLUGINS/AttachedPanelExample/README.md":"# Attached panel example\n\nA normal widget plugin that also sets `attachedContent`. Enable it in Settings → Plugins\nand add it to a bar, a dock, or both.\n\n- On a bar, a click opens the regular popout.\n- On a dock, what a click does is the dock's choice. With Settings → Dock → Apps & widgets\n  → Open widgets set to Inline, the dock grows and shows the attached panel next to the\n  apps. With Popout (the default) it opens the same popout the bar would.\n\nEscape or Close dismisses the panel and hands focus back to the widget. Opening another\nattached widget replaces the panel. No background process, no timer.\n\nSee [the surface widget contract](../SURFACE_WIDGETS.md).\n","quickshell/PLUGINS/AttachedPanelExample/plugin.json":"{\n    \"id\": \"attachedPanelExample\",\n    \"name\": \"Attached Panel Example\",\n    \"description\": \"A widget with an optional attached dock panel and a normal bar popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS contributors\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./AttachedPanelExample.qml\",\n    \"icon\": \"sticky_note_2\",\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/ColorDemoPlugin/plugin.json":"{\n    \"id\": \"colorDemo\",\n    \"name\": \"Color Demo\",\n    \"description\": \"Demonstrates color picker plugin setting\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"icon\": \"palette\",\n    \"type\": \"widget\",\n    \"component\": \"./ColorDemoWidget.qml\",\n    \"settings\": \"./ColorDemoSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/ControlCenterDetailExample/plugin.json":"{\n    \"id\": \"controlCenterDetailExample\",\n    \"name\": \"CC Detail Example\",\n    \"description\": \"Example plugin with Control Center detail dropdown\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./DetailExampleWidget.qml\",\n    \"icon\": \"settings\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/ControlCenterExample/plugin.json":"{\n    \"id\": \"controlCenterExample\",\n    \"name\": \"CC Toggle Example\",\n    \"description\": \"Example plugin with Control Center toggle widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./ControlCenterExampleWidget.qml\",\n    \"icon\": \"toggle_on\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/DashCardExample/README.md":"# Progress Example\n\nA standalone overview card: how much of today, this week, this month or this year has passed. The manifest uses the single file form, `\"type\": \"dashCard\"` plus `\"component\"`.\n\nWhat it shows:\n\n- `DashCardComponent` as the root. It has no tab, so it is not clickable and keyboard navigation skips it.\n- A layout for every size in its range (1x1 up to 4x2). `wide` and `tall` come from the card's pixel size against `DashMetrics.gridRowUnit`.\n- `live` gating. `SystemClock` is disabled while the dash is hidden.\n- `tone` and the card colors (`accentColor`, `contentColor`, `mutedColor`, `chipColor`), so the card follows the Surface, Primary, Secondary and Tertiary tones like the built-in cards.\n- `dash.options`: a choice for the period, a toggle for the time left and a choice for the tone. They show up in the card's options sheet in edit mode and under Settings → Dashboard.\n\nAdd it from the dash: open the overview, hover the selected page icon, click the pencil, then Add widget.\n","quickshell/PLUGINS/DashCardExample/plugin.json":"{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card showing how much of the day, week, month or year has passed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"icon\": \"hourglass_bottom\",\n    \"dash\": {\n        \"card\": {\n            \"title\": \"Progress\",\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 4,\n            \"maxH\": 2\n        },\n        \"options\": [\n            {\n                \"key\": \"period\",\n                \"text\": \"Period\",\n                \"type\": \"choice\",\n                \"def\": \"day\",\n                \"choices\": [\n                    { \"value\": \"day\", \"text\": \"Day\" },\n                    { \"value\": \"week\", \"text\": \"Week\" },\n                    { \"value\": \"month\", \"text\": \"Month\" },\n                    { \"value\": \"year\", \"text\": \"Year\" }\n                ]\n            },\n            {\n                \"key\": \"remaining\",\n                \"text\": \"Show time left\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" },\n                    { \"value\": \"secondary\", \"text\": \"Secondary\" },\n                    { \"value\": \"tertiary\", \"text\": \"Tertiary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/DashCounterExample/README.md":"# Counter Example\n\nA composite plugin with three surfaces that share one counter:\n\n- `components.dash` (`CounterTab.qml`): a dash tab built on `DashTabComponent`. It hosts a `DashWidgetGrid`, so the dash Edit, Add widget, Reset and Clear All actions work on it. The Reset widget starts in the Add menu (`enabled: false`). `+` and `-` change the count.\n- `components.dashCard` (`CounterCard.qml`): an overview card built on `DashCardComponent`. Clicking it opens the tab. Left and Right change the count while it has focus.\n- `components.widget` (`CounterWidget.qml`): a bar pill. Left click toggles the dash on the counter tab, right click adds one. The same file works in a dock with no extra code.\n\nThe count is stored with `setData(\"count\", value)` / `savePluginData` and read back through `pluginData.count`. Every surface reloads `pluginData` on `pluginDataChanged`, so they stay in sync.\n\nOpen the tab from a terminal with `dms ipc call dash open plugin_dashCounterExample`. If the tab is hidden from the tab bar it opens as a detail page with a back button.\n","quickshell/PLUGINS/DashCounterExample/plugin.json":"{\n    \"id\": \"dashCounterExample\",\n    \"name\": \"Counter Example\",\n    \"description\": \"One counter shared by a dash tab, an overview card and a bar or dock widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"dash-tab\", \"dash-card\", \"dankbar-widget\"],\n    \"icon\": \"counter_1\",\n    \"components\": {\n        \"dash\": \"./CounterTab.qml\",\n        \"dashCard\": \"./CounterCard.qml\",\n        \"widget\": \"./CounterWidget.qml\"\n    },\n    \"dash\": {\n        \"title\": \"Counter\",\n        \"card\": {\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 3,\n            \"maxH\": 2\n        }\n    },\n    \"requires_dms\": \">=1.7.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/DashTabExample/README.md":"# Stopwatch Example\n\nA standalone dash tab. The manifest uses the single file form, `\"type\": \"dash\"` plus `\"component\"`, the same way a bar widget uses `\"type\": \"widget\"`.\n\nWhat it shows:\n\n- `DashTabComponent` as the root, sized with `implicitHeight` so the dash grows when many laps are shown.\n- `live` gating. The display timer only runs while the tab is on screen and the stopwatch is running. Time is computed from stored timestamps, so the stopwatch keeps counting while the dash is closed or another tab is open.\n- The plugin state API (`loadPluginState` / `savePluginState`) for runtime data. Nothing goes into settings, so no permissions are needed.\n- `focusTarget` and `restoreFocus()`. Opening the tab focuses Start without a focus ring, so Space starts it right away.\n- `handleKeyEvent`: Space starts or pauses, L records a lap, R resets.\n- `menuActions`: Reset in the tab's edit-mode controls.\n- `dash.options`: a toggle for hundredths and a number for how many laps to list, read as `options.hundredths` and `options.laps`.\n\nOpen it with `dms ipc call dash open plugin_dashTabExample`.\n","quickshell/PLUGINS/DashTabExample/plugin.json":"{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab: a stopwatch with laps that keeps time while the dash is closed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"icon\": \"timer\",\n    \"dash\": {\n        \"title\": \"Stopwatch\",\n        \"options\": [\n            {\n                \"key\": \"hundredths\",\n                \"text\": \"Show hundredths\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"laps\",\n                \"text\": \"Laps shown\",\n                \"type\": \"number\",\n                \"def\": 5,\n                \"min\": 0,\n                \"max\": 10\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","quickshell/PLUGINS/ExampleCompositePlugin/README.md":"# Composite Example\n\nA single plugin that provides **all three surfaces at once** by combining three of\nthe standalone example plugins:\n\n| Surface | Source example | File |\n|---------|----------------|------|\n| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |\n| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |\n| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |\n\nIt demonstrates the `components` manifest map, where each surface points at its own\nQML file:\n\n```json\n\"type\": \"composite\",\n\"components\": {\n    \"daemon\":  \"./CompositeDaemon.qml\",\n    \"widget\":  \"./CompositeBarWidget.qml\",\n    \"desktop\": \"./CompositeDesktopWidget.qml\"\n}\n```\n\nAll surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings\nnamespace (`exampleComposite`), so `pluginData` is the same for every surface.\n\n## Surfaces\n\n- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script\n  on change. Also registers an `IpcHandler` (`target: \"compositeExample\"`) exposing a\n  `runHook` call, so you can trigger the hook over IPC.\n- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout\n  that copies to the clipboard.\n- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.\n\n## Usage\n\n1. Copy this directory into `$CONFIGPATH/dms/plugins/`.\n2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.\n   (Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they\n   do not auto-load, because they also carry a daemon.)\n3. Add the bar widget via Settings → Appearance → DankBar Layout.\n4. Place the desktop clock via Settings → Desktop Widgets.\n\n## Notes\n\n- The daemon surface is instantiated once and lives for as long as the plugin is\n  enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.\n- Cross-surface runtime state (not needed here) is best shared via\n  `PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each\n  surface is a separate object.\n- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only\n  understood by DMS 1.5.0 and later.\n","quickshell/PLUGINS/ExampleCompositePlugin/plugin.json":"{\n    \"id\": \"exampleComposite\",\n    \"name\": \"Composite Example\",\n    \"description\": \"One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\", \"clipboard\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./CompositeDaemon.qml\",\n        \"widget\": \"./CompositeBarWidget.qml\",\n        \"desktop\": \"./CompositeDesktopWidget.qml\"\n    },\n    \"settings\": \"./CompositeSettings.qml\",\n    \"requires_dms\": \">=1.5.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleDesktopClock/plugin.json":"{\n    \"id\": \"exampleDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"An example desktop widget displaying an analog clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\", \"clock\"],\n    \"component\": \"./DesktopClock.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./DesktopClockSettings.qml\",\n    \"requires_dms\": \">=1.2.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleEmojiPlugin/README.md":"# Emoji Cycler Plugin\n\nAn example dms plugin that displays cycling emojis in your bar with an emoji picker popout.\n\n## Features\n\n- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar\n- **Emoji Picker**: Click the widget to open a grid of 120+ emojis\n- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard\n- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown\n\n## Installation\n\n1. Copy this directory to `~/.config/dms/plugins/ExampleEmojiPlugin`\n2. Open DMS Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Emoji Cycler\"\n5. Add `exampleEmojiPlugin` to your DankBar widget list\n\n## Settings\n\n### Emoji Set\nChoose from different emoji collections:\n- **Happy & Sad**: Mix of emotional faces\n- **Hearts**: Various colored hearts\n- **Hand Gestures**: Thumbs up, peace signs, etc.\n- **All Mixed**: A bit of everything\n\n### Cycle Speed\nControl how fast emojis rotate (500ms - 10000ms)\n\n### Max Bar Emojis\nHow many emojis to display at once (1-8)\n\n## Usage\n\n**In the bar**: Watch emojis cycle through automatically\n**Click the widget**: Opens emoji picker with 120+ emojis\n**Click any emoji**: Copies it to clipboard and shows toast\n\n## Example Code Highlights\n\nThis plugin demonstrates:\n- Using `PluginComponent` for bar integration\n- `SelectionSetting`, `SliderSetting` for configuration\n- Plugin-local translations (`translations/` + `I18n.trFor`)\n- Timer-based animation\n- Popout content with grid layout\n- External command execution (`Quickshell.execDetached`)\n- Toast notifications (`ToastService.show`)\n- Dynamic settings loading/saving\n\nPerfect template for creating your own DMS plugins!\n","quickshell/PLUGINS/ExampleEmojiPlugin/plugin.json":"{\n    \"id\": \"exampleEmojiPlugin\",\n    \"name\": \"Emoji Cycler\",\n    \"description\": \"Display cycling emojis in your bar with a handy emoji picker popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"emoji-search\", \"clipboard\", \"dankbar-widget\"],\n    \"component\": \"./EmojiWidget.qml\",\n    \"icon\": \"mood\",\n    \"settings\": \"./EmojiSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/ExampleStartupCheck/plugin.json":"{\n    \"id\": \"exampleStartupCheck\",\n    \"name\": \"Startup Check Example\",\n    \"description\": \"Demonstrates startupCheck - blocks activation when the 'boregard' dependency is missing\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./StartupCheckWidget.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"icon\": \"verified_user\",\n    \"dependencies\": [\"boregard\"],\n    \"permissions\": [\"process\"]\n}\n","quickshell/PLUGINS/ExampleWithVariants/README.md":"# Example with Variants Plugin\n\nThis plugin demonstrates the dynamic variant system for Dux Shell plugins.\n\n## What are Variants?\n\nVariants allow a single plugin to create multiple widget instances with different configurations. Each variant appears as a separate widget option in the Bar Settings \"Add Widget\" menu.\n\n## How It Works\n\n### Plugin Architecture\n\n1. **Single Component**: The plugin defines one widget component (`VariantWidget.qml`)\n2. **Variant Data**: Each variant stores its configuration in plugin settings\n3. **Dynamic Creation**: Variants are created through the plugin's settings UI\n4. **Widget ID Format**: Variants use the format `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Widget Properties\n\nEach variant widget receives:\n- `pluginService`: Reference to PluginService\n- `pluginId`: The base plugin ID (e.g., \"exampleVariants\")\n- `variantId`: The specific variant ID (e.g., \"variant_1234567890\")\n- `variantData`: The variant's configuration object\n\n### Variant Configuration\n\nThis example stores:\n```javascript\n{\n    id: \"variant_1234567890\",\n    name: \"My Variant\",\n    text: \"Display Text\",\n    icon: \"star\",\n    color: \"#FF5722\"\n}\n```\n\n## Creating Your Own Variant Plugin\n\n### 1. Widget Component\n\nCreate a widget that accepts variant data:\n\n```qml\nRectangle {\n    property var pluginService: null\n    property string pluginId: \"\"\n    property string variantId: \"\"\n    property var variantData: null\n\n    // Use variantData for configuration\n    property string displayText: variantData?.text || \"Default\"\n}\n```\n\n### 2. Settings Component\n\nProvide UI to manage variants:\n\n```qml\nFocusScope {\n    property var pluginService: null\n\n    // Create variant\n    pluginService.createPluginVariant(pluginId, variantName, variantConfig)\n\n    // Remove variant\n    pluginService.removePluginVariant(pluginId, variantId)\n\n    // Update variant\n    pluginService.updatePluginVariant(pluginId, variantId, variantConfig)\n\n    // Get all variants\n    var variants = pluginService.getPluginVariants(pluginId)\n}\n```\n\n### 3. Plugin Manifest\n\nStandard plugin manifest - no special configuration needed:\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin\",\n    \"component\": \"./Widget.qml\",\n    \"settings\": \"./Settings.qml\"\n}\n```\n\n## Use Cases\n\n- **Script Runner**: Multiple variants running different scripts\n- **System Monitors**: Different monitoring targets (CPU, GPU, Network)\n- **Quick Launchers**: Different apps or commands per variant\n- **Custom Indicators**: Different data sources or APIs\n- **Time Zones**: Multiple clocks for different time zones\n\n## API Reference\n\n### PluginService Functions\n\n- `getPluginVariants(pluginId)`: Get all variants for a plugin\n- `getAllPluginVariants()`: Get all variants across all plugins\n- `createPluginVariant(pluginId, name, config)`: Create new variant\n- `removePluginVariant(pluginId, variantId)`: Delete variant\n- `updatePluginVariant(pluginId, variantId, config)`: Update variant\n- `getPluginVariantData(pluginId, variantId)`: Get specific variant data\n\n### Widget Properties\n\n- `pluginId`: Base plugin identifier\n- `variantId`: Variant identifier (null if no variant)\n- `variantData`: Variant configuration object\n- `pluginService`: PluginService reference\n","quickshell/PLUGINS/ExampleWithVariants/plugin.json":"{\n    \"id\": \"exampleVariants\",\n    \"name\": \"Example with Variants\",\n    \"description\": \"Demonstrates dynamic variant creation for plugins\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"multiple-usecases\", \"variants\"],\n    \"component\": \"./VariantWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./VariantSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/LauncherExample/README.md":"# LauncherExample Plugin\n\nA demonstration plugin that showcases the DMS launcher plugin system capabilities.\n\n## Purpose\n\nThis plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:\n\n- **Plugin Structure**: Proper manifest, launcher, and settings components\n- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)\n- **Item Management**: Providing searchable items to the launcher\n- **Action Execution**: Handling different types of actions (toast, copy, script)\n- **Settings Integration**: Configurable plugin settings with persistence\n\n## Features\n\n### Example Items\n- **Test Items 1-3**: Demonstrate toast notifications\n- **Copy Action**: Shows clipboard integration\n- **Script Action**: Demonstrates command execution\n\n### Trigger System\n- **Default Trigger**: `#` (configurable in settings)\n- **Empty Trigger Option**: Items can always be visible without needing a trigger\n- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)\n- **Search**: Type `# test` to search within plugin items\n\n### Action Types\n- `toast:message` - Shows toast notification\n- `copy:text` - Copies text to clipboard\n- `script:command` - Executes shell command (demo only)\n\n## File Structure\n\n```\nPLUGINS/LauncherExample/\n├── plugin.json                    # Plugin manifest\n├── LauncherExampleLauncher.qml    # Main launcher component\n├── LauncherExampleSettings.qml    # Settings interface\n└── README.md                      # This documentation\n```\n\n## Installation\n\n1. **Plugin Directory**: Copy to `~/.config/dms/plugins/LauncherExample`\n2. **Enable Plugin**: Settings → Plugins → Enable \"LauncherExample\"\n3. **Configure**: Set custom trigger in plugin settings if desired\n\n## Usage\n\n### With Trigger (Default)\n1. Open launcher (Ctrl+Space or launcher button)\n2. Type `#` to activate plugin trigger\n3. Browse available items or add search terms\n4. Press Enter to execute selected item\n\n### Without Trigger (Empty Trigger Mode)\n1. Enable \"No trigger (always show)\" in plugin settings\n2. Open launcher - plugin items are always visible\n3. Search works normally with plugin items included\n4. Press Enter to execute selected item\n\n### Search Examples\n- `#` - Show all plugin items (with trigger enabled)\n- `# test` - Show items matching \"test\"\n- `# copy` - Show items matching \"copy\"\n- `test` - Show all items matching \"test\" (with empty trigger enabled)\n\n## Developer Guide\n\n### Plugin Contract\n\n**Launcher Component Requirements**:\n```qml\n// Required properties\nproperty var pluginService: null\nproperty string trigger: \"#\"\n\n// Required signals\nsignal itemsChanged()\n\n// Required functions\nfunction getItems(query): array\nfunction executeItem(item): void\n\n// Optional functions (for Shift+Enter paste support)\nfunction getPasteText(item): string|null\nfunction getPasteArgs(item): array|null\n```\n\n**Item Structure**:\n```javascript\n{\n    name: \"Item Name\",           // Display name\n    icon: \"icon_name\",           // Icon (optional, see Icon Types below)\n    comment: \"Description\",      // Subtitle text\n    action: \"type:data\",         // Action to execute\n    categories: [\"PluginName\"]   // Category array\n}\n```\n\n**Icon Types**:\n\nThe `icon` field supports four formats:\n\n1. **Material Design Icons** - Use `material:` prefix:\n   ```javascript\n   icon: \"material:lightbulb\"  // Material Symbols Rounded font\n   ```\n   Examples: `material:star`, `material:favorite`, `material:settings`\n\n2. **Unicode/Emoji Icons** - Use `unicode:` prefix:\n   ```javascript\n   icon: \"unicode:🚀\"  // Unicode character or emoji\n   ```\n   Display any Unicode character or emoji as the icon. Examples: `unicode:😀`, `unicode:⚡`, `unicode:🎨`\n\n3. **Desktop Theme Icons** - Use icon name directly:\n   ```javascript\n   icon: \"firefox\"  // Uses system icon theme\n   ```\n   Examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n4. **No Icon** - Omit the `icon` field entirely:\n   ```javascript\n   {\n       name: \"😀  Grinning Face\",\n       // No icon field\n       comment: \"Copy emoji\",\n       action: \"copy:😀\",\n       categories: [\"MyPlugin\"]\n   }\n   ```\n   When icon is omitted, the launcher hides the icon area and displays only text\n\n**Action Format**: `type:data` where:\n- `type` - Action handler (toast, copy, script, etc.)\n- `data` - Action-specific data\n\n### Settings Integration\n```qml\n// Save setting\npluginService.savePluginData(\"pluginId\", \"key\", value)\n\n// Load setting\npluginService.loadPluginData(\"pluginId\", \"key\", defaultValue)\n```\n\n### Trigger Configuration\n\nThe trigger can be configured in two ways:\n\n1. **Empty Trigger** (No Trigger Mode):\n   - Check \"No trigger (always show)\" in settings\n   - Saves `trigger: \"\"` and `noTrigger: true`\n   - Items always appear in launcher alongside regular apps\n\n2. **Custom Trigger**:\n   - Enter any string (e.g., `#`, `!`, `@`, `!ex`)\n   - Uncheck \"No trigger\" checkbox\n   - Items only appear when trigger is typed\n\n### Manifest Structure\n```json\n{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nNote: The `trigger` field in the manifest is optional and serves as the default trigger value.\n\n## Extending This Plugin\n\n### Adding New Items\n```qml\nfunction getItems(query) {\n    return [\n        {\n            name: \"My Item\",\n            icon: \"custom_icon\",\n            comment: \"Does something cool\",\n            action: \"custom:action_data\",\n            categories: [\"LauncherExample\"]\n        }\n    ]\n}\n```\n\n### Adding New Actions\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"custom\":\n            handleCustomAction(actionData)\n            break\n    }\n}\n```\n\n### Custom Trigger Logic\n```qml\nComponent.onCompleted: {\n    if (pluginService) {\n        trigger = pluginService.loadPluginData(\"launcherExample\", \"trigger\", \"#\")\n    }\n}\n\nonTriggerChanged: {\n    if (pluginService) {\n        pluginService.savePluginData(\"launcherExample\", \"trigger\", trigger)\n    }\n}\n```\n\n## Best Practices\n\n1. **Unique Triggers**: Choose triggers that don't conflict with other plugins\n2. **Clear Descriptions**: Write helpful item comments\n3. **Error Handling**: Gracefully handle action failures\n4. **Performance**: Return results quickly in getItems()\n5. **Cleanup**: Destroy temporary objects in executeItem()\n6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode\n\n## Testing\n\nTest the plugin by:\n1. Installing and enabling in DMS\n2. Testing with trigger enabled\n3. Testing with empty trigger (no trigger mode)\n4. Trying each action type\n5. Testing search functionality\n6. Verifying settings persistence\n\nThis plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!\n","quickshell/PLUGINS/LauncherExample/plugin.json":"{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"description\": \"Example launcher plugin demonstrating the launcher plugin system\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"extension\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\", \"command-execution\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"trigger\": \"#\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/LauncherImageExample/README.md":"# LauncherImageExample\n\nExample launcher plugin demonstrating tile mode with URL-based images.\n\n## Features\n\n- **Tile Mode**: Uses `viewMode: \"tile\"` in plugin.json to display results as image tiles\n- **Enforced View Mode**: Uses `viewModeEnforced: true` to lock the view to tile mode (users cannot change it)\n- **URL Images**: Demonstrates using `imageUrl` property for remote images\n\n## Usage\n\n1. Open the launcher (DankLauncherV2)\n2. Type `img` to activate the plugin\n3. Browse Dux Shell screenshots in tile view\n\n## Plugin Configuration\n\n```json\n{\n  \"viewMode\": \"tile\",\n  \"viewModeEnforced\": true\n}\n```\n\n- `viewMode`: Sets the default view mode (\"list\", \"grid\", or \"tile\")\n- `viewModeEnforced`: When true, users cannot switch view modes for this plugin\n\n## Item Data Structure\n\nTo display images in tile mode, set `imageUrl` directly on the item:\n\n```javascript\n{\n    name: \"Image Title\",\n    icon: \"material:image\",\n    comment: \"Image description\",\n    categories: [\"Category\"],\n    imageUrl: \"https://example.com/image.png\"\n}\n```\n\nThe `imageUrl` property supports remote URLs or local files, use `file://` prefix for local files.\n","quickshell/PLUGINS/LauncherImageExample/plugin.json":"{\n    \"id\": \"launcherImageExample\",\n    \"name\": \"Image Gallery Example\",\n    \"description\": \"Example launcher plugin demonstrating tile mode with images\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"photo_library\",\n    \"type\": \"launcher\",\n    \"trigger\": \"img\",\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true,\n    \"component\": \"./LauncherImageExample.qml\",\n    \"permissions\": []\n}\n","quickshell/PLUGINS/PopoutControlExample/README.md":"# Popout Control Example Plugin\n\nThis example plugin demonstrates:\n- Using `PopoutService` to trigger positioned popouts and modals\n- Using `pillClickAction` with position parameters\n- Using `PluginSettings` with dropdown selection\n- Dynamic widget text based on settings\n\nThe `pillClickAction` receives position parameters `(x, y, width, section, screen)` which are passed to PopoutService functions to properly position popouts relative to the widget.\n\n## PopoutService API\n\nThe `PopoutService` is automatically injected into plugin widgets and daemons as `popoutService`. It provides access to all shell popouts and modals.\n\n### Available Popouts\n\n#### Control Center\n```qml\npopoutService.openControlCenter()\npopoutService.closeControlCenter()\npopoutService.toggleControlCenter()\n```\n\n#### Notification Center\n```qml\npopoutService.openNotificationCenter()\npopoutService.closeNotificationCenter()\npopoutService.toggleNotificationCenter()\n```\n\n#### App Drawer\n```qml\npopoutService.openAppDrawer()\npopoutService.closeAppDrawer()\npopoutService.toggleAppDrawer()\n```\n\n#### Process List (Popout)\n```qml\npopoutService.openProcessList()\npopoutService.closeProcessList()\npopoutService.toggleProcessList()\n```\n\n#### DankDash\n```qml\npopoutService.openDankDash(tabIndex)    // tabIndex: 0=Calendar, 1=Media, 2=Weather\npopoutService.closeDankDash()\npopoutService.toggleDankDash(tabIndex)\n```\n\n#### Battery Popout\n```qml\npopoutService.openBattery()\npopoutService.closeBattery()\npopoutService.toggleBattery()\n```\n\n#### VPN Popout\n```qml\npopoutService.openVpn()\npopoutService.closeVpn()\npopoutService.toggleVpn()\n```\n\n#### System Update Popout\n```qml\npopoutService.openSystemUpdate()\npopoutService.closeSystemUpdate()\npopoutService.toggleSystemUpdate()\n```\n\n### Available Modals\n\n#### Settings Modal\n```qml\npopoutService.openSettings()\npopoutService.closeSettings()\n```\n\n#### Clipboard History Modal\n```qml\npopoutService.openClipboardHistory()\npopoutService.closeClipboardHistory()\n```\n\n#### Launcher Modal\n```qml\npopoutService.openDankLauncherV2()\npopoutService.closeDankLauncherV2()\npopoutService.toggleDankLauncherV2()\n```\n\n#### Power Menu Modal\n```qml\npopoutService.openPowerMenu()\npopoutService.closePowerMenu()\npopoutService.togglePowerMenu()\n```\n\n#### Process List Modal (fullscreen)\n```qml\npopoutService.showProcessListModal()\npopoutService.hideProcessListModal()\npopoutService.toggleProcessListModal()\n```\n\n#### Color Picker Modal\n```qml\npopoutService.showColorPicker()\npopoutService.hideColorPicker()\n```\n\n#### Notification Modal\n```qml\npopoutService.showNotificationModal()\npopoutService.hideNotificationModal()\n```\n\n#### WiFi Password Modal\n```qml\npopoutService.showWifiPasswordModal()\npopoutService.hideWifiPasswordModal()\n```\n\n#### Network Info Modal\n```qml\npopoutService.showNetworkInfoModal()\npopoutService.hideNetworkInfoModal()\n```\n\n#### Notepad Slideout\n```qml\npopoutService.openNotepad()\npopoutService.closeNotepad()\npopoutService.toggleNotepad()\n```\n\n## Usage in Plugins\n\n### Widget Plugins\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\n\nRectangle {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            popoutService?.toggleControlCenter()\n        }\n    }\n}\n```\n\n### Daemon Plugins\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    Connections {\n        target: NotificationService\n        function onNotificationReceived() {\n            popoutService?.openNotificationCenter()\n        }\n    }\n}\n```\n\n**Important**: The `popoutService` property **must** be declared in your plugin component. Without it, you'll get errors like:\n```\nError: Cannot assign to non-existent property \"popoutService\"\n```\n\n## Example Use Cases\n\n1. **Custom Launcher**: Create a widget that opens the app drawer\n2. **Quick Settings**: Toggle control center from a custom button\n3. **Notification Manager**: Open notification center on new notifications\n4. **System Monitor**: Open process list on high CPU usage\n5. **Power Management**: Trigger power menu from a custom widget\n\n## Installation\n\n1. Copy the plugin directory to `~/.config/dms/plugins/`\n2. Open Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Popout Control Example\"\n5. Add `popoutControlExample` to your DankBar widget list\n\n## Notes\n\n- The `popoutService` property is automatically injected - no manual setup required\n- Always use optional chaining (`?.`) when calling methods to handle null cases\n- Popouts are lazily loaded - first access may activate the loader\n- Some popouts require specific system features to be available\n","quickshell/PLUGINS/PopoutControlExample/plugin.json":"{\n    \"id\": \"popoutControlExample\",\n    \"name\": \"Popout Control Example\",\n    \"description\": \"Example widget demonstrating PopoutService usage with pillClickAction\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./PopoutControlWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./PopoutControlSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","quickshell/PLUGINS/QuickNotesExample/plugin.json":"{\n    \"id\": \"quickNotesExample\",\n    \"name\": \"Quick Notes\",\n    \"description\": \"Example launcher plugin demonstrating the plugin state API for persistent data like history and notes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"icon\": \"sticky_note_2\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\"],\n    \"component\": \"./QuickNotesLauncher.qml\",\n    \"settings\": \"./QuickNotesSettings.qml\",\n    \"trigger\": \"n\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","quickshell/PLUGINS/README.md":"# Plugin System\n\nExtend DMS with dynamically loaded QML components: bar and dock widgets, Control Center tiles, dash tabs and overview cards, desktop widgets, launcher providers and background daemons.\n\n## Plugin Registry\n\nBrowse and discover community plugins at **https://plugins.duxlabs.dev/**\n\n## Overview\n\nPlugins are discovered from `~/.config/dms/plugins/` and managed by PluginService. One plugin can provide a single surface or several (see [Composite Plugins](#composite-plugins)).\n\n## Architecture\n\n### Core Components\n\n1. **PluginService** (`Services/PluginService.qml`)\n   - Singleton service managing plugin lifecycle\n   - Discovers plugins from `$CONFIGPATH/dms/plugins/`\n   - Handles loading, unloading, and state management\n   - Provides data persistence for plugin settings\n\n2. **Plugins hub** (`Modules/Settings/PluginsHubHeader.qml`, `Modules/Settings/PluginsManageTab.qml`)\n   - UI for managing available plugins and registries\n\n3. **Plugin settings page** (`Modules/Settings/PluginSettingsPage.qml`)\n   - Loads a plugin's settings component through `PluginSettingsHost`\n\n4. **Bar and dock integration** (`Modules/SurfaceWidgets/SurfaceWidgetFactory.qml`, `SurfaceWidgetHost.qml`)\n   - Renders plugin widgets in bars and docks with the same component\n   - Merges plugin components with built-in widgets\n   - Supports every bar and dock edge\n\n5. **Dash integration** (`Modules/DDash/DashRegistry.qml`)\n   - Lists built-in and plugin tabs and overview cards\n   - Hosts plugin tabs in `DashTabHost.qml` and cards in `Overview/DashCardSlot.qml`\n\nMany widgets are implemented in the shared [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) library and re-exported by DMS. Plugins should keep importing `qs.Common`, `qs.Services`, `qs.Widgets`, and `qs.Modules.Plugins` — these remain the supported plugin API and are unaffected by where a widget is implemented.\n\n## Plugin Structure\n\nEach plugin must be a directory in `$CONFIGPATH/dms/plugins/` containing:\n\n```\n$CONFIGPATH/dms/plugins/YourPlugin/\n├── plugin.json          # Required: Plugin manifest\n├── YourWidget.qml       # Required: Widget component\n├── YourSettings.qml     # Optional: Settings UI\n├── *.js                 # Optional: JavaScript utilities\n└── translations/        # Optional: per-locale translation files (see Translations)\n```\n\n### Plugin Manifest (plugin.json)\n\nThe manifest file defines plugin metadata and configuration.\n\n**JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema.\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"thing-my-plugin-does\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"material_icon_name\",\n    \"settings\": \"./YourSettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"requires\": [\"some-system-tool\"],\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n```\n\n**Required Fields:**\n- `id`: Unique plugin identifier (camelCase, no spaces)\n- `name`: Human-readable plugin name\n- `description`: Short description of plugin functionality (displayed in UI)\n- `version`: Semantic version string (e.g., \"1.0.0\")\n- `author`: Plugin creator name or email\n- `type`: Plugin type - \"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", or \"composite\" (with `components`)\n- `capabilities`: Array of plugin capabilities  (e.g., [\"dankbar-widget\"], [\"control-center\"], [\"monitoring\"])\n- `component`: Relative path to main QML component file, or `components` for a multi-surface plugin\n\n**Required for Launcher Type:**\n- `trigger`: Trigger string for launcher activation (e.g., \"=\", \"#\", \"!\")\n\n**Optional Fields:**\n- `icon`: Material Design icon name (displayed in UI)\n- `settings`: Path to settings component (enables settings UI)\n- `requires_dms`: Minimum DMS version requirement (e.g., \">=0.1.18\", \">0.1.0\")\n- `requires`: Array of required system tools/dependencies (e.g., [\"curl\", \"jq\"])\n- `permissions`: Required DMS permissions (e.g., [\"settings_read\", \"settings_write\"])\n\n**Permissions:**\n\nThe plugin system enforces permissions when settings are accessed:\n- `settings_read`: Required to read plugin settings (currently not enforced)\n- `settings_write`: **Required** to use PluginSettings component and save settings\n\nIf your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI.\n\n### Widget Component\n\nThe main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    // Define horizontal bar pill, for top and bottom DankBar positions (optional)\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    // Define vertical bar pill, for left and right DankBar positions (optional)\n    verticalBarPill: Component {\n        // Same as horizontal but optimized for vertical layout\n    }\n\n    // Define popout content, opens when clicking the bar pill (optional)\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional description text goes here\"\n\n            // Your popout content goes here\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Popout Content\"\n                    font.pixelSize: Theme.fontSizeLarge\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    // Popout dimensions (required if popoutContent is set)\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n**PluginComponent Properties (automatically injected):**\n- `axis`: Bar axis information (horizontal/vertical)\n- `section`: Bar section (\"left\", \"center\", \"right\")\n- `parentScreen`: Screen reference for multi-monitor support\n- `widgetThickness`: Recommended widget size perpendicular to bar\n- `barThickness`: Bar thickness parallel to edge\n\n**Component Options:**\n- `horizontalBarPill`: Component shown in horizontal bars\n- `verticalBarPill`: Component shown in vertical bars\n- `popoutContent`: Optional popout window content\n- `popoutWidth`: Popout window width\n- `popoutHeight`: Popout window height\n- `pillClickAction`: Custom click handler function (overrides popout)\n- `pillRightClickAction`: Custom right click handler function\n\n### Control Center Integration\n\nAdd your plugin to Control Center by defining CC properties:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isEnabled ? \"Active\" : \"Inactive\"\n    ccWidgetIsActive: isEnabled\n\n    onCcWidgetToggled: {\n        isEnabled = !isEnabled\n        if (pluginService) {\n            pluginService.savePluginData(\"myPlugin\", \"isEnabled\", isEnabled)\n        }\n    }\n\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.foregroundColor(Theme.cardSurface)\n            radius: Theme.cornerRadius\n            // Your detail UI here\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n}\n```\n\n**CC Properties:**\n- `ccWidgetIcon`: Material icon name\n- `ccWidgetPrimaryText`: Main label\n- `ccWidgetSecondaryText`: Subtitle/status\n- `ccWidgetIsActive`: Active state styling\n- `ccWidgetIsToggle`: Whether the icon toggles state; set false for action-only tiles\n- `ccExpandedContent`: Optional inline controls for larger tiles\n- `ccExpandedMinimumHeight`: Minimum height for inline controls, default `Theme.listItemHeight`\n- `ccFooterContent`: Accepted but ignored; the footer strip is gone and users shrink tiles to half-height pills instead\n- `ccDetailContent`: Optional detail page\n\n**Signals:**\n- `ccWidgetToggled()`: Fired when icon clicked\n- `ccWidgetExpanded()`: Fired when opening the detail page\n\n**Widget sizing:**\n\nThe grid uses square cells, with eight columns by default. A standard strip is\n4×1; a 2×2 tile is square. Users resize tiles and the panel in edit mode. Panel\nwidth and tile height are limited by the current screen, independently of each\nother. Saved spans are retained when displaying a tile on a smaller screen.\nThe standard tile adapts its icon and labels automatically. Larger tiles can load\n`ccExpandedContent` below the header when enough space is available.\n\nUse `CcTileContent` from `qs.Modules.ControlCenter.Widgets` for inline content.\nIts `tile` is supplied by the host. It exposes `columns`, `rows`, `live`,\n`contentColor`, and `subtitleColor`; `width` and `height` are the available space.\nContent is disabled in edit mode and destroyed when the Control Center closes or\nshrinks below the minimum size. Keep persistent state in the plugin instance.\n\n```qml\nccExpandedContent: Component {\n    CcTileContent {\n        DButton {\n            maximumWidth: parent.width\n            text: I18n.trFor(\"myPlugin\", \"Run\")\n            onClicked: root.runAction()\n        }\n    }\n}\n```\n\nExisting plugins need no changes to use the standard responsive tile. Custom\ninline controls must fit their allocated space and gate ongoing work on `live`.\n\n**Custom Click Actions:**\n\nOverride default popout with `pillClickAction` and `pillRightClickAction`:\n\n```qml\npillClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Clicked!'\"])\n}\n\n// Or with position params: (x, y, width, section, screen)\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n\npillRightClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Right clicked!'\"])\n}\n\npillRightClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\nThe PluginComponent automatically handles:\n- Bar orientation detection\n- Click handlers for popouts\n- Proper positioning and anchoring\n- Theme integration\n\n### PopoutComponent\n\nPopoutComponent provides a consistent header/content layout for plugin popouts:\n\n```qml\nimport qs.Modules.Plugins\nimport qs.DCommon.Widgets\n\nPopoutComponent {\n    headerText: \"Header Title\"        // Main header text (bold, large)\n    detailsText: \"Description text\"   // Optional description (smaller, gray)\n\n    // Access to exposed properties for dynamic sizing\n    readonly property int headerHeight    // Height of header area\n    readonly property int detailsHeight   // Height of description area\n\n    // Your content here - use parent.width for full width\n    // Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing\n    DGridView {\n        width: parent.width\n        height: parent.height\n        // ...\n    }\n}\n```\n\n**PopoutComponent Properties:**\n- `headerText`: Main header text (optional, hidden if empty)\n- `detailsText`: Description text below header (optional, hidden if empty)\n- `closePopout`: Function to close popout (auto-injected by PluginPopout)\n- `headerHeight`: Readonly height of header (0 if not visible)\n- `detailsHeight`: Readonly height of description (0 if not visible)\n\nThe component automatically handles spacing and layout. Content children are rendered below the description with proper padding.\n\n### Settings Component\n\nOptional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    id: root\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key for accessing the service\"\n        placeholder: \"Enter API key...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"notifications\"\n        label: \"Enable Notifications\"\n        description: \"Show desktop notifications for updates\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"updateInterval\"\n        label: \"Update Interval\"\n        description: \"How often to refresh data\"\n        options: [\n            {label: \"1 minute\", value: \"60\"},\n            {label: \"5 minutes\", value: \"300\"},\n            {label: \"15 minutes\", value: \"900\"}\n        ]\n        defaultValue: \"300\"\n    }\n\n    ListSetting {\n        id: itemList\n        settingKey: \"items\"\n        label: \"Saved Items\"\n        description: \"List of configured items\"\n        delegate: Component {\n            StyledRect {\n                width: parent.width\n                height: 40\n                radius: Theme.cornerRadius\n                color: Theme.chipSurface\n\n                StyledText {\n                    anchors.left: parent.left\n                    anchors.leftMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    text: modelData.name\n                    color: Theme.surfaceText\n                }\n\n                Rectangle {\n                    anchors.right: parent.right\n                    anchors.rightMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    width: 60\n                    height: 28\n                    color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                    radius: Theme.cornerRadius\n\n                    StyledText {\n                        anchors.centerIn: parent\n                        text: \"Remove\"\n                        color: Theme.errorText\n                        font.pixelSize: Theme.fontSizeSmall\n                        font.weight: Font.Medium\n                    }\n\n                    MouseArea {\n                        id: removeArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        cursorShape: Qt.PointingHandCursor\n                        onClicked: itemList.removeItem(index)\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n**Available Setting Components:**\n\nAll settings automatically save on change and load on component creation.\n\n**How Default Values Work:**\n\nEach setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI:\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"\n    defaultValue: \"\"  // Empty string if no key saved\n}\n\nToggleSetting {\n    settingKey: \"enabled\"\n    defaultValue: true  // Enabled by default\n}\n\nListSettingWithInput {\n    settingKey: \"locations\"\n    defaultValue: []  // Empty array if no locations saved\n}\n```\n\n1. **PluginSettings** - Root wrapper for all plugin settings\n   - `pluginId`: Your plugin ID (required)\n   - Auto-handles storage and provides saveValue/loadValue to children\n   - Place all other setting components inside this wrapper\n\n2. **StringSetting** - Text input field\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `placeholder`: Input placeholder (optional)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Vertical stack (label, description, input field)\n\n3. **ToggleSetting** - Boolean toggle switch\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default boolean (optional, default: `false`)\n   - Layout: Horizontal (label/description left, toggle right)\n\n4. **SelectionSetting** - Dropdown menu\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `options`: Array of `{label, value}` objects or simple strings (required)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Horizontal (label/description left, dropdown right)\n   - Stores the `value` field, displays the `label` field\n\n5. **ListSetting** - Manage list of items (manual add/remove)\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `delegate`: Custom item delegate Component (optional)\n   - `addItem(item)`: Add item to list\n   - `removeItem(index)`: Remove item from list\n   - Use when you need custom UI for adding items\n\n6. **ListSettingWithInput** - Complete list management with built-in form\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `fields`: Array of field definitions (required)\n     - `id`: Field ID in saved object (required)\n     - `label`: Column header text (required)\n     - `placeholder`: Input placeholder (optional)\n     - `width`: Column width in pixels (optional, default 200)\n     - `required`: Must have value to add (optional, default false)\n     - `default`: Default value if empty (optional)\n   - Automatically generates:\n     - Column headers from field labels\n     - Input fields with placeholders\n     - Add button with validation\n     - List display showing all field values\n     - Remove buttons for each item\n   - Best for collecting structured data (servers, locations, etc.)\n\n**Complete Settings Example:**\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your service API key\"\n        placeholder: \"sk-...\"\n        defaultValue: \"\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        description: \"Turn this feature on or off\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"theme\"\n        label: \"Theme\"\n        description: \"Choose your preferred theme\"\n        options: [\n            {label: \"Dark\", value: \"dark\"},\n            {label: \"Light\", value: \"light\"},\n            {label: \"Auto\", value: \"auto\"}\n        ]\n        defaultValue: \"dark\"\n    }\n\n    ListSettingWithInput {\n        settingKey: \"locations\"\n        label: \"Locations\"\n        description: \"Track multiple locations\"\n        defaultValue: []\n        fields: [\n            {id: \"name\", label: \"Name\", placeholder: \"Home\", width: 150, required: true},\n            {id: \"timezone\", label: \"Timezone\", placeholder: \"America/New_York\", width: 200, required: true}\n        ]\n    }\n}\n```\n\n**Key Benefits:**\n- Zero boilerplate - just define your settings\n- Automatic persistence to `settings.json`\n- Clean, consistent UI across all plugins\n- No manual `pluginService` calls needed\n- Proper layout and spacing handled automatically\n\n## Translations\n\nPlugins ship their own translations. Drop a `translations/` directory next to `plugin.json`, wrap your strings in `I18n.trFor()`, done — nothing needs to change in the DMS repo, and users get your translations just by installing the plugin.\n\n```\nYourPlugin/\n├── plugin.json\n├── YourWidget.qml\n└── translations/\n    ├── es.json\n    ├── fr.json\n    └── zh_CN.json\n```\n\nDMS loads the file matching the user's locale when it discovers the plugin, and re-reads it whenever the locale changes. No restart, no registration call.\n\n### Marking Strings\n\nWrap every user-facing string in `I18n.trFor()`, with your plugin id as the first argument:\n\n```qml\nimport qs.Common\n\nStyledText {\n    text: I18n.trFor(\"yourPlugin\", \"Cycle Speed\")\n}\n```\n\nPlaceholders work the same as anywhere else in DMS:\n\n```qml\nToastService.showInfo(I18n.trFor(\"yourPlugin\", \"Copied %1 to clipboard\").arg(item))\n```\n\nLookup order is: your plugin's translation file → the global DMS catalog → the English term itself. Strings DMS already translates (\"Settings\", \"Close\", etc.) resolve from the global catalog for free, and anything untranslated renders as your English text instead of breaking.\n\nTwo rules:\n\n- The plugin id must be a **literal string** that exactly matches `id` in your `plugin.json`. The string extraction tooling for central translation reads call sites — a variable there means your strings never get extracted.\n- `I18n.trFor` doesn't exist on DMS versions without plugin translation support, and calling a missing function in QML is a TypeError, not a silent no-op. Bump `requires_dms` when you adopt it.\n\nPlain `I18n.tr()` still works inside plugins, but it only checks the global catalog — it will never see your `translations/` files.\n\n### Translation Files\n\nOne JSON file per locale. Top-level keys are context buckets; unless you have a reason to do otherwise, use the English term as its own bucket:\n\n```json\n{\n    \"Cycle Speed\": {\n        \"Cycle Speed\": \"Velocidad de ciclo\"\n    },\n    \"Copied %1 to clipboard\": {\n        \"Copied %1 to clipboard\": \"%1 copiado al portapapeles\"\n    }\n}\n```\n\nThere is no `en.json` — the strings in your QML are the English source. Translate what you want, skip what you don't; missing terms fall through the lookup order above.\n\nFile names match what DMS ships in its own catalog (`es.json`, `pt.json`, `zh_CN.json`, ...), but you're not limited to that list — resolution is driven by the user's locale. For a user on `zh_CN`, DMS tries `zh_CN.json`, then `zh-CN.json`, then `zh.json`, and uses the first one that exists.\n\n### Testing\n\n1. Reload after editing a file: `dms ipc call plugins reload yourPlugin`\n2. Switch languages in Settings → Locale — plugin strings retranslate live along with the rest of the shell\n3. Broken JSON logs a `bad plugin translations` warning in shell output and falls back to English\n\n`ExampleEmojiPlugin` is a complete working example — `trFor` calls in the widget and settings, plus `es.json` and `fr.json`.\n\n### Central Translation via POEditor\n\nPlugins in the [plugin registry](https://github.com/duxlabs-dev/dux-plugin-registry) can apply to join the DankPlugins POEditor project, translated by the same community that translates DMS itself. Approved plugins get every `I18n.trFor()` string uploaded under a `<pluginId>:` context, so your \"Auto\" and another plugin's \"Auto\" are separate terms with separate translations. Finished translations come back to the plugin repo as PRs to `translations/`, in the file format above. See the registry's CONTRIBUTING guide for the application process.\n\nStrings you wrap in plain `I18n.tr()` are not uploaded. They resolve from the shell catalog only, which is the right choice for terms DMS already has (\"Cancel\", \"Settings\"). A term DMS does not have stays English, and the sync warns about it.\n\n## PluginService API\n\n### Properties\n\n```qml\nPluginService.pluginDirectory: string\n// Path to plugins directory ($CONFIGPATH/dms/plugins)\n\nPluginService.availablePlugins: object\n// Map of all discovered plugins {pluginId: pluginInfo}\n\nPluginService.loadedPlugins: object\n// Map of currently loaded plugins {pluginId: pluginInfo}\n\nPluginService.pluginWidgetComponents: object\n// Map of loaded widget components {pluginId: Component}\n```\n\n### Functions\n\n```qml\n// Plugin Management\nPluginService.loadPlugin(pluginId: string): bool\nPluginService.unloadPlugin(pluginId: string): bool\nPluginService.reloadPlugin(pluginId: string): bool\nPluginService.enablePlugin(pluginId: string, onResult?: (ok: bool, error: string) => void): bool\nPluginService.disablePlugin(pluginId: string): bool\n\n// Plugin Discovery\nPluginService.scanPlugins(): void\nPluginService.getAvailablePlugins(): array\nPluginService.getLoadedPlugins(): array\nPluginService.isPluginLoaded(pluginId: string): bool\nPluginService.getWidgetComponents(): object\n\n// Data Persistence\nPluginService.savePluginData(pluginId: string, key: string, value: any): bool\nPluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any\n\n// Global Variables - Shared state across all plugin instances\nPluginService.getGlobalVar(pluginId: string, varName: string, defaultValue: any): any\nPluginService.setGlobalVar(pluginId: string, varName: string, value: any): void\n```\n\n### Signals\n\n```qml\nPluginService.pluginLoaded(pluginId: string)\nPluginService.pluginUnloaded(pluginId: string)\nPluginService.pluginLoadFailed(pluginId: string, error: string)\nPluginService.globalVarChanged(pluginId: string, varName: string)\n```\n\n## Startup Check (Dependency Gate)\n\nA plugin may optionally gate activation behind a dependency check. Point the manifest's `startupCheck` field at a small, **non-visual** component (a `QtObject` - it must not render in the graphics scene):\n\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"boregard\"]\n}\n```\n\nThe component exposes a `check` function that runs before the plugin loads, both on manual enable and on auto-load at startup. Call `done(null)` to allow activation, or `done(error)` to block it. The error can be a short string (title only) or an object with an expandable `details` body for long-form instructions:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"which\", \"boregard\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null)\n                return\n            }\n            done({\n                title: I18n.tr(\"boregard is required\"),\n                details: I18n.tr(\"Install it from https://duxlabs.dev, then re-enable this plugin.\")\n            })\n        })\n    }\n}\n```\n\nA synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). A check that fails to load, has no `check` function, or throws blocks activation the same way, with its error as the message. The check also runs again on reload. Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`.\n\n## Plugin Global Variables\n\nPlugins can share state across multiple instances using global variables. This is useful when you have the same widget displayed on multiple monitors or multiple instances of the same widget on different bars.\n\n### Why Use Global Variables?\n\nUnlike regular properties which are scoped to each component instance, global variables are synchronized across all instances of your plugin. This enables:\n\n- **Multi-monitor consistency**: Same data displayed across all monitors\n- **Multi-instance widgets**: Multiple instances of the same widget sharing state\n- **Cross-component communication**: Share data between widget and settings components\n\n### Using PluginGlobalVar\n\nThe `PluginGlobalVar` helper component provides reactive global variable access:\n\n```qml\nimport QtQuick\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Count: \" + globalCounter.value\n                color: Theme.surfaceText\n            }\n\n            MouseArea {\n                anchors.fill: parent\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar Properties:**\n- `varName` (required): Name of the global variable\n- `defaultValue` (optional): Default value if not set\n- `value` (readonly): Current value of the global variable\n\n**PluginGlobalVar Methods:**\n- `set(newValue)`: Update the global variable (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\nFor more control, use the PluginService API directly:\n\n```qml\nimport QtQuick\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\n    Connections {\n        target: PluginService\n        function onGlobalVarChanged(pluginId, varName) {\n            if (pluginId === \"myPlugin\" && varName === \"counter\") {\n                counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n            }\n        }\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            MouseArea {\n                anchors.fill: parent\n                onClicked: {\n                    const current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n                    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n                }\n            }\n        }\n    }\n}\n```\n\n### Global Variables vs Settings\n\n**Global Variables** (`getGlobalVar`/`setGlobalVar`):\n- Runtime state only (not persisted to disk)\n- Synchronized across all plugin instances\n- Changes trigger `globalVarChanged` signal for reactivity\n- Use for: counters, current selection, temporary UI state\n\n**Settings** (`savePluginData`/`loadPluginData`):\n- Persisted to `settings.json` across sessions\n- Loaded once per plugin instance\n- Use for: user preferences, API keys, configuration\n\n### Important Notes\n\n1. **Reactivity**: Global variables are reactive - all instances update when a value changes\n2. **Namespacing**: Variables are namespaced by plugin ID to avoid conflicts\n3. **Type Safety**: Values can be any QML/JavaScript type (numbers, strings, objects, arrays)\n4. **Not Persistent**: Global variables are cleared when the shell restarts (use settings for persistence)\n5. **Performance**: Efficient for frequent updates - changes only trigger updates for the specific variable\n\n## Creating a Plugin\n\n### Step 1: Create Plugin Directory\n\n```bash\nmkdir -p $CONFIGPATH/dms/plugins/MyPlugin\ncd $CONFIGPATH/dms/plugins/MyPlugin\n```\n\n### Step 2: Create Manifest\n\nCreate `plugin.json`:\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"my-functionality\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n### Step 3: Create Widget Component\n\nCreate `MyWidget.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    horizontalBarPill: Component {\n        StyledRect {\n            width: textItem.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: textItem.implicitWidth + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\n**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.\n\n### Step 4: Create Settings Component (Optional)\n\nCreate `MySettings.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"Configure your plugin settings\"\n        font.pixelSize: Theme.fontSizeMedium\n        color: Theme.surfaceVariantText\n        wrapMode: Text.WordWrap\n    }\n\n    StringSetting {\n        settingKey: \"text\"\n        label: \"Display Text\"\n        description: \"Text shown in the bar widget\"\n        placeholder: \"Hello World\"\n        defaultValue: \"Hello World\"\n    }\n\n    ToggleSetting {\n        settingKey: \"showIcon\"\n        label: \"Show Icon\"\n        description: \"Display an icon next to the text\"\n        defaultValue: true\n    }\n}\n```\n\n### Step 5: Enable Plugin\n\n1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml`\n2. Open Settings (Ctrl+,)\n3. Navigate to Plugins tab\n4. Click \"Scan for Plugins\"\n5. Enable your plugin with the toggle switch\n6. Add the plugin to your DankBar configuration\n\n## Adding Plugin to DankBar\n\nAfter enabling a plugin, add it to the bar:\n\n1. Open Settings → Appearance → DankBar Layout\n2. Add a new widget entry with your plugin ID\n3. Choose section (left, center, right)\n4. Save and reload\n\nOr edit `$CONFIGPATH/quickshell/dms/config.json`:\n\n```json\n{\n    \"dankBarLeftWidgets\": [\n        {\"widgetId\": \"myPlugin\", \"enabled\": true}\n    ]\n}\n```\n\n## Best Practices\n\n1. **Use Existing Widgets**: Leverage `qs.DCommon.Widgets` components (DIcon, DToggle, etc.) for consistency\n2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts\n3. **Data Persistence**: Use PluginService data APIs instead of manual file operations\n4. **Error Handling**: Gracefully handle missing dependencies and invalid data\n5. **Performance**: Keep widgets lightweight, avoid long operations that block the UI loop\n6. **Responsive Design**: Adapt to `compactMode` and different screen sizes\n7. **Documentation**: Include README.md explaining plugin usage\n8. **Versioning**: Use semantic versioning for updates\n9. **Dependencies**: Document external library requirements\n\n## Clipboard Access\n\nPlugins that need to copy text to the clipboard should use the built-in `dms cl copy` command through Quickshell's `execDetached` function.\n\n### Correct Method\n\nImport Quickshell and use `execDetached` with `dms cl copy`:\n\n```qml\nimport QtQuick\nimport Quickshell\n\nItem {\n    function copyToClipboard(text) {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    }\n}\n```\n\n### Example Usage\n\nFrom the ExampleEmojiPlugin (EmojiWidget.qml):\n\n```qml\nMouseArea {\n    onClicked: {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n        ToastService.showInfo(\"Copied \" + modelData + \" to clipboard\")\n        popoutColumn.closePopout()\n    }\n}\n```\n\n### Important Notes\n\n1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime\n2. **Always** import `Quickshell` at the top of your QML file\n3. Consider showing a toast notification to confirm the copy action to users\n\n### Dependencies\n\nThis method uses the built-in DMS clipboard functionality which has native Wayland support.\n\n## Running External Commands\n\nPlugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup.\n\n### Correct Method\n\nImport the `Proc` singleton from `qs.Common` and use `runCommand`:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function fetchData() {\n        Proc.runCommand(\n            \"myPlugin.fetchData\",\n            [\"curl\", \"-s\", \"https://api.example.com/data\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Success:\", stdout)\n                    processData(stdout)\n                } else {\n                    console.error(\"Command failed with exit code:\", exitCode)\n                }\n            },\n            100\n        )\n    }\n}\n```\n\n### Function Signature\n\n```qml\nProc.runCommand(id, command, callback, debounceMs)\n```\n\n**Parameters:**\n- `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one\n- `command` (array): Command and arguments as an array (e.g., `[\"sh\", \"-c\", \"echo hello\"]`)\n- `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes\n  - `stdout` (string): Captured standard output from the command\n  - `exitCode` (number): Exit code of the process (0 typically means success)\n- `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified\n\n### Key Features\n\n1. **Automatic Cleanup**: Process objects are automatically destroyed after completion\n2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one\n3. **Output Capture**: Automatically captures stdout for processing\n4. **Error Handling**: Exit codes are passed to the callback for error detection\n\n### Example Usage\n\n#### Simple Command Execution\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function checkNetwork() {\n        Proc.runCommand(\n            \"myPlugin.ping\",\n            [\"ping\", \"-c\", \"1\", \"8.8.8.8\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Network is up\")\n                } else {\n                    console.log(\"Network is down\")\n                }\n            }\n        )\n    }\n}\n```\n\n#### Parsing Command Output\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    property var diskUsage: ({})\n\n    function updateDiskUsage() {\n        Proc.runCommand(\n            \"myPlugin.df\",\n            [\"df\", \"-h\", \"/home\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    const lines = output.trim().split(\"\\n\")\n                    if (lines.length > 1) {\n                        const parts = lines[1].split(/\\s+/)\n                        diskUsage = {\n                            total: parts[1],\n                            used: parts[2],\n                            available: parts[3],\n                            percent: parts[4]\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n```\n\n#### Shell Commands with Pipes\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function getTopProcess() {\n        Proc.runCommand(\n            \"myPlugin.topProcess\",\n            [\"sh\", \"-c\", \"ps aux | sort -nrk 3,3 | head -n 1\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Top process:\", output)\n                }\n            }\n        )\n    }\n}\n```\n\n#### Debouncing Rapid Updates\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nItem {\n    DTextField {\n        id: searchField\n        placeholderText: \"Search files...\"\n\n        onTextChanged: {\n            Proc.runCommand(\n                \"myPlugin.search\",\n                [\"find\", \"/home\", \"-name\", \"*\" + text + \"*\"],\n                (output, exitCode) => {\n                    if (exitCode === 0) {\n                        updateSearchResults(output)\n                    }\n                },\n                500\n            )\n        }\n    }\n}\n```\n\n### Important Notes\n\n1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `\"myPlugin.actionName\"`) to avoid conflicts\n2. **Debouncing**: Use appropriate debounce delays for your use case:\n   - Fast updates (50-100ms): System monitoring, real-time data\n   - User input (300-500ms): Search fields, text input processing\n   - Network requests (500-1000ms): API calls, web scraping\n3. **Error Handling**: Always check the exit code in your callback before processing output\n4. **Shell Commands**: Use `[\"sh\", \"-c\", \"command\"]` for complex shell commands with pipes or redirects\n5. **Security**: Sanitize user input before passing to commands to prevent command injection\n6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely\n\n### Comparison with Other Methods\n\n**Proc.runCommand** vs **Quickshell.execDetached**:\n- Use `Proc.runCommand` when you need to capture output or check exit codes\n- Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy)\n\n**Proc.runCommand** vs **Process component**:\n- Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup\n- Use `Process` component for long-running processes or when you need fine-grained control\n\n## Debugging\n\n### Console Logging\n\nView plugin logs:\n\n```bash\nqs -v -p $CONFIGPATH/quickshell/dms/shell.qml\n```\n\nLook for lines prefixed with:\n- `PluginService:` - Service operations\n- `PluginsTab:` - UI interactions\n- `PluginsTab:` - Settings loading and accordion interface\n\n### Common Issues\n\n1. **Plugin Not Detected**\n   - Check plugin.json syntax (use `jq` or JSON validator)\n   - Verify directory is in `$CONFIGPATH/dms/plugins/`\n   - Click \"Scan for Plugins\" in Settings\n\n2. **Widget Not Displaying**\n   - Ensure plugin is enabled in Settings\n   - Add plugin ID to DankBar widget list\n   - Check widget width/height properties\n\n3. **Settings Not Loading**\n   - Verify `settings` path in plugin.json\n   - Check settings component for errors\n   - Ensure plugin is enabled and loaded\n   - Review PluginsTab console output for injection issues\n\n4. **Data Not Persisting**\n   - Confirm pluginService.savePluginData() calls (with injection)\n   - Check `$CONFIGPATH/dms/settings.json` for pluginSettings data\n   - Verify plugin has settings permissions\n   - Ensure PluginService was properly injected into settings component\n\n## Security Considerations\n\nPlugins run with full QML runtime access. Only install plugins from trusted sources.\n\n**Permissions System:**\n- `settings_read`: Read plugin configuration (not currently enforced)\n- `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced)\n- `process`: Execute system commands (not currently enforced)\n- `network`: Network access (not currently enforced)\n\nCurrently, only `settings_write` is enforced by the PluginSettings component.\n\n## API Stability\n\nThe plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.\n\n**Roadmap:**\n- Plugin marketplace/repository\n- Sandboxed plugin execution\n- Enhanced permission system\n- Plugin update notifications\n- Inter-plugin communication\n\n## Launcher Plugins\n\nLauncher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.\n\n### Overview\n\nLauncher plugins enable you to:\n- Add custom items to the launcher/app drawer\n- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)\n- Execute custom actions when items are selected\n- Provide searchable, categorized content\n- Integrate seamlessly with the existing launcher\n\n### Plugin Type Configuration\n\nTo create a launcher plugin, set the plugin type in `plugin.json`:\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher Plugin\",\n    \"description\": \"A custom launcher plugin for quick actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"show-thing\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nLauncher components are instantiated the first time the launcher asks them for items, not at shell startup or launcher open, so a plugin not allowed without a trigger is created once its trigger is typed or the launcher is filtered to it. Put background work (timers, processes, IPC handlers) in a daemon surface.\n\nLauncher results use shared Expressive rows and tiles in every launcher style. The item and action contracts are unchanged. List rows use a second line when `comment` is present; image and window preview tiles remain supported. Plugins do not need to supply a visual delegate.\n\n### Launcher Component Contract\n\nCreate `MyLauncher.qml` with the following interface:\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    // Required properties\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    // Required signals\n    signal itemsChanged()\n\n    // Required: Return array of launcher items\n    function getItems(query) {\n        return [\n            {\n                name: \"Item Name\",\n                icon: \"icon_name\",\n                comment: \"Description\",\n                action: \"type:data\",\n                categories: [\"MyLauncher\"]\n            }\n        ]\n    }\n\n    // Required: Execute item action\n    function executeItem(item) {\n        const [type, data] = item.action.split(\":\", 2)\n        // Handle action based on type\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"myLauncher\", \"trigger\", \"#\")\n        }\n    }\n}\n```\n\n### Item Structure\n\nEach item returned by `getItems()` must include:\n\n- `name` (string): Display name shown in launcher\n- `icon` (string, optional): Icon specification (see Icon Types below)\n- `comment` (string): Description/subtitle text\n- `action` (string): Action identifier in `type:data` format\n- `categories` (array): Array containing your plugin name\n\n### Icon Types\n\nThe `icon` field supports four formats:\n\n**1. Material Design Icons** - Use the `material:` prefix:\n```javascript\n{\n    name: \"My Item\",\n    icon: \"material:lightbulb\",  // Material Symbols Rounded font\n    comment: \"Uses Material Design icon\",\n    action: \"toast:Hello!\",\n    categories: [\"MyPlugin\"]\n}\n```\nAvailable icons: Any icon from Material Symbols font (e.g., `lightbulb`, `star`, `favorite`, `settings`, `terminal`, `translate`, `sentiment_satisfied`)\n\n**2. Unicode/Emoji Icons** - Use the `unicode:` prefix:\n```javascript\n{\n    name: \"Grinning Face\",\n    icon: \"unicode:😀\",  // Unicode character or emoji\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nDisplay any Unicode character or emoji as the icon. The character is rendered at 70-80% of the icon size with proper theming. Perfect for emoji pickers, symbol selectors, or character libraries.\n\n**3. Desktop Theme Icons** - Use icon name directly:\n```javascript\n{\n    name: \"Firefox\",\n    icon: \"firefox\",  // Uses system icon theme\n    comment: \"Launches Firefox browser\",\n    action: \"exec:firefox\",\n    categories: [\"MyPlugin\"]\n}\n```\nUses the user's installed icon theme. Common examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n**4. No Icon** - Omit the `icon` field entirely:\n```javascript\n{\n    name: \"😀  Grinning Face\",\n    // No icon field - emoji/unicode in name displays without icon area\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nWhen `icon` is omitted, the launcher shows the first letter of the item name in a themed circle.\n\n### Trigger System\n\nTriggers control when your plugin's items appear in the launcher:\n\n**Empty Trigger Mode** (No trigger):\n- Items always visible alongside regular apps\n- Search includes your items automatically\n- Configure by saving empty trigger: `trigger: \"\"`\n\n**Custom Trigger Mode**:\n- Items only appear when trigger is typed\n- Example: Type `#` to show only your plugin's items\n- Type `# query` to search within your plugin\n- Configure any string: `#`, `!`, `@`, `!custom`, etc.\n\n### Trigger Configuration in Settings\n\nProvide a settings component with trigger configuration:\n\n```qml\nimport QtQuick\nimport QtQuick.Controls\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nFocusScope {\n    id: root\n\n    property var pluginService: null\n\n    Column {\n        spacing: 12\n\n        CheckBox {\n            id: noTriggerToggle\n            text: \"No trigger (always show)\"\n            checked: loadSettings(\"noTrigger\", false)\n\n            onCheckedChanged: {\n                saveSettings(\"noTrigger\", checked)\n                if (checked) {\n                    saveSettings(\"trigger\", \"\")\n                } else {\n                    saveSettings(\"trigger\", triggerField.text || \"#\")\n                }\n            }\n        }\n\n        DTextField {\n            id: triggerField\n            visible: !noTriggerToggle.checked\n            text: loadSettings(\"trigger\", \"#\")\n            placeholderText: \"#\"\n\n            onTextEdited: {\n                saveSettings(\"trigger\", text || \"#\")\n            }\n        }\n    }\n\n    function saveSettings(key, value) {\n        if (pluginService) {\n            pluginService.savePluginData(\"myLauncher\", key, value)\n        }\n    }\n\n    function loadSettings(key, defaultValue) {\n        if (pluginService) {\n            return pluginService.loadPluginData(\"myLauncher\", key, defaultValue)\n        }\n        return defaultValue\n    }\n}\n```\n\n### Action Execution\n\nHandle different action types in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            if (typeof ToastService !== \"undefined\") {\n                ToastService.showInfo(\"Plugin\", actionData)\n            }\n            break\n        case \"copy\":\n            // Copy to clipboard\n            break\n        case \"script\":\n            // Execute command\n         \n\n<!-- [TRUNCATED AT 50KB] -->","quickshell/PLUGINS/WallpaperWatcherDaemon/README.md":"# Wallpaper Watcher Daemon\n\nRun a script whenever your wallpaper changes.\n\n## What it does\n\nThis daemon monitors wallpaper changes and executes a script you specify. The new wallpaper path gets passed as the first argument to your script.\n\n## Setup\n\n1. Enable the plugin in Settings → Plugins\n2. Configure the script path in the plugin settings\n3. Make sure your script is executable (`chmod +x /path/to/script.sh`)\n\n## Example script\n\n```bash\n#!/bin/bash\necho \"New wallpaper: $1\"\n# Do something with the wallpaper path\n```\n\nSave this to a file, make it executable, and point the plugin to it.\n\n## Use cases\n\n- Generate color schemes from the new wallpaper\n- Update theme files based on wallpaper colors\n- Send notifications when wallpaper changes\n- Sync wallpaper info to other devices\n- Log wallpaper history\n\n## Notes\n\n- Script errors show up as toast notifications\n- Script output goes to console logs\n- The daemon runs invisibly in the background\n","quickshell/PLUGINS/WallpaperWatcherDaemon/plugin.json":"{\n    \"id\": \"wallpaperWatcherDaemon\",\n    \"name\": \"Wallpaper Watcher Daemon\",\n    \"description\": \"Background daemon that monitors wallpaper changes and logs them\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper\", \"monitoring\"],\n    \"component\": \"./WallpaperWatcherDaemon.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./WallpaperWatcherSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"## What is this?\n\nDMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.\n\n## Repo Structure\n\n- core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.\n- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/\n- dux-qml-common/ - submodule of shared QML widgets\n- docs/, distro/ - IPC and theme docs, packaging\n\n## Rules\n\n- Keep it simple.\n- Do not edit code in response to a question.\n- Never commit, push, or open a PR unless asked.\n- Point out glaring issues we missed.\n- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.\n- `core` and `quickshell` share the socket protocol. Changing one means checking the other.\n- QML uses Theme tokens, never hardcoded colors, spacing, or constants.\n- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.\n- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.\n- Guard clauses, early returns.\n- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.\n- Do not edit generated code. Mocks come from mockery.\n- No library for a simple problem. Use a modern one for a complex, common problem.\n\n## Tests\n\n- A test must fail on the broken code. Otherwise do not write it.\n- No useless tests: \"main.go contains func main()\", defaults, \"it instantiates\", behavior the change didn't touch.\n- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.\n- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.\n\n## Connections vs bound properties\n\n- In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.\n- Source in the same file: put the handler on it.\n- Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.\n- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.\n- A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.\n- Singletons under Services/ and Common/ keep their `Connections`.\n\n## Commit Messages\n\n`area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.\n","isInternal":false,"tokens":783,"sizeBytes":3335},{"name":"plugin-schema.json","path":".agents/skills/dms-plugin-dev/assets/plugin-schema.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/plugin-schema.json","title":"Assets Skill","category":"anthropic-skill","format":"json","content":"{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://duxlabs.dev/schemas/plugin.json\",\n  \"title\": \"Dux Shell Plugin Manifest\",\n  \"description\": \"Schema for Dux Shell plugin.json manifest files\",\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"name\",\n    \"description\",\n    \"version\",\n    \"author\",\n    \"type\",\n    \"capabilities\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"type\": \"string\",\n      \"description\": \"Unique plugin identifier (camelCase, no spaces)\",\n      \"pattern\": \"^[a-zA-Z][a-zA-Z0-9]*$\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable plugin name\",\n      \"minLength\": 1\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"Short description of plugin functionality\",\n      \"minLength\": 1\n    },\n    \"version\": {\n      \"type\": \"string\",\n      \"description\": \"Semantic version string (e.g., '1.0.0')\",\n      \"pattern\": \"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-[a-zA-Z0-9.-]+)?(\\\\+[a-zA-Z0-9.-]+)?$\"\n    },\n    \"author\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin creator name or email\",\n      \"minLength\": 1\n    },\n    \"type\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.\",\n      \"enum\": [\"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", \"composite\"]\n    },\n    \"capabilities\": {\n      \"type\": \"array\",\n      \"description\": \"Array of plugin capabilities\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"minItems\": 1\n    },\n    \"component\": {\n      \"type\": \"string\",\n      \"description\": \"Relative path to main QML component file. Required unless 'components' is provided.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"components\": {\n      \"type\": \"object\",\n      \"description\": \"Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.\",\n      \"properties\": {\n        \"widget\": {\n          \"type\": \"string\",\n          \"description\": \"Bar/Control Center widget component (PluginComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"desktop\": {\n          \"type\": \"string\",\n          \"description\": \"Desktop widget component\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"daemon\": {\n          \"type\": \"string\",\n          \"description\": \"Background daemon component (instantiated once)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"launcher\": {\n          \"type\": \"string\",\n          \"description\": \"Launcher provider component (requires 'trigger')\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dash\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard tab component (DashTabComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dashCard\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard overview card component (DashCardComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"dash\": {\n      \"type\": \"object\",\n      \"description\": \"Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.\",\n      \"properties\": {\n        \"icon\": {\n          \"type\": \"string\",\n          \"description\": \"Material icon for the dashboard tab\"\n        },\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"Tab label\"\n        },\n        \"card\": {\n          \"type\": \"object\",\n          \"description\": \"Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).\",\n          \"properties\": {\n            \"icon\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"w\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"h\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"minW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"minH\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"maxW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"maxH\": { \"type\": \"integer\", \"minimum\": 1 }\n          },\n          \"additionalProperties\": false\n        },\n        \"options\": {\n          \"type\": \"array\",\n          \"description\": \"User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\"key\", \"type\"],\n            \"properties\": {\n              \"key\": { \"type\": \"string\", \"not\": { \"const\": \"widgets\" }, \"description\": \"Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts\" },\n              \"text\": { \"type\": \"string\", \"description\": \"Row label, translated through the plugin catalog\" },\n              \"description\": { \"type\": \"string\", \"description\": \"Secondary text under a toggle row, translated through the plugin catalog\" },\n              \"type\": { \"type\": \"string\", \"enum\": [\"toggle\", \"choice\", \"number\"] },\n              \"def\": { \"description\": \"Default value: boolean for toggle, choice value for choice, number for number\" },\n              \"choices\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"required\": [\"value\"],\n                  \"properties\": {\n                    \"value\": { \"type\": \"string\" },\n                    \"text\": { \"type\": \"string\" }\n                  },\n                  \"additionalProperties\": false\n                }\n              },\n              \"min\": { \"type\": \"number\" },\n              \"max\": { \"type\": \"number\" },\n              \"step\": { \"type\": \"number\", \"exclusiveMinimum\": 0 },\n              \"unit\": { \"type\": \"string\" }\n            },\n            \"additionalProperties\": false\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"trigger\": {\n      \"type\": \"string\",\n      \"description\": \"Trigger string for launcher activation (required for launcher type)\"\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Material Design icon name\"\n    },\n    \"settings\": {\n      \"type\": \"string\",\n      \"description\": \"Path to settings component QML file\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"startupCheck\": {\n      \"type\": \"string\",\n      \"description\": \"Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"requires_dms\": {\n      \"type\": \"string\",\n      \"description\": \"Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')\",\n      \"pattern\": \"^(>=?|<=?|=|>|<)\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\"\n    },\n    \"dependencies\": {\n      \"type\": \"array\",\n      \"description\": \"Array of required system tools/dependencies (registry metadata)\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"requires\": {\n      \"type\": \"array\",\n      \"description\": \"Deprecated alias for 'dependencies'.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"permissions\": {\n      \"type\": \"array\",\n      \"description\": \"Required capabilities\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"settings_read\",\n          \"settings_write\",\n          \"process\",\n          \"network\"\n        ]\n      }\n    }\n  },\n  \"allOf\": [\n    {\n      \"if\": {\n        \"properties\": {\n          \"type\": {\n            \"const\": \"launcher\"\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"if\": {\n        \"required\": [\"components\"],\n        \"properties\": {\n          \"components\": {\n            \"required\": [\"launcher\"]\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"anyOf\": [\n        {\n          \"required\": [\"component\"]\n        },\n        {\n          \"required\": [\"components\"]\n        }\n      ]\n    }\n  ],\n  \"additionalProperties\": true\n}\n","frontmatter":{"$schema":"http://json-schema.org/draft-07/schema#","$id":"https://duxlabs.dev/schemas/plugin.json","title":"Dux Shell Plugin Manifest","description":"Schema for Dux Shell plugin.json manifest files","type":"object","required":["id","name","description","version","author","type","capabilities"],"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. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.","enum":["widget","daemon","launcher","desktop","dash","dashCard","composite"]},"capabilities":{"type":"array","description":"Array of plugin capabilities","items":{"type":"string"},"minItems":1},"component":{"type":"string","description":"Relative path to main QML component file. Required unless 'components' is provided.","pattern":"^\\./.*\\.qml$"},"components":{"type":"object","description":"Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.","properties":{"widget":{"type":"string","description":"Bar/Control Center widget component (PluginComponent)","pattern":"^\\./.*\\.qml$"},"desktop":{"type":"string","description":"Desktop widget component","pattern":"^\\./.*\\.qml$"},"daemon":{"type":"string","description":"Background daemon component (instantiated once)","pattern":"^\\./.*\\.qml$"},"launcher":{"type":"string","description":"Launcher provider component (requires 'trigger')","pattern":"^\\./.*\\.qml$"},"dash":{"type":"string","description":"Dashboard tab component (DashTabComponent)","pattern":"^\\./.*\\.qml$"},"dashCard":{"type":"string","description":"Dashboard overview card component (DashCardComponent)","pattern":"^\\./.*\\.qml$"}},"additionalProperties":false,"minProperties":1},"dash":{"type":"object","description":"Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.","properties":{"icon":{"type":"string","description":"Material icon for the dashboard tab"},"title":{"type":"string","description":"Tab label"},"card":{"type":"object","description":"Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).","properties":{"icon":{"type":"string"},"title":{"type":"string"},"w":{"type":"integer","minimum":1,"maximum":8},"h":{"type":"integer","minimum":1},"minW":{"type":"integer","minimum":1,"maximum":8},"minH":{"type":"integer","minimum":1},"maxW":{"type":"integer","minimum":1,"maximum":8},"maxH":{"type":"integer","minimum":1}},"additionalProperties":false},"options":{"type":"array","description":"User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`","items":{"type":"object","required":["key","type"],"properties":{"key":{"type":"string","not":{"const":"widgets"},"description":"Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts"},"text":{"type":"string","description":"Row label, translated through the plugin catalog"},"description":{"type":"string","description":"Secondary text under a toggle row, translated through the plugin catalog"},"type":{"type":"string","enum":["toggle","choice","number"]},"def":{"description":"Default value: boolean for toggle, choice value for choice, number for number"},"choices":{"type":"array","items":{"type":"object","required":["value"],"properties":{"value":{"type":"string"},"text":{"type":"string"}},"additionalProperties":false}},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false},"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$"},"startupCheck":{"type":"string","description":"Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.","pattern":"^\\./.*\\.qml$"},"requires_dms":{"type":"string","description":"Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')","pattern":"^(>=?|<=?|=|>|<)\\d+\\.\\d+\\.\\d+$"},"dependencies":{"type":"array","description":"Array of required system tools/dependencies (registry metadata)","items":{"type":"string"}},"requires":{"type":"array","description":"Deprecated alias for '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"]}},{"if":{"required":["components"],"properties":{"components":{"required":["launcher"]}}},"then":{"required":["trigger"]}},{"anyOf":[{"required":["component"]},{"required":["components"]}]}],"additionalProperties":true},"isInternal":false,"tokens":2003,"sizeBytes":8008},{"name":"advanced-patterns.md","path":".agents/skills/dms-plugin-dev/references/advanced-patterns.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/advanced-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Advanced Patterns\n\nPatterns observed in production DMS plugins that go beyond the basics.\n\n## Plugin Variants\n\nCreate multiple widget instances from a single plugin definition. Each variant has its own configuration.\n\n### Manifest\n\nNo special manifest changes needed - the variant system is built into PluginComponent.\n\n### Widget with Variant Support\n\n```qml\nPluginComponent {\n    property string variantId: \"\"\n    property var variantData: ({})\n\n    property string displayText: variantData?.text || \"Default\"\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.displayText\n            }\n        }\n    }\n}\n```\n\nWidget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Settings with Variant Management\n\n```qml\nPluginSettings {\n    pluginId: \"exampleVariants\"\n\n    // Variant creation UI\n    DButton {\n        text: \"Add New Instance\"\n        onClicked: {\n            var id = \"variant_\" + Date.now()\n            root.createVariant(id, { name: \"New Instance\", text: \"Hello\" })\n        }\n    }\n\n    // Per-variant configuration\n    Repeater {\n        model: root.variants\n        delegate: Column {\n            StringSetting {\n                settingKey: modelData.id + \"_text\"\n                label: modelData.name || modelData.id\n            }\n        }\n    }\n}\n```\n\n## JavaScript Utility Files\n\nFor complex logic, split into `.js` files:\n\n### utils.js\n\n```javascript\n.pragma library\n\nfunction formatDuration(ms) {\n    if (ms < 60000) return \"just now\"\n    if (ms < 3600000) return Math.floor(ms / 60000) + \"m ago\"\n    return Math.floor(ms / 3600000) + \"h ago\"\n}\n\nfunction parseResponse(json) {\n    try {\n        return JSON.parse(json)\n    } catch (e) {\n        return null\n    }\n}\n```\n\n### Using in QML\n\n```qml\nimport \"utils.js\" as Utils\n\nItem {\n    StyledText {\n        text: Utils.formatDuration(Date.now() - timestamp)\n    }\n}\n```\n\nThe `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.\n\n## qmldir for Singleton Services\n\nFor plugins with internal singleton services:\n\n### qmldir\n\n```\nsingleton MyService 1.0 MyService.qml\n```\n\n### MyService.qml\n\n```qml\npragma Singleton\nimport QtQuick\n\nQtObject {\n    property var cache: ({})\n\n    function getData(key) {\n        return cache[key] || null\n    }\n\n    function setData(key, value) {\n        cache[key] = value\n    }\n}\n```\n\n### Using the singleton\n\n```qml\nimport \".\" as Local\n\nItem {\n    Component.onCompleted: {\n        Local.MyService.setData(\"key\", \"value\")\n    }\n}\n```\n\n## Inline Component Declarations\n\nReusable sub-components defined inline:\n\n```qml\nItem {\n    component StatusBadge: Rectangle {\n        property string label: \"\"\n        property color badgeColor: Theme.primary\n\n        width: badgeText.implicitWidth + Theme.spacingM * 2\n        height: 24\n        radius: 12\n        color: badgeColor\n\n        StyledText {\n            id: badgeText\n            anchors.centerIn: parent\n            text: label\n            color: Theme.onPrimary\n            font.pixelSize: Theme.fontSizeSmall\n        }\n    }\n\n    Row {\n        spacing: Theme.spacingS\n        StatusBadge { label: \"Running\"; badgeColor: Theme.success }\n        StatusBadge { label: \"Stopped\"; badgeColor: Theme.error }\n    }\n}\n```\n\n## Multi-Provider Adapter Pattern\n\nFor plugins supporting multiple backends (AI providers, API services):\n\n### apiAdapters.js\n\n```javascript\n.pragma library\n\nfunction createAdapter(provider) {\n    switch (provider) {\n        case \"openai\": return {\n            url: \"https://api.openai.com/v1/chat/completions\",\n            headers: (key) => ({ \"Authorization\": \"Bearer \" + key }),\n            formatRequest: (messages) => JSON.stringify({ model: \"gpt-4\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).choices[0].message.content\n        }\n        case \"anthropic\": return {\n            url: \"https://api.anthropic.com/v1/messages\",\n            headers: (key) => ({ \"x-api-key\": key, \"anthropic-version\": \"2023-06-01\" }),\n            formatRequest: (messages) => JSON.stringify({ model: \"claude-sonnet-4-20250514\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).content[0].text\n        }\n        default: return null\n    }\n}\n```\n\n## IPC Integration\n\nFor plugins that respond to keyboard shortcuts or external commands:\n\n```qml\nPluginComponent {\n    Connections {\n        target: DMSIpc\n        function onCommandReceived(command, args) {\n            if (command === \"myPlugin.toggle\") {\n                doToggle()\n            } else if (command === \"myPlugin.next\") {\n                goNext()\n            }\n        }\n    }\n}\n```\n\nExternal trigger: `dms ipc call myPlugin.toggle`\n\n## Networking with Quickshell.Networking\n\nFor API calls using the built-in networking module:\n\n```qml\nimport Quickshell.Networking\n\nItem {\n    NetworkRequest {\n        id: request\n        url: \"https://api.example.com/data\"\n        method: \"GET\"\n\n        onResponseReceived: (response) => {\n            const data = JSON.parse(response.body)\n            processData(data)\n        }\n\n        onErrorOccurred: (error) => {\n            console.error(\"Network error:\", error)\n        }\n    }\n\n    function fetchData() {\n        request.send()\n    }\n}\n```\n\n## Toast Notifications\n\nShow user feedback:\n\n```qml\nimport qs.Services\n\n// Info toast\nToastService?.showInfo(\"Operation completed\")\n\n// With title\nToastService?.showInfo(\"Plugin Name\", \"Data refreshed successfully\")\n```\n\nAlways use optional chaining since ToastService may not be available in all contexts.\n\n## Clipboard Operations\n\n```qml\nimport Quickshell\n\nfunction copyToClipboard(text) {\n    Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    ToastService?.showInfo(\"Copied to clipboard\")\n}\n```\n\nDo NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.\n\n## Multi-File Plugin Architecture\n\nLarge plugins can be split across multiple files:\n\n```\nMyPlugin/\n  plugin.json\n  Main.qml           # Main widget component\n  Settings.qml       # Settings UI\n  DetailView.qml     # Popout detail view\n  utils.js            # Utility functions\n  apiAdapter.js       # API adapter layer\n  qmldir              # Optional: singleton registrations\n```\n\nImport sibling files:\n\n```qml\n// In Main.qml\nimport \".\" as Local\n\nItem {\n    Loader {\n        source: \"DetailView.qml\"\n    }\n}\n```\n\n## Performance Tips\n\n1. Use `Proc.runCommand` with appropriate debounce for external commands\n2. Pre-cache images and thumbnails for image-heavy plugins\n3. Limit concurrent network requests\n4. Use `Timer` with reasonable intervals (don't poll faster than needed)\n5. Lazy-load heavy content (use `Loader` for complex popout content)\n6. Avoid blocking the UI thread with synchronous operations\n","isInternal":false,"tokens":1613,"sizeBytes":7044},{"name":"daemon-plugin-guide.md","path":".agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Daemon Plugin Guide\n\nDaemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.\n\n## Base Component\n\nDaemons use `PluginComponent` with no bar pills:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    // Event-driven logic goes here\n}\n```\n\n## When to Use Daemons\n\n- Monitor system events (wallpaper changes, battery level, notifications)\n- Run periodic background tasks (polling APIs, checking system state)\n- Execute scripts in response to events\n- Control shell UI via PopoutService based on conditions\n\n## Event-Driven Pattern\n\nUse `Connections` to react to service signals:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            console.log(\"Wallpaper changed to:\", SessionData.wallpaperPath)\n            runScript(SessionData.wallpaperPath)\n        }\n    }\n\n    Connections {\n        target: BatteryService\n        function onPercentageChanged() {\n            if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n                popoutService?.openBattery()\n            }\n        }\n    }\n}\n```\n\n## Available Services\n\nCommon services daemons can connect to:\n\n| Service | Signals/Properties | Description |\n|---------|-------------------|-------------|\n| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |\n| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |\n| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |\n| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |\n\nImport services from `qs.Services`.\n\n## Process Execution\n\n### Simple command with Proc\n\n```qml\nimport qs.Common\n\nPluginComponent {\n    function runScript(arg) {\n        Proc.runCommand(\n            \"myDaemon.script\",\n            [\"bash\", \"-c\", \"echo 'Processing: \" + arg + \"'\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Script output:\", stdout)\n                } else {\n                    ToastService?.showInfo(\"Script failed: exit \" + exitCode)\n                }\n            }\n        )\n    }\n}\n```\n\n### Long-running process with Process component\n\n```qml\nimport Quickshell.Io\n\nPluginComponent {\n    property string scriptPath: \"\"\n\n    Process {\n        id: proc\n        command: [\"bash\", scriptPath]\n        running: false\n\n        stdout: StdioCollector {\n            onTextReceived: (text) => {\n                console.log(\"stdout:\", text)\n            }\n        }\n\n        stderr: StdioCollector {\n            onTextReceived: (text) => {\n                console.error(\"stderr:\", text)\n            }\n        }\n\n        onExited: (exitCode) => {\n            if (exitCode !== 0) {\n                ToastService?.showInfo(\"Process failed: exit \" + exitCode)\n            }\n        }\n    }\n\n    function startProcess() {\n        if (scriptPath && !proc.running) {\n            proc.running = true\n        }\n    }\n}\n```\n\n## Timer-Based Polling\n\n```qml\nPluginComponent {\n    Timer {\n        interval: 60000  // every minute\n        running: true\n        repeat: true\n        onTriggered: checkStatus()\n    }\n\n    function checkStatus() {\n        Proc.runCommand(\n            \"myDaemon.check\",\n            [\"sh\", \"-c\", \"systemctl is-active myservice\"],\n            (stdout, exitCode) => {\n                const active = stdout.trim() === \"active\"\n                PluginService.setGlobalVar(\"myDaemon\", \"serviceActive\", active)\n            }\n        )\n    }\n}\n```\n\n## Data Persistence\n\nDaemons access PluginService directly (it's injected via PluginComponent):\n\n```qml\nPluginComponent {\n    property string configuredScript: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                configuredScript = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n}\n```\n\n## PopoutService Usage\n\nDaemons can control shell UI via the injected popoutService:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    function showAlert() {\n        popoutService?.openNotificationCenter()\n    }\n\n    function openSettings() {\n        popoutService?.openSettings()\n    }\n}\n```\n\nSee [popout-service-reference.md](popout-service-reference.md) for the full API.\n\n## Complete Example\n\nBased on the WallpaperWatcherDaemon:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport Quickshell.Io\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property string scriptPath: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                scriptPath = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            if (scriptPath) {\n                runWallpaperScript(SessionData.wallpaperPath)\n            }\n        }\n    }\n\n    function runWallpaperScript(wallpaperPath) {\n        console.log(\"[WallpaperWatcher] Running script:\", scriptPath, wallpaperPath)\n\n        Proc.runCommand(\n            \"wallpaperWatcher.run\",\n            [\"bash\", scriptPath, wallpaperPath],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"[WallpaperWatcher] Script output:\", stdout)\n                } else {\n                    console.error(\"[WallpaperWatcher] Script failed:\", exitCode)\n                    ToastService?.showInfo(\"Wallpaper script failed\")\n                }\n            }\n        )\n    }\n\n    Component.onCompleted: {\n        console.log(\"[WallpaperWatcher] Daemon started\")\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"wallpaperWatcher\",\n    \"name\": \"Wallpaper Watcher\",\n    \"description\": \"Runs a script when the wallpaper changes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper-automation\"],\n    \"component\": \"./WallpaperWatcher.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n```\n","isInternal":false,"tokens":1421,"sizeBytes":6546},{"name":"dash-plugin-guide.md","path":".agents/skills/dms-plugin-dev/references/dash-plugin-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/dash-plugin-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Dash Plugin Guide\n\nThe dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).\n\nRequires DMS 1.7 or later: set `\"requires_dms\": \">=1.7.0\"`.\n\n## Picking the manifest form\n\n| Plugin provides | Manifest |\n|-----------------|----------|\n| Tab only | `\"type\": \"dash\"`, `\"component\": \"./Tab.qml\"` |\n| Card only | `\"type\": \"dashCard\"`, `\"component\": \"./Card.qml\"` |\n| Tab and card | `\"type\": \"composite\"`, `\"components\": { \"dash\": ..., \"dashCard\": ... }` |\n| Dash plus bar/dock widget, daemon, ... | `\"type\": \"composite\"` with every surface in `components` |\n\nAdd the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).\n\n## Rules that trip people up\n\n- Only enabled plugins appear in the dash. There is no \"disabled\" placeholder.\n- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).\n- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash(\"plugin_\" + pluginId, x, y, width, section, screen)`.\n- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.\n- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.\n- `dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.\n- `widgets` is a reserved option key.\n- Use `I18n.trFor(\"<plugin id literal>\", \"...\")` for every string.\n\n## Tab: DashTabComponent\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    focusTarget: button\n    implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)\n    menuActions: [\n        {\n            label: I18n.trFor(\"myPlugin\", \"Refresh\"),\n            iconName: \"refresh\",\n            action: () => root.refresh()\n        }\n    ]\n\n    function refresh() {\n    }\n\n    function restoreFocus() {\n        button.forceActiveFocus(Qt.OtherFocusReason);\n    }\n\n    function handleKeyEvent(event) {\n        if (event.key !== Qt.Key_R)\n            return false;\n        refresh();\n        return true;\n    }\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.refresh()\n    }\n\n    Column {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingM\n\n        StyledText {\n            text: root.options.compact ? \"\" : I18n.trFor(\"myPlugin\", \"Hello\")\n            color: Theme.surfaceText\n        }\n\n        DButton {\n            id: button\n            text: I18n.trFor(\"myPlugin\", \"Refresh\")\n            onClicked: root.refresh()\n        }\n    }\n}\n```\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (`dashVisible`, `requestTab(id)`, `editMode`), `editMode`, `live`. Also `targetScreen`, `active` and `rowBudget` when the tab declares them.\n\nBase provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`.\n\nOptional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`.\n\nFor tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview.\n\n### Editable widgets in a tab\n\nAssign a `DashWidgetGrid` (from `qs.Modules.DankDash`) to `widgetGrid` to get Edit, Add widget, Reset and Clear All:\n\n```qml\nDashTabComponent {\n    id: tab\n    widgetGrid: grid\n    implicitHeight: grid.implicitHeight\n    focusTarget: grid.focusTarget\n\n    DashWidgetGrid {\n        id: grid\n        width: parent.width\n        entryId: tab.entryId\n        live: tab.live\n        editMode: tab.editMode\n        definitions: [\n            { id: \"main\", text: I18n.trFor(\"myPlugin\", \"Main\"), icon: \"star\", component: mainWidget, w: 2, h: 2, minW: 1, minH: 1, maxW: 4, maxH: 3 }\n        ]\n    }\n\n    Component {\n        id: mainWidget\n        Card {}\n    }\n}\n```\n\nThe grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.\n\n## Card: DashCardComponent\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashCardComponent {\n    id: root\n\n    readonly property bool wide: width >= DashMetrics.gridRowUnit * 2\n    readonly property bool tall: height >= DashMetrics.gridRowUnit * 2\n\n    tone: options.tone ?? \"\"\n\n    SystemClock {\n        id: clock\n        enabled: root.live\n        precision: SystemClock.Minutes\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: clock.date.toLocaleTimeString(I18n.locale(), \"hh:mm\")\n        font.pixelSize: root.tall ? Theme.fontSizeDisplay : Theme.fontSizeXLarge\n        color: root.accentColor\n    }\n}\n```\n\nBase (`Card`) provides: `pad`, `title`, `tone` (`\"\"`, `\"primary\"`, `\"secondary\"`, `\"tertiary\"`), `containerColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor`, `chipColor`, `tinted`, `clickable`, `signal clicked`, `options`, `pluginData`, `getData`, `setData`.\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`; bound: `live` (overview visible), `interactive` (false in edit mode).\n\nClicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.\n\nUse the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.\n\n## Testing\n\n```bash\ndms ipc call plugins reload myPlugin\ndms ipc call dash open plugin_myPlugin\n```\n\nWorking examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).\n","isInternal":false,"tokens":1693,"sizeBytes":6546},{"name":"data-persistence-guide.md","path":".agents/skills/dms-plugin-dev/references/data-persistence-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/data-persistence-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Data Persistence Guide\n\nDMS plugins have three tiers of data persistence, each suited for different use cases.\n\n## Tier 1: Plugin Data (Settings)\n\nPersisted to `settings.json`. Use for user preferences and configuration.\n\n### Saving\n\n```qml\npluginService.savePluginData(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar value = pluginService.loadPluginData(pluginId, \"key\", defaultValue)\n```\n\n### Reactive Access via pluginData\n\n`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n    property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true\n}\n```\n\n### Reacting to Settings Changes\n\nWhen settings are changed (e.g., from the settings UI), react with `Connections`:\n\n```qml\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        showIcon = pluginService.loadPluginData(pluginId, \"showIcon\", true)\n    }\n}\n```\n\n## Tier 2: Plugin State\n\nPersisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).\n\n### Saving\n\n```qml\npluginService.savePluginState(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar state = pluginService.loadPluginState(pluginId, \"key\", defaultValue)\n```\n\n### Additional Methods\n\n```qml\npluginService.clearPluginState(pluginId)\npluginService.removePluginStateKey(pluginId, \"key\")\n```\n\n### Example: Persistent History\n\n```qml\nItem {\n    property var history: []\n\n    Component.onCompleted: {\n        history = pluginService?.loadPluginState(pluginId, \"history\", []) || []\n    }\n\n    function addToHistory(entry) {\n        history.unshift({\n            text: entry,\n            timestamp: Date.now()\n        })\n        if (history.length > 100) history = history.slice(0, 100)\n        pluginService?.savePluginState(pluginId, \"history\", history)\n    }\n\n    function clearHistory() {\n        history = []\n        pluginService?.removePluginStateKey(pluginId, \"history\")\n    }\n}\n```\n\n## Tier 3: Global Variables (Runtime Only)\n\nNOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).\n\n### Using PluginGlobalVar Component\n\n```qml\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            // ...\n            StyledText {\n                text: \"Count: \" + globalCounter.value\n            }\n\n            MouseArea {\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `varName` | string | Required: name of the global variable |\n| `defaultValue` | any | Optional: default if not set |\n| `value` | any | Readonly: current value |\n\n**Methods:**\n- `set(newValue)` - update the value (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\n```qml\nimport qs.Services\n\nproperty int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\nConnections {\n    target: PluginService\n    function onGlobalVarChanged(pluginId, varName) {\n        if (pluginId === \"myPlugin\" && varName === \"counter\") {\n            counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n        }\n    }\n}\n\nfunction increment() {\n    var current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n}\n```\n\n## Decision Matrix\n\n| Need | API | Persisted | Scope |\n|------|-----|-----------|-------|\n| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |\n| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |\n| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |\n| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |\n\n## Plugin Path\n\nRetrieve a plugin's installation directory at runtime:\n\n```qml\nvar dir = pluginService.getPluginPath(pluginId)\n```\n\nReturns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.\n\n## Important Notes\n\n1. **pluginData is reactive** - bindings update automatically when data changes\n2. **Global vars are NOT persistent** - they reset when the shell restarts\n3. **State vs Data** - data is for user-facing settings, state is for internal runtime data\n4. **Null safety** - always check `pluginService` is not null before calling methods\n5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin\n6. **Performance** - global vars are efficient for frequent updates; settings writes are batched\n","isInternal":false,"tokens":1226,"sizeBytes":5280},{"name":"desktop-plugin-guide.md","path":".agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Desktop Plugin Guide\n\nDesktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.\n\n## Base Component\n\nDesktop widgets use a plain `Item` with injected properties:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n\n        // Your content here\n    }\n}\n```\n\n## Injected Properties\n\nThese are set automatically by the DesktopPluginWrapper:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `pluginService` | object | PluginService reference for data persistence |\n| `pluginId` | string | Plugin's unique identifier |\n| `editMode` | bool | `true` when user is dragging/resizing |\n| `widgetWidth` | real | Current widget container width |\n| `widgetHeight` | real | Current widget container height |\n\n## Optional Properties\n\nDefine these on your root item to customize behavior:\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `minWidth` | real | 100 | Minimum allowed width during resize |\n| `minHeight` | real | 100 | Minimum allowed height during resize |\n\n## Position and Size Persistence\n\nPosition (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.\n\n## Edit Mode\n\nWhen `editMode` is true, the user is repositioning or resizing. Use this to:\n- Show visual indicators (borders, handles)\n- Disable interactive elements to prevent accidental actions\n- Display additional controls\n\n```qml\nRectangle {\n    anchors.fill: parent\n    border.color: root.editMode ? Theme.primary : \"transparent\"\n    border.width: root.editMode ? 2 : 0\n\n    MouseArea {\n        anchors.fill: parent\n        enabled: !root.editMode\n        onClicked: doSomething()\n    }\n}\n```\n\n## Loading and Saving Data\n\nUse the injected `pluginService` for data persistence:\n\n```qml\nproperty string displayMode: {\n    if (!pluginService) return \"default\"\n    return pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n}\n\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        root.displayMode = pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n    }\n}\n\nfunction saveMode(mode) {\n    pluginService?.savePluginData(pluginId, \"displayMode\", mode)\n}\n```\n\n## Settings Component\n\nDesktop plugin settings use the same `PluginSettings` wrapper as other types:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myDesktopWidget\"\n\n    SliderSetting {\n        settingKey: \"opacity\"\n        label: \"Opacity\"\n        description: \"Widget background opacity\"\n        defaultValue: 85\n        minimum: 10\n        maximum: 100\n        unit: \"%\"\n    }\n\n    SelectionSetting {\n        settingKey: \"style\"\n        label: \"Display Style\"\n        options: [\n            { label: \"Compact\", value: \"compact\" },\n            { label: \"Expanded\", value: \"expanded\" }\n        ]\n        defaultValue: \"compact\"\n    }\n}\n```\n\n## User Interaction\n\nDesktop widgets support:\n1. **Drag** - click and drag anywhere (in edit mode)\n2. **Resize** - drag bottom-right corner handle (in edit mode)\n3. **Edit mode toggle** - via the desktop edit button\n\n## Complete Example\n\nBased on the ExampleDesktopClock pattern:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 250\n    property real widgetHeight: 250\n    property real minWidth: 150\n    property real minHeight: 150\n\n    property string clockStyle: {\n        if (!pluginService) return \"digital\"\n        return pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n    }\n\n    property real bgOpacity: {\n        if (!pluginService) return 0.85\n        var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n        return val / 100\n    }\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId !== pluginId) return\n            clockStyle = pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n            var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n            bgOpacity = val / 100\n        }\n    }\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: root.bgOpacity\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Column {\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatTime(new Date(), \"hh:mm:ss\")\n                color: Theme.surfaceText\n                font.pixelSize: root.widgetWidth * 0.15\n                font.weight: Font.Bold\n            }\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatDate(new Date(), \"ddd, MMM d\")\n                color: Theme.onSurfaceVariant\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    Timer {\n        interval: 1000\n        running: true\n        repeat: true\n        onTriggered: root.widgetWidth = root.widgetWidth // force update\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"myDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"Analog and digital clock for the desktop\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./ClockWidget.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n","isInternal":false,"tokens":1421,"sizeBytes":6296},{"name":"launcher-plugin-guide.md","path":".agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Launcher Plugin Guide\n\nLauncher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.\n\n## Base Component\n\nLaunchers use a plain `Item` (not PluginComponent):\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        // Return array of items\n        return []\n    }\n\n    function executeItem(item) {\n        // Handle item selection\n    }\n}\n```\n\n## Required Interface\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `pluginService` | property | Injected PluginService reference (declare as `null`) |\n| `trigger` | property | Trigger string for activation |\n| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |\n| `getItems(query)` | function | Return array of items matching query |\n| `executeItem(item)` | function | Handle item selection |\n\n## Item Structure\n\nEach item returned by `getItems()`:\n\n```javascript\n{\n    name: \"Item Display Name\",       // Required: shown in launcher\n    icon: \"material:star\",           // Optional: icon specification\n    comment: \"Description text\",     // Required: subtitle text\n    action: \"type:data\",             // Required: action identifier\n    categories: [\"MyPlugin\"],        // Required: array with plugin category\n    imageUrl: \"https://...\"          // Optional: image for tile view\n}\n```\n\n## Icon Types\n\n### 1. Material Design Icons\n\n```javascript\n{ icon: \"material:lightbulb\" }\n{ icon: \"material:terminal\" }\n{ icon: \"material:translate\" }\n```\n\nUses the Material Symbols Rounded font.\n\n### 2. Unicode / Emoji Icons\n\n```javascript\n{ icon: \"unicode:smile_face\" }\n```\n\nRendered at 70-80% of icon size with theming.\n\n### 3. Desktop Theme Icons\n\n```javascript\n{ icon: \"firefox\" }\n{ icon: \"folder\" }\n```\n\nUses the user's installed icon theme.\n\n### 4. No Icon\n\nOmit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.\n\n## Trigger System\n\n**Custom trigger** (items only appear when trigger is typed):\n\n```json\n{ \"trigger\": \"#\" }\n```\n\n- Type `#` alone: shows all plugin items\n- Type `# query`: filters plugin items by query\n- The query string (without trigger) is passed to `getItems(query)`\n\n**No trigger** (items always visible alongside regular apps):\n\n```json\n{ \"trigger\": \"\" }\n```\n\nSave empty trigger at runtime:\n```qml\nComponent.onCompleted: {\n    trigger = pluginService?.loadPluginData(pluginId, \"trigger\", \"#\") ?? \"#\"\n}\n```\n\n## Action Execution\n\nParse action strings in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            ToastService?.showInfo(actionData)\n            break\n        case \"copy\":\n            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", actionData])\n            ToastService?.showInfo(\"Copied to clipboard\")\n            break\n        case \"exec\":\n            Quickshell.execDetached(actionData.split(\" \"))\n            break\n        case \"url\":\n            Quickshell.execDetached([\"xdg-open\", actionData])\n            break\n        default:\n            console.warn(\"Unknown action type:\", actionType)\n    }\n}\n```\n\n## Search / Filtering\n\nThe `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).\n\n```qml\nfunction getItems(query) {\n    const allItems = [\n        { name: \"Calculator\", icon: \"material:calculate\",\n          comment: \"Open calculator\", action: \"exec:gnome-calculator\",\n          categories: [\"Tools\"] },\n        { name: \"Terminal\", icon: \"material:terminal\",\n          comment: \"Open terminal\", action: \"exec:alacritty\",\n          categories: [\"Tools\"] }\n    ]\n\n    if (!query || query.length === 0) return allItems\n\n    const q = query.toLowerCase()\n    return allItems.filter(item =>\n        item.name.toLowerCase().includes(q) ||\n        item.comment.toLowerCase().includes(q)\n    )\n}\n```\n\n## Context Menu Actions\n\nAdd right-click actions to launcher items:\n\n```qml\nfunction getContextMenuActions(item) {\n    return [\n        { name: \"Copy\", icon: \"material:content_copy\",\n          action: \"copy:\" + item.name },\n        { name: \"Open in Browser\", icon: \"material:open_in_new\",\n          action: \"url:\" + item.url }\n    ]\n}\n```\n\nContext menu actions use the same `executeItem()` handler.\n\n## Image Tile View\n\nFor image-heavy launchers (GIF search, sticker pickers), use tile view:\n\nIn `plugin.json`:\n```json\n{\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true\n}\n```\n\nIn items:\n```javascript\n{\n    name: \"Image Title\",\n    imageUrl: \"https://example.com/image.png\",\n    comment: \"Description\",\n    action: \"copy:https://example.com/image.png\",\n    categories: [\"MyPlugin\"]\n}\n```\n\n## State Persistence\n\nFor plugins with persistent state (notes, history, favorites):\n\n```qml\nproperty var notes: []\n\nComponent.onCompleted: {\n    const saved = pluginService?.loadPluginState(pluginId, \"notes\", [])\n    if (saved) notes = saved\n}\n\nfunction addNote(text) {\n    notes.push({ text: text, timestamp: Date.now() })\n    pluginService?.savePluginState(pluginId, \"notes\", notes)\n    itemsChanged()\n}\n```\n\nUse `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.\n\n## Settings for Trigger Configuration\n\nProvide a PluginSettings component for trigger customization:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myLauncher\"\n\n    StringSetting {\n        settingKey: \"trigger\"\n        label: \"Trigger\"\n        description: \"Type this prefix to activate the launcher plugin\"\n        placeholder: \"#\"\n        defaultValue: \"#\"\n    }\n\n    ToggleSetting {\n        settingKey: \"noTrigger\"\n        label: \"Always Visible\"\n        description: \"Show items alongside regular apps (no trigger needed)\"\n        defaultValue: false\n    }\n}\n```\n\n## Complete Example\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"!\"\n\n    signal itemsChanged()\n\n    property var commands: [\n        { name: \"Lock Screen\", icon: \"material:lock\",\n          comment: \"Lock the session\", action: \"exec:loginctl lock-session\" },\n        { name: \"Screenshot\", icon: \"material:screenshot_monitor\",\n          comment: \"Take a screenshot\", action: \"exec:grim\" },\n        { name: \"File Manager\", icon: \"material:folder\",\n          comment: \"Open file manager\", action: \"exec:nautilus\" }\n    ]\n\n    function getItems(query) {\n        if (!query) return commands\n        const q = query.toLowerCase()\n        return commands.filter(c =>\n            c.name.toLowerCase().includes(q) ||\n            c.comment.toLowerCase().includes(q)\n        )\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"exec\") {\n            Quickshell.execDetached(data.split(\" \"))\n        }\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"quickCommands\", \"trigger\", \"!\")\n        }\n    }\n}\n```\n","isInternal":false,"tokens":1721,"sizeBytes":7372},{"name":"plugin-manifest-reference.md","path":".agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Plugin Manifest Reference (plugin.json)\n\n## Required Fields\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |\n| `name` | string | Human-readable name | Non-empty |\n| `description` | string | Short description (shown in UI) | Non-empty |\n| `version` | string | Semantic version | Pattern `^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$` |\n| `author` | string | Creator name or email | Non-empty |\n| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `dash`, `dashCard`, `composite` |\n| `capabilities` | array | Plugin capabilities | At least 1 string item |\n\nOne of `component` or `components` is required (not both):\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |\n| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |\n\n## Conditional Requirements\n\n| Condition | Required Field | Description |\n|-----------|---------------|-------------|\n| `type: \"launcher\"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |\n| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `icon` | string | Material Design icon name (displayed in plugin list UI) |\n| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |\n| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |\n| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\\|<=?\\|=\\|>\\|<)\\d+\\.\\d+\\.\\d+$` |\n| `dependencies` | array | System tool dependencies (e.g., `[\"curl\", \"jq\"]`). Registry metadata. |\n| `requires` | array | Deprecated alias for `dependencies` |\n| `permissions` | array | Required permissions |\n| `trigger` | string | Launcher trigger string (required for launcher type) |\n| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |\n\n## Permissions\n\n| Permission | Description | Enforced |\n|------------|-------------|----------|\n| `settings_read` | Read plugin configuration | No (not currently enforced) |\n| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |\n| `process` | Execute system commands | No (not currently enforced) |\n| `network` | Network access | No (not currently enforced) |\n\nIf your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.\n\n## Capabilities\n\nCapabilities are free-form strings that describe what the plugin does. Common values:\n\n- `dankbar-widget` - general bar widget\n- `control-center` - integrates with Control Center\n- `monitoring` - system/service monitoring\n- `launcher` - launcher search provider\n- `desktop-widget` - desktop background widget\n- `dash-tab` - dash tab\n- `dash-card` - dash overview card\n- `ai` - AI/LLM integration\n- `slideout` - uses slideout panel\n\n## Startup Check\n\nThe `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\n## Components (Composite Plugins)\n\nThe `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nValid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.\n\n## Dash Block\n\nPlugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.\n\n```json\n\"dash\": {\n    \"icon\": \"timer\",\n    \"title\": \"Stopwatch\",\n    \"card\": { \"title\": \"Progress\", \"icon\": \"hourglass_bottom\", \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 4, \"maxH\": 2 },\n    \"options\": [\n        { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false, \"description\": \"Hide the secondary line\" },\n        { \"key\": \"style\", \"text\": \"Style\", \"type\": \"choice\", \"def\": \"bars\", \"choices\": [{ \"value\": \"bars\", \"text\": \"Bars\" }, { \"value\": \"dots\", \"text\": \"Dots\" }] },\n        { \"key\": \"limit\", \"text\": \"Items\", \"type\": \"number\", \"def\": 5, \"min\": 1, \"max\": 20, \"step\": 1 }\n    ]\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |\n| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |\n| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |\n| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |\n| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |\n\n## Dash Examples\n\nStandalone tab and standalone card use the single file form:\n\n```json\n{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"dash\": { \"title\": \"Stopwatch\" },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\n```json\n{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"dash\": { \"card\": { \"w\": 2, \"h\": 1, \"maxW\": 4, \"maxH\": 2 } },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\nA tab plus a card, or dash surfaces plus a bar widget, use `components` with `\"type\": \"composite\"`.\n\n## Complete Example\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin demonstrating all fields\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\", \"control-center\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"dependencies\": [\"curl\", \"jq\"],\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\", \"network\"]\n}\n```\n\n## Launcher Example\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Search and execute custom actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n## JSON Schema\n\nThe complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:\n\n```bash\n# Using python\npython3 -c \"\nimport json, jsonschema\nschema = json.load(open('path/to/plugin-schema.json'))\nmanifest = json.load(open('plugin.json'))\njsonschema.validate(manifest, schema)\nprint('Valid!')\n\"\n\n# Using jq (syntax check only)\njq . plugin.json\n```\n\n## Additional Properties\n\nThe schema allows additional properties (`\"additionalProperties\": true`), so plugins can include custom fields. Common custom fields seen in production plugins:\n\n- `viewMode` - launcher display mode (`\"tile\"` for image grids)\n- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)\n","isInternal":false,"tokens":2476,"sizeBytes":9338},{"name":"popout-service-reference.md","path":".agents/skills/dms-plugin-dev/references/popout-service-reference.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/popout-service-reference.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# PopoutService Reference\n\nThe `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.\n\n## Setup\n\nDeclare the property in your component for injection to work:\n\n```qml\nproperty var popoutService: null\n```\n\nWithout this declaration, injection fails with: `Cannot assign to non-existent property \"popoutService\"`\n\n## Popouts (DankPopout-based)\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |\n| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |\n| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |\n| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |\n| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |\n| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |\n| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |\n| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |\n\n## Modals (DankModal-based)\n\n| Modal | Show | Hide | Notes |\n|-------|------|------|-------|\n| Settings | `openSettings()` | `closeSettings()` | Full settings interface |\n| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |\n| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |\n| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |\n| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |\n| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |\n| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |\n| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |\n| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |\n\n## Slideouts\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |\n\n## Usage Examples\n\n### Simple toggle\n\n```qml\nMouseArea {\n    onClicked: popoutService?.toggleControlCenter()\n}\n```\n\n### Conditional popout\n\n```qml\nConnections {\n    target: BatteryService\n    function onPercentageChanged() {\n        if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n            popoutService?.openBattery()\n        }\n    }\n}\n```\n\n### Context menu with multiple actions\n\n```qml\nMouseArea {\n    acceptedButtons: Qt.LeftButton | Qt.RightButton\n    onClicked: (mouse) => {\n        if (mouse.button === Qt.RightButton) contextMenu.popup()\n        else popoutService?.toggleControlCenter()\n    }\n}\n\nMenu {\n    id: contextMenu\n    MenuItem { text: \"Settings\"; onClicked: popoutService?.openSettings() }\n    MenuItem { text: \"Notifications\"; onClicked: popoutService?.toggleNotificationCenter() }\n    MenuItem { text: \"Power\"; onClicked: popoutService?.openPowerMenu() }\n}\n```\n\n### Position-aware toggle (from bar pill)\n\nSome toggle functions accept position parameters for proper popout placement:\n\n```qml\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\n## Best Practices\n\n1. **Always use optional chaining** (`?.`) - the service may not be injected yet\n2. **Check feature availability** before opening feature-specific popouts:\n   ```qml\n   if (BatteryService.batteryAvailable) {\n       popoutService?.openBattery()\n   }\n   ```\n3. **Lazy loading** - first access may activate lazy loaders; this is normal\n4. **Popouts are shared** - avoid opening conflicting popouts simultaneously\n5. **User intent** - only trigger popouts from user actions or critical system events\n6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters\n\n## Injection Locations\n\nThe service is injected at these points:\n- `DMSShell.qml` - daemon plugins\n- `WidgetHost.qml` - widget plugins in left/right bar sections\n- `CenterSection.qml` - center bar widgets\n- `PluginsTab.qml` - settings components\n","isInternal":false,"tokens":1028,"sizeBytes":4281},{"name":"settings-components-reference.md","path":".agents/skills/dms-plugin-dev/references/settings-components-reference.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/settings-components-reference.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Settings Components Reference\n\nAll plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.\n\n## PluginSettings Wrapper\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"  // Required: must match plugin.json id\n\n    // Setting components go here\n}\n```\n\n**Important:** The plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json for the settings UI to render. Without it, users see an error.\n\n**PluginSettings provides to children:**\n- `saveValue(key, value)` - save a setting value\n- `loadValue(key, defaultValue)` - load a setting value\n- `saveState(key, value)` - save plugin state (separate file)\n- `loadState(key, defaultValue)` - load plugin state\n- `clearState()` - clear all plugin state\n- Variant management functions (for variant plugins)\n\n## StringSetting\n\nText input field.\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"        // Required: storage key\n    label: \"API Key\"            // Required: display label\n    description: \"Your API key\" // Optional: help text\n    placeholder: \"sk-...\"      // Optional: input placeholder\n    defaultValue: \"\"            // Optional: default (default: \"\")\n}\n```\n\n**Layout:** Vertical stack - label, description, input field.\n\n## ToggleSetting\n\nBoolean toggle switch.\n\n```qml\nToggleSetting {\n    settingKey: \"notifications\"    // Required: storage key\n    label: \"Enable Notifications\"  // Required: display label\n    description: \"Show alerts\"     // Optional: help text\n    defaultValue: true             // Optional: default (default: false)\n}\n```\n\n**Layout:** Horizontal - label/description on left, toggle on right.\n\n## SelectionSetting\n\nDropdown menu.\n\n```qml\nSelectionSetting {\n    settingKey: \"theme\"           // Required: storage key\n    label: \"Theme\"                // Required: display label\n    description: \"Color scheme\"   // Optional: help text\n    options: [                    // Required: array of options\n        { label: \"Dark\", value: \"dark\" },\n        { label: \"Light\", value: \"light\" },\n        { label: \"Auto\", value: \"auto\" }\n    ]\n    defaultValue: \"dark\"          // Optional: default value\n}\n```\n\nOptions can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.\n\n**Layout:** Horizontal - label/description on left, dropdown on right.\n\n**Reacting to changes:**\n```qml\nSelectionSetting {\n    settingKey: \"updateInterval\"\n    label: \"Update Interval\"\n    options: [\n        { label: \"1 minute\", value: \"60\" },\n        { label: \"5 minutes\", value: \"300\" }\n    ]\n    defaultValue: \"300\"\n    onValueChanged: (newValue) => {\n        console.log(\"Interval changed to:\", newValue)\n    }\n}\n```\n\n## SliderSetting\n\nNumeric slider with min/max.\n\n```qml\nSliderSetting {\n    settingKey: \"opacity\"      // Required: storage key\n    label: \"Opacity\"           // Required: display label\n    description: \"Background\"  // Optional: help text\n    defaultValue: 85           // Optional: default value\n    minimum: 0                 // Required: min value\n    maximum: 100               // Required: max value\n    unit: \"%\"                  // Optional: unit label shown after value\n    leftIcon: \"dark_mode\"      // Optional: Material icon on left\n    rightIcon: \"light_mode\"    // Optional: Material icon on right\n}\n```\n\n## ColorSetting\n\nColor picker.\n\n```qml\nColorSetting {\n    settingKey: \"accentColor\"      // Required: storage key\n    label: \"Accent Color\"          // Required: display label\n    description: \"Custom accent\"   // Optional: help text\n    defaultValue: \"#ff5722\"        // Optional: default hex color\n}\n```\n\nDisplays a color swatch that opens a color picker dialog.\n\n## ListSetting\n\nManage a list of items with manual add/remove. Use when you need custom UI for adding items.\n\n```qml\nListSetting {\n    id: itemList\n    settingKey: \"items\"           // Required: storage key\n    label: \"Saved Items\"          // Required: display label\n    description: \"Your items\"     // Optional: help text\n    defaultValue: []              // Optional: default array\n    delegate: Component {         // Optional: custom item display\n        StyledRect {\n            width: parent.width\n            height: 40\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                anchors.left: parent.left\n                anchors.leftMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                text: modelData.name\n                color: Theme.surfaceText\n            }\n\n            Rectangle {\n                anchors.right: parent.right\n                anchors.rightMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                width: 60\n                height: 28\n                color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                radius: Theme.cornerRadius\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: \"Remove\"\n                    color: Theme.errorText\n                    font.pixelSize: Theme.fontSizeSmall\n                    font.weight: Font.Medium\n                }\n\n                MouseArea {\n                    id: removeArea\n                    anchors.fill: parent\n                    hoverEnabled: true\n                    cursorShape: Qt.PointingHandCursor\n                    onClicked: itemList.removeItem(index)\n                }\n            }\n        }\n    }\n}\n```\n\n**Methods:**\n- `addItem(item)` - add an item to the list\n- `removeItem(index)` - remove item at index\n\n## ListSettingWithInput\n\nComplete list management with built-in form. Best for collecting structured data.\n\n```qml\nListSettingWithInput {\n    settingKey: \"locations\"        // Required: storage key\n    label: \"Locations\"             // Required: display label\n    description: \"Track zones\"     // Optional: help text\n    defaultValue: []               // Optional: default array\n    fields: [                      // Required: field definitions\n        {\n            id: \"name\",            // Required: key in saved object\n            label: \"Name\",         // Required: column header\n            placeholder: \"Home\",   // Optional: input placeholder\n            width: 150,            // Optional: column width (default: 200)\n            required: true         // Optional: must have value to add\n        },\n        {\n            id: \"timezone\",\n            label: \"Timezone\",\n            placeholder: \"America/New_York\",\n            width: 200,\n            required: true\n        }\n    ]\n}\n```\n\nAutomatically generates: column headers, input fields, add button with validation, list display, remove buttons.\n\n## Mixing Custom UI with Settings\n\nYou can interleave regular QML elements with setting components:\n\n```qml\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"name\"\n        label: \"Display Name\"\n    }\n\n    StyledText {\n        width: parent.width\n        text: \"Advanced Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n        topPadding: Theme.spacingL\n    }\n\n    ToggleSetting {\n        settingKey: \"debug\"\n        label: \"Debug Mode\"\n        defaultValue: false\n    }\n}\n```\n\n## Default Values\n\nDefine sensible defaults in every setting component. The default is used when no saved value exists:\n\n```qml\nStringSetting { settingKey: \"text\"; defaultValue: \"Hello\" }\nToggleSetting { settingKey: \"enabled\"; defaultValue: true }\nSelectionSetting { settingKey: \"mode\"; defaultValue: \"auto\" }\nSliderSetting { settingKey: \"opacity\"; defaultValue: 85 }\nColorSetting { settingKey: \"color\"; defaultValue: \"#ff5722\" }\nListSetting { settingKey: \"items\"; defaultValue: [] }\nListSettingWithInput { settingKey: \"data\"; defaultValue: [] }\n```\n","isInternal":false,"tokens":1773,"sizeBytes":8124},{"name":"theme-reference.md","path":".agents/skills/dms-plugin-dev/references/theme-reference.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/theme-reference.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Theme Property Reference\n\nAll theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.\n\n## Font Sizes\n\n```qml\nTheme.fontSizeSmall     // 12px (scaled by SettingsData.fontScale)\nTheme.fontSizeMedium    // 14px (scaled)\nTheme.fontSizeLarge     // 16px (scaled)\nTheme.fontSizeXLarge    // 20px (scaled)\n```\n\n## Icon Sizes\n\n```qml\nTheme.iconSizeSmall     // 16px\nTheme.iconSize          // 24px (default)\nTheme.iconSizeLarge     // 32px\n```\n\n## Spacing\n\n```qml\nTheme.spacingXS         // Extra small\nTheme.spacingS          // Small\nTheme.spacingM          // Medium\nTheme.spacingL          // Large\nTheme.spacingXL         // Extra large\n```\n\n## Border Radius\n\n```qml\nTheme.cornerRadius      // Standard\nTheme.cornerRadiusSmall // Smaller\nTheme.cornerRadiusLarge // Larger\n```\n\n## Surface Colors\n\n```qml\nTheme.surface\nTheme.surfaceContainerLow\nTheme.surfaceContainer\nTheme.surfaceContainerHigh\nTheme.surfaceContainerHighest\n```\n\n## Text Colors\n\n```qml\nTheme.onSurface          // Primary text on surface\nTheme.onSurfaceVariant   // Secondary text on surface\nTheme.surfaceText        // Alias for primary surface text\nTheme.surfaceVariantText // Alias for secondary surface text\nTheme.outline            // Border/divider color\n```\n\n## Semantic Colors\n\n```qml\nTheme.primary\nTheme.onPrimary\nTheme.secondary\nTheme.onSecondary\nTheme.error\nTheme.errorHover\nTheme.errorText\nTheme.warning\nTheme.success\n```\n\n## Special Functions\n\n```qml\nTheme.popupBackground()  // Popup background with proper opacity\n```\n\n## Common Widget Patterns\n\n### Icon with Text\n\n```qml\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nRow {\n    spacing: Theme.spacingS\n\n    DIcon {\n        name: \"icon_name\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.iconSize\n    }\n\n    StyledText {\n        text: \"Label\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.fontSizeMedium\n    }\n}\n```\n\n### Container with Border\n\n```qml\nRectangle {\n    color: Theme.surfaceContainerHigh\n    radius: Theme.cornerRadius\n    border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)\n    border.width: 1\n}\n```\n\n### Hover Effect\n\n```qml\nRectangle {\n    id: container\n    color: Theme.surfaceContainerHigh\n\n    MouseArea {\n        anchors.fill: parent\n        hoverEnabled: true\n        onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        onExited: container.color = Theme.surfaceContainerHigh\n    }\n}\n```\n\n### Clickable Pill\n\n```qml\nStyledRect {\n    width: content.implicitWidth + Theme.spacingM * 2\n    height: parent.widgetThickness\n    radius: Theme.cornerRadius\n    color: mouseArea.containsMouse\n        ? Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        : Theme.surfaceContainerHigh\n\n    MouseArea {\n        id: mouseArea\n        anchors.fill: parent\n        hoverEnabled: true\n        cursorShape: Qt.PointingHandCursor\n    }\n\n    Row {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingS\n\n        DIcon {\n            name: \"star\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.iconSize\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        StyledText {\n            text: \"Label\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.fontSizeMedium\n            anchors.verticalCenter: parent.verticalCenter\n        }\n    }\n}\n```\n\n## Common Mistakes\n\n**Wrong property names** (these do NOT exist):\n```qml\nTheme.fontSizeS        // Use Theme.fontSizeSmall\nTheme.iconSizeS        // Use Theme.iconSizeSmall\nTheme.spacingSmall     // Use Theme.spacingS\nTheme.borderRadius     // Use Theme.cornerRadius\n```\n\n**Hardcoded values** (do NOT do this):\n```qml\ncolor: \"#1e1e1e\"       // Use Theme.surfaceContainerHigh\ncolor: \"white\"         // Use Theme.surfaceText\nfont.pixelSize: 14     // Use Theme.fontSizeMedium\n```\n\n## Available Widgets from qs.Widgets\n\n| Widget | Description |\n|--------|-------------|\n| `StyledText` | Themed text with proper color defaults |\n| `StyledRect` | Themed rectangle |\n| `DIcon` | Material Symbols icon renderer |\n| `DankNFIcon` | Nerd Font icon renderer |\n| `DButton` | Themed button |\n| `DToggle` | Toggle switch |\n| `DTextField` | Text input field |\n| `DSlider` | Slider control |\n| `DDropdown` | Dropdown menu |\n| `DGridView` | Grid layout view |\n| `DListView` | List layout view |\n| `DFlickable` | Scrollable container |\n| `DTabBar` | Tab bar navigation |\n| `DankCollapsibleSection` | Collapsible content section |\n| `DankTooltip` | Hover tooltip |\n| `DankNumberStepper` | Number +/- control |\n| `DankFilterChips` | Filter chip row |\n| `CachingImage` | Image with disk cache |\n| `NumericText` | Fixed-width numeric display |\n\n## Checking All Properties\n\n```bash\ngrep \"property\" Common/Theme.qml\n```\n","isInternal":false,"tokens":1162,"sizeBytes":4815},{"name":"widget-plugin-guide.md","path":".agents/skills/dms-plugin-dev/references/widget-plugin-guide.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/references/widget-plugin-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Widget Plugin Guide\n\nWidgets display pills in DankBar and in docks (same component, no extra code), optionally open popouts, and can integrate with the Control Center.\n\n## Base Component\n\nWidgets use `PluginComponent` from `qs.Modules.Plugins`.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n    popoutContent: Component { /* ... */ }\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n## Injected Properties\n\nThese are automatically set by the plugin host:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `axis` | object | Bar axis info (horizontal/vertical) |\n| `section` | string | Bar section: `\"left\"`, `\"center\"`, or `\"right\"` |\n| `parentScreen` | object | Screen reference for multi-monitor |\n| `widgetThickness` | real | Widget size perpendicular to bar edge |\n| `barThickness` | real | Bar thickness parallel to edge |\n| `pluginId` | string | This plugin's ID |\n| `pluginService` | object | PluginService reference |\n| `pluginData` | object | Reactive plugin settings data |\n| `surfaceContext` | object | The host surface: `kind` (`\"bar\"` or `\"dock\"`), `id`, `screen`, `isVertical`, `thickness`, `live`, `editMode`, `inlineExpansion` |\n| `widgetInstanceId` | string | This occurrence; the same widget can appear on several bars and docks |\n\n## Bar Pills\n\nDefine `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).\n\n### Horizontal Bar Pill\n\n```qml\nhorizontalBarPill: Component {\n    StyledRect {\n        width: content.implicitWidth + Theme.spacingM * 2\n        height: parent.widgetThickness\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Row {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSize\n                anchors.verticalCenter: parent.verticalCenter\n            }\n\n            StyledText {\n                text: \"Label\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n                anchors.verticalCenter: parent.verticalCenter\n            }\n        }\n    }\n}\n```\n\n### Vertical Bar Pill\n\n```qml\nverticalBarPill: Component {\n    StyledRect {\n        width: parent.widgetThickness\n        height: content.implicitHeight + Theme.spacingM * 2\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Column {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSizeSmall\n            }\n        }\n    }\n}\n```\n\n**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.\n\n## Popout Content\n\nOpen a popout window when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                    font.pixelSize: Theme.fontSizeMedium\n                }\n            }\n        }\n    }\n}\n```\n\n**PopoutComponent properties:**\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `headerText` | string | `\"\"` | Main header (bold, large). Hidden if empty. |\n| `detailsText` | string | `\"\"` | Subtitle below header. Hidden if empty. |\n| `closePopout` | function | (injected) | Call to close the popout programmatically. |\n| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |\n| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |\n\n**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Custom Click Actions\n\nOverride the default popout behavior:\n\n```qml\nPluginComponent {\n    // Simple no-args handler\n    pillClickAction: () => {\n        popoutService?.toggleControlCenter()\n    }\n\n    // With position params (x, y, width, section, screen)\n    pillClickAction: (x, y, width, section, screen) => {\n        popoutService?.toggleControlCenter(x, y, width, section, screen)\n    }\n\n    pillRightClickAction: () => {\n        popoutService?.openSettings()\n    }\n}\n```\n\n## Control Center Integration\n\nAdd CC properties to show your widget in the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"Feature Name\"\n    ccWidgetSecondaryText: isActive ? \"Active\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n}\n```\n\n**CC properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `ccWidgetIcon` | string | Material icon name |\n| `ccWidgetPrimaryText` | string | Main label |\n| `ccWidgetSecondaryText` | string | Subtitle / status text |\n| `ccWidgetIsActive` | bool | Active state (changes styling) |\n\n**CC signals:**\n\n| Signal | When fired |\n|--------|-----------|\n| `ccWidgetToggled()` | Icon area clicked |\n| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |\n\n**CC sizing rules:**\n- 25% width - SmallToggleButton (icon only)\n- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)\n- Users can resize in CC edit mode\n\n### Detail Content (CompoundPill)\n\nAdd an expandable panel below the CC widget:\n\n```qml\nccDetailContent: Component {\n    Rectangle {\n        implicitHeight: 200\n        color: Theme.surfaceContainerHigh\n        radius: Theme.cornerRadius\n\n        Column {\n            anchors.fill: parent\n            anchors.margins: Theme.spacingM\n            spacing: Theme.spacingS\n\n            // Detail UI here\n        }\n    }\n}\n```\n\n## Visibility Control\n\nConditionally show/hide the bar pill:\n\n```qml\nPluginComponent {\n    visibilityCommand: \"pgrep -x myapp\"\n    visibilityInterval: 5  // seconds between checks\n}\n```\n\n**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.\n\n## Docks\n\nUsers add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.\n\n- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.\n- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.\n- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.\n- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.\n\nOptional attached panel: set `attachedContent: Component { ... }`. When the dock's \"Open widgets\" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.\n\n```qml\nPluginComponent {\n    id: root\n\n    attachedContent: Component {\n        Column {\n            padding: Theme.spacingM\n            spacing: Theme.spacingM\n\n            DButton {\n                text: I18n.trFor(\"myPlugin\", \"Close\")\n                onClicked: root.surfaceContext.dismissExpansion()\n            }\n        }\n    }\n}\n```\n\nThe panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.\n\n## Popout Namespace\n\nFor plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:\n\n```qml\nPluginComponent {\n    layerNamespacePlugin: \"my-plugin\"\n}\n```\n\nThe shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.\n\n## Reading Plugin Data\n\nAccess saved settings reactively via the injected `pluginData`:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId)\n                displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        }\n    }\n}\n```\n\n## Complete Example\n\nBased on the ExampleEmojiPlugin pattern:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property var emojis: [\"star\", \"heart\", \"smile\"]\n    property int currentIndex: 0\n\n    Timer {\n        interval: 2000\n        running: true\n        repeat: true\n        onTriggered: currentIndex = (currentIndex + 1) % emojis.length\n    }\n\n    popoutWidth: 350\n    popoutHeight: 400\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeLarge\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"Emoji Picker\"\n\n            DGridView {\n                width: parent.width\n                height: 300\n                cellWidth: 50\n                cellHeight: 50\n                model: root.emojis\n\n                delegate: Rectangle {\n                    width: 48\n                    height: 48\n                    radius: Theme.cornerRadius\n                    color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : \"transparent\"\n\n                    Text {\n                        anchors.centerIn: parent\n                        text: modelData\n                        font.pixelSize: 24\n                    }\n\n                    MouseArea {\n                        id: mouseArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        onClicked: {\n                            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n                            ToastService?.showInfo(\"Copied \" + modelData)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n","isInternal":false,"tokens":2630,"sizeBytes":11912},{"name":"SKILL.md","path":".agents/skills/dms-plugin-dev/SKILL.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/SKILL.md","title":"dms-plugin-dev","category":"anthropic-skill","format":"markdown","content":"---\nname: dms-plugin-dev\ndescription: >\n  Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on\n  Quickshell. Supports widget (bar, dock + Control Center), daemon (background service),\n  launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab),\n  dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest\n  creation, QML component development, startup checks,\n  settings UI, data persistence, theme integration, PopoutService usage, IPC runtime\n  discovery, and external command execution. Use when the user wants to create, modify,\n  or debug a DMS plugin, or asks about the DMS plugin API.\ncompatibility: Designed for Claude Code (or similar products)\nmetadata:\n  author: Dux Shell\n  version: \"1.2\"\n  domain: qml-desktop-development\n  framework: Dux Shell\n  languages: qml, javascript\nallowed-tools: Bash Read Write Edit\n---\n\n# Dux Shell Plugin Development\n\n## Overview\n\nDMS plugins extend the desktop shell with bar and dock widgets, background services, launcher\nintegrations, desktop widgets, and dash tabs and cards. Plugins are QML components discovered from\n`~/.config/dms/plugins/`.\n\n**Minimum plugin structure:**\n\n```\n~/.config/dms/plugins/YourPlugin/\n  plugin.json        # Required: manifest with metadata\n  YourComponent.qml  # Required: main QML component\n  YourSettings.qml   # Optional: settings UI\n  *.js               # Optional: JavaScript utilities\n```\n\n**Plugin registry:** Community plugins are available at https://plugins.duxlabs.dev/\n\n**Plugin types:**\n\n| Type        | Purpose                        | Base Component             | Bar/dock pills | CC integration |\n|-------------|--------------------------------|----------------------------|----------------|----------------|\n| `widget`    | Bar or dock widget + popout    | `PluginComponent`          | Yes            | Yes            |\n| `daemon`    | Background service             | `PluginComponent` (no UI)  | No             | Optional       |\n| `launcher`  | Searchable items in launcher   | `Item`                     | No             | No             |\n| `desktop`   | Draggable desktop widget       | `DesktopPluginComponent`   | No             | No             |\n| `dash`      | Tab in the dash popout         | `DashTabComponent`         | No             | No             |\n| `dashCard`  | Card in the dash overview grid | `DashCardComponent`        | No             | No             |\n| `composite` | Multi-surface plugin           | One component per surface  | Optional       | Optional       |\n\n## Step 1: Determine Plugin Type\n\nChoose the type based on what the plugin does:\n\n- **Shows in the bar or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same\n  component), optionally opens a popout or an attached dock panel, optionally integrates with\n  Control Center.\n- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper\n  changes, notifications, battery level, etc.).\n- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher\n  with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).\n- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the\n  desktop layer.\n- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.\n- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.\n- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination\n  of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own\n  QML component file.\n\n## Step 2: Create the Manifest\n\nCreate `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.\n\n**Minimal manifest:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\"\n}\n```\n\n**With settings, startup check, and permissions:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"dependencies\": [\"mytool\"],\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Composite plugin (multi-surface):**\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Standalone dash tab or card:** same single file form, `\"type\": \"dash\"` or `\"type\": \"dashCard\"`\nplus `\"component\"`. A tab and a card together (or with a bar widget) use `components` with the\nkeys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and\noptions. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n**Key rules:**\n- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`\n- `version` must be semver (e.g., `1.0.0`)\n- Provide either `component` (single-surface) or `components` (multi-surface), not both\n- `component` / component paths must start with `./` and end with `.qml`\n- `type: \"launcher\"` (or a `components` object with a `launcher` key) requires a `trigger` field\n- `settings_write` permission is **required** if the plugin has a settings component\n- `dependencies` replaces the deprecated `requires` field\n\n## Step 3: Create the Main Component\n\n### Widget\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hi\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\nSee [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.\n\n### Launcher\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        const items = [\n            { name: \"Item One\", icon: \"material:star\", comment: \"Description\",\n              action: \"toast:Hello!\", categories: [\"MyPlugin\"] }\n        ]\n        if (!query) return items\n        const q = query.toLowerCase()\n        return items.filter(i => i.name.toLowerCase().includes(q))\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"toast\") ToastService?.showInfo(data)\n        else if (type === \"copy\") Quickshell.execDetached([\"dms\", \"cl\", \"copy\", data])\n    }\n}\n```\n\nSee [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.\n\n### Desktop\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Text {\n            anchors.centerIn: parent\n            text: \"Desktop Widget\"\n            color: Theme.surfaceText\n        }\n    }\n}\n```\n\nSee [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.\n\n### Daemon\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onSomeSignal() {\n            console.log(\"Event received\")\n        }\n    }\n}\n```\n\nSee [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.\n\n### Dash Tab\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    property date now: new Date()\n\n    implicitHeight: DashMetrics.tabMinHeight\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.now = new Date()\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: root.now.toLocaleTimeString(I18n.locale())\n        color: Theme.surfaceText\n    }\n}\n```\n\n### Dash Card\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nDashCardComponent {\n    id: root\n\n    tone: options.tone ?? \"\"\n\n    StyledText {\n        anchors.centerIn: parent\n        text: I18n.trFor(\"myPlugin\", \"Hello\")\n        color: root.contentColor\n    }\n}\n```\n\nGate timers and animations on `live`; the dash content stays alive after the popout closes.\nCards must lay out for every size in their manifest range. Use the card colors (`accentColor`,\n`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n### Composite\n\nFor composite plugins, create a separate QML file per surface. Each surface uses the same\nbase component as the corresponding single-surface type (PluginComponent for widget/daemon,\nItem for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.\n\n```\nMyCompositePlugin/\n  plugin.json\n  MyBarWidget.qml      # PluginComponent (widget surface)\n  MyDaemon.qml         # PluginComponent (daemon surface)\n  MyDesktopWidget.qml  # Item with desktop widget properties\n  Settings.qml         # Shared settings for all surfaces\n```\n\n`pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.\n\n## Step 4: Add Startup Check (Optional)\n\nGate plugin activation on dependency checks by providing a `startupCheck` component. This\nruns before the plugin loads and blocks activation if a required tool or condition is missing.\n\nCreate a `StartupCheck.qml` (non-visual QtObject):\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details body\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\nAdd to your manifest:\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"mytool\"]\n}\n```\n\n## Step 5: Add Settings (Optional)\n\nWrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key\"\n        placeholder: \"sk-...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"interval\"\n        label: \"Refresh Interval\"\n        options: [\n            { label: \"1 min\", value: \"60\" },\n            { label: \"5 min\", value: \"300\" }\n        ]\n        defaultValue: \"300\"\n    }\n}\n```\n\n**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.\n\nSee [settings-components-reference.md](references/settings-components-reference.md) for full property lists.\n\n**Important:** Your plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json, or the settings UI will show an error.\n\n## Step 6: Use Data Persistence\n\nThree tiers of persistence:\n\n| API | Persisted | Use case |\n|-----|-----------|----------|\n| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |\n| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |\n| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |\n| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |\n\n- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings\n- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`\n- Global vars sync across all instances (multi-monitor, multiple bar sections)\n\nSee [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.\n\n## Step 7: Theme Integration\n\nAlways use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.\n\n**Essential properties:**\n- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`\n- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)\n- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`\n- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`\n- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)\n\n**Common widgets from `qs.DCommon.Widgets`:** `StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.\n\nSee [theme-reference.md](references/theme-reference.md) for the complete property list.\n\n## Step 8: Add Popout Content (Widgets Only)\n\nAdd a popout that opens when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n}\n```\n\n**PopoutComponent properties:** `headerText`, `detailsText`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).\n\nCalculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Step 9: Control Center Integration (Widgets Only)\n\nAdd your widget to the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isActive ? \"On\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n\n    // Optional: expandable detail panel (for CompoundPill)\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.surfaceContainerHigh\n            radius: Theme.cornerRadius\n        }\n    }\n}\n```\n\n**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).\n\n## Step 10: External Commands and Clipboard\n\n**Run commands and capture output:**\n\n```qml\nimport qs.Common\n\nProc.runCommand(\n    \"myPlugin.fetch\",\n    [\"curl\", \"-s\", \"https://api.example.com/data\"],\n    (stdout, exitCode) => {\n        if (exitCode === 0) processData(stdout)\n    },\n    500  // debounce ms\n)\n```\n\n**Fire-and-forget (clipboard, notifications):**\n\n```qml\nimport Quickshell\n\nQuickshell.execDetached([\"dms\", \"cl\", \"copy\", textToCopy])\n```\n\n**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.\n\n**Shell commands with pipes:** `[\"sh\", \"-c\", \"ps aux | grep foo\"]`\n\n**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.\n\n## Translations (Optional)\n\nShip per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:\n\n```\nMyPlugin/\n└── translations/\n    ├── es.json\n    └── zh_CN.json\n```\n\nFile names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.\n\nEach file maps the English term to its translation, using the term as its own context bucket:\n\n```json\n{\n  \"Cycle Speed\": {\n    \"Cycle Speed\": \"Velocidad de ciclo\"\n  }\n}\n```\n\nIn QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:\n\n```qml\ntext: I18n.trFor(\"myPlugin\", \"Cycle Speed\")\n```\n\nLookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.\n\n## Step 11: Validate and Test\n\n1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)\n2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`\n3. Open Settings > Plugins > Scan for Plugins\n4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)\n\n**Runtime plugin discovery via IPC:**\n\nPlugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:\n\n```bash\ndms ipc plugin-scan scan          # Trigger a full rescan of all plugin directories\ndms ipc plugin-scan rescan <id>   # Force rescan of a specific plugin\ndms ipc plugin-scan reload <id>   # Force reload of a loaded plugin\ndms ipc plugin-scan list          # List all known plugins (TSV: id, loaded, type, name)\ndms ipc plugin-scan status <id>   # Get status of a specific plugin (TSV: loaded, type, error)\n```\n\nPlugin IDs are validated against `^[a-zA-Z0-9_\\-:]{1,64}$`.\n\n**Common issues:**\n- Plugin not detected: check plugin.json syntax with `jq . plugin.json`\n- Widget not showing: ensure it's enabled AND added to a DankBar section\n- Settings error: verify `settings_write` permission is declared\n- Data not persisting: check pluginService injection and permissions\n- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`\n\n## Common Mistakes\n\n1. **Missing `settings_write` permission** - Settings UI shows error without it\n2. **Missing `property var popoutService: null`** - Must declare for injection to work\n3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge\n4. **Hardcoded colors** - Use `Theme.*` properties, not hex values\n5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])`\n6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`\n7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)\n8. **Forgetting `categories` in launcher items** - Items won't display without it\n9. **Not handling null pluginService** - Always use optional chaining or null checks\n10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`\n11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`\n12. **Providing both `component` and `components`** - Use one or the other, not both\n13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key\n14. **Using `\"type\": \"widget\"` for a dash tab** - Dash surfaces need `\"type\": \"dash\"` / `\"dashCard\"` or a `components` key\n15. **Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`\n16. **Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`\n17. **Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone\n\n## Quick Reference: Imports\n\n**Widget / Daemon:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n```\n\n**Launcher:**\n```qml\nimport QtQuick\nimport qs.Services\n```\n\n**Desktop:**\n```qml\nimport QtQuick\nimport qs.Common\n```\n\n**Dash tab / card:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash          // DashMetrics, DashWidgetGrid\nimport qs.Modules.DankDash.Overview // Card, for tiles inside a tab\n```\n\n**For clipboard/exec:** `import Quickshell`\n**For processes:** `import Quickshell.Io`\n**For networking:** `import Quickshell.Networking`\n**For toast notifications:** access `ToastService` from `qs.Services`\n\n## Quick Reference: File Naming\n\n- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)\n- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)\n- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)\n- **Component paths in manifest:** relative with `./` prefix (e.g., `\"./MyWidget.qml\"`)\n- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)\n\n## Reference Files\n\nLoad these on demand for detailed API documentation:\n\n- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema\n- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration\n- [dash-plugin-guide.md](references/dash-plugin-guide.md) - DashTabComponent, DashCardComponent, DashWidgetGrid, dash options, card sizing\n- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view\n- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence\n- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution\n- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists\n- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns\n- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables\n- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals\n- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins\n","frontmatter":{"name":"dms-plugin-dev","description":"Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on Quickshell. Supports widget (bar, dock + Control Center), daemon (background service), launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab), dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest creation, QML component development, startup checks, settings UI, data persistence, theme integration, PopoutService usage, IPC runtime discovery, and external command execution. Use when the user wants to create, modify, or debug a DMS plugin, or asks about the DMS plugin API.\n","compatibility":"Designed for Claude Code (or similar products)","metadata":{"author":"Dux Shell","version":"1.2","domain":"qml-desktop-development","framework":"Dux Shell","languages":"qml, javascript"},"allowed-tools":"Bash Read Write Edit","allowedTools":["Bash","Read","Write","Edit"]},"allowedTools":["Bash","Read","Write","Edit"],"isInternal":false,"tokens":5979,"sizeBytes":24911},{"name":"README.md","path":".agents/skills/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/README.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Agent Skills\n\nThis directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.\n\nEach 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.\n\n## Available Skills\n\n| Skill | Description |\n|-------|-------------|\n| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for Dux Shell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |\n\n## Installation\n\nThe `.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.\n\n### Claude Code\n\nClaude 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:\n\n**Project-level** (this repo only):\n\n```bash\nmkdir -p .claude/skills\nln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev\n```\n\n**Personal** (all your projects):\n\n```bash\nln -s /path/to/dms/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev\n```\n\nAfter 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.\n\nSee the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.\n\n### Cursor\n\nCursor discovers skills from `.cursor/skills/` in the project root:\n\n```bash\nmkdir -p .cursor/skills\nln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev\n```\n\nSee [Cursor skills docs](https://cursor.com/docs/context/skills) for details.\n\n### VS Code (Copilot)\n\nVS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:\n\n```bash\nmkdir -p .github/skills\nln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev\n```\n\nSee [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.\n\n### Gemini CLI\n\nGemini CLI discovers skills from `.gemini/skills/` in the project root:\n\n```bash\nmkdir -p .gemini/skills\nln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev\n```\n\nSee [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.\n\n### OpenAI Codex\n\nCodex discovers skills from `.codex/skills/` in the project root:\n\n```bash\nmkdir -p .codex/skills\nln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev\n```\n\nSee [Codex skills docs](https://developers.openai.com/codex/skills/) for details.\n\n### Other Agents\n\nThe 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.\n\nCheck the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.\n\nThe general pattern is:\n\n```bash\nmkdir -p .<agent>/skills\nln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev\n```\n\n## Adding New Skills\n\nTo add a new skill to this directory:\n\n1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)\n2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions\n3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories\n4. Keep `SKILL.md` under 500 lines - move detailed content to reference files\n\nSee the [Agent Skills specification](https://agentskills.io/specification) for the full format.\n","isInternal":false,"tokens":1001,"sizeBytes":4033},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json","title":"My Daemon","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myDaemon\",\n    \"name\": \"My Daemon\",\n    \"description\": \"A background service that reacts to events\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"background-service\"],\n    \"component\": \"./Daemon.qml\",\n    \"icon\": \"settings\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n","frontmatter":{"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"]},"isInternal":false,"tokens":103,"sizeBytes":390},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json","title":"My Dash Card","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myDashCard\",\n    \"name\": \"My Dash Card\",\n    \"description\": \"A custom dash overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./Card.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"card\": { \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 3, \"maxH\": 2 },\n        \"options\": [\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","frontmatter":{"id":"myDashCard","name":"My Dash Card","description":"A custom dash overview card","version":"1.0.0","author":"Your Name","type":"dashCard","capabilities":["dash-card"],"component":"./Card.qml","icon":"extension","dash":{"card":{"w":2,"h":1,"minW":1,"minH":1,"maxW":3,"maxH":2},"options":[{"key":"tone","text":"Tone","type":"choice","def":"","choices":[{"value":"","text":"Surface"},{"value":"primary","text":"Primary"}]}]},"requires_dms":">=1.7.0"},"isInternal":false,"tokens":220,"sizeBytes":751},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json","title":"My Dash Tab","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myDashTab\",\n    \"name\": \"My Dash Tab\",\n    \"description\": \"A custom dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./Tab.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"title\": \"My Tab\",\n        \"options\": [\n            { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","frontmatter":{"id":"myDashTab","name":"My Dash Tab","description":"A custom dash tab","version":"1.0.0","author":"Your Name","type":"dash","capabilities":["dash-tab"],"component":"./Tab.qml","icon":"extension","dash":{"title":"My Tab","options":[{"key":"compact","text":"Compact layout","type":"toggle","def":false}]},"requires_dms":">=1.7.0"},"isInternal":false,"tokens":141,"sizeBytes":454},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json","title":"My Desktop Widget","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myDesktopWidget\",\n    \"name\": \"My Desktop Widget\",\n    \"description\": \"A custom desktop widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"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"]},"isInternal":false,"tokens":99,"sizeBytes":371},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json","title":"My Launcher","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Custom launcher plugin with searchable items\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./Launcher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"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"]},"isInternal":false,"tokens":104,"sizeBytes":397},{"name":"plugin.json","path":".agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/.agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json","title":"My Widget","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"myWidget\",\n    \"name\": \"My Widget\",\n    \"description\": \"A custom bar widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"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"]},"isInternal":false,"tokens":99,"sizeBytes":353},{"name":"plugin.json","path":"quickshell/PLUGINS/AttachedPanelExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/AttachedPanelExample/plugin.json","title":"Attached Panel Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"attachedPanelExample\",\n    \"name\": \"Attached Panel Example\",\n    \"description\": \"A widget with an optional attached dock panel and a normal bar popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS contributors\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./AttachedPanelExample.qml\",\n    \"icon\": \"sticky_note_2\",\n    \"requires_dms\": \">=1.7.0\"\n}\n","frontmatter":{"id":"attachedPanelExample","name":"Attached Panel Example","description":"A widget with an optional attached dock panel and a normal bar popout","version":"1.0.0","author":"DMS contributors","type":"widget","capabilities":["dankbar-widget"],"component":"./AttachedPanelExample.qml","icon":"sticky_note_2","requires_dms":">=1.7.0"},"isInternal":false,"tokens":111,"sizeBytes":394},{"name":"README.md","path":"quickshell/PLUGINS/AttachedPanelExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/AttachedPanelExample/README.md","title":"AttachedPanelExample Documentation","category":"plugin-manifest","format":"markdown","content":"# Attached panel example\n\nA normal widget plugin that also sets `attachedContent`. Enable it in Settings → Plugins\nand add it to a bar, a dock, or both.\n\n- On a bar, a click opens the regular popout.\n- On a dock, what a click does is the dock's choice. With Settings → Dock → Apps & widgets\n  → Open widgets set to Inline, the dock grows and shows the attached panel next to the\n  apps. With Popout (the default) it opens the same popout the bar would.\n\nEscape or Close dismisses the panel and hands focus back to the widget. Opening another\nattached widget replaces the panel. No background process, no timer.\n\nSee [the surface widget contract](../SURFACE_WIDGETS.md).\n","isInternal":false,"tokens":158,"sizeBytes":678},{"name":"plugin.json","path":"quickshell/PLUGINS/ColorDemoPlugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ColorDemoPlugin/plugin.json","title":"Color Demo","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"colorDemo\",\n    \"name\": \"Color Demo\",\n    \"description\": \"Demonstrates color picker plugin setting\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"icon\": \"palette\",\n    \"type\": \"widget\",\n    \"component\": \"./ColorDemoWidget.qml\",\n    \"settings\": \"./ColorDemoSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"id":"colorDemo","name":"Color Demo","description":"Demonstrates color picker plugin setting","version":"1.0.0","author":"DMS","icon":"palette","type":"widget","component":"./ColorDemoWidget.qml","settings":"./ColorDemoSettings.qml","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":96,"sizeBytes":346},{"name":"plugin.json","path":"quickshell/PLUGINS/ControlCenterDetailExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ControlCenterDetailExample/plugin.json","title":"CC Detail Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"controlCenterDetailExample\",\n    \"name\": \"CC Detail Example\",\n    \"description\": \"Example plugin with Control Center detail dropdown\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./DetailExampleWidget.qml\",\n    \"icon\": \"settings\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"id":"controlCenterDetailExample","name":"CC Detail Example","description":"Example plugin with Control Center detail dropdown","version":"1.0.0","author":"Dux Shell","type":"widget","capabilities":["control-center"],"component":"./DetailExampleWidget.qml","icon":"settings","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":98,"sizeBytes":388},{"name":"plugin.json","path":"quickshell/PLUGINS/ControlCenterExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ControlCenterExample/plugin.json","title":"CC Toggle Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"controlCenterExample\",\n    \"name\": \"CC Toggle Example\",\n    \"description\": \"Example plugin with Control Center toggle widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./ControlCenterExampleWidget.qml\",\n    \"icon\": \"toggle_on\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"id":"controlCenterExample","name":"CC Toggle Example","description":"Example plugin with Control Center toggle widget","version":"1.0.0","author":"Dux Shell","type":"widget","capabilities":["control-center"],"component":"./ControlCenterExampleWidget.qml","icon":"toggle_on","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":99,"sizeBytes":388},{"name":"plugin.json","path":"quickshell/PLUGINS/DashCardExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashCardExample/plugin.json","title":"Progress Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card showing how much of the day, week, month or year has passed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"icon\": \"hourglass_bottom\",\n    \"dash\": {\n        \"card\": {\n            \"title\": \"Progress\",\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 4,\n            \"maxH\": 2\n        },\n        \"options\": [\n            {\n                \"key\": \"period\",\n                \"text\": \"Period\",\n                \"type\": \"choice\",\n                \"def\": \"day\",\n                \"choices\": [\n                    { \"value\": \"day\", \"text\": \"Day\" },\n                    { \"value\": \"week\", \"text\": \"Week\" },\n                    { \"value\": \"month\", \"text\": \"Month\" },\n                    { \"value\": \"year\", \"text\": \"Year\" }\n                ]\n            },\n            {\n                \"key\": \"remaining\",\n                \"text\": \"Show time left\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" },\n                    { \"value\": \"secondary\", \"text\": \"Secondary\" },\n                    { \"value\": \"tertiary\", \"text\": \"Tertiary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","frontmatter":{"id":"dashCardExample","name":"Progress Example","description":"A standalone overview card showing how much of the day, week, month or year has passed","version":"1.0.0","author":"Dux Shell","type":"dashCard","capabilities":["dash-card"],"component":"./ProgressCard.qml","icon":"hourglass_bottom","dash":{"card":{"title":"Progress","w":2,"h":1,"minW":1,"minH":1,"maxW":4,"maxH":2},"options":[{"key":"period","text":"Period","type":"choice","def":"day","choices":[{"value":"day","text":"Day"},{"value":"week","text":"Week"},{"value":"month","text":"Month"},{"value":"year","text":"Year"}]},{"key":"remaining","text":"Show time left","type":"toggle","def":true},{"key":"tone","text":"Tone","type":"choice","def":"","choices":[{"value":"","text":"Surface"},{"value":"primary","text":"Primary"},{"value":"secondary","text":"Secondary"},{"value":"tertiary","text":"Tertiary"}]}]},"requires_dms":">=1.7.0"},"isInternal":false,"tokens":416,"sizeBytes":1682},{"name":"README.md","path":"quickshell/PLUGINS/DashCardExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashCardExample/README.md","title":"DashCardExample Documentation","category":"plugin-manifest","format":"markdown","content":"# Progress Example\n\nA standalone overview card: how much of today, this week, this month or this year has passed. The manifest uses the single file form, `\"type\": \"dashCard\"` plus `\"component\"`.\n\nWhat it shows:\n\n- `DashCardComponent` as the root. It has no tab, so it is not clickable and keyboard navigation skips it.\n- A layout for every size in its range (1x1 up to 4x2). `wide` and `tall` come from the card's pixel size against `DashMetrics.gridRowUnit`.\n- `live` gating. `SystemClock` is disabled while the dash is hidden.\n- `tone` and the card colors (`accentColor`, `contentColor`, `mutedColor`, `chipColor`), so the card follows the Surface, Primary, Secondary and Tertiary tones like the built-in cards.\n- `dash.options`: a choice for the period, a toggle for the time left and a choice for the tone. They show up in the card's options sheet in edit mode and under Settings → Dashboard.\n\nAdd it from the dash: open the overview, hover the selected page icon, click the pencil, then Add widget.\n","isInternal":false,"tokens":248,"sizeBytes":1006},{"name":"plugin.json","path":"quickshell/PLUGINS/DashCounterExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashCounterExample/plugin.json","title":"Counter Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"dashCounterExample\",\n    \"name\": \"Counter Example\",\n    \"description\": \"One counter shared by a dash tab, an overview card and a bar or dock widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"dash-tab\", \"dash-card\", \"dankbar-widget\"],\n    \"icon\": \"counter_1\",\n    \"components\": {\n        \"dash\": \"./CounterTab.qml\",\n        \"dashCard\": \"./CounterCard.qml\",\n        \"widget\": \"./CounterWidget.qml\"\n    },\n    \"dash\": {\n        \"title\": \"Counter\",\n        \"card\": {\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 3,\n            \"maxH\": 2\n        }\n    },\n    \"requires_dms\": \">=1.7.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"id":"dashCounterExample","name":"Counter Example","description":"One counter shared by a dash tab, an overview card and a bar or dock widget","version":"1.0.0","author":"Dux Shell","type":"composite","capabilities":["dash-tab","dash-card","dankbar-widget"],"icon":"counter_1","components":{"dash":"./CounterTab.qml","dashCard":"./CounterCard.qml","widget":"./CounterWidget.qml"},"dash":{"title":"Counter","card":{"w":2,"h":1,"minW":1,"minH":1,"maxW":3,"maxH":2}},"requires_dms":">=1.7.0","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":224,"sizeBytes":769},{"name":"README.md","path":"quickshell/PLUGINS/DashCounterExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashCounterExample/README.md","title":"DashCounterExample Documentation","category":"plugin-manifest","format":"markdown","content":"# Counter Example\n\nA composite plugin with three surfaces that share one counter:\n\n- `components.dash` (`CounterTab.qml`): a dash tab built on `DashTabComponent`. It hosts a `DashWidgetGrid`, so the dash Edit, Add widget, Reset and Clear All actions work on it. The Reset widget starts in the Add menu (`enabled: false`). `+` and `-` change the count.\n- `components.dashCard` (`CounterCard.qml`): an overview card built on `DashCardComponent`. Clicking it opens the tab. Left and Right change the count while it has focus.\n- `components.widget` (`CounterWidget.qml`): a bar pill. Left click toggles the dash on the counter tab, right click adds one. The same file works in a dock with no extra code.\n\nThe count is stored with `setData(\"count\", value)` / `savePluginData` and read back through `pluginData.count`. Every surface reloads `pluginData` on `pluginDataChanged`, so they stay in sync.\n\nOpen the tab from a terminal with `dms ipc call dash open plugin_dashCounterExample`. If the tab is hidden from the tab bar it opens as a detail page with a back button.\n","isInternal":false,"tokens":255,"sizeBytes":1065},{"name":"plugin.json","path":"quickshell/PLUGINS/DashTabExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashTabExample/plugin.json","title":"Stopwatch Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab: a stopwatch with laps that keeps time while the dash is closed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"icon\": \"timer\",\n    \"dash\": {\n        \"title\": \"Stopwatch\",\n        \"options\": [\n            {\n                \"key\": \"hundredths\",\n                \"text\": \"Show hundredths\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"laps\",\n                \"text\": \"Laps shown\",\n                \"type\": \"number\",\n                \"def\": 5,\n                \"min\": 0,\n                \"max\": 10\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n","frontmatter":{"id":"dashTabExample","name":"Stopwatch Example","description":"A standalone dash tab: a stopwatch with laps that keeps time while the dash is closed","version":"1.0.0","author":"Dux Shell","type":"dash","capabilities":["dash-tab"],"component":"./StopwatchTab.qml","icon":"timer","dash":{"title":"Stopwatch","options":[{"key":"hundredths","text":"Show hundredths","type":"toggle","def":true},{"key":"laps","text":"Laps shown","type":"number","def":5,"min":0,"max":10}]},"requires_dms":">=1.7.0"},"isInternal":false,"tokens":214,"sizeBytes":830},{"name":"README.md","path":"quickshell/PLUGINS/DashTabExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/DashTabExample/README.md","title":"DashTabExample Documentation","category":"plugin-manifest","format":"markdown","content":"# Stopwatch Example\n\nA standalone dash tab. The manifest uses the single file form, `\"type\": \"dash\"` plus `\"component\"`, the same way a bar widget uses `\"type\": \"widget\"`.\n\nWhat it shows:\n\n- `DashTabComponent` as the root, sized with `implicitHeight` so the dash grows when many laps are shown.\n- `live` gating. The display timer only runs while the tab is on screen and the stopwatch is running. Time is computed from stored timestamps, so the stopwatch keeps counting while the dash is closed or another tab is open.\n- The plugin state API (`loadPluginState` / `savePluginState`) for runtime data. Nothing goes into settings, so no permissions are needed.\n- `focusTarget` and `restoreFocus()`. Opening the tab focuses Start without a focus ring, so Space starts it right away.\n- `handleKeyEvent`: Space starts or pauses, L records a lap, R resets.\n- `menuActions`: Reset in the tab's edit-mode controls.\n- `dash.options`: a toggle for hundredths and a number for how many laps to list, read as `options.hundredths` and `options.laps`.\n\nOpen it with `dms ipc call dash open plugin_dashTabExample`.\n","isInternal":false,"tokens":258,"sizeBytes":1099},{"name":"plugin.json","path":"quickshell/PLUGINS/ExampleCompositePlugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleCompositePlugin/plugin.json","title":"Composite Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"exampleComposite\",\n    \"name\": \"Composite Example\",\n    \"description\": \"One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\", \"clipboard\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./CompositeDaemon.qml\",\n        \"widget\": \"./CompositeBarWidget.qml\",\n        \"desktop\": \"./CompositeDesktopWidget.qml\"\n    },\n    \"settings\": \"./CompositeSettings.qml\",\n    \"requires_dms\": \">=1.5.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"exampleComposite","name":"Composite Example","description":"One plugin providing all three surfaces at once: a wallpaper-watcher daemon, an emoji bar widget with popout, and a desktop clock","version":"1.0.0","author":"Dux Shell","type":"composite","capabilities":["daemon","dankbar-widget","desktop-widget","clipboard"],"icon":"extension","components":{"daemon":"./CompositeDaemon.qml","widget":"./CompositeBarWidget.qml","desktop":"./CompositeDesktopWidget.qml"},"settings":"./CompositeSettings.qml","requires_dms":">=1.5.0","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":181,"sizeBytes":718},{"name":"README.md","path":"quickshell/PLUGINS/ExampleCompositePlugin/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleCompositePlugin/README.md","title":"ExampleCompositePlugin Documentation","category":"plugin-manifest","format":"markdown","content":"# Composite Example\n\nA single plugin that provides **all three surfaces at once** by combining three of\nthe standalone example plugins:\n\n| Surface | Source example | File |\n|---------|----------------|------|\n| `daemon` | WallpaperWatcherDaemon | `CompositeDaemon.qml` |\n| `widget` | Emoji Cycler (bar widget + popout) | `CompositeBarWidget.qml` |\n| `desktop` | Desktop Clock | `CompositeDesktopWidget.qml` |\n\nIt demonstrates the `components` manifest map, where each surface points at its own\nQML file:\n\n```json\n\"type\": \"composite\",\n\"components\": {\n    \"daemon\":  \"./CompositeDaemon.qml\",\n    \"widget\":  \"./CompositeBarWidget.qml\",\n    \"desktop\": \"./CompositeDesktopWidget.qml\"\n}\n```\n\nAll surfaces share one settings UI (`CompositeSettings.qml`) and one plugin-settings\nnamespace (`exampleComposite`), so `pluginData` is the same for every surface.\n\n## Surfaces\n\n- **Daemon** — watches `SessionData.wallpaperPath` and runs a user-configured script\n  on change. Also registers an `IpcHandler` (`target: \"compositeExample\"`) exposing a\n  `runHook` call, so you can trigger the hook over IPC.\n- **Bar widget** — cycles emojis in the bar; click the pill for an emoji picker popout\n  that copies to the clipboard.\n- **Desktop widget** — an analog/digital clock you can drag and resize on the desktop.\n\n## Usage\n\n1. Copy this directory into `$CONFIGPATH/dms/plugins/`.\n2. Settings → Plugins → **Scan for Plugins**, then enable **Composite Example**.\n   (Composite plugins respect the enable toggle — unlike a pure `desktop` plugin they\n   do not auto-load, because they also carry a daemon.)\n3. Add the bar widget via Settings → Appearance → DankBar Layout.\n4. Place the desktop clock via Settings → Desktop Widgets.\n\n## Notes\n\n- The daemon surface is instantiated once and lives for as long as the plugin is\n  enabled. The bar and desktop surfaces are instantiated per bar/placement per screen.\n- Cross-surface runtime state (not needed here) is best shared via\n  `PluginService.getGlobalVar` / `setGlobalVar` or the daemon instance, since each\n  surface is a separate object.\n- `requires_dms` is `>=1.5.0` because the `components` multi-surface manifest is only\n  understood by DMS 1.5.0 and later.\n","isInternal":false,"tokens":524,"sizeBytes":2214},{"name":"plugin.json","path":"quickshell/PLUGINS/ExampleDesktopClock/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleDesktopClock/plugin.json","title":"Desktop Clock","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"exampleDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"An example desktop widget displaying an analog clock\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\", \"clock\"],\n    \"component\": \"./DesktopClock.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./DesktopClockSettings.qml\",\n    \"requires_dms\": \">=1.2.0\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"exampleDesktopClock","name":"Desktop Clock","description":"An example desktop widget displaying an analog clock","version":"1.0.0","author":"Dux Shell","type":"desktop","capabilities":["desktop-widget","clock"],"component":"./DesktopClock.qml","icon":"schedule","settings":"./DesktopClockSettings.qml","requires_dms":">=1.2.0","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":128,"sizeBytes":481},{"name":"plugin.json","path":"quickshell/PLUGINS/ExampleEmojiPlugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleEmojiPlugin/plugin.json","title":"Emoji Cycler","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"exampleEmojiPlugin\",\n    \"name\": \"Emoji Cycler\",\n    \"description\": \"Display cycling emojis in your bar with a handy emoji picker popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"emoji-search\", \"clipboard\", \"dankbar-widget\"],\n    \"component\": \"./EmojiWidget.qml\",\n    \"icon\": \"mood\",\n    \"settings\": \"./EmojiSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"exampleEmojiPlugin","name":"Emoji Cycler","description":"Display cycling emojis in your bar with a handy emoji picker popout","version":"1.0.0","author":"Dux Labs","type":"widget","capabilities":["emoji-search","clipboard","dankbar-widget"],"component":"./EmojiWidget.qml","icon":"mood","settings":"./EmojiSettings.qml","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":126,"sizeBytes":469},{"name":"README.md","path":"quickshell/PLUGINS/ExampleEmojiPlugin/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleEmojiPlugin/README.md","title":"ExampleEmojiPlugin Documentation","category":"plugin-manifest","format":"markdown","content":"# Emoji Cycler Plugin\n\nAn example dms plugin that displays cycling emojis in your bar with an emoji picker popout.\n\n## Features\n\n- **Cycling Emojis**: Automatically rotates through your selected emoji set in the bar\n- **Emoji Picker**: Click the widget to open a grid of 120+ emojis\n- **Copy to Clipboard**: Click any emoji in the picker to copy it to clipboard\n- **Customizable**: Choose emoji sets, cycle speed, and max emojis shown\n\n## Installation\n\n1. Copy this directory to `~/.config/dms/plugins/ExampleEmojiPlugin`\n2. Open DMS Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Emoji Cycler\"\n5. Add `exampleEmojiPlugin` to your DankBar widget list\n\n## Settings\n\n### Emoji Set\nChoose from different emoji collections:\n- **Happy & Sad**: Mix of emotional faces\n- **Hearts**: Various colored hearts\n- **Hand Gestures**: Thumbs up, peace signs, etc.\n- **All Mixed**: A bit of everything\n\n### Cycle Speed\nControl how fast emojis rotate (500ms - 10000ms)\n\n### Max Bar Emojis\nHow many emojis to display at once (1-8)\n\n## Usage\n\n**In the bar**: Watch emojis cycle through automatically\n**Click the widget**: Opens emoji picker with 120+ emojis\n**Click any emoji**: Copies it to clipboard and shows toast\n\n## Example Code Highlights\n\nThis plugin demonstrates:\n- Using `PluginComponent` for bar integration\n- `SelectionSetting`, `SliderSetting` for configuration\n- Plugin-local translations (`translations/` + `I18n.trFor`)\n- Timer-based animation\n- Popout content with grid layout\n- External command execution (`Quickshell.execDetached`)\n- Toast notifications (`ToastService.show`)\n- Dynamic settings loading/saving\n\nPerfect template for creating your own DMS plugins!\n","isInternal":false,"tokens":391,"sizeBytes":1676},{"name":"plugin.json","path":"quickshell/PLUGINS/ExampleStartupCheck/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleStartupCheck/plugin.json","title":"Startup Check Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"exampleStartupCheck\",\n    \"name\": \"Startup Check Example\",\n    \"description\": \"Demonstrates startupCheck - blocks activation when the 'boregard' dependency is missing\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Labs\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./StartupCheckWidget.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"icon\": \"verified_user\",\n    \"dependencies\": [\"boregard\"],\n    \"permissions\": [\"process\"]\n}\n","frontmatter":{"id":"exampleStartupCheck","name":"Startup Check Example","description":"Demonstrates startupCheck - blocks activation when the 'boregard' dependency is missing","version":"1.0.0","author":"Dux Labs","type":"widget","capabilities":["dankbar-widget"],"component":"./StartupCheckWidget.qml","startupCheck":"./StartupCheck.qml","icon":"verified_user","dependencies":["boregard"],"permissions":["process"]},"isInternal":false,"tokens":125,"sizeBytes":477},{"name":"plugin.json","path":"quickshell/PLUGINS/ExampleWithVariants/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleWithVariants/plugin.json","title":"Example with Variants","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"exampleVariants\",\n    \"name\": \"Example with Variants\",\n    \"description\": \"Demonstrates dynamic variant creation for plugins\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"multiple-usecases\", \"variants\"],\n    \"component\": \"./VariantWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./VariantSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"exampleVariants","name":"Example with Variants","description":"Demonstrates dynamic variant creation for plugins","version":"1.0.0","author":"DMS","type":"widget","capabilities":["multiple-usecases","variants"],"component":"./VariantWidget.qml","icon":"widgets","settings":"./VariantSettings.qml","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":115,"sizeBytes":445},{"name":"README.md","path":"quickshell/PLUGINS/ExampleWithVariants/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/ExampleWithVariants/README.md","title":"ExampleWithVariants Documentation","category":"plugin-manifest","format":"markdown","content":"# Example with Variants Plugin\n\nThis plugin demonstrates the dynamic variant system for Dux Shell plugins.\n\n## What are Variants?\n\nVariants allow a single plugin to create multiple widget instances with different configurations. Each variant appears as a separate widget option in the Bar Settings \"Add Widget\" menu.\n\n## How It Works\n\n### Plugin Architecture\n\n1. **Single Component**: The plugin defines one widget component (`VariantWidget.qml`)\n2. **Variant Data**: Each variant stores its configuration in plugin settings\n3. **Dynamic Creation**: Variants are created through the plugin's settings UI\n4. **Widget ID Format**: Variants use the format `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Widget Properties\n\nEach variant widget receives:\n- `pluginService`: Reference to PluginService\n- `pluginId`: The base plugin ID (e.g., \"exampleVariants\")\n- `variantId`: The specific variant ID (e.g., \"variant_1234567890\")\n- `variantData`: The variant's configuration object\n\n### Variant Configuration\n\nThis example stores:\n```javascript\n{\n    id: \"variant_1234567890\",\n    name: \"My Variant\",\n    text: \"Display Text\",\n    icon: \"star\",\n    color: \"#FF5722\"\n}\n```\n\n## Creating Your Own Variant Plugin\n\n### 1. Widget Component\n\nCreate a widget that accepts variant data:\n\n```qml\nRectangle {\n    property var pluginService: null\n    property string pluginId: \"\"\n    property string variantId: \"\"\n    property var variantData: null\n\n    // Use variantData for configuration\n    property string displayText: variantData?.text || \"Default\"\n}\n```\n\n### 2. Settings Component\n\nProvide UI to manage variants:\n\n```qml\nFocusScope {\n    property var pluginService: null\n\n    // Create variant\n    pluginService.createPluginVariant(pluginId, variantName, variantConfig)\n\n    // Remove variant\n    pluginService.removePluginVariant(pluginId, variantId)\n\n    // Update variant\n    pluginService.updatePluginVariant(pluginId, variantId, variantConfig)\n\n    // Get all variants\n    var variants = pluginService.getPluginVariants(pluginId)\n}\n```\n\n### 3. Plugin Manifest\n\nStandard plugin manifest - no special configuration needed:\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin\",\n    \"component\": \"./Widget.qml\",\n    \"settings\": \"./Settings.qml\"\n}\n```\n\n## Use Cases\n\n- **Script Runner**: Multiple variants running different scripts\n- **System Monitors**: Different monitoring targets (CPU, GPU, Network)\n- **Quick Launchers**: Different apps or commands per variant\n- **Custom Indicators**: Different data sources or APIs\n- **Time Zones**: Multiple clocks for different time zones\n\n## API Reference\n\n### PluginService Functions\n\n- `getPluginVariants(pluginId)`: Get all variants for a plugin\n- `getAllPluginVariants()`: Get all variants across all plugins\n- `createPluginVariant(pluginId, name, config)`: Create new variant\n- `removePluginVariant(pluginId, variantId)`: Delete variant\n- `updatePluginVariant(pluginId, variantId, config)`: Update variant\n- `getPluginVariantData(pluginId, variantId)`: Get specific variant data\n\n### Widget Properties\n\n- `pluginId`: Base plugin identifier\n- `variantId`: Variant identifier (null if no variant)\n- `variantData`: Variant configuration object\n- `pluginService`: PluginService reference\n","isInternal":false,"tokens":739,"sizeBytes":3251},{"name":"plugin.json","path":"quickshell/PLUGINS/LauncherExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/LauncherExample/plugin.json","title":"LauncherExample","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"description\": \"Example launcher plugin demonstrating the launcher plugin system\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"extension\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\", \"command-execution\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"trigger\": \"#\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"launcherExample","name":"LauncherExample","description":"Example launcher plugin demonstrating the launcher plugin system","version":"1.0.0","author":"DMS Team","icon":"extension","type":"launcher","capabilities":["clipboard","command-execution"],"component":"./LauncherExampleLauncher.qml","settings":"./LauncherExampleSettings.qml","trigger":"#","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":121,"sizeBytes":502},{"name":"README.md","path":"quickshell/PLUGINS/LauncherExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/LauncherExample/README.md","title":"LauncherExample Documentation","category":"plugin-manifest","format":"markdown","content":"# LauncherExample Plugin\n\nA demonstration plugin that showcases the DMS launcher plugin system capabilities.\n\n## Purpose\n\nThis plugin serves as a comprehensive example for developers creating launcher plugins for DMS. It demonstrates:\n\n- **Plugin Structure**: Proper manifest, launcher, and settings components\n- **Trigger System**: Customizable trigger strings for plugin activation (including empty triggers)\n- **Item Management**: Providing searchable items to the launcher\n- **Action Execution**: Handling different types of actions (toast, copy, script)\n- **Settings Integration**: Configurable plugin settings with persistence\n\n## Features\n\n### Example Items\n- **Test Items 1-3**: Demonstrate toast notifications\n- **Copy Action**: Shows clipboard integration\n- **Script Action**: Demonstrates command execution\n\n### Trigger System\n- **Default Trigger**: `#` (configurable in settings)\n- **Empty Trigger Option**: Items can always be visible without needing a trigger\n- **Usage**: Type `#` in launcher to filter to this plugin (when trigger is set)\n- **Search**: Type `# test` to search within plugin items\n\n### Action Types\n- `toast:message` - Shows toast notification\n- `copy:text` - Copies text to clipboard\n- `script:command` - Executes shell command (demo only)\n\n## File Structure\n\n```\nPLUGINS/LauncherExample/\n├── plugin.json                    # Plugin manifest\n├── LauncherExampleLauncher.qml    # Main launcher component\n├── LauncherExampleSettings.qml    # Settings interface\n└── README.md                      # This documentation\n```\n\n## Installation\n\n1. **Plugin Directory**: Copy to `~/.config/dms/plugins/LauncherExample`\n2. **Enable Plugin**: Settings → Plugins → Enable \"LauncherExample\"\n3. **Configure**: Set custom trigger in plugin settings if desired\n\n## Usage\n\n### With Trigger (Default)\n1. Open launcher (Ctrl+Space or launcher button)\n2. Type `#` to activate plugin trigger\n3. Browse available items or add search terms\n4. Press Enter to execute selected item\n\n### Without Trigger (Empty Trigger Mode)\n1. Enable \"No trigger (always show)\" in plugin settings\n2. Open launcher - plugin items are always visible\n3. Search works normally with plugin items included\n4. Press Enter to execute selected item\n\n### Search Examples\n- `#` - Show all plugin items (with trigger enabled)\n- `# test` - Show items matching \"test\"\n- `# copy` - Show items matching \"copy\"\n- `test` - Show all items matching \"test\" (with empty trigger enabled)\n\n## Developer Guide\n\n### Plugin Contract\n\n**Launcher Component Requirements**:\n```qml\n// Required properties\nproperty var pluginService: null\nproperty string trigger: \"#\"\n\n// Required signals\nsignal itemsChanged()\n\n// Required functions\nfunction getItems(query): array\nfunction executeItem(item): void\n\n// Optional functions (for Shift+Enter paste support)\nfunction getPasteText(item): string|null\nfunction getPasteArgs(item): array|null\n```\n\n**Item Structure**:\n```javascript\n{\n    name: \"Item Name\",           // Display name\n    icon: \"icon_name\",           // Icon (optional, see Icon Types below)\n    comment: \"Description\",      // Subtitle text\n    action: \"type:data\",         // Action to execute\n    categories: [\"PluginName\"]   // Category array\n}\n```\n\n**Icon Types**:\n\nThe `icon` field supports four formats:\n\n1. **Material Design Icons** - Use `material:` prefix:\n   ```javascript\n   icon: \"material:lightbulb\"  // Material Symbols Rounded font\n   ```\n   Examples: `material:star`, `material:favorite`, `material:settings`\n\n2. **Unicode/Emoji Icons** - Use `unicode:` prefix:\n   ```javascript\n   icon: \"unicode:🚀\"  // Unicode character or emoji\n   ```\n   Display any Unicode character or emoji as the icon. Examples: `unicode:😀`, `unicode:⚡`, `unicode:🎨`\n\n3. **Desktop Theme Icons** - Use icon name directly:\n   ```javascript\n   icon: \"firefox\"  // Uses system icon theme\n   ```\n   Examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n4. **No Icon** - Omit the `icon` field entirely:\n   ```javascript\n   {\n       name: \"😀  Grinning Face\",\n       // No icon field\n       comment: \"Copy emoji\",\n       action: \"copy:😀\",\n       categories: [\"MyPlugin\"]\n   }\n   ```\n   When icon is omitted, the launcher hides the icon area and displays only text\n\n**Action Format**: `type:data` where:\n- `type` - Action handler (toast, copy, script, etc.)\n- `data` - Action-specific data\n\n### Settings Integration\n```qml\n// Save setting\npluginService.savePluginData(\"pluginId\", \"key\", value)\n\n// Load setting\npluginService.loadPluginData(\"pluginId\", \"key\", defaultValue)\n```\n\n### Trigger Configuration\n\nThe trigger can be configured in two ways:\n\n1. **Empty Trigger** (No Trigger Mode):\n   - Check \"No trigger (always show)\" in settings\n   - Saves `trigger: \"\"` and `noTrigger: true`\n   - Items always appear in launcher alongside regular apps\n\n2. **Custom Trigger**:\n   - Enter any string (e.g., `#`, `!`, `@`, `!ex`)\n   - Uncheck \"No trigger\" checkbox\n   - Items only appear when trigger is typed\n\n### Manifest Structure\n```json\n{\n    \"id\": \"launcherExample\",\n    \"name\": \"LauncherExample\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./LauncherExampleLauncher.qml\",\n    \"settings\": \"./LauncherExampleSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nNote: The `trigger` field in the manifest is optional and serves as the default trigger value.\n\n## Extending This Plugin\n\n### Adding New Items\n```qml\nfunction getItems(query) {\n    return [\n        {\n            name: \"My Item\",\n            icon: \"custom_icon\",\n            comment: \"Does something cool\",\n            action: \"custom:action_data\",\n            categories: [\"LauncherExample\"]\n        }\n    ]\n}\n```\n\n### Adding New Actions\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"custom\":\n            handleCustomAction(actionData)\n            break\n    }\n}\n```\n\n### Custom Trigger Logic\n```qml\nComponent.onCompleted: {\n    if (pluginService) {\n        trigger = pluginService.loadPluginData(\"launcherExample\", \"trigger\", \"#\")\n    }\n}\n\nonTriggerChanged: {\n    if (pluginService) {\n        pluginService.savePluginData(\"launcherExample\", \"trigger\", trigger)\n    }\n}\n```\n\n## Best Practices\n\n1. **Unique Triggers**: Choose triggers that don't conflict with other plugins\n2. **Clear Descriptions**: Write helpful item comments\n3. **Error Handling**: Gracefully handle action failures\n4. **Performance**: Return results quickly in getItems()\n5. **Cleanup**: Destroy temporary objects in executeItem()\n6. **Empty Trigger Support**: Consider if your plugin should support empty trigger mode\n\n## Testing\n\nTest the plugin by:\n1. Installing and enabling in DMS\n2. Testing with trigger enabled\n3. Testing with empty trigger (no trigger mode)\n4. Trying each action type\n5. Testing search functionality\n6. Verifying settings persistence\n\nThis plugin provides a solid foundation for building more sophisticated launcher plugins with custom functionality!\n","isInternal":false,"tokens":1637,"sizeBytes":7124},{"name":"plugin.json","path":"quickshell/PLUGINS/LauncherImageExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/LauncherImageExample/plugin.json","title":"Image Gallery Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"launcherImageExample\",\n    \"name\": \"Image Gallery Example\",\n    \"description\": \"Example launcher plugin demonstrating tile mode with images\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS Team\",\n    \"icon\": \"photo_library\",\n    \"type\": \"launcher\",\n    \"trigger\": \"img\",\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true,\n    \"component\": \"./LauncherImageExample.qml\",\n    \"permissions\": []\n}\n","frontmatter":{"id":"launcherImageExample","name":"Image Gallery Example","description":"Example launcher plugin demonstrating tile mode with images","version":"1.0.0","author":"DMS Team","icon":"photo_library","type":"launcher","trigger":"img","viewMode":"tile","viewModeEnforced":true,"component":"./LauncherImageExample.qml","permissions":[]},"isInternal":false,"tokens":108,"sizeBytes":405},{"name":"README.md","path":"quickshell/PLUGINS/LauncherImageExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/LauncherImageExample/README.md","title":"LauncherImageExample Documentation","category":"plugin-manifest","format":"markdown","content":"# LauncherImageExample\n\nExample launcher plugin demonstrating tile mode with URL-based images.\n\n## Features\n\n- **Tile Mode**: Uses `viewMode: \"tile\"` in plugin.json to display results as image tiles\n- **Enforced View Mode**: Uses `viewModeEnforced: true` to lock the view to tile mode (users cannot change it)\n- **URL Images**: Demonstrates using `imageUrl` property for remote images\n\n## Usage\n\n1. Open the launcher (DankLauncherV2)\n2. Type `img` to activate the plugin\n3. Browse Dux Shell screenshots in tile view\n\n## Plugin Configuration\n\n```json\n{\n  \"viewMode\": \"tile\",\n  \"viewModeEnforced\": true\n}\n```\n\n- `viewMode`: Sets the default view mode (\"list\", \"grid\", or \"tile\")\n- `viewModeEnforced`: When true, users cannot switch view modes for this plugin\n\n## Item Data Structure\n\nTo display images in tile mode, set `imageUrl` directly on the item:\n\n```javascript\n{\n    name: \"Image Title\",\n    icon: \"material:image\",\n    comment: \"Image description\",\n    categories: [\"Category\"],\n    imageUrl: \"https://example.com/image.png\"\n}\n```\n\nThe `imageUrl` property supports remote URLs or local files, use `file://` prefix for local files.\n","isInternal":false,"tokens":279,"sizeBytes":1137},{"name":"plugin.json","path":"quickshell/PLUGINS/PopoutControlExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/PopoutControlExample/plugin.json","title":"Popout Control Example","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"popoutControlExample\",\n    \"name\": \"Popout Control Example\",\n    \"description\": \"Example widget demonstrating PopoutService usage with pillClickAction\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./PopoutControlWidget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./PopoutControlSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n","frontmatter":{"id":"popoutControlExample","name":"Popout Control Example","description":"Example widget demonstrating PopoutService usage with pillClickAction","version":"1.0.0","author":"Dux Shell","type":"widget","capabilities":["dankbar-widget"],"component":"./PopoutControlWidget.qml","icon":"widgets","settings":"./PopoutControlSettings.qml","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":117,"sizeBytes":452},{"name":"README.md","path":"quickshell/PLUGINS/PopoutControlExample/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/PopoutControlExample/README.md","title":"PopoutControlExample Documentation","category":"plugin-manifest","format":"markdown","content":"# Popout Control Example Plugin\n\nThis example plugin demonstrates:\n- Using `PopoutService` to trigger positioned popouts and modals\n- Using `pillClickAction` with position parameters\n- Using `PluginSettings` with dropdown selection\n- Dynamic widget text based on settings\n\nThe `pillClickAction` receives position parameters `(x, y, width, section, screen)` which are passed to PopoutService functions to properly position popouts relative to the widget.\n\n## PopoutService API\n\nThe `PopoutService` is automatically injected into plugin widgets and daemons as `popoutService`. It provides access to all shell popouts and modals.\n\n### Available Popouts\n\n#### Control Center\n```qml\npopoutService.openControlCenter()\npopoutService.closeControlCenter()\npopoutService.toggleControlCenter()\n```\n\n#### Notification Center\n```qml\npopoutService.openNotificationCenter()\npopoutService.closeNotificationCenter()\npopoutService.toggleNotificationCenter()\n```\n\n#### App Drawer\n```qml\npopoutService.openAppDrawer()\npopoutService.closeAppDrawer()\npopoutService.toggleAppDrawer()\n```\n\n#### Process List (Popout)\n```qml\npopoutService.openProcessList()\npopoutService.closeProcessList()\npopoutService.toggleProcessList()\n```\n\n#### DankDash\n```qml\npopoutService.openDankDash(tabIndex)    // tabIndex: 0=Calendar, 1=Media, 2=Weather\npopoutService.closeDankDash()\npopoutService.toggleDankDash(tabIndex)\n```\n\n#### Battery Popout\n```qml\npopoutService.openBattery()\npopoutService.closeBattery()\npopoutService.toggleBattery()\n```\n\n#### VPN Popout\n```qml\npopoutService.openVpn()\npopoutService.closeVpn()\npopoutService.toggleVpn()\n```\n\n#### System Update Popout\n```qml\npopoutService.openSystemUpdate()\npopoutService.closeSystemUpdate()\npopoutService.toggleSystemUpdate()\n```\n\n### Available Modals\n\n#### Settings Modal\n```qml\npopoutService.openSettings()\npopoutService.closeSettings()\n```\n\n#### Clipboard History Modal\n```qml\npopoutService.openClipboardHistory()\npopoutService.closeClipboardHistory()\n```\n\n#### Launcher Modal\n```qml\npopoutService.openDankLauncherV2()\npopoutService.closeDankLauncherV2()\npopoutService.toggleDankLauncherV2()\n```\n\n#### Power Menu Modal\n```qml\npopoutService.openPowerMenu()\npopoutService.closePowerMenu()\npopoutService.togglePowerMenu()\n```\n\n#### Process List Modal (fullscreen)\n```qml\npopoutService.showProcessListModal()\npopoutService.hideProcessListModal()\npopoutService.toggleProcessListModal()\n```\n\n#### Color Picker Modal\n```qml\npopoutService.showColorPicker()\npopoutService.hideColorPicker()\n```\n\n#### Notification Modal\n```qml\npopoutService.showNotificationModal()\npopoutService.hideNotificationModal()\n```\n\n#### WiFi Password Modal\n```qml\npopoutService.showWifiPasswordModal()\npopoutService.hideWifiPasswordModal()\n```\n\n#### Network Info Modal\n```qml\npopoutService.showNetworkInfoModal()\npopoutService.hideNetworkInfoModal()\n```\n\n#### Notepad Slideout\n```qml\npopoutService.openNotepad()\npopoutService.closeNotepad()\npopoutService.toggleNotepad()\n```\n\n## Usage in Plugins\n\n### Widget Plugins\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\n\nRectangle {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    MouseArea {\n        anchors.fill: parent\n        onClicked: {\n            popoutService?.toggleControlCenter()\n        }\n    }\n}\n```\n\n### Daemon Plugins\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var popoutService: null  // REQUIRED: Must declare for injection\n\n    Connections {\n        target: NotificationService\n        function onNotificationReceived() {\n            popoutService?.openNotificationCenter()\n        }\n    }\n}\n```\n\n**Important**: The `popoutService` property **must** be declared in your plugin component. Without it, you'll get errors like:\n```\nError: Cannot assign to non-existent property \"popoutService\"\n```\n\n## Example Use Cases\n\n1. **Custom Launcher**: Create a widget that opens the app drawer\n2. **Quick Settings**: Toggle control center from a custom button\n3. **Notification Manager**: Open notification center on new notifications\n4. **System Monitor**: Open process list on high CPU usage\n5. **Power Management**: Trigger power menu from a custom widget\n\n## Installation\n\n1. Copy the plugin directory to `~/.config/dms/plugins/`\n2. Open Settings → Plugins\n3. Click \"Scan for Plugins\"\n4. Enable \"Popout Control Example\"\n5. Add `popoutControlExample` to your DankBar widget list\n\n## Notes\n\n- The `popoutService` property is automatically injected - no manual setup required\n- Always use optional chaining (`?.`) when calling methods to handle null cases\n- Popouts are lazily loaded - first access may activate the loader\n- Some popouts require specific system features to be available\n","isInternal":false,"tokens":1086,"sizeBytes":4718},{"name":"plugin.json","path":"quickshell/PLUGINS/QuickNotesExample/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/QuickNotesExample/plugin.json","title":"Quick Notes","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"quickNotesExample\",\n    \"name\": \"Quick Notes\",\n    \"description\": \"Example launcher plugin demonstrating the plugin state API for persistent data like history and notes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"icon\": \"sticky_note_2\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"clipboard\"],\n    \"component\": \"./QuickNotesLauncher.qml\",\n    \"settings\": \"./QuickNotesSettings.qml\",\n    \"trigger\": \"n\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"quickNotesExample","name":"Quick Notes","description":"Example launcher plugin demonstrating the plugin state API for persistent data like history and notes","version":"1.0.0","author":"Dux Shell","icon":"sticky_note_2","type":"launcher","capabilities":["clipboard"],"component":"./QuickNotesLauncher.qml","settings":"./QuickNotesSettings.qml","trigger":"n","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":128,"sizeBytes":511},{"name":"README.md","path":"quickshell/PLUGINS/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/README.md","title":"PLUGINS Documentation","category":"plugin-manifest","format":"markdown","content":"# Plugin System\n\nExtend DMS with dynamically loaded QML components: bar and dock widgets, Control Center tiles, dash tabs and overview cards, desktop widgets, launcher providers and background daemons.\n\n## Plugin Registry\n\nBrowse and discover community plugins at **https://plugins.duxlabs.dev/**\n\n## Overview\n\nPlugins are discovered from `~/.config/dms/plugins/` and managed by PluginService. One plugin can provide a single surface or several (see [Composite Plugins](#composite-plugins)).\n\n## Architecture\n\n### Core Components\n\n1. **PluginService** (`Services/PluginService.qml`)\n   - Singleton service managing plugin lifecycle\n   - Discovers plugins from `$CONFIGPATH/dms/plugins/`\n   - Handles loading, unloading, and state management\n   - Provides data persistence for plugin settings\n\n2. **Plugins hub** (`Modules/Settings/PluginsHubHeader.qml`, `Modules/Settings/PluginsManageTab.qml`)\n   - UI for managing available plugins and registries\n\n3. **Plugin settings page** (`Modules/Settings/PluginSettingsPage.qml`)\n   - Loads a plugin's settings component through `PluginSettingsHost`\n\n4. **Bar and dock integration** (`Modules/SurfaceWidgets/SurfaceWidgetFactory.qml`, `SurfaceWidgetHost.qml`)\n   - Renders plugin widgets in bars and docks with the same component\n   - Merges plugin components with built-in widgets\n   - Supports every bar and dock edge\n\n5. **Dash integration** (`Modules/DDash/DashRegistry.qml`)\n   - Lists built-in and plugin tabs and overview cards\n   - Hosts plugin tabs in `DashTabHost.qml` and cards in `Overview/DashCardSlot.qml`\n\nMany widgets are implemented in the shared [dux-qml-common](https://github.com/duxlabs-dev/dux-qml-common) library and re-exported by DMS. Plugins should keep importing `qs.Common`, `qs.Services`, `qs.Widgets`, and `qs.Modules.Plugins` — these remain the supported plugin API and are unaffected by where a widget is implemented.\n\n## Plugin Structure\n\nEach plugin must be a directory in `$CONFIGPATH/dms/plugins/` containing:\n\n```\n$CONFIGPATH/dms/plugins/YourPlugin/\n├── plugin.json          # Required: Plugin manifest\n├── YourWidget.qml       # Required: Widget component\n├── YourSettings.qml     # Optional: Settings UI\n├── *.js                 # Optional: JavaScript utilities\n└── translations/        # Optional: per-locale translation files (see Translations)\n```\n\n### Plugin Manifest (plugin.json)\n\nThe manifest file defines plugin metadata and configuration.\n\n**JSON Schema:** See `plugin-schema.json` for the complete specification and validation schema.\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"thing-my-plugin-does\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"material_icon_name\",\n    \"settings\": \"./YourSettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"requires\": [\"some-system-tool\"],\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n```\n\n**Required Fields:**\n- `id`: Unique plugin identifier (camelCase, no spaces)\n- `name`: Human-readable plugin name\n- `description`: Short description of plugin functionality (displayed in UI)\n- `version`: Semantic version string (e.g., \"1.0.0\")\n- `author`: Plugin creator name or email\n- `type`: Plugin type - \"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", or \"composite\" (with `components`)\n- `capabilities`: Array of plugin capabilities  (e.g., [\"dankbar-widget\"], [\"control-center\"], [\"monitoring\"])\n- `component`: Relative path to main QML component file, or `components` for a multi-surface plugin\n\n**Required for Launcher Type:**\n- `trigger`: Trigger string for launcher activation (e.g., \"=\", \"#\", \"!\")\n\n**Optional Fields:**\n- `icon`: Material Design icon name (displayed in UI)\n- `settings`: Path to settings component (enables settings UI)\n- `requires_dms`: Minimum DMS version requirement (e.g., \">=0.1.18\", \">0.1.0\")\n- `requires`: Array of required system tools/dependencies (e.g., [\"curl\", \"jq\"])\n- `permissions`: Required DMS permissions (e.g., [\"settings_read\", \"settings_write\"])\n\n**Permissions:**\n\nThe plugin system enforces permissions when settings are accessed:\n- `settings_read`: Required to read plugin settings (currently not enforced)\n- `settings_write`: **Required** to use PluginSettings component and save settings\n\nIf your plugin includes a settings component but doesn't declare `settings_write` permission, users will see an error message instead of the settings UI.\n\n### Widget Component\n\nThe main widget component uses the **PluginComponent** wrapper which provides automatic property injection and bar integration:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    // Define horizontal bar pill, for top and bottom DankBar positions (optional)\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    // Define vertical bar pill, for left and right DankBar positions (optional)\n    verticalBarPill: Component {\n        // Same as horizontal but optimized for vertical layout\n    }\n\n    // Define popout content, opens when clicking the bar pill (optional)\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional description text goes here\"\n\n            // Your popout content goes here\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Popout Content\"\n                    font.pixelSize: Theme.fontSizeLarge\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    // Popout dimensions (required if popoutContent is set)\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n**PluginComponent Properties (automatically injected):**\n- `axis`: Bar axis information (horizontal/vertical)\n- `section`: Bar section (\"left\", \"center\", \"right\")\n- `parentScreen`: Screen reference for multi-monitor support\n- `widgetThickness`: Recommended widget size perpendicular to bar\n- `barThickness`: Bar thickness parallel to edge\n\n**Component Options:**\n- `horizontalBarPill`: Component shown in horizontal bars\n- `verticalBarPill`: Component shown in vertical bars\n- `popoutContent`: Optional popout window content\n- `popoutWidth`: Popout window width\n- `popoutHeight`: Popout window height\n- `pillClickAction`: Custom click handler function (overrides popout)\n- `pillRightClickAction`: Custom right click handler function\n\n### Control Center Integration\n\nAdd your plugin to Control Center by defining CC properties:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isEnabled ? \"Active\" : \"Inactive\"\n    ccWidgetIsActive: isEnabled\n\n    onCcWidgetToggled: {\n        isEnabled = !isEnabled\n        if (pluginService) {\n            pluginService.savePluginData(\"myPlugin\", \"isEnabled\", isEnabled)\n        }\n    }\n\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.foregroundColor(Theme.cardSurface)\n            radius: Theme.cornerRadius\n            // Your detail UI here\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n}\n```\n\n**CC Properties:**\n- `ccWidgetIcon`: Material icon name\n- `ccWidgetPrimaryText`: Main label\n- `ccWidgetSecondaryText`: Subtitle/status\n- `ccWidgetIsActive`: Active state styling\n- `ccWidgetIsToggle`: Whether the icon toggles state; set false for action-only tiles\n- `ccExpandedContent`: Optional inline controls for larger tiles\n- `ccExpandedMinimumHeight`: Minimum height for inline controls, default `Theme.listItemHeight`\n- `ccFooterContent`: Accepted but ignored; the footer strip is gone and users shrink tiles to half-height pills instead\n- `ccDetailContent`: Optional detail page\n\n**Signals:**\n- `ccWidgetToggled()`: Fired when icon clicked\n- `ccWidgetExpanded()`: Fired when opening the detail page\n\n**Widget sizing:**\n\nThe grid uses square cells, with eight columns by default. A standard strip is\n4×1; a 2×2 tile is square. Users resize tiles and the panel in edit mode. Panel\nwidth and tile height are limited by the current screen, independently of each\nother. Saved spans are retained when displaying a tile on a smaller screen.\nThe standard tile adapts its icon and labels automatically. Larger tiles can load\n`ccExpandedContent` below the header when enough space is available.\n\nUse `CcTileContent` from `qs.Modules.ControlCenter.Widgets` for inline content.\nIts `tile` is supplied by the host. It exposes `columns`, `rows`, `live`,\n`contentColor`, and `subtitleColor`; `width` and `height` are the available space.\nContent is disabled in edit mode and destroyed when the Control Center closes or\nshrinks below the minimum size. Keep persistent state in the plugin instance.\n\n```qml\nccExpandedContent: Component {\n    CcTileContent {\n        DButton {\n            maximumWidth: parent.width\n            text: I18n.trFor(\"myPlugin\", \"Run\")\n            onClicked: root.runAction()\n        }\n    }\n}\n```\n\nExisting plugins need no changes to use the standard responsive tile. Custom\ninline controls must fit their allocated space and gate ongoing work on `live`.\n\n**Custom Click Actions:**\n\nOverride default popout with `pillClickAction` and `pillRightClickAction`:\n\n```qml\npillClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Clicked!'\"])\n}\n\n// Or with position params: (x, y, width, section, screen)\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n\npillRightClickAction: () => {\n    Process.exec(\"bash\", [\"-c\", \"notify-send 'Right clicked!'\"])\n}\n\npillRightClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\nThe PluginComponent automatically handles:\n- Bar orientation detection\n- Click handlers for popouts\n- Proper positioning and anchoring\n- Theme integration\n\n### PopoutComponent\n\nPopoutComponent provides a consistent header/content layout for plugin popouts:\n\n```qml\nimport qs.Modules.Plugins\nimport qs.DCommon.Widgets\n\nPopoutComponent {\n    headerText: \"Header Title\"        // Main header text (bold, large)\n    detailsText: \"Description text\"   // Optional description (smaller, gray)\n\n    // Access to exposed properties for dynamic sizing\n    readonly property int headerHeight    // Height of header area\n    readonly property int detailsHeight   // Height of description area\n\n    // Your content here - use parent.width for full width\n    // Calculate available height: root.popoutHeight - headerHeight - detailsHeight - spacing\n    DGridView {\n        width: parent.width\n        height: parent.height\n        // ...\n    }\n}\n```\n\n**PopoutComponent Properties:**\n- `headerText`: Main header text (optional, hidden if empty)\n- `detailsText`: Description text below header (optional, hidden if empty)\n- `closePopout`: Function to close popout (auto-injected by PluginPopout)\n- `headerHeight`: Readonly height of header (0 if not visible)\n- `detailsHeight`: Readonly height of description (0 if not visible)\n\nThe component automatically handles spacing and layout. Content children are rendered below the description with proper padding.\n\n### Settings Component\n\nOptional settings UI loaded inline in the PluginsTab accordion interface. Use the simplified settings API with auto-storage components:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    id: root\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key for accessing the service\"\n        placeholder: \"Enter API key...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"notifications\"\n        label: \"Enable Notifications\"\n        description: \"Show desktop notifications for updates\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"updateInterval\"\n        label: \"Update Interval\"\n        description: \"How often to refresh data\"\n        options: [\n            {label: \"1 minute\", value: \"60\"},\n            {label: \"5 minutes\", value: \"300\"},\n            {label: \"15 minutes\", value: \"900\"}\n        ]\n        defaultValue: \"300\"\n    }\n\n    ListSetting {\n        id: itemList\n        settingKey: \"items\"\n        label: \"Saved Items\"\n        description: \"List of configured items\"\n        delegate: Component {\n            StyledRect {\n                width: parent.width\n                height: 40\n                radius: Theme.cornerRadius\n                color: Theme.chipSurface\n\n                StyledText {\n                    anchors.left: parent.left\n                    anchors.leftMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    text: modelData.name\n                    color: Theme.surfaceText\n                }\n\n                Rectangle {\n                    anchors.right: parent.right\n                    anchors.rightMargin: Theme.spacingM\n                    anchors.verticalCenter: parent.verticalCenter\n                    width: 60\n                    height: 28\n                    color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                    radius: Theme.cornerRadius\n\n                    StyledText {\n                        anchors.centerIn: parent\n                        text: \"Remove\"\n                        color: Theme.errorText\n                        font.pixelSize: Theme.fontSizeSmall\n                        font.weight: Font.Medium\n                    }\n\n                    MouseArea {\n                        id: removeArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        cursorShape: Qt.PointingHandCursor\n                        onClicked: itemList.removeItem(index)\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n**Available Setting Components:**\n\nAll settings automatically save on change and load on component creation.\n\n**How Default Values Work:**\n\nEach setting component has a `defaultValue` property that is used when no saved value exists. Define sensible defaults in your settings UI:\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"\n    defaultValue: \"\"  // Empty string if no key saved\n}\n\nToggleSetting {\n    settingKey: \"enabled\"\n    defaultValue: true  // Enabled by default\n}\n\nListSettingWithInput {\n    settingKey: \"locations\"\n    defaultValue: []  // Empty array if no locations saved\n}\n```\n\n1. **PluginSettings** - Root wrapper for all plugin settings\n   - `pluginId`: Your plugin ID (required)\n   - Auto-handles storage and provides saveValue/loadValue to children\n   - Place all other setting components inside this wrapper\n\n2. **StringSetting** - Text input field\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `placeholder`: Input placeholder (optional)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Vertical stack (label, description, input field)\n\n3. **ToggleSetting** - Boolean toggle switch\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default boolean (optional, default: `false`)\n   - Layout: Horizontal (label/description left, toggle right)\n\n4. **SelectionSetting** - Dropdown menu\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `options`: Array of `{label, value}` objects or simple strings (required)\n   - `defaultValue`: Default value (optional, default: `\"\"`)\n   - Layout: Horizontal (label/description left, dropdown right)\n   - Stores the `value` field, displays the `label` field\n\n5. **ListSetting** - Manage list of items (manual add/remove)\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `delegate`: Custom item delegate Component (optional)\n   - `addItem(item)`: Add item to list\n   - `removeItem(index)`: Remove item from list\n   - Use when you need custom UI for adding items\n\n6. **ListSettingWithInput** - Complete list management with built-in form\n   - `settingKey`: Storage key (required)\n   - `label`: Display label (required)\n   - `description`: Help text (optional)\n   - `defaultValue`: Default array (optional, default: `[]`)\n   - `fields`: Array of field definitions (required)\n     - `id`: Field ID in saved object (required)\n     - `label`: Column header text (required)\n     - `placeholder`: Input placeholder (optional)\n     - `width`: Column width in pixels (optional, default 200)\n     - `required`: Must have value to add (optional, default false)\n     - `default`: Default value if empty (optional)\n   - Automatically generates:\n     - Column headers from field labels\n     - Input fields with placeholders\n     - Add button with validation\n     - List display showing all field values\n     - Remove buttons for each item\n   - Best for collecting structured data (servers, locations, etc.)\n\n**Complete Settings Example:**\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your service API key\"\n        placeholder: \"sk-...\"\n        defaultValue: \"\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        description: \"Turn this feature on or off\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"theme\"\n        label: \"Theme\"\n        description: \"Choose your preferred theme\"\n        options: [\n            {label: \"Dark\", value: \"dark\"},\n            {label: \"Light\", value: \"light\"},\n            {label: \"Auto\", value: \"auto\"}\n        ]\n        defaultValue: \"dark\"\n    }\n\n    ListSettingWithInput {\n        settingKey: \"locations\"\n        label: \"Locations\"\n        description: \"Track multiple locations\"\n        defaultValue: []\n        fields: [\n            {id: \"name\", label: \"Name\", placeholder: \"Home\", width: 150, required: true},\n            {id: \"timezone\", label: \"Timezone\", placeholder: \"America/New_York\", width: 200, required: true}\n        ]\n    }\n}\n```\n\n**Key Benefits:**\n- Zero boilerplate - just define your settings\n- Automatic persistence to `settings.json`\n- Clean, consistent UI across all plugins\n- No manual `pluginService` calls needed\n- Proper layout and spacing handled automatically\n\n## Translations\n\nPlugins ship their own translations. Drop a `translations/` directory next to `plugin.json`, wrap your strings in `I18n.trFor()`, done — nothing needs to change in the DMS repo, and users get your translations just by installing the plugin.\n\n```\nYourPlugin/\n├── plugin.json\n├── YourWidget.qml\n└── translations/\n    ├── es.json\n    ├── fr.json\n    └── zh_CN.json\n```\n\nDMS loads the file matching the user's locale when it discovers the plugin, and re-reads it whenever the locale changes. No restart, no registration call.\n\n### Marking Strings\n\nWrap every user-facing string in `I18n.trFor()`, with your plugin id as the first argument:\n\n```qml\nimport qs.Common\n\nStyledText {\n    text: I18n.trFor(\"yourPlugin\", \"Cycle Speed\")\n}\n```\n\nPlaceholders work the same as anywhere else in DMS:\n\n```qml\nToastService.showInfo(I18n.trFor(\"yourPlugin\", \"Copied %1 to clipboard\").arg(item))\n```\n\nLookup order is: your plugin's translation file → the global DMS catalog → the English term itself. Strings DMS already translates (\"Settings\", \"Close\", etc.) resolve from the global catalog for free, and anything untranslated renders as your English text instead of breaking.\n\nTwo rules:\n\n- The plugin id must be a **literal string** that exactly matches `id` in your `plugin.json`. The string extraction tooling for central translation reads call sites — a variable there means your strings never get extracted.\n- `I18n.trFor` doesn't exist on DMS versions without plugin translation support, and calling a missing function in QML is a TypeError, not a silent no-op. Bump `requires_dms` when you adopt it.\n\nPlain `I18n.tr()` still works inside plugins, but it only checks the global catalog — it will never see your `translations/` files.\n\n### Translation Files\n\nOne JSON file per locale. Top-level keys are context buckets; unless you have a reason to do otherwise, use the English term as its own bucket:\n\n```json\n{\n    \"Cycle Speed\": {\n        \"Cycle Speed\": \"Velocidad de ciclo\"\n    },\n    \"Copied %1 to clipboard\": {\n        \"Copied %1 to clipboard\": \"%1 copiado al portapapeles\"\n    }\n}\n```\n\nThere is no `en.json` — the strings in your QML are the English source. Translate what you want, skip what you don't; missing terms fall through the lookup order above.\n\nFile names match what DMS ships in its own catalog (`es.json`, `pt.json`, `zh_CN.json`, ...), but you're not limited to that list — resolution is driven by the user's locale. For a user on `zh_CN`, DMS tries `zh_CN.json`, then `zh-CN.json`, then `zh.json`, and uses the first one that exists.\n\n### Testing\n\n1. Reload after editing a file: `dms ipc call plugins reload yourPlugin`\n2. Switch languages in Settings → Locale — plugin strings retranslate live along with the rest of the shell\n3. Broken JSON logs a `bad plugin translations` warning in shell output and falls back to English\n\n`ExampleEmojiPlugin` is a complete working example — `trFor` calls in the widget and settings, plus `es.json` and `fr.json`.\n\n### Central Translation via POEditor\n\nPlugins in the [plugin registry](https://github.com/duxlabs-dev/dux-plugin-registry) can apply to join the DankPlugins POEditor project, translated by the same community that translates DMS itself. Approved plugins get every `I18n.trFor()` string uploaded under a `<pluginId>:` context, so your \"Auto\" and another plugin's \"Auto\" are separate terms with separate translations. Finished translations come back to the plugin repo as PRs to `translations/`, in the file format above. See the registry's CONTRIBUTING guide for the application process.\n\nStrings you wrap in plain `I18n.tr()` are not uploaded. They resolve from the shell catalog only, which is the right choice for terms DMS already has (\"Cancel\", \"Settings\"). A term DMS does not have stays English, and the sync warns about it.\n\n## PluginService API\n\n### Properties\n\n```qml\nPluginService.pluginDirectory: string\n// Path to plugins directory ($CONFIGPATH/dms/plugins)\n\nPluginService.availablePlugins: object\n// Map of all discovered plugins {pluginId: pluginInfo}\n\nPluginService.loadedPlugins: object\n// Map of currently loaded plugins {pluginId: pluginInfo}\n\nPluginService.pluginWidgetComponents: object\n// Map of loaded widget components {pluginId: Component}\n```\n\n### Functions\n\n```qml\n// Plugin Management\nPluginService.loadPlugin(pluginId: string): bool\nPluginService.unloadPlugin(pluginId: string): bool\nPluginService.reloadPlugin(pluginId: string): bool\nPluginService.enablePlugin(pluginId: string, onResult?: (ok: bool, error: string) => void): bool\nPluginService.disablePlugin(pluginId: string): bool\n\n// Plugin Discovery\nPluginService.scanPlugins(): void\nPluginService.getAvailablePlugins(): array\nPluginService.getLoadedPlugins(): array\nPluginService.isPluginLoaded(pluginId: string): bool\nPluginService.getWidgetComponents(): object\n\n// Data Persistence\nPluginService.savePluginData(pluginId: string, key: string, value: any): bool\nPluginService.loadPluginData(pluginId: string, key: string, defaultValue: any): any\n\n// Global Variables - Shared state across all plugin instances\nPluginService.getGlobalVar(pluginId: string, varName: string, defaultValue: any): any\nPluginService.setGlobalVar(pluginId: string, varName: string, value: any): void\n```\n\n### Signals\n\n```qml\nPluginService.pluginLoaded(pluginId: string)\nPluginService.pluginUnloaded(pluginId: string)\nPluginService.pluginLoadFailed(pluginId: string, error: string)\nPluginService.globalVarChanged(pluginId: string, varName: string)\n```\n\n## Startup Check (Dependency Gate)\n\nA plugin may optionally gate activation behind a dependency check. Point the manifest's `startupCheck` field at a small, **non-visual** component (a `QtObject` - it must not render in the graphics scene):\n\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"boregard\"]\n}\n```\n\nThe component exposes a `check` function that runs before the plugin loads, both on manual enable and on auto-load at startup. Call `done(null)` to allow activation, or `done(error)` to block it. The error can be a short string (title only) or an object with an expandable `details` body for long-form instructions:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"which\", \"boregard\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null)\n                return\n            }\n            done({\n                title: I18n.tr(\"boregard is required\"),\n                details: I18n.tr(\"Install it from https://duxlabs.dev, then re-enable this plugin.\")\n            })\n        })\n    }\n}\n```\n\nA synchronous variant is supported too - declare `check()` with no argument and return the result directly. When the check fails the enable toggle reverts and the error is shown as a toast (the `details` are expandable, and any `http(s)` URL in them becomes a clickable link). A check that fails to load, has no `check` function, or throws blocks activation the same way, with its error as the message. The check also runs again on reload. Plugins without a `startupCheck` are unaffected. See `ExampleStartupCheck` for a complete plugin; the last error per plugin is available at `PluginService.pluginLoadErrors[pluginId]`.\n\n## Plugin Global Variables\n\nPlugins can share state across multiple instances using global variables. This is useful when you have the same widget displayed on multiple monitors or multiple instances of the same widget on different bars.\n\n### Why Use Global Variables?\n\nUnlike regular properties which are scoped to each component instance, global variables are synchronized across all instances of your plugin. This enables:\n\n- **Multi-monitor consistency**: Same data displayed across all monitors\n- **Multi-instance widgets**: Multiple instances of the same widget sharing state\n- **Cross-component communication**: Share data between widget and settings components\n\n### Using PluginGlobalVar\n\nThe `PluginGlobalVar` helper component provides reactive global variable access:\n\n```qml\nimport QtQuick\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: content.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: content\n                anchors.centerIn: parent\n                text: \"Count: \" + globalCounter.value\n                color: Theme.surfaceText\n            }\n\n            MouseArea {\n                anchors.fill: parent\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar Properties:**\n- `varName` (required): Name of the global variable\n- `defaultValue` (optional): Default value if not set\n- `value` (readonly): Current value of the global variable\n\n**PluginGlobalVar Methods:**\n- `set(newValue)`: Update the global variable (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\nFor more control, use the PluginService API directly:\n\n```qml\nimport QtQuick\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\n    Connections {\n        target: PluginService\n        function onGlobalVarChanged(pluginId, varName) {\n            if (pluginId === \"myPlugin\" && varName === \"counter\") {\n                counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n            }\n        }\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            MouseArea {\n                anchors.fill: parent\n                onClicked: {\n                    const current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n                    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n                }\n            }\n        }\n    }\n}\n```\n\n### Global Variables vs Settings\n\n**Global Variables** (`getGlobalVar`/`setGlobalVar`):\n- Runtime state only (not persisted to disk)\n- Synchronized across all plugin instances\n- Changes trigger `globalVarChanged` signal for reactivity\n- Use for: counters, current selection, temporary UI state\n\n**Settings** (`savePluginData`/`loadPluginData`):\n- Persisted to `settings.json` across sessions\n- Loaded once per plugin instance\n- Use for: user preferences, API keys, configuration\n\n### Important Notes\n\n1. **Reactivity**: Global variables are reactive - all instances update when a value changes\n2. **Namespacing**: Variables are namespaced by plugin ID to avoid conflicts\n3. **Type Safety**: Values can be any QML/JavaScript type (numbers, strings, objects, arrays)\n4. **Not Persistent**: Global variables are cleared when the shell restarts (use settings for persistence)\n5. **Performance**: Efficient for frequent updates - changes only trigger updates for the specific variable\n\n## Creating a Plugin\n\n### Step 1: Create Plugin Directory\n\n```bash\nmkdir -p $CONFIGPATH/dms/plugins/MyPlugin\ncd $CONFIGPATH/dms/plugins/MyPlugin\n```\n\n### Step 2: Create Manifest\n\nCreate `plugin.json`:\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"my-functionality\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n### Step 3: Create Widget Component\n\nCreate `MyWidget.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    horizontalBarPill: Component {\n        StyledRect {\n            width: textItem.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello World\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: textItem.implicitWidth + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.chipSurface\n\n            StyledText {\n                id: textItem\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\n**Note:** Use `PluginComponent` wrapper for automatic property injection and bar integration. Define separate components for horizontal and vertical orientations.\n\n### Step 4: Create Settings Component (Optional)\n\nCreate `MySettings.qml`:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"Configure your plugin settings\"\n        font.pixelSize: Theme.fontSizeMedium\n        color: Theme.surfaceVariantText\n        wrapMode: Text.WordWrap\n    }\n\n    StringSetting {\n        settingKey: \"text\"\n        label: \"Display Text\"\n        description: \"Text shown in the bar widget\"\n        placeholder: \"Hello World\"\n        defaultValue: \"Hello World\"\n    }\n\n    ToggleSetting {\n        settingKey: \"showIcon\"\n        label: \"Show Icon\"\n        description: \"Display an icon next to the text\"\n        defaultValue: true\n    }\n}\n```\n\n### Step 5: Enable Plugin\n\n1. Run the shell: `qs -p $CONFIGPATH/quickshell/dms/shell.qml`\n2. Open Settings (Ctrl+,)\n3. Navigate to Plugins tab\n4. Click \"Scan for Plugins\"\n5. Enable your plugin with the toggle switch\n6. Add the plugin to your DankBar configuration\n\n## Adding Plugin to DankBar\n\nAfter enabling a plugin, add it to the bar:\n\n1. Open Settings → Appearance → DankBar Layout\n2. Add a new widget entry with your plugin ID\n3. Choose section (left, center, right)\n4. Save and reload\n\nOr edit `$CONFIGPATH/quickshell/dms/config.json`:\n\n```json\n{\n    \"dankBarLeftWidgets\": [\n        {\"widgetId\": \"myPlugin\", \"enabled\": true}\n    ]\n}\n```\n\n## Best Practices\n\n1. **Use Existing Widgets**: Leverage `qs.DCommon.Widgets` components (DIcon, DToggle, etc.) for consistency\n2. **Follow Theme**: Use `Theme` singleton for colors, spacing, and fonts\n3. **Data Persistence**: Use PluginService data APIs instead of manual file operations\n4. **Error Handling**: Gracefully handle missing dependencies and invalid data\n5. **Performance**: Keep widgets lightweight, avoid long operations that block the UI loop\n6. **Responsive Design**: Adapt to `compactMode` and different screen sizes\n7. **Documentation**: Include README.md explaining plugin usage\n8. **Versioning**: Use semantic versioning for updates\n9. **Dependencies**: Document external library requirements\n\n## Clipboard Access\n\nPlugins that need to copy text to the clipboard should use the built-in `dms cl copy` command through Quickshell's `execDetached` function.\n\n### Correct Method\n\nImport Quickshell and use `execDetached` with `dms cl copy`:\n\n```qml\nimport QtQuick\nimport Quickshell\n\nItem {\n    function copyToClipboard(text) {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    }\n}\n```\n\n### Example Usage\n\nFrom the ExampleEmojiPlugin (EmojiWidget.qml):\n\n```qml\nMouseArea {\n    onClicked: {\n        Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n        ToastService.showInfo(\"Copied \" + modelData + \" to clipboard\")\n        popoutColumn.closePopout()\n    }\n}\n```\n\n### Important Notes\n\n1. **Do NOT** use `globalThis.clipboard` or similar JavaScript APIs - they don't exist in the QML runtime\n2. **Always** import `Quickshell` at the top of your QML file\n3. Consider showing a toast notification to confirm the copy action to users\n\n### Dependencies\n\nThis method uses the built-in DMS clipboard functionality which has native Wayland support.\n\n## Running External Commands\n\nPlugins that need to execute external commands and capture their output should use the `Proc` singleton, which provides debounced command execution with automatic cleanup.\n\n### Correct Method\n\nImport the `Proc` singleton from `qs.Common` and use `runCommand`:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function fetchData() {\n        Proc.runCommand(\n            \"myPlugin.fetchData\",\n            [\"curl\", \"-s\", \"https://api.example.com/data\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Success:\", stdout)\n                    processData(stdout)\n                } else {\n                    console.error(\"Command failed with exit code:\", exitCode)\n                }\n            },\n            100\n        )\n    }\n}\n```\n\n### Function Signature\n\n```qml\nProc.runCommand(id, command, callback, debounceMs)\n```\n\n**Parameters:**\n- `id` (string): Unique identifier for this command. Used for debouncing - multiple calls with the same ID within the debounce window will only execute the last one\n- `command` (array): Command and arguments as an array (e.g., `[\"sh\", \"-c\", \"echo hello\"]`)\n- `callback` (function): Callback function receiving `(stdout, exitCode)` when the command completes\n  - `stdout` (string): Captured standard output from the command\n  - `exitCode` (number): Exit code of the process (0 typically means success)\n- `debounceMs` (number, optional): Debounce delay in milliseconds. Defaults to 50ms if not specified\n\n### Key Features\n\n1. **Automatic Cleanup**: Process objects are automatically destroyed after completion\n2. **Debouncing**: Rapid successive calls with the same ID are debounced, only executing the last one\n3. **Output Capture**: Automatically captures stdout for processing\n4. **Error Handling**: Exit codes are passed to the callback for error detection\n\n### Example Usage\n\n#### Simple Command Execution\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function checkNetwork() {\n        Proc.runCommand(\n            \"myPlugin.ping\",\n            [\"ping\", \"-c\", \"1\", \"8.8.8.8\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Network is up\")\n                } else {\n                    console.log(\"Network is down\")\n                }\n            }\n        )\n    }\n}\n```\n\n#### Parsing Command Output\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    property var diskUsage: ({})\n\n    function updateDiskUsage() {\n        Proc.runCommand(\n            \"myPlugin.df\",\n            [\"df\", \"-h\", \"/home\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    const lines = output.trim().split(\"\\n\")\n                    if (lines.length > 1) {\n                        const parts = lines[1].split(/\\s+/)\n                        diskUsage = {\n                            total: parts[1],\n                            used: parts[2],\n                            available: parts[3],\n                            percent: parts[4]\n                        }\n                    }\n                }\n            }\n        )\n    }\n}\n```\n\n#### Shell Commands with Pipes\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    function getTopProcess() {\n        Proc.runCommand(\n            \"myPlugin.topProcess\",\n            [\"sh\", \"-c\", \"ps aux | sort -nrk 3,3 | head -n 1\"],\n            (output, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Top process:\", output)\n                }\n            }\n        )\n    }\n}\n```\n\n#### Debouncing Rapid Updates\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nItem {\n    DTextField {\n        id: searchField\n        placeholderText: \"Search files...\"\n\n        onTextChanged: {\n            Proc.runCommand(\n                \"myPlugin.search\",\n                [\"find\", \"/home\", \"-name\", \"*\" + text + \"*\"],\n                (output, exitCode) => {\n                    if (exitCode === 0) {\n                        updateSearchResults(output)\n                    }\n                },\n                500\n            )\n        }\n    }\n}\n```\n\n### Important Notes\n\n1. **Unique IDs**: Use descriptive, namespaced IDs (e.g., `\"myPlugin.actionName\"`) to avoid conflicts\n2. **Debouncing**: Use appropriate debounce delays for your use case:\n   - Fast updates (50-100ms): System monitoring, real-time data\n   - User input (300-500ms): Search fields, text input processing\n   - Network requests (500-1000ms): API calls, web scraping\n3. **Error Handling**: Always check the exit code in your callback before processing output\n4. **Shell Commands**: Use `[\"sh\", \"-c\", \"command\"]` for complex shell commands with pipes or redirects\n5. **Security**: Sanitize user input before passing to commands to prevent command injection\n6. **Performance**: Avoid running expensive commands too frequently - use debouncing wisely\n\n### Comparison with Other Methods\n\n**Proc.runCommand** vs **Quickshell.execDetached**:\n- Use `Proc.runCommand` when you need to capture output or check exit codes\n- Use `Quickshell.execDetached` for fire-and-forget operations (like clipboard copy)\n\n**Proc.runCommand** vs **Process component**:\n- Use `Proc.runCommand` for simple, one-off command executions with automatic cleanup\n- Use `Process` component for long-running processes or when you need fine-grained control\n\n## Debugging\n\n### Console Logging\n\nView plugin logs:\n\n```bash\nqs -v -p $CONFIGPATH/quickshell/dms/shell.qml\n```\n\nLook for lines prefixed with:\n- `PluginService:` - Service operations\n- `PluginsTab:` - UI interactions\n- `PluginsTab:` - Settings loading and accordion interface\n\n### Common Issues\n\n1. **Plugin Not Detected**\n   - Check plugin.json syntax (use `jq` or JSON validator)\n   - Verify directory is in `$CONFIGPATH/dms/plugins/`\n   - Click \"Scan for Plugins\" in Settings\n\n2. **Widget Not Displaying**\n   - Ensure plugin is enabled in Settings\n   - Add plugin ID to DankBar widget list\n   - Check widget width/height properties\n\n3. **Settings Not Loading**\n   - Verify `settings` path in plugin.json\n   - Check settings component for errors\n   - Ensure plugin is enabled and loaded\n   - Review PluginsTab console output for injection issues\n\n4. **Data Not Persisting**\n   - Confirm pluginService.savePluginData() calls (with injection)\n   - Check `$CONFIGPATH/dms/settings.json` for pluginSettings data\n   - Verify plugin has settings permissions\n   - Ensure PluginService was properly injected into settings component\n\n## Security Considerations\n\nPlugins run with full QML runtime access. Only install plugins from trusted sources.\n\n**Permissions System:**\n- `settings_read`: Read plugin configuration (not currently enforced)\n- `settings_write`: **Required** to use PluginSettings - write plugin configuration (enforced)\n- `process`: Execute system commands (not currently enforced)\n- `network`: Network access (not currently enforced)\n\nCurrently, only `settings_write` is enforced by the PluginSettings component.\n\n## API Stability\n\nThe plugin API is currently **experimental**. Breaking changes may occur in minor version updates. Pin to specific DMS versions for production use.\n\n**Roadmap:**\n- Plugin marketplace/repository\n- Sandboxed plugin execution\n- Enhanced permission system\n- Plugin update notifications\n- Inter-plugin communication\n\n## Launcher Plugins\n\nLauncher plugins extend the DMS application launcher by adding custom searchable items with trigger-based filtering.\n\n### Overview\n\nLauncher plugins enable you to:\n- Add custom items to the launcher/app drawer\n- Use trigger strings for quick filtering (e.g., `!`, `#`, `@`)\n- Execute custom actions when items are selected\n- Provide searchable, categorized content\n- Integrate seamlessly with the existing launcher\n\n### Plugin Type Configuration\n\nTo create a launcher plugin, set the plugin type in `plugin.json`:\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher Plugin\",\n    \"description\": \"A custom launcher plugin for quick actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"show-thing\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./MySettings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nLauncher components are instantiated the first time the launcher asks them for items, not at shell startup or launcher open, so a plugin not allowed without a trigger is created once its trigger is typed or the launcher is filtered to it. Put background work (timers, processes, IPC handlers) in a daemon surface.\n\nLauncher results use shared Expressive rows and tiles in every launcher style. The item and action contracts are unchanged. List rows use a second line when `comment` is present; image and window preview tiles remain supported. Plugins do not need to supply a visual delegate.\n\n### Launcher Component Contract\n\nCreate `MyLauncher.qml` with the following interface:\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    // Required properties\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    // Required signals\n    signal itemsChanged()\n\n    // Required: Return array of launcher items\n    function getItems(query) {\n        return [\n            {\n                name: \"Item Name\",\n                icon: \"icon_name\",\n                comment: \"Description\",\n                action: \"type:data\",\n                categories: [\"MyLauncher\"]\n            }\n        ]\n    }\n\n    // Required: Execute item action\n    function executeItem(item) {\n        const [type, data] = item.action.split(\":\", 2)\n        // Handle action based on type\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"myLauncher\", \"trigger\", \"#\")\n        }\n    }\n}\n```\n\n### Item Structure\n\nEach item returned by `getItems()` must include:\n\n- `name` (string): Display name shown in launcher\n- `icon` (string, optional): Icon specification (see Icon Types below)\n- `comment` (string): Description/subtitle text\n- `action` (string): Action identifier in `type:data` format\n- `categories` (array): Array containing your plugin name\n\n### Icon Types\n\nThe `icon` field supports four formats:\n\n**1. Material Design Icons** - Use the `material:` prefix:\n```javascript\n{\n    name: \"My Item\",\n    icon: \"material:lightbulb\",  // Material Symbols Rounded font\n    comment: \"Uses Material Design icon\",\n    action: \"toast:Hello!\",\n    categories: [\"MyPlugin\"]\n}\n```\nAvailable icons: Any icon from Material Symbols font (e.g., `lightbulb`, `star`, `favorite`, `settings`, `terminal`, `translate`, `sentiment_satisfied`)\n\n**2. Unicode/Emoji Icons** - Use the `unicode:` prefix:\n```javascript\n{\n    name: \"Grinning Face\",\n    icon: \"unicode:😀\",  // Unicode character or emoji\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nDisplay any Unicode character or emoji as the icon. The character is rendered at 70-80% of the icon size with proper theming. Perfect for emoji pickers, symbol selectors, or character libraries.\n\n**3. Desktop Theme Icons** - Use icon name directly:\n```javascript\n{\n    name: \"Firefox\",\n    icon: \"firefox\",  // Uses system icon theme\n    comment: \"Launches Firefox browser\",\n    action: \"exec:firefox\",\n    categories: [\"MyPlugin\"]\n}\n```\nUses the user's installed icon theme. Common examples: `firefox`, `chrome`, `folder`, `text-editor`\n\n**4. No Icon** - Omit the `icon` field entirely:\n```javascript\n{\n    name: \"😀  Grinning Face\",\n    // No icon field - emoji/unicode in name displays without icon area\n    comment: \"Copy emoji to clipboard\",\n    action: \"copy:😀\",\n    categories: [\"MyPlugin\"]\n}\n```\nWhen `icon` is omitted, the launcher shows the first letter of the item name in a themed circle.\n\n### Trigger System\n\nTriggers control when your plugin's items appear in the launcher:\n\n**Empty Trigger Mode** (No trigger):\n- Items always visible alongside regular apps\n- Search includes your items automatically\n- Configure by saving empty trigger: `trigger: \"\"`\n\n**Custom Trigger Mode**:\n- Items only appear when trigger is typed\n- Example: Type `#` to show only your plugin's items\n- Type `# query` to search within your plugin\n- Configure any string: `#`, `!`, `@`, `!custom`, etc.\n\n### Trigger Configuration in Settings\n\nProvide a settings component with trigger configuration:\n\n```qml\nimport QtQuick\nimport QtQuick.Controls\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nFocusScope {\n    id: root\n\n    property var pluginService: null\n\n    Column {\n        spacing: 12\n\n        CheckBox {\n            id: noTriggerToggle\n            text: \"No trigger (always show)\"\n            checked: loadSettings(\"noTrigger\", false)\n\n            onCheckedChanged: {\n                saveSettings(\"noTrigger\", checked)\n                if (checked) {\n                    saveSettings(\"trigger\", \"\")\n                } else {\n                    saveSettings(\"trigger\", triggerField.text || \"#\")\n                }\n            }\n        }\n\n        DTextField {\n            id: triggerField\n            visible: !noTriggerToggle.checked\n            text: loadSettings(\"trigger\", \"#\")\n            placeholderText: \"#\"\n\n            onTextEdited: {\n                saveSettings(\"trigger\", text || \"#\")\n            }\n        }\n    }\n\n    function saveSettings(key, value) {\n        if (pluginService) {\n            pluginService.savePluginData(\"myLauncher\", key, value)\n        }\n    }\n\n    function loadSettings(key, defaultValue) {\n        if (pluginService) {\n            return pluginService.loadPluginData(\"myLauncher\", key, defaultValue)\n        }\n        return defaultValue\n    }\n}\n```\n\n### Action Execution\n\nHandle different action types in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            if (typeof ToastService !== \"undefined\") {\n                ToastService.showInfo(\"Plugin\", actionData)\n            }\n            break\n        case \"copy\":\n            // Copy to clipboard\n            break\n        case \"script\":\n            // Execute command\n         \n\n<!-- [TRUNCATED AT 50KB] -->","isInternal":false,"tokens":11152,"sizeBytes":50132},{"name":"plugin.json","path":"quickshell/PLUGINS/WallpaperWatcherDaemon/plugin.json","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/WallpaperWatcherDaemon/plugin.json","title":"Wallpaper Watcher Daemon","category":"plugin-manifest","format":"json","content":"{\n    \"id\": \"wallpaperWatcherDaemon\",\n    \"name\": \"Wallpaper Watcher Daemon\",\n    \"description\": \"Background daemon that monitors wallpaper changes and logs them\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper\", \"monitoring\"],\n    \"component\": \"./WallpaperWatcherDaemon.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./WallpaperWatcherSettings.qml\",\n    \"permissions\": [\n        \"settings_read\",\n        \"settings_write\"\n    ]\n}\n","frontmatter":{"id":"wallpaperWatcherDaemon","name":"Wallpaper Watcher Daemon","description":"Background daemon that monitors wallpaper changes and logs them","version":"1.0.0","author":"Dux Shell","type":"daemon","capabilities":["wallpaper","monitoring"],"component":"./WallpaperWatcherDaemon.qml","icon":"wallpaper","settings":"./WallpaperWatcherSettings.qml","permissions":["settings_read","settings_write"]},"isInternal":false,"tokens":124,"sizeBytes":489},{"name":"README.md","path":"quickshell/PLUGINS/WallpaperWatcherDaemon/README.md","rawUrl":"https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/HEAD/quickshell/PLUGINS/WallpaperWatcherDaemon/README.md","title":"WallpaperWatcherDaemon Documentation","category":"plugin-manifest","format":"markdown","content":"# Wallpaper Watcher Daemon\n\nRun a script whenever your wallpaper changes.\n\n## What it does\n\nThis daemon monitors wallpaper changes and executes a script you specify. The new wallpaper path gets passed as the first argument to your script.\n\n## Setup\n\n1. Enable the plugin in Settings → Plugins\n2. Configure the script path in the plugin settings\n3. Make sure your script is executable (`chmod +x /path/to/script.sh`)\n\n## Example script\n\n```bash\n#!/bin/bash\necho \"New wallpaper: $1\"\n# Do something with the wallpaper path\n```\n\nSave this to a file, make it executable, and point the plugin to it.\n\n## Use cases\n\n- Generate color schemes from the new wallpaper\n- Update theme files based on wallpaper colors\n- Send notifications when wallpaper changes\n- Sync wallpaper info to other devices\n- Log wallpaper history\n\n## Notes\n\n- Script errors show up as toast notifications\n- Script output goes to console logs\n- The daemon runs invisibly in the background\n","isInternal":false,"tokens":206,"sizeBytes":954}],"systemPromptSnippet":"<yakaai_skills repo=\"AvengeMedia/DankMaterialShell\">\n<!-- File: AGENTS.md (Tokens: ~783 | Category: root-instruction) -->\n## What is this?\n\nDMS is an open-source desktop shell for Wayland compositors on linux, built to work with many compositors.\n\n## Repo Structure\n\n- core/ - Go. `cmd/dms` runs quickshell, CLI utilities, and the unix socket server the UI depends on. `cmd/dankinstall` is a separate installer TUI.\n- quickshell/ - the entire UI: Common/ (singletons, Theme, settings), Services/ (system and core access), Modules/ (bar, dock, lock, ...), Modals/, Widgets/ (shell-specific widgets and deprecated Dank* shims), PLUGINS/, translations/\n- dux-qml-common/ - submodule of shared QML widgets\n- docs/, distro/ - IPC and theme docs, packaging\n\n## Rules\n\n- Keep it simple.\n- Do not edit code in response to a question.\n- Never commit, push, or open a PR unless asked.\n- Point out glaring issues we missed.\n- The shell runs 24/7. Audit every change for idle CPU, extra processes, timers, and retained memory.\n- `core` and `quickshell` share the socket protocol. Changing one means checking the other.\n- QML uses Theme tokens, never hardcoded colors, spacing, or constants.\n- Use the D* widgets from qs.DCommon.Widgets (DListView, DFlickable, and so on) instead of raw ListView/Flickable/ScrollView. The Dank* names in Widgets/ are deprecated shims.\n- User-facing text goes through I18n.tr(). Reuse catalog terms. Never edit translation catalogs.\n- Guard clauses, early returns.\n- Comment only what the code cannot say: a non-obvious why, a trap, a workaround. Never narrate what the code does or restate a name. Keep it to a line or two, and delete useless or paragraph comments you come across.\n- Do not edit generated code. Mocks come from mockery.\n- No library for a simple problem. Use a modern one for a complex, common problem.\n\n## Tests\n\n- A test must fail on the broken code. Otherwise do not write it.\n- No useless tests: \"main.go contains func main()\", defaults, \"it instantiates\", behavior the change didn't touch.\n- QML tests must be fast. Call logic directly, load the smallest component, no fixed delays or animation waits, few meaningful assertions over a matrix.\n- Tests cover `.js` modules, functions of `Services/` and `Common/` singletons, and IPC handlers. Never load, slice or mock anything under `Modules/`, `Modals/` or `Widgets/`, and never spawn a compositor, display server or shell. Those tests only ever broke on UI tweaks.\n\n## Connections vs bound properties\n\n- In per-item or per-screen components (delegates, bar widgets, popout and modal hosts, OSDs), no `Connections` for `on<Prop>Changed` handlers.\n- Source in the same file: put the handler on it.\n- Source outside (singleton, loader item, delegate parent): bind to a typed readonly property, never `var`, and handle that.\n- Keep `Connections` for plain signals, hand-emitted change signals (`SettingsData.barConfigsChanged()`), constant-pointer targets, reassignable targets, and per-frame `var` sources.\n- A bound handler fires once at creation if the initial value differs from the type default, and never on a same-value write. Convert only idempotent bodies. `enabled:` conditions become a guard inside the handler.\n- Singletons under Services/ and Common/ keep their `Connections`.\n\n## Commit Messages\n\n`area: short lowercase explanation`, for example `network: report wifi state from the associated adapter`. Description only if required, in simple language.\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/plugin-schema.json (Tokens: ~2003 | Category: anthropic-skill) -->\n{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"$id\": \"https://duxlabs.dev/schemas/plugin.json\",\n  \"title\": \"Dux Shell Plugin Manifest\",\n  \"description\": \"Schema for Dux Shell plugin.json manifest files\",\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"name\",\n    \"description\",\n    \"version\",\n    \"author\",\n    \"type\",\n    \"capabilities\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"type\": \"string\",\n      \"description\": \"Unique plugin identifier (camelCase, no spaces)\",\n      \"pattern\": \"^[a-zA-Z][a-zA-Z0-9]*$\"\n    },\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"Human-readable plugin name\",\n      \"minLength\": 1\n    },\n    \"description\": {\n      \"type\": \"string\",\n      \"description\": \"Short description of plugin functionality\",\n      \"minLength\": 1\n    },\n    \"version\": {\n      \"type\": \"string\",\n      \"description\": \"Semantic version string (e.g., '1.0.0')\",\n      \"pattern\": \"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-[a-zA-Z0-9.-]+)?(\\\\+[a-zA-Z0-9.-]+)?$\"\n    },\n    \"author\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin creator name or email\",\n      \"minLength\": 1\n    },\n    \"type\": {\n      \"type\": \"string\",\n      \"description\": \"Plugin type. Use 'composite' (or any value) together with 'components' to provide multiple surfaces from one plugin.\",\n      \"enum\": [\"widget\", \"daemon\", \"launcher\", \"desktop\", \"dash\", \"dashCard\", \"composite\"]\n    },\n    \"capabilities\": {\n      \"type\": \"array\",\n      \"description\": \"Array of plugin capabilities\",\n      \"items\": {\n        \"type\": \"string\"\n      },\n      \"minItems\": 1\n    },\n    \"component\": {\n      \"type\": \"string\",\n      \"description\": \"Relative path to main QML component file. Required unless 'components' is provided.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"components\": {\n      \"type\": \"object\",\n      \"description\": \"Map of surface name to relative QML component path, for multi-surface (composite) plugins. Provide any subset of surfaces; each is loaded independently.\",\n      \"properties\": {\n        \"widget\": {\n          \"type\": \"string\",\n          \"description\": \"Bar/Control Center widget component (PluginComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"desktop\": {\n          \"type\": \"string\",\n          \"description\": \"Desktop widget component\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"daemon\": {\n          \"type\": \"string\",\n          \"description\": \"Background daemon component (instantiated once)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"launcher\": {\n          \"type\": \"string\",\n          \"description\": \"Launcher provider component (requires 'trigger')\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dash\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard tab component (DashTabComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        },\n        \"dashCard\": {\n          \"type\": \"string\",\n          \"description\": \"Dashboard overview card component (DashCardComponent)\",\n          \"pattern\": \"^\\\\./.*\\\\.qml$\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"minProperties\": 1\n    },\n    \"dash\": {\n      \"type\": \"object\",\n      \"description\": \"Presentation for the dashboard surfaces. Falls back to 'icon' and 'name'.\",\n      \"properties\": {\n        \"icon\": {\n          \"type\": \"string\",\n          \"description\": \"Material icon for the dashboard tab\"\n        },\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"Tab label\"\n        },\n        \"card\": {\n          \"type\": \"object\",\n          \"description\": \"Overview card presentation and default size in grid units. Widths are clamped to the user's column count (3 to 8).\",\n          \"properties\": {\n            \"icon\": { \"type\": \"string\" },\n            \"title\": { \"type\": \"string\" },\n            \"w\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"h\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"minW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"minH\": { \"type\": \"integer\", \"minimum\": 1 },\n            \"maxW\": { \"type\": \"integer\", \"minimum\": 1, \"maximum\": 8 },\n            \"maxH\": { \"type\": \"integer\", \"minimum\": 1 }\n          },\n          \"additionalProperties\": false\n        },\n        \"options\": {\n          \"type\": \"array\",\n          \"description\": \"User options shown in the dashboard options sheet and Settings > Dashboard; values reach the tab and card as `options.<key>`\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\"key\", \"type\"],\n            \"properties\": {\n              \"key\": { \"type\": \"string\", \"not\": { \"const\": \"widgets\" }, \"description\": \"Option key, read as options.<key>. 'widgets' is reserved for DashWidgetGrid layouts\" },\n              \"text\": { \"type\": \"string\", \"description\": \"Row label, translated through the plugin catalog\" },\n              \"description\": { \"type\": \"string\", \"description\": \"Secondary text under a toggle row, translated through the plugin catalog\" },\n              \"type\": { \"type\": \"string\", \"enum\": [\"toggle\", \"choice\", \"number\"] },\n              \"def\": { \"description\": \"Default value: boolean for toggle, choice value for choice, number for number\" },\n              \"choices\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"object\",\n                  \"required\": [\"value\"],\n                  \"properties\": {\n                    \"value\": { \"type\": \"string\" },\n                    \"text\": { \"type\": \"string\" }\n                  },\n                  \"additionalProperties\": false\n                }\n              },\n              \"min\": { \"type\": \"number\" },\n              \"max\": { \"type\": \"number\" },\n              \"step\": { \"type\": \"number\", \"exclusiveMinimum\": 0 },\n              \"unit\": { \"type\": \"string\" }\n            },\n            \"additionalProperties\": false\n          }\n        }\n      },\n      \"additionalProperties\": false\n    },\n    \"trigger\": {\n      \"type\": \"string\",\n      \"description\": \"Trigger string for launcher activation (required for launcher type)\"\n    },\n    \"icon\": {\n      \"type\": \"string\",\n      \"description\": \"Material Design icon name\"\n    },\n    \"settings\": {\n      \"type\": \"string\",\n      \"description\": \"Path to settings component QML file\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"startupCheck\": {\n      \"type\": \"string\",\n      \"description\": \"Path to a non-visual (QtObject) component exposing a check(done) function that gates activation. done(null) allows; done(error) blocks, where error is a string or { title, details }.\",\n      \"pattern\": \"^\\\\./.*\\\\.qml$\"\n    },\n    \"requires_dms\": {\n      \"type\": \"string\",\n      \"description\": \"Minimum DMS version requirement (e.g., '>=0.1.18', '>0.1.0')\",\n      \"pattern\": \"^(>=?|<=?|=|>|<)\\\\d+\\\\.\\\\d+\\\\.\\\\d+$\"\n    },\n    \"dependencies\": {\n      \"type\": \"array\",\n      \"description\": \"Array of required system tools/dependencies (registry metadata)\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"requires\": {\n      \"type\": \"array\",\n      \"description\": \"Deprecated alias for 'dependencies'.\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"permissions\": {\n      \"type\": \"array\",\n      \"description\": \"Required capabilities\",\n      \"items\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"settings_read\",\n          \"settings_write\",\n          \"process\",\n          \"network\"\n        ]\n      }\n    }\n  },\n  \"allOf\": [\n    {\n      \"if\": {\n        \"properties\": {\n          \"type\": {\n            \"const\": \"launcher\"\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"if\": {\n        \"required\": [\"components\"],\n        \"properties\": {\n          \"components\": {\n            \"required\": [\"launcher\"]\n          }\n        }\n      },\n      \"then\": {\n        \"required\": [\"trigger\"]\n      }\n    },\n    {\n      \"anyOf\": [\n        {\n          \"required\": [\"component\"]\n        },\n        {\n          \"required\": [\"components\"]\n        }\n      ]\n    }\n  ],\n  \"additionalProperties\": true\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/references/advanced-patterns.md (Tokens: ~1613 | Category: anthropic-skill) -->\n# Advanced Patterns\n\nPatterns observed in production DMS plugins that go beyond the basics.\n\n## Plugin Variants\n\nCreate multiple widget instances from a single plugin definition. Each variant has its own configuration.\n\n### Manifest\n\nNo special manifest changes needed - the variant system is built into PluginComponent.\n\n### Widget with Variant Support\n\n```qml\nPluginComponent {\n    property string variantId: \"\"\n    property var variantData: ({})\n\n    property string displayText: variantData?.text || \"Default\"\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.displayText\n            }\n        }\n    }\n}\n```\n\nWidget format in bar config: `pluginId:variantId` (e.g., `exampleVariants:variant_1234567890`)\n\n### Settings with Variant Management\n\n```qml\nPluginSettings {\n    pluginId: \"exampleVariants\"\n\n    // Variant creation UI\n    DButton {\n        text: \"Add New Instance\"\n        onClicked: {\n            var id = \"variant_\" + Date.now()\n            root.createVariant(id, { name: \"New Instance\", text: \"Hello\" })\n        }\n    }\n\n    // Per-variant configuration\n    Repeater {\n        model: root.variants\n        delegate: Column {\n            StringSetting {\n                settingKey: modelData.id + \"_text\"\n                label: modelData.name || modelData.id\n            }\n        }\n    }\n}\n```\n\n## JavaScript Utility Files\n\nFor complex logic, split into `.js` files:\n\n### utils.js\n\n```javascript\n.pragma library\n\nfunction formatDuration(ms) {\n    if (ms < 60000) return \"just now\"\n    if (ms < 3600000) return Math.floor(ms / 60000) + \"m ago\"\n    return Math.floor(ms / 3600000) + \"h ago\"\n}\n\nfunction parseResponse(json) {\n    try {\n        return JSON.parse(json)\n    } catch (e) {\n        return null\n    }\n}\n```\n\n### Using in QML\n\n```qml\nimport \"utils.js\" as Utils\n\nItem {\n    StyledText {\n        text: Utils.formatDuration(Date.now() - timestamp)\n    }\n}\n```\n\nThe `.pragma library` directive makes the JS file a shared singleton - it is loaded once and shared across all QML instances that import it.\n\n## qmldir for Singleton Services\n\nFor plugins with internal singleton services:\n\n### qmldir\n\n```\nsingleton MyService 1.0 MyService.qml\n```\n\n### MyService.qml\n\n```qml\npragma Singleton\nimport QtQuick\n\nQtObject {\n    property var cache: ({})\n\n    function getData(key) {\n        return cache[key] || null\n    }\n\n    function setData(key, value) {\n        cache[key] = value\n    }\n}\n```\n\n### Using the singleton\n\n```qml\nimport \".\" as Local\n\nItem {\n    Component.onCompleted: {\n        Local.MyService.setData(\"key\", \"value\")\n    }\n}\n```\n\n## Inline Component Declarations\n\nReusable sub-components defined inline:\n\n```qml\nItem {\n    component StatusBadge: Rectangle {\n        property string label: \"\"\n        property color badgeColor: Theme.primary\n\n        width: badgeText.implicitWidth + Theme.spacingM * 2\n        height: 24\n        radius: 12\n        color: badgeColor\n\n        StyledText {\n            id: badgeText\n            anchors.centerIn: parent\n            text: label\n            color: Theme.onPrimary\n            font.pixelSize: Theme.fontSizeSmall\n        }\n    }\n\n    Row {\n        spacing: Theme.spacingS\n        StatusBadge { label: \"Running\"; badgeColor: Theme.success }\n        StatusBadge { label: \"Stopped\"; badgeColor: Theme.error }\n    }\n}\n```\n\n## Multi-Provider Adapter Pattern\n\nFor plugins supporting multiple backends (AI providers, API services):\n\n### apiAdapters.js\n\n```javascript\n.pragma library\n\nfunction createAdapter(provider) {\n    switch (provider) {\n        case \"openai\": return {\n            url: \"https://api.openai.com/v1/chat/completions\",\n            headers: (key) => ({ \"Authorization\": \"Bearer \" + key }),\n            formatRequest: (messages) => JSON.stringify({ model: \"gpt-4\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).choices[0].message.content\n        }\n        case \"anthropic\": return {\n            url: \"https://api.anthropic.com/v1/messages\",\n            headers: (key) => ({ \"x-api-key\": key, \"anthropic-version\": \"2023-06-01\" }),\n            formatRequest: (messages) => JSON.stringify({ model: \"claude-sonnet-4-20250514\", messages: messages }),\n            parseResponse: (text) => JSON.parse(text).content[0].text\n        }\n        default: return null\n    }\n}\n```\n\n## IPC Integration\n\nFor plugins that respond to keyboard shortcuts or external commands:\n\n```qml\nPluginComponent {\n    Connections {\n        target: DMSIpc\n        function onCommandReceived(command, args) {\n            if (command === \"myPlugin.toggle\") {\n                doToggle()\n            } else if (command === \"myPlugin.next\") {\n                goNext()\n            }\n        }\n    }\n}\n```\n\nExternal trigger: `dms ipc call myPlugin.toggle`\n\n## Networking with Quickshell.Networking\n\nFor API calls using the built-in networking module:\n\n```qml\nimport Quickshell.Networking\n\nItem {\n    NetworkRequest {\n        id: request\n        url: \"https://api.example.com/data\"\n        method: \"GET\"\n\n        onResponseReceived: (response) => {\n            const data = JSON.parse(response.body)\n            processData(data)\n        }\n\n        onErrorOccurred: (error) => {\n            console.error(\"Network error:\", error)\n        }\n    }\n\n    function fetchData() {\n        request.send()\n    }\n}\n```\n\n## Toast Notifications\n\nShow user feedback:\n\n```qml\nimport qs.Services\n\n// Info toast\nToastService?.showInfo(\"Operation completed\")\n\n// With title\nToastService?.showInfo(\"Plugin Name\", \"Data refreshed successfully\")\n```\n\nAlways use optional chaining since ToastService may not be available in all contexts.\n\n## Clipboard Operations\n\n```qml\nimport Quickshell\n\nfunction copyToClipboard(text) {\n    Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])\n    ToastService?.showInfo(\"Copied to clipboard\")\n}\n```\n\nDo NOT use `globalThis.clipboard`, `navigator.clipboard`, or any browser API - they do not exist in the QML runtime.\n\n## Multi-File Plugin Architecture\n\nLarge plugins can be split across multiple files:\n\n```\nMyPlugin/\n  plugin.json\n  Main.qml           # Main widget component\n  Settings.qml       # Settings UI\n  DetailView.qml     # Popout detail view\n  utils.js            # Utility functions\n  apiAdapter.js       # API adapter layer\n  qmldir              # Optional: singleton registrations\n```\n\nImport sibling files:\n\n```qml\n// In Main.qml\nimport \".\" as Local\n\nItem {\n    Loader {\n        source: \"DetailView.qml\"\n    }\n}\n```\n\n## Performance Tips\n\n1. Use `Proc.runCommand` with appropriate debounce for external commands\n2. Pre-cache images and thumbnails for image-heavy plugins\n3. Limit concurrent network requests\n4. Use `Timer` with reasonable intervals (don't poll faster than needed)\n5. Lazy-load heavy content (use `Loader` for complex popout content)\n6. Avoid blocking the UI thread with synchronous operations\n\n<!-- File: .agents/skills/dms-plugin-dev/references/daemon-plugin-guide.md (Tokens: ~1421 | Category: anthropic-skill) -->\n# Daemon Plugin Guide\n\nDaemon plugins are invisible background services that react to events and execute actions. They have no bar pills or desktop presence.\n\n## Base Component\n\nDaemons use `PluginComponent` with no bar pills:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    // Event-driven logic goes here\n}\n```\n\n## When to Use Daemons\n\n- Monitor system events (wallpaper changes, battery level, notifications)\n- Run periodic background tasks (polling APIs, checking system state)\n- Execute scripts in response to events\n- Control shell UI via PopoutService based on conditions\n\n## Event-Driven Pattern\n\nUse `Connections` to react to service signals:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            console.log(\"Wallpaper changed to:\", SessionData.wallpaperPath)\n            runScript(SessionData.wallpaperPath)\n        }\n    }\n\n    Connections {\n        target: BatteryService\n        function onPercentageChanged() {\n            if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n                popoutService?.openBattery()\n            }\n        }\n    }\n}\n```\n\n## Available Services\n\nCommon services daemons can connect to:\n\n| Service | Signals/Properties | Description |\n|---------|-------------------|-------------|\n| `SessionData` | `wallpaperPath`, `onWallpaperPathChanged` | Desktop session state |\n| `BatteryService` | `percentage`, `isCharging`, `batteryAvailable` | Battery status |\n| `NotificationService` | `onNotificationReceived(notification)` | Desktop notifications |\n| `PluginService` | `onPluginLoaded`, `onGlobalVarChanged` | Plugin lifecycle |\n\nImport services from `qs.Services`.\n\n## Process Execution\n\n### Simple command with Proc\n\n```qml\nimport qs.Common\n\nPluginComponent {\n    function runScript(arg) {\n        Proc.runCommand(\n            \"myDaemon.script\",\n            [\"bash\", \"-c\", \"echo 'Processing: \" + arg + \"'\"],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"Script output:\", stdout)\n                } else {\n                    ToastService?.showInfo(\"Script failed: exit \" + exitCode)\n                }\n            }\n        )\n    }\n}\n```\n\n### Long-running process with Process component\n\n```qml\nimport Quickshell.Io\n\nPluginComponent {\n    property string scriptPath: \"\"\n\n    Process {\n        id: proc\n        command: [\"bash\", scriptPath]\n        running: false\n\n        stdout: StdioCollector {\n            onTextReceived: (text) => {\n                console.log(\"stdout:\", text)\n            }\n        }\n\n        stderr: StdioCollector {\n            onTextReceived: (text) => {\n                console.error(\"stderr:\", text)\n            }\n        }\n\n        onExited: (exitCode) => {\n            if (exitCode !== 0) {\n                ToastService?.showInfo(\"Process failed: exit \" + exitCode)\n            }\n        }\n    }\n\n    function startProcess() {\n        if (scriptPath && !proc.running) {\n            proc.running = true\n        }\n    }\n}\n```\n\n## Timer-Based Polling\n\n```qml\nPluginComponent {\n    Timer {\n        interval: 60000  // every minute\n        running: true\n        repeat: true\n        onTriggered: checkStatus()\n    }\n\n    function checkStatus() {\n        Proc.runCommand(\n            \"myDaemon.check\",\n            [\"sh\", \"-c\", \"systemctl is-active myservice\"],\n            (stdout, exitCode) => {\n                const active = stdout.trim() === \"active\"\n                PluginService.setGlobalVar(\"myDaemon\", \"serviceActive\", active)\n            }\n        )\n    }\n}\n```\n\n## Data Persistence\n\nDaemons access PluginService directly (it's injected via PluginComponent):\n\n```qml\nPluginComponent {\n    property string configuredScript: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                configuredScript = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n}\n```\n\n## PopoutService Usage\n\nDaemons can control shell UI via the injected popoutService:\n\n```qml\nPluginComponent {\n    property var popoutService: null\n\n    function showAlert() {\n        popoutService?.openNotificationCenter()\n    }\n\n    function openSettings() {\n        popoutService?.openSettings()\n    }\n}\n```\n\nSee [popout-service-reference.md](popout-service-reference.md) for the full API.\n\n## Complete Example\n\nBased on the WallpaperWatcherDaemon:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport Quickshell.Io\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property string scriptPath: pluginData?.scriptPath || \"\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId) {\n                scriptPath = pluginService.loadPluginData(pluginId, \"scriptPath\", \"\")\n            }\n        }\n    }\n\n    Connections {\n        target: SessionData\n        function onWallpaperPathChanged() {\n            if (scriptPath) {\n                runWallpaperScript(SessionData.wallpaperPath)\n            }\n        }\n    }\n\n    function runWallpaperScript(wallpaperPath) {\n        console.log(\"[WallpaperWatcher] Running script:\", scriptPath, wallpaperPath)\n\n        Proc.runCommand(\n            \"wallpaperWatcher.run\",\n            [\"bash\", scriptPath, wallpaperPath],\n            (stdout, exitCode) => {\n                if (exitCode === 0) {\n                    console.log(\"[WallpaperWatcher] Script output:\", stdout)\n                } else {\n                    console.error(\"[WallpaperWatcher] Script failed:\", exitCode)\n                    ToastService?.showInfo(\"Wallpaper script failed\")\n                }\n            }\n        )\n    }\n\n    Component.onCompleted: {\n        console.log(\"[WallpaperWatcher] Daemon started\")\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"wallpaperWatcher\",\n    \"name\": \"Wallpaper Watcher\",\n    \"description\": \"Runs a script when the wallpaper changes\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"wallpaper-automation\"],\n    \"component\": \"./WallpaperWatcher.qml\",\n    \"icon\": \"wallpaper\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/references/dash-plugin-guide.md (Tokens: ~1693 | Category: anthropic-skill) -->\n# Dash Plugin Guide\n\nThe dash popout (`dms ipc call dash open`) holds tabs and an overview grid of cards. A plugin can add a tab (`dash` surface), a card (`dashCard` surface), or both. Built-in tabs and cards use the same registry (`quickshell/Modules/DankDash/DashRegistry.qml`).\n\nRequires DMS 1.7 or later: set `\"requires_dms\": \">=1.7.0\"`.\n\n## Picking the manifest form\n\n| Plugin provides | Manifest |\n|-----------------|----------|\n| Tab only | `\"type\": \"dash\"`, `\"component\": \"./Tab.qml\"` |\n| Card only | `\"type\": \"dashCard\"`, `\"component\": \"./Card.qml\"` |\n| Tab and card | `\"type\": \"composite\"`, `\"components\": { \"dash\": ..., \"dashCard\": ... }` |\n| Dash plus bar/dock widget, daemon, ... | `\"type\": \"composite\"` with every surface in `components` |\n\nAdd the optional `dash` block for labels, card size and options (see plugin-manifest-reference.md, Dash Block).\n\n## Rules that trip people up\n\n- Only enabled plugins appear in the dash. There is no \"disabled\" placeholder.\n- A plugin tab shows in the tab bar as soon as the plugin loads. Cards are never placed automatically; users add them in edit mode (three-dot menu, Edit, Add widget).\n- Tab id is `plugin_<pluginId>`. Use it with `dms ipc call dash open plugin_<pluginId>` and `popoutService.toggleDankDash(\"plugin_\" + pluginId, x, y, width, section, screen)`.\n- A tab is created when it becomes current and destroyed when the user leaves it. The dash content stays alive after the popout closes, so gate every Timer, animation and service ref on `live`.\n- Widths are grid columns (user picks 3 to 8, default 6). Rows are 96 px (`DashMetrics.gridRowUnit`). Cards must lay out for every size in `minW..maxW` x `minH..maxH`.\n- `dash.options` values reach only the dash surfaces as `options.<key>`. A bar widget or daemon in the same plugin cannot read them; use a `settings` component and `pluginData` for values every surface needs.\n- `widgets` is a reserved option key.\n- Use `I18n.trFor(\"<plugin id literal>\", \"...\")` for every string.\n\n## Tab: DashTabComponent\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    focusTarget: button\n    implicitHeight: Math.max(DashMetrics.tabMinHeight, content.implicitHeight + Theme.spacingL * 2)\n    menuActions: [\n        {\n            label: I18n.trFor(\"myPlugin\", \"Refresh\"),\n            iconName: \"refresh\",\n            action: () => root.refresh()\n        }\n    ]\n\n    function refresh() {\n    }\n\n    function restoreFocus() {\n        button.forceActiveFocus(Qt.OtherFocusReason);\n    }\n\n    function handleKeyEvent(event) {\n        if (event.key !== Qt.Key_R)\n            return false;\n        refresh();\n        return true;\n    }\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.refresh()\n    }\n\n    Column {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingM\n\n        StyledText {\n            text: root.options.compact ? \"\" : I18n.trFor(\"myPlugin\", \"Hello\")\n            color: Theme.surfaceText\n        }\n\n        DButton {\n            id: button\n            text: I18n.trFor(\"myPlugin\", \"Refresh\")\n            onClicked: root.refresh()\n        }\n    }\n}\n```\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`, `dashHost` (`dashVisible`, `requestTab(id)`, `editMode`), `editMode`, `live`. Also `targetScreen`, `active` and `rowBudget` when the tab declares them.\n\nBase provides: `options`, `pluginData`, `getData(key, def)`, `setData(key, value)`.\n\nOptional: `implicitHeight`, `focusTarget`, `restoreFocus()`, `handleKeyEvent(event)` (return true to consume; Ctrl+Tab is reserved), `blocksTabNavigation`, `menuActions`, `signal tabRequested(string id)`, `signal navFocusRequested`.\n\nFor tiles inside a tab, use `Card` from `qs.Modules.DankDash.Overview` so colors, radius and tones match the overview.\n\n### Editable widgets in a tab\n\nAssign a `DashWidgetGrid` (from `qs.Modules.DankDash`) to `widgetGrid` to get Edit, Add widget, Reset and Clear All:\n\n```qml\nDashTabComponent {\n    id: tab\n    widgetGrid: grid\n    implicitHeight: grid.implicitHeight\n    focusTarget: grid.focusTarget\n\n    DashWidgetGrid {\n        id: grid\n        width: parent.width\n        entryId: tab.entryId\n        live: tab.live\n        editMode: tab.editMode\n        definitions: [\n            { id: \"main\", text: I18n.trFor(\"myPlugin\", \"Main\"), icon: \"star\", component: mainWidget, w: 2, h: 2, minW: 1, minH: 1, maxW: 4, maxH: 3 }\n        ]\n    }\n\n    Component {\n        id: mainWidget\n        Card {}\n    }\n}\n```\n\nThe grid is 4 columns (2 when narrower than `Theme.smallBreakpoint`). `enabled: false` on a definition leaves it in the Add menu. The layout is saved in `dashOptions[entryId].widgets`.\n\n## Card: DashCardComponent\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashCardComponent {\n    id: root\n\n    readonly property bool wide: width >= DashMetrics.gridRowUnit * 2\n    readonly property bool tall: height >= DashMetrics.gridRowUnit * 2\n\n    tone: options.tone ?? \"\"\n\n    SystemClock {\n        id: clock\n        enabled: root.live\n        precision: SystemClock.Minutes\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: clock.date.toLocaleTimeString(I18n.locale(), \"hh:mm\")\n        font.pixelSize: root.tall ? Theme.fontSizeDisplay : Theme.fontSizeXLarge\n        color: root.accentColor\n    }\n}\n```\n\nBase (`Card`) provides: `pad`, `title`, `tone` (`\"\"`, `\"primary\"`, `\"secondary\"`, `\"tertiary\"`), `containerColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor`, `chipColor`, `tinted`, `clickable`, `signal clicked`, `options`, `pluginData`, `getData`, `setData`.\n\nInjected: `pluginId`, `pluginService`, `popoutService`, `entryId`; bound: `live` (overview visible), `interactive` (false in edit mode).\n\nClicks: when the plugin also has a tab, a click on a `clickable` card opens it. Set `opensTab: false` to handle the click yourself. Non-clickable cards are skipped by keyboard navigation unless `focusTarget` points at a child control.\n\nUse the card colors, never raw `Theme.primary` or `Theme.surfaceText`, so tones work.\n\n## Testing\n\n```bash\ndms ipc call plugins reload myPlugin\ndms ipc call dash open plugin_myPlugin\n```\n\nWorking examples in `quickshell/PLUGINS/`: `DashTabExample` (standalone tab), `DashCardExample` (standalone card), `DashCounterExample` (tab + card + bar/dock widget).\n\n<!-- File: .agents/skills/dms-plugin-dev/references/data-persistence-guide.md (Tokens: ~1226 | Category: anthropic-skill) -->\n# Data Persistence Guide\n\nDMS plugins have three tiers of data persistence, each suited for different use cases.\n\n## Tier 1: Plugin Data (Settings)\n\nPersisted to `settings.json`. Use for user preferences and configuration.\n\n### Saving\n\n```qml\npluginService.savePluginData(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar value = pluginService.loadPluginData(pluginId, \"key\", defaultValue)\n```\n\n### Reactive Access via pluginData\n\n`PluginComponent` has a reactive `pluginData` property that auto-loads from settings:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n    property bool showIcon: pluginData?.showIcon !== undefined ? pluginData.showIcon : true\n}\n```\n\n### Reacting to Settings Changes\n\nWhen settings are changed (e.g., from the settings UI), react with `Connections`:\n\n```qml\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        showIcon = pluginService.loadPluginData(pluginId, \"showIcon\", true)\n    }\n}\n```\n\n## Tier 2: Plugin State\n\nPersisted to a separate state file. Use for runtime state that should survive restarts but is not user-configurable (history, cache, counters).\n\n### Saving\n\n```qml\npluginService.savePluginState(pluginId, \"key\", value)\n```\n\n### Loading\n\n```qml\nvar state = pluginService.loadPluginState(pluginId, \"key\", defaultValue)\n```\n\n### Additional Methods\n\n```qml\npluginService.clearPluginState(pluginId)\npluginService.removePluginStateKey(pluginId, \"key\")\n```\n\n### Example: Persistent History\n\n```qml\nItem {\n    property var history: []\n\n    Component.onCompleted: {\n        history = pluginService?.loadPluginState(pluginId, \"history\", []) || []\n    }\n\n    function addToHistory(entry) {\n        history.unshift({\n            text: entry,\n            timestamp: Date.now()\n        })\n        if (history.length > 100) history = history.slice(0, 100)\n        pluginService?.savePluginState(pluginId, \"history\", history)\n    }\n\n    function clearHistory() {\n        history = []\n        pluginService?.removePluginStateKey(pluginId, \"history\")\n    }\n}\n```\n\n## Tier 3: Global Variables (Runtime Only)\n\nNOT persisted. Shared across all instances of a plugin. Use for cross-instance state synchronization (multi-monitor consistency, multi-instance widgets).\n\n### Using PluginGlobalVar Component\n\n```qml\nimport qs.Modules.Plugins\n\nPluginComponent {\n    PluginGlobalVar {\n        id: globalCounter\n        varName: \"counter\"\n        defaultValue: 0\n    }\n\n    horizontalBarPill: Component {\n        StyledRect {\n            // ...\n            StyledText {\n                text: \"Count: \" + globalCounter.value\n            }\n\n            MouseArea {\n                onClicked: globalCounter.set(globalCounter.value + 1)\n            }\n        }\n    }\n}\n```\n\n**PluginGlobalVar properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `varName` | string | Required: name of the global variable |\n| `defaultValue` | any | Optional: default if not set |\n| `value` | any | Readonly: current value |\n\n**Methods:**\n- `set(newValue)` - update the value (triggers reactivity across all instances)\n\n### Using PluginService API Directly\n\n```qml\nimport qs.Services\n\nproperty int counter: PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n\nConnections {\n    target: PluginService\n    function onGlobalVarChanged(pluginId, varName) {\n        if (pluginId === \"myPlugin\" && varName === \"counter\") {\n            counter = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n        }\n    }\n}\n\nfunction increment() {\n    var current = PluginService.getGlobalVar(\"myPlugin\", \"counter\", 0)\n    PluginService.setGlobalVar(\"myPlugin\", \"counter\", current + 1)\n}\n```\n\n## Decision Matrix\n\n| Need | API | Persisted | Scope |\n|------|-----|-----------|-------|\n| User preferences (API keys, themes, intervals) | `savePluginData` / `loadPluginData` | Yes (settings.json) | Per plugin |\n| Runtime state (history, cache, counters) | `savePluginState` / `loadPluginState` | Yes (state file) | Per plugin |\n| Cross-instance sync (multi-monitor data) | `PluginGlobalVar` or `getGlobalVar`/`setGlobalVar` | No (runtime only) | All instances |\n| Quick reactive reads from settings | `pluginData` property | N/A (read-only) | Per instance |\n\n## Plugin Path\n\nRetrieve a plugin's installation directory at runtime:\n\n```qml\nvar dir = pluginService.getPluginPath(pluginId)\n```\n\nReturns the absolute path to the plugin's directory (e.g., `~/.config/dms/plugins/MyPlugin`), or an empty string if the plugin is not found. Useful for loading bundled assets (images, data files) relative to the plugin's location.\n\n## Important Notes\n\n1. **pluginData is reactive** - bindings update automatically when data changes\n2. **Global vars are NOT persistent** - they reset when the shell restarts\n3. **State vs Data** - data is for user-facing settings, state is for internal runtime data\n4. **Null safety** - always check `pluginService` is not null before calling methods\n5. **Signal namespacing** - global var signals include `pluginId` to filter for your plugin\n6. **Performance** - global vars are efficient for frequent updates; settings writes are batched\n\n<!-- File: .agents/skills/dms-plugin-dev/references/desktop-plugin-guide.md (Tokens: ~1421 | Category: anthropic-skill) -->\n# Desktop Plugin Guide\n\nDesktop plugins are widgets that appear on the desktop background layer. They support drag-and-drop positioning and resize via corner handles.\n\n## Base Component\n\nDesktop widgets use a plain `Item` with injected properties:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n\n        // Your content here\n    }\n}\n```\n\n## Injected Properties\n\nThese are set automatically by the DesktopPluginWrapper:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `pluginService` | object | PluginService reference for data persistence |\n| `pluginId` | string | Plugin's unique identifier |\n| `editMode` | bool | `true` when user is dragging/resizing |\n| `widgetWidth` | real | Current widget container width |\n| `widgetHeight` | real | Current widget container height |\n\n## Optional Properties\n\nDefine these on your root item to customize behavior:\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `minWidth` | real | 100 | Minimum allowed width during resize |\n| `minHeight` | real | 100 | Minimum allowed height during resize |\n\n## Position and Size Persistence\n\nPosition (`desktopX`, `desktopY`) and size (`desktopWidth`, `desktopHeight`) are automatically managed by the DesktopPluginWrapper. You do not need to handle persistence for positioning.\n\n## Edit Mode\n\nWhen `editMode` is true, the user is repositioning or resizing. Use this to:\n- Show visual indicators (borders, handles)\n- Disable interactive elements to prevent accidental actions\n- Display additional controls\n\n```qml\nRectangle {\n    anchors.fill: parent\n    border.color: root.editMode ? Theme.primary : \"transparent\"\n    border.width: root.editMode ? 2 : 0\n\n    MouseArea {\n        anchors.fill: parent\n        enabled: !root.editMode\n        onClicked: doSomething()\n    }\n}\n```\n\n## Loading and Saving Data\n\nUse the injected `pluginService` for data persistence:\n\n```qml\nproperty string displayMode: {\n    if (!pluginService) return \"default\"\n    return pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n}\n\nConnections {\n    target: pluginService\n    function onPluginDataChanged(changedId) {\n        if (changedId !== pluginId) return\n        root.displayMode = pluginService.loadPluginData(pluginId, \"displayMode\", \"default\")\n    }\n}\n\nfunction saveMode(mode) {\n    pluginService?.savePluginData(pluginId, \"displayMode\", mode)\n}\n```\n\n## Settings Component\n\nDesktop plugin settings use the same `PluginSettings` wrapper as other types:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myDesktopWidget\"\n\n    SliderSetting {\n        settingKey: \"opacity\"\n        label: \"Opacity\"\n        description: \"Widget background opacity\"\n        defaultValue: 85\n        minimum: 10\n        maximum: 100\n        unit: \"%\"\n    }\n\n    SelectionSetting {\n        settingKey: \"style\"\n        label: \"Display Style\"\n        options: [\n            { label: \"Compact\", value: \"compact\" },\n            { label: \"Expanded\", value: \"expanded\" }\n        ]\n        defaultValue: \"compact\"\n    }\n}\n```\n\n## User Interaction\n\nDesktop widgets support:\n1. **Drag** - click and drag anywhere (in edit mode)\n2. **Resize** - drag bottom-right corner handle (in edit mode)\n3. **Edit mode toggle** - via the desktop edit button\n\n## Complete Example\n\nBased on the ExampleDesktopClock pattern:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 250\n    property real widgetHeight: 250\n    property real minWidth: 150\n    property real minHeight: 150\n\n    property string clockStyle: {\n        if (!pluginService) return \"digital\"\n        return pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n    }\n\n    property real bgOpacity: {\n        if (!pluginService) return 0.85\n        var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n        return val / 100\n    }\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId !== pluginId) return\n            clockStyle = pluginService.loadPluginData(pluginId, \"clockStyle\", \"digital\")\n            var val = pluginService.loadPluginData(pluginId, \"opacity\", 85)\n            bgOpacity = val / 100\n        }\n    }\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: root.bgOpacity\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Column {\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatTime(new Date(), \"hh:mm:ss\")\n                color: Theme.surfaceText\n                font.pixelSize: root.widgetWidth * 0.15\n                font.weight: Font.Bold\n            }\n\n            Text {\n                anchors.horizontalCenter: parent.horizontalCenter\n                text: Qt.formatDate(new Date(), \"ddd, MMM d\")\n                color: Theme.onSurfaceVariant\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    Timer {\n        interval: 1000\n        running: true\n        repeat: true\n        onTriggered: root.widgetWidth = root.widgetWidth // force update\n    }\n}\n```\n\n## Manifest Example\n\n```json\n{\n    \"id\": \"myDesktopClock\",\n    \"name\": \"Desktop Clock\",\n    \"description\": \"Analog and digital clock for the desktop\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./ClockWidget.qml\",\n    \"icon\": \"schedule\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/references/launcher-plugin-guide.md (Tokens: ~1721 | Category: anthropic-skill) -->\n# Launcher Plugin Guide\n\nLauncher plugins extend the DMS launcher with custom searchable items and actions. They use trigger-based filtering and integrate directly into the app drawer.\n\n## Base Component\n\nLaunchers use a plain `Item` (not PluginComponent):\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        // Return array of items\n        return []\n    }\n\n    function executeItem(item) {\n        // Handle item selection\n    }\n}\n```\n\n## Required Interface\n\n| Member | Type | Description |\n|--------|------|-------------|\n| `pluginService` | property | Injected PluginService reference (declare as `null`) |\n| `trigger` | property | Trigger string for activation |\n| `itemsChanged` | signal | Emit when item list changes (triggers UI refresh) |\n| `getItems(query)` | function | Return array of items matching query |\n| `executeItem(item)` | function | Handle item selection |\n\n## Item Structure\n\nEach item returned by `getItems()`:\n\n```javascript\n{\n    name: \"Item Display Name\",       // Required: shown in launcher\n    icon: \"material:star\",           // Optional: icon specification\n    comment: \"Description text\",     // Required: subtitle text\n    action: \"type:data\",             // Required: action identifier\n    categories: [\"MyPlugin\"],        // Required: array with plugin category\n    imageUrl: \"https://...\"          // Optional: image for tile view\n}\n```\n\n## Icon Types\n\n### 1. Material Design Icons\n\n```javascript\n{ icon: \"material:lightbulb\" }\n{ icon: \"material:terminal\" }\n{ icon: \"material:translate\" }\n```\n\nUses the Material Symbols Rounded font.\n\n### 2. Unicode / Emoji Icons\n\n```javascript\n{ icon: \"unicode:smile_face\" }\n```\n\nRendered at 70-80% of icon size with theming.\n\n### 3. Desktop Theme Icons\n\n```javascript\n{ icon: \"firefox\" }\n{ icon: \"folder\" }\n```\n\nUses the user's installed icon theme.\n\n### 4. No Icon\n\nOmit the `icon` field entirely. The launcher hides the icon area and gives full width to the item name.\n\n## Trigger System\n\n**Custom trigger** (items only appear when trigger is typed):\n\n```json\n{ \"trigger\": \"#\" }\n```\n\n- Type `#` alone: shows all plugin items\n- Type `# query`: filters plugin items by query\n- The query string (without trigger) is passed to `getItems(query)`\n\n**No trigger** (items always visible alongside regular apps):\n\n```json\n{ \"trigger\": \"\" }\n```\n\nSave empty trigger at runtime:\n```qml\nComponent.onCompleted: {\n    trigger = pluginService?.loadPluginData(pluginId, \"trigger\", \"#\") ?? \"#\"\n}\n```\n\n## Action Execution\n\nParse action strings in `executeItem()`:\n\n```qml\nfunction executeItem(item) {\n    const actionParts = item.action.split(\":\")\n    const actionType = actionParts[0]\n    const actionData = actionParts.slice(1).join(\":\")\n\n    switch (actionType) {\n        case \"toast\":\n            ToastService?.showInfo(actionData)\n            break\n        case \"copy\":\n            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", actionData])\n            ToastService?.showInfo(\"Copied to clipboard\")\n            break\n        case \"exec\":\n            Quickshell.execDetached(actionData.split(\" \"))\n            break\n        case \"url\":\n            Quickshell.execDetached([\"xdg-open\", actionData])\n            break\n        default:\n            console.warn(\"Unknown action type:\", actionType)\n    }\n}\n```\n\n## Search / Filtering\n\nThe `query` parameter in `getItems()` contains the user's search text (without the trigger prefix).\n\n```qml\nfunction getItems(query) {\n    const allItems = [\n        { name: \"Calculator\", icon: \"material:calculate\",\n          comment: \"Open calculator\", action: \"exec:gnome-calculator\",\n          categories: [\"Tools\"] },\n        { name: \"Terminal\", icon: \"material:terminal\",\n          comment: \"Open terminal\", action: \"exec:alacritty\",\n          categories: [\"Tools\"] }\n    ]\n\n    if (!query || query.length === 0) return allItems\n\n    const q = query.toLowerCase()\n    return allItems.filter(item =>\n        item.name.toLowerCase().includes(q) ||\n        item.comment.toLowerCase().includes(q)\n    )\n}\n```\n\n## Context Menu Actions\n\nAdd right-click actions to launcher items:\n\n```qml\nfunction getContextMenuActions(item) {\n    return [\n        { name: \"Copy\", icon: \"material:content_copy\",\n          action: \"copy:\" + item.name },\n        { name: \"Open in Browser\", icon: \"material:open_in_new\",\n          action: \"url:\" + item.url }\n    ]\n}\n```\n\nContext menu actions use the same `executeItem()` handler.\n\n## Image Tile View\n\nFor image-heavy launchers (GIF search, sticker pickers), use tile view:\n\nIn `plugin.json`:\n```json\n{\n    \"viewMode\": \"tile\",\n    \"viewModeEnforced\": true\n}\n```\n\nIn items:\n```javascript\n{\n    name: \"Image Title\",\n    imageUrl: \"https://example.com/image.png\",\n    comment: \"Description\",\n    action: \"copy:https://example.com/image.png\",\n    categories: [\"MyPlugin\"]\n}\n```\n\n## State Persistence\n\nFor plugins with persistent state (notes, history, favorites):\n\n```qml\nproperty var notes: []\n\nComponent.onCompleted: {\n    const saved = pluginService?.loadPluginState(pluginId, \"notes\", [])\n    if (saved) notes = saved\n}\n\nfunction addNote(text) {\n    notes.push({ text: text, timestamp: Date.now() })\n    pluginService?.savePluginState(pluginId, \"notes\", notes)\n    itemsChanged()\n}\n```\n\nUse `savePluginState/loadPluginState` for runtime data and `savePluginData/loadPluginData` for user preferences.\n\n## Settings for Trigger Configuration\n\nProvide a PluginSettings component for trigger customization:\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"myLauncher\"\n\n    StringSetting {\n        settingKey: \"trigger\"\n        label: \"Trigger\"\n        description: \"Type this prefix to activate the launcher plugin\"\n        placeholder: \"#\"\n        defaultValue: \"#\"\n    }\n\n    ToggleSetting {\n        settingKey: \"noTrigger\"\n        label: \"Always Visible\"\n        description: \"Show items alongside regular apps (no trigger needed)\"\n        defaultValue: false\n    }\n}\n```\n\n## Complete Example\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"!\"\n\n    signal itemsChanged()\n\n    property var commands: [\n        { name: \"Lock Screen\", icon: \"material:lock\",\n          comment: \"Lock the session\", action: \"exec:loginctl lock-session\" },\n        { name: \"Screenshot\", icon: \"material:screenshot_monitor\",\n          comment: \"Take a screenshot\", action: \"exec:grim\" },\n        { name: \"File Manager\", icon: \"material:folder\",\n          comment: \"Open file manager\", action: \"exec:nautilus\" }\n    ]\n\n    function getItems(query) {\n        if (!query) return commands\n        const q = query.toLowerCase()\n        return commands.filter(c =>\n            c.name.toLowerCase().includes(q) ||\n            c.comment.toLowerCase().includes(q)\n        )\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"exec\") {\n            Quickshell.execDetached(data.split(\" \"))\n        }\n    }\n\n    Component.onCompleted: {\n        if (pluginService) {\n            trigger = pluginService.loadPluginData(\"quickCommands\", \"trigger\", \"!\")\n        }\n    }\n}\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/references/plugin-manifest-reference.md (Tokens: ~2476 | Category: anthropic-skill) -->\n# Plugin Manifest Reference (plugin.json)\n\n## Required Fields\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `id` | string | Unique plugin identifier | camelCase, pattern `^[a-zA-Z][a-zA-Z0-9]*$` |\n| `name` | string | Human-readable name | Non-empty |\n| `description` | string | Short description (shown in UI) | Non-empty |\n| `version` | string | Semantic version | Pattern `^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9.-]+)?(\\+[a-zA-Z0-9.-]+)?$` |\n| `author` | string | Creator name or email | Non-empty |\n| `type` | string | Plugin type | One of: `widget`, `daemon`, `launcher`, `desktop`, `dash`, `dashCard`, `composite` |\n| `capabilities` | array | Plugin capabilities | At least 1 string item |\n\nOne of `component` or `components` is required (not both):\n\n| Field | Type | Description | Validation |\n|-------|------|-------------|------------|\n| `component` | string | Path to main QML file (single-surface plugins) | Must start with `./`, end with `.qml` |\n| `components` | object | Map of surface name to QML path (multi-surface plugins) | At least 1 entry; keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard` |\n\n## Conditional Requirements\n\n| Condition | Required Field | Description |\n|-----------|---------------|-------------|\n| `type: \"launcher\"` | `trigger` | Trigger string for launcher activation (e.g., `=`, `#`, `!`) |\n| `components` has `launcher` key | `trigger` | Same requirement applies to composite plugins with a launcher surface |\n\n## Optional Fields\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `icon` | string | Material Design icon name (displayed in plugin list UI) |\n| `settings` | string | Path to settings QML file (must start with `./`, end with `.qml`) |\n| `startupCheck` | string | Path to a QtObject component that gates plugin activation via a `check(done)` function (must start with `./`, end with `.qml`). See Startup Check section below. |\n| `requires_dms` | string | Minimum DMS version (e.g., `>=0.1.18`), pattern `^(>=?\\|<=?\\|=\\|>\\|<)\\d+\\.\\d+\\.\\d+$` |\n| `dependencies` | array | System tool dependencies (e.g., `[\"curl\", \"jq\"]`). Registry metadata. |\n| `requires` | array | Deprecated alias for `dependencies` |\n| `permissions` | array | Required permissions |\n| `trigger` | string | Launcher trigger string (required for launcher type) |\n| `dash` | object | Dash tab label, overview card size range and dash options. See Dash Block below. |\n\n## Permissions\n\n| Permission | Description | Enforced |\n|------------|-------------|----------|\n| `settings_read` | Read plugin configuration | No (not currently enforced) |\n| `settings_write` | Write plugin configuration / use PluginSettings | **Yes** |\n| `process` | Execute system commands | No (not currently enforced) |\n| `network` | Network access | No (not currently enforced) |\n\nIf your plugin has a `settings` component but does not declare `settings_write`, users will see an error instead of the settings UI.\n\n## Capabilities\n\nCapabilities are free-form strings that describe what the plugin does. Common values:\n\n- `dankbar-widget` - general bar widget\n- `control-center` - integrates with Control Center\n- `monitoring` - system/service monitoring\n- `launcher` - launcher search provider\n- `desktop-widget` - desktop background widget\n- `dash-tab` - dash tab\n- `dash-card` - dash overview card\n- `ai` - AI/LLM integration\n- `slideout` - uses slideout panel\n\n## Startup Check\n\nThe `startupCheck` field points to a non-visual `QtObject` component that gates plugin activation on dependency checks. The component must expose a `check(done)` function:\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed startup checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\n## Components (Composite Plugins)\n\nThe `components` field maps surface names to QML paths, allowing a single plugin to register multiple surfaces:\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\nValid surface keys: `widget`, `desktop`, `daemon`, `launcher`, `dash`, `dashCard`. Provide any subset. Each surface is loaded independently in the appropriate registry.\n\n## Dash Block\n\nPlugins with a `dash` or `dashCard` surface can describe them in a `dash` object. Everything is optional.\n\n```json\n\"dash\": {\n    \"icon\": \"timer\",\n    \"title\": \"Stopwatch\",\n    \"card\": { \"title\": \"Progress\", \"icon\": \"hourglass_bottom\", \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 4, \"maxH\": 2 },\n    \"options\": [\n        { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false, \"description\": \"Hide the secondary line\" },\n        { \"key\": \"style\", \"text\": \"Style\", \"type\": \"choice\", \"def\": \"bars\", \"choices\": [{ \"value\": \"bars\", \"text\": \"Bars\" }, { \"value\": \"dots\", \"text\": \"Dots\" }] },\n        { \"key\": \"limit\", \"text\": \"Items\", \"type\": \"number\", \"def\": 5, \"min\": 1, \"max\": 20, \"step\": 1 }\n    ]\n}\n```\n\n| Field | Description |\n|-------|-------------|\n| `icon`, `title` | Tab icon and label. Fall back to the top-level `icon` and `name` |\n| `card.title`, `card.icon` | Card name and icon in the Add widget menu. Fall back to the tab label and icon |\n| `card.w`, `card.h` | Default size in grid cells. Columns are 3 to 8 (default 6), rows are 96 px |\n| `card.minW` .. `card.maxH` | Resize range. Widths clamp to the user's column count; heights have no fixed limit |\n| `options` | Rows in the dash options sheet and Settings > Dashboard. Read as `options.<key>` in the tab and card. Types: `toggle`, `choice`, `number`. `widgets` is a reserved key |\n\n## Dash Examples\n\nStandalone tab and standalone card use the single file form:\n\n```json\n{\n    \"id\": \"dashTabExample\",\n    \"name\": \"Stopwatch Example\",\n    \"description\": \"A standalone dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./StopwatchTab.qml\",\n    \"dash\": { \"title\": \"Stopwatch\" },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\n```json\n{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"dash\": { \"card\": { \"w\": 2, \"h\": 1, \"maxW\": 4, \"maxH\": 2 } },\n    \"requires_dms\": \">=1.7.0\"\n}\n```\n\nA tab plus a card, or dash surfaces plus a bar widget, use `components` with `\"type\": \"composite\"`.\n\n## Complete Example\n\n```json\n{\n    \"id\": \"myPlugin\",\n    \"name\": \"My Plugin\",\n    \"description\": \"A sample plugin demonstrating all fields\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\", \"control-center\"],\n    \"component\": \"./MyWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"dependencies\": [\"curl\", \"jq\"],\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\", \"network\"]\n}\n```\n\n## Launcher Example\n\n```json\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Search and execute custom actions\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Developer Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./MyLauncher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"requires_dms\": \">=0.1.18\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n## JSON Schema\n\nThe complete JSON schema is available at `assets/plugin-schema.json` in this skill. Validate with:\n\n```bash\n# Using python\npython3 -c \"\nimport json, jsonschema\nschema = json.load(open('path/to/plugin-schema.json'))\nmanifest = json.load(open('plugin.json'))\njsonschema.validate(manifest, schema)\nprint('Valid!')\n\"\n\n# Using jq (syntax check only)\njq . plugin.json\n```\n\n## Additional Properties\n\nThe schema allows additional properties (`\"additionalProperties\": true`), so plugins can include custom fields. Common custom fields seen in production plugins:\n\n- `viewMode` - launcher display mode (`\"tile\"` for image grids)\n- `viewModeEnforced` - lock launcher to specific view mode (`true`/`false`)\n\n<!-- File: .agents/skills/dms-plugin-dev/references/popout-service-reference.md (Tokens: ~1028 | Category: anthropic-skill) -->\n# PopoutService Reference\n\nThe `PopoutService` singleton lets plugins control all DMS popouts and modals. It is automatically injected into widget, daemon, and settings components.\n\n## Setup\n\nDeclare the property in your component for injection to work:\n\n```qml\nproperty var popoutService: null\n```\n\nWithout this declaration, injection fails with: `Cannot assign to non-existent property \"popoutService\"`\n\n## Popouts (DankPopout-based)\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |\n| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |\n| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |\n| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |\n| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |\n| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |\n| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |\n| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |\n\n## Modals (DankModal-based)\n\n| Modal | Show | Hide | Notes |\n|-------|------|------|-------|\n| Settings | `openSettings()` | `closeSettings()` | Full settings interface |\n| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |\n| Launcher | `openDankLauncherV2()` | `closeDankLauncherV2()` | Also has `toggleDankLauncherV2()` |\n| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |\n| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Has `toggleProcessListModal()` |\n| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |\n| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |\n| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network auth |\n| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |\n\n## Slideouts\n\n| Component | Open | Close | Toggle |\n|-----------|------|-------|--------|\n| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |\n\n## Usage Examples\n\n### Simple toggle\n\n```qml\nMouseArea {\n    onClicked: popoutService?.toggleControlCenter()\n}\n```\n\n### Conditional popout\n\n```qml\nConnections {\n    target: BatteryService\n    function onPercentageChanged() {\n        if (BatteryService.percentage < 10 && !BatteryService.isCharging) {\n            popoutService?.openBattery()\n        }\n    }\n}\n```\n\n### Context menu with multiple actions\n\n```qml\nMouseArea {\n    acceptedButtons: Qt.LeftButton | Qt.RightButton\n    onClicked: (mouse) => {\n        if (mouse.button === Qt.RightButton) contextMenu.popup()\n        else popoutService?.toggleControlCenter()\n    }\n}\n\nMenu {\n    id: contextMenu\n    MenuItem { text: \"Settings\"; onClicked: popoutService?.openSettings() }\n    MenuItem { text: \"Notifications\"; onClicked: popoutService?.toggleNotificationCenter() }\n    MenuItem { text: \"Power\"; onClicked: popoutService?.openPowerMenu() }\n}\n```\n\n### Position-aware toggle (from bar pill)\n\nSome toggle functions accept position parameters for proper popout placement:\n\n```qml\npillClickAction: (x, y, width, section, screen) => {\n    popoutService?.toggleControlCenter(x, y, width, section, screen)\n}\n```\n\n## Best Practices\n\n1. **Always use optional chaining** (`?.`) - the service may not be injected yet\n2. **Check feature availability** before opening feature-specific popouts:\n   ```qml\n   if (BatteryService.batteryAvailable) {\n       popoutService?.openBattery()\n   }\n   ```\n3. **Lazy loading** - first access may activate lazy loaders; this is normal\n4. **Popouts are shared** - avoid opening conflicting popouts simultaneously\n5. **User intent** - only trigger popouts from user actions or critical system events\n6. **Multi-monitor** - positioned popouts are screen-aware when using position parameters\n\n## Injection Locations\n\nThe service is injected at these points:\n- `DMSShell.qml` - daemon plugins\n- `WidgetHost.qml` - widget plugins in left/right bar sections\n- `CenterSection.qml` - center bar widgets\n- `PluginsTab.qml` - settings components\n\n<!-- File: .agents/skills/dms-plugin-dev/references/settings-components-reference.md (Tokens: ~1773 | Category: anthropic-skill) -->\n# Settings Components Reference\n\nAll plugin settings use the `PluginSettings` wrapper. Setting components auto-save on change and auto-load on creation.\n\n## PluginSettings Wrapper\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"  // Required: must match plugin.json id\n\n    // Setting components go here\n}\n```\n\n**Important:** The plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json for the settings UI to render. Without it, users see an error.\n\n**PluginSettings provides to children:**\n- `saveValue(key, value)` - save a setting value\n- `loadValue(key, defaultValue)` - load a setting value\n- `saveState(key, value)` - save plugin state (separate file)\n- `loadState(key, defaultValue)` - load plugin state\n- `clearState()` - clear all plugin state\n- Variant management functions (for variant plugins)\n\n## StringSetting\n\nText input field.\n\n```qml\nStringSetting {\n    settingKey: \"apiKey\"        // Required: storage key\n    label: \"API Key\"            // Required: display label\n    description: \"Your API key\" // Optional: help text\n    placeholder: \"sk-...\"      // Optional: input placeholder\n    defaultValue: \"\"            // Optional: default (default: \"\")\n}\n```\n\n**Layout:** Vertical stack - label, description, input field.\n\n## ToggleSetting\n\nBoolean toggle switch.\n\n```qml\nToggleSetting {\n    settingKey: \"notifications\"    // Required: storage key\n    label: \"Enable Notifications\"  // Required: display label\n    description: \"Show alerts\"     // Optional: help text\n    defaultValue: true             // Optional: default (default: false)\n}\n```\n\n**Layout:** Horizontal - label/description on left, toggle on right.\n\n## SelectionSetting\n\nDropdown menu.\n\n```qml\nSelectionSetting {\n    settingKey: \"theme\"           // Required: storage key\n    label: \"Theme\"                // Required: display label\n    description: \"Color scheme\"   // Optional: help text\n    options: [                    // Required: array of options\n        { label: \"Dark\", value: \"dark\" },\n        { label: \"Light\", value: \"light\" },\n        { label: \"Auto\", value: \"auto\" }\n    ]\n    defaultValue: \"dark\"          // Optional: default value\n}\n```\n\nOptions can be `{ label, value }` objects or simple strings. Stores the `value` field, displays the `label` field.\n\n**Layout:** Horizontal - label/description on left, dropdown on right.\n\n**Reacting to changes:**\n```qml\nSelectionSetting {\n    settingKey: \"updateInterval\"\n    label: \"Update Interval\"\n    options: [\n        { label: \"1 minute\", value: \"60\" },\n        { label: \"5 minutes\", value: \"300\" }\n    ]\n    defaultValue: \"300\"\n    onValueChanged: (newValue) => {\n        console.log(\"Interval changed to:\", newValue)\n    }\n}\n```\n\n## SliderSetting\n\nNumeric slider with min/max.\n\n```qml\nSliderSetting {\n    settingKey: \"opacity\"      // Required: storage key\n    label: \"Opacity\"           // Required: display label\n    description: \"Background\"  // Optional: help text\n    defaultValue: 85           // Optional: default value\n    minimum: 0                 // Required: min value\n    maximum: 100               // Required: max value\n    unit: \"%\"                  // Optional: unit label shown after value\n    leftIcon: \"dark_mode\"      // Optional: Material icon on left\n    rightIcon: \"light_mode\"    // Optional: Material icon on right\n}\n```\n\n## ColorSetting\n\nColor picker.\n\n```qml\nColorSetting {\n    settingKey: \"accentColor\"      // Required: storage key\n    label: \"Accent Color\"          // Required: display label\n    description: \"Custom accent\"   // Optional: help text\n    defaultValue: \"#ff5722\"        // Optional: default hex color\n}\n```\n\nDisplays a color swatch that opens a color picker dialog.\n\n## ListSetting\n\nManage a list of items with manual add/remove. Use when you need custom UI for adding items.\n\n```qml\nListSetting {\n    id: itemList\n    settingKey: \"items\"           // Required: storage key\n    label: \"Saved Items\"          // Required: display label\n    description: \"Your items\"     // Optional: help text\n    defaultValue: []              // Optional: default array\n    delegate: Component {         // Optional: custom item display\n        StyledRect {\n            width: parent.width\n            height: 40\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                anchors.left: parent.left\n                anchors.leftMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                text: modelData.name\n                color: Theme.surfaceText\n            }\n\n            Rectangle {\n                anchors.right: parent.right\n                anchors.rightMargin: Theme.spacingM\n                anchors.verticalCenter: parent.verticalCenter\n                width: 60\n                height: 28\n                color: removeArea.containsMouse ? Theme.errorHover : Theme.error\n                radius: Theme.cornerRadius\n\n                StyledText {\n                    anchors.centerIn: parent\n                    text: \"Remove\"\n                    color: Theme.errorText\n                    font.pixelSize: Theme.fontSizeSmall\n                    font.weight: Font.Medium\n                }\n\n                MouseArea {\n                    id: removeArea\n                    anchors.fill: parent\n                    hoverEnabled: true\n                    cursorShape: Qt.PointingHandCursor\n                    onClicked: itemList.removeItem(index)\n                }\n            }\n        }\n    }\n}\n```\n\n**Methods:**\n- `addItem(item)` - add an item to the list\n- `removeItem(index)` - remove item at index\n\n## ListSettingWithInput\n\nComplete list management with built-in form. Best for collecting structured data.\n\n```qml\nListSettingWithInput {\n    settingKey: \"locations\"        // Required: storage key\n    label: \"Locations\"             // Required: display label\n    description: \"Track zones\"     // Optional: help text\n    defaultValue: []               // Optional: default array\n    fields: [                      // Required: field definitions\n        {\n            id: \"name\",            // Required: key in saved object\n            label: \"Name\",         // Required: column header\n            placeholder: \"Home\",   // Optional: input placeholder\n            width: 150,            // Optional: column width (default: 200)\n            required: true         // Optional: must have value to add\n        },\n        {\n            id: \"timezone\",\n            label: \"Timezone\",\n            placeholder: \"America/New_York\",\n            width: 200,\n            required: true\n        }\n    ]\n}\n```\n\nAutomatically generates: column headers, input fields, add button with validation, list display, remove buttons.\n\n## Mixing Custom UI with Settings\n\nYou can interleave regular QML elements with setting components:\n\n```qml\nPluginSettings {\n    pluginId: \"myPlugin\"\n\n    StyledText {\n        width: parent.width\n        text: \"General Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n    }\n\n    StringSetting {\n        settingKey: \"name\"\n        label: \"Display Name\"\n    }\n\n    StyledText {\n        width: parent.width\n        text: \"Advanced Settings\"\n        font.pixelSize: Theme.fontSizeLarge\n        font.weight: Font.Bold\n        color: Theme.surfaceText\n        topPadding: Theme.spacingL\n    }\n\n    ToggleSetting {\n        settingKey: \"debug\"\n        label: \"Debug Mode\"\n        defaultValue: false\n    }\n}\n```\n\n## Default Values\n\nDefine sensible defaults in every setting component. The default is used when no saved value exists:\n\n```qml\nStringSetting { settingKey: \"text\"; defaultValue: \"Hello\" }\nToggleSetting { settingKey: \"enabled\"; defaultValue: true }\nSelectionSetting { settingKey: \"mode\"; defaultValue: \"auto\" }\nSliderSetting { settingKey: \"opacity\"; defaultValue: 85 }\nColorSetting { settingKey: \"color\"; defaultValue: \"#ff5722\" }\nListSetting { settingKey: \"items\"; defaultValue: [] }\nListSettingWithInput { settingKey: \"data\"; defaultValue: [] }\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/references/theme-reference.md (Tokens: ~1162 | Category: anthropic-skill) -->\n# Theme Property Reference\n\nAll theme properties are accessed via the `Theme` singleton from `qs.Common`. Always use these instead of hardcoded values.\n\n## Font Sizes\n\n```qml\nTheme.fontSizeSmall     // 12px (scaled by SettingsData.fontScale)\nTheme.fontSizeMedium    // 14px (scaled)\nTheme.fontSizeLarge     // 16px (scaled)\nTheme.fontSizeXLarge    // 20px (scaled)\n```\n\n## Icon Sizes\n\n```qml\nTheme.iconSizeSmall     // 16px\nTheme.iconSize          // 24px (default)\nTheme.iconSizeLarge     // 32px\n```\n\n## Spacing\n\n```qml\nTheme.spacingXS         // Extra small\nTheme.spacingS          // Small\nTheme.spacingM          // Medium\nTheme.spacingL          // Large\nTheme.spacingXL         // Extra large\n```\n\n## Border Radius\n\n```qml\nTheme.cornerRadius      // Standard\nTheme.cornerRadiusSmall // Smaller\nTheme.cornerRadiusLarge // Larger\n```\n\n## Surface Colors\n\n```qml\nTheme.surface\nTheme.surfaceContainerLow\nTheme.surfaceContainer\nTheme.surfaceContainerHigh\nTheme.surfaceContainerHighest\n```\n\n## Text Colors\n\n```qml\nTheme.onSurface          // Primary text on surface\nTheme.onSurfaceVariant   // Secondary text on surface\nTheme.surfaceText        // Alias for primary surface text\nTheme.surfaceVariantText // Alias for secondary surface text\nTheme.outline            // Border/divider color\n```\n\n## Semantic Colors\n\n```qml\nTheme.primary\nTheme.onPrimary\nTheme.secondary\nTheme.onSecondary\nTheme.error\nTheme.errorHover\nTheme.errorText\nTheme.warning\nTheme.success\n```\n\n## Special Functions\n\n```qml\nTheme.popupBackground()  // Popup background with proper opacity\n```\n\n## Common Widget Patterns\n\n### Icon with Text\n\n```qml\nimport qs.Widgets\nimport qs.DCommon.Widgets\n\nRow {\n    spacing: Theme.spacingS\n\n    DIcon {\n        name: \"icon_name\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.iconSize\n    }\n\n    StyledText {\n        text: \"Label\"\n        color: Theme.onSurface\n        font.pixelSize: Theme.fontSizeMedium\n    }\n}\n```\n\n### Container with Border\n\n```qml\nRectangle {\n    color: Theme.surfaceContainerHigh\n    radius: Theme.cornerRadius\n    border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)\n    border.width: 1\n}\n```\n\n### Hover Effect\n\n```qml\nRectangle {\n    id: container\n    color: Theme.surfaceContainerHigh\n\n    MouseArea {\n        anchors.fill: parent\n        hoverEnabled: true\n        onEntered: container.color = Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        onExited: container.color = Theme.surfaceContainerHigh\n    }\n}\n```\n\n### Clickable Pill\n\n```qml\nStyledRect {\n    width: content.implicitWidth + Theme.spacingM * 2\n    height: parent.widgetThickness\n    radius: Theme.cornerRadius\n    color: mouseArea.containsMouse\n        ? Qt.lighter(Theme.surfaceContainerHigh, 1.1)\n        : Theme.surfaceContainerHigh\n\n    MouseArea {\n        id: mouseArea\n        anchors.fill: parent\n        hoverEnabled: true\n        cursorShape: Qt.PointingHandCursor\n    }\n\n    Row {\n        id: content\n        anchors.centerIn: parent\n        spacing: Theme.spacingS\n\n        DIcon {\n            name: \"star\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.iconSize\n            anchors.verticalCenter: parent.verticalCenter\n        }\n\n        StyledText {\n            text: \"Label\"\n            color: Theme.surfaceText\n            font.pixelSize: Theme.fontSizeMedium\n            anchors.verticalCenter: parent.verticalCenter\n        }\n    }\n}\n```\n\n## Common Mistakes\n\n**Wrong property names** (these do NOT exist):\n```qml\nTheme.fontSizeS        // Use Theme.fontSizeSmall\nTheme.iconSizeS        // Use Theme.iconSizeSmall\nTheme.spacingSmall     // Use Theme.spacingS\nTheme.borderRadius     // Use Theme.cornerRadius\n```\n\n**Hardcoded values** (do NOT do this):\n```qml\ncolor: \"#1e1e1e\"       // Use Theme.surfaceContainerHigh\ncolor: \"white\"         // Use Theme.surfaceText\nfont.pixelSize: 14     // Use Theme.fontSizeMedium\n```\n\n## Available Widgets from qs.Widgets\n\n| Widget | Description |\n|--------|-------------|\n| `StyledText` | Themed text with proper color defaults |\n| `StyledRect` | Themed rectangle |\n| `DIcon` | Material Symbols icon renderer |\n| `DankNFIcon` | Nerd Font icon renderer |\n| `DButton` | Themed button |\n| `DToggle` | Toggle switch |\n| `DTextField` | Text input field |\n| `DSlider` | Slider control |\n| `DDropdown` | Dropdown menu |\n| `DGridView` | Grid layout view |\n| `DListView` | List layout view |\n| `DFlickable` | Scrollable container |\n| `DTabBar` | Tab bar navigation |\n| `DankCollapsibleSection` | Collapsible content section |\n| `DankTooltip` | Hover tooltip |\n| `DankNumberStepper` | Number +/- control |\n| `DankFilterChips` | Filter chip row |\n| `CachingImage` | Image with disk cache |\n| `NumericText` | Fixed-width numeric display |\n\n## Checking All Properties\n\n```bash\ngrep \"property\" Common/Theme.qml\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/references/widget-plugin-guide.md (Tokens: ~2630 | Category: anthropic-skill) -->\n# Widget Plugin Guide\n\nWidgets display pills in DankBar and in docks (same component, no extra code), optionally open popouts, and can integrate with the Control Center.\n\n## Base Component\n\nWidgets use `PluginComponent` from `qs.Modules.Plugins`.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n    popoutContent: Component { /* ... */ }\n    popoutWidth: 400\n    popoutHeight: 300\n}\n```\n\n## Injected Properties\n\nThese are automatically set by the plugin host:\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `axis` | object | Bar axis info (horizontal/vertical) |\n| `section` | string | Bar section: `\"left\"`, `\"center\"`, or `\"right\"` |\n| `parentScreen` | object | Screen reference for multi-monitor |\n| `widgetThickness` | real | Widget size perpendicular to bar edge |\n| `barThickness` | real | Bar thickness parallel to edge |\n| `pluginId` | string | This plugin's ID |\n| `pluginService` | object | PluginService reference |\n| `pluginData` | object | Reactive plugin settings data |\n| `surfaceContext` | object | The host surface: `kind` (`\"bar\"` or `\"dock\"`), `id`, `screen`, `isVertical`, `thickness`, `live`, `editMode`, `inlineExpansion` |\n| `widgetInstanceId` | string | This occurrence; the same widget can appear on several bars and docks |\n\n## Bar Pills\n\nDefine `horizontalBarPill` (for top/bottom bars) and `verticalBarPill` (for left/right bars).\n\n### Horizontal Bar Pill\n\n```qml\nhorizontalBarPill: Component {\n    StyledRect {\n        width: content.implicitWidth + Theme.spacingM * 2\n        height: parent.widgetThickness\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Row {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSize\n                anchors.verticalCenter: parent.verticalCenter\n            }\n\n            StyledText {\n                text: \"Label\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n                anchors.verticalCenter: parent.verticalCenter\n            }\n        }\n    }\n}\n```\n\n### Vertical Bar Pill\n\n```qml\nverticalBarPill: Component {\n    StyledRect {\n        width: parent.widgetThickness\n        height: content.implicitHeight + Theme.spacingM * 2\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainerHigh\n\n        Column {\n            id: content\n            anchors.centerIn: parent\n            spacing: Theme.spacingS\n\n            DIcon {\n                name: \"star\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.iconSizeSmall\n            }\n        }\n    }\n}\n```\n\n**Important:** Always define both pills. If a pill is missing, the widget disappears when the bar is on that orientation's edge.\n\n## Popout Content\n\nOpen a popout window when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                    font.pixelSize: Theme.fontSizeMedium\n                }\n            }\n        }\n    }\n}\n```\n\n**PopoutComponent properties:**\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `headerText` | string | `\"\"` | Main header (bold, large). Hidden if empty. |\n| `detailsText` | string | `\"\"` | Subtitle below header. Hidden if empty. |\n| `closePopout` | function | (injected) | Call to close the popout programmatically. |\n| `headerHeight` | int | (readonly) | Height of header area (0 if hidden). |\n| `detailsHeight` | int | (readonly) | Height of details area (0 if hidden). |\n\n**Content sizing:** Content children render below the header/details. Calculate available height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Custom Click Actions\n\nOverride the default popout behavior:\n\n```qml\nPluginComponent {\n    // Simple no-args handler\n    pillClickAction: () => {\n        popoutService?.toggleControlCenter()\n    }\n\n    // With position params (x, y, width, section, screen)\n    pillClickAction: (x, y, width, section, screen) => {\n        popoutService?.toggleControlCenter(x, y, width, section, screen)\n    }\n\n    pillRightClickAction: () => {\n        popoutService?.openSettings()\n    }\n}\n```\n\n## Control Center Integration\n\nAdd CC properties to show your widget in the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"Feature Name\"\n    ccWidgetSecondaryText: isActive ? \"Active\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n}\n```\n\n**CC properties:**\n\n| Property | Type | Description |\n|----------|------|-------------|\n| `ccWidgetIcon` | string | Material icon name |\n| `ccWidgetPrimaryText` | string | Main label |\n| `ccWidgetSecondaryText` | string | Subtitle / status text |\n| `ccWidgetIsActive` | bool | Active state (changes styling) |\n\n**CC signals:**\n\n| Signal | When fired |\n|--------|-----------|\n| `ccWidgetToggled()` | Icon area clicked |\n| `ccWidgetExpanded()` | Expand area clicked (CompoundPill only) |\n\n**CC sizing rules:**\n- 25% width - SmallToggleButton (icon only)\n- 50% width - ToggleButton (no detail) or CompoundPill (with ccDetailContent)\n- Users can resize in CC edit mode\n\n### Detail Content (CompoundPill)\n\nAdd an expandable panel below the CC widget:\n\n```qml\nccDetailContent: Component {\n    Rectangle {\n        implicitHeight: 200\n        color: Theme.surfaceContainerHigh\n        radius: Theme.cornerRadius\n\n        Column {\n            anchors.fill: parent\n            anchors.margins: Theme.spacingM\n            spacing: Theme.spacingS\n\n            // Detail UI here\n        }\n    }\n}\n```\n\n## Visibility Control\n\nConditionally show/hide the bar pill:\n\n```qml\nPluginComponent {\n    visibilityCommand: \"pgrep -x myapp\"\n    visibilityInterval: 5  // seconds between checks\n}\n```\n\n**Bar reveal optimization:** The visibility timer automatically pauses while the bar is hidden (auto-hide mode) and resumes checks when the bar is revealed. This is handled via the internal `_barRevealed` property - no plugin code needed. Plugins using `visibilityCommand` with `visibilityInterval` benefit from this automatically.\n\n## Docks\n\nUsers add any loaded widget plugin to a dock from the dock's edit mode or Settings > Dock > Apps & widgets. Nothing in the manifest is needed.\n\n- Define both `horizontalBarPill` and `verticalBarPill`; docks sit on any edge.\n- Size icons with `root.iconSize` and text with `Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)`. The dock hands its own thickness and scale in those properties.\n- Gate timers and animations on `surfaceLive` (host visible and `visibilityCommand` passing). Docks auto-hide.\n- Don't assume one instance. Use `widgetInstanceId` if an instance needs its own state.\n\nOptional attached panel: set `attachedContent: Component { ... }`. When the dock's \"Open widgets\" setting is Inline (`surfaceContext.inlineExpansion`), a click grows the dock and shows the panel next to the apps. With Popout (the default) the normal `popoutContent` opens instead; a widget with no popout still expands inline. Bars always use `popoutContent`. A `pillClickAction` always wins over both.\n\n```qml\nPluginComponent {\n    id: root\n\n    attachedContent: Component {\n        Column {\n            padding: Theme.spacingM\n            spacing: Theme.spacingM\n\n            DButton {\n                text: I18n.trFor(\"myPlugin\", \"Close\")\n                onClicked: root.surfaceContext.dismissExpansion()\n            }\n        }\n    }\n}\n```\n\nThe panel spans the dock's length and grows up to 320 px away from the edge (at most half the screen). The plugin cannot size it. `attachedActive` is true while this widget owns the panel. Escape closes it. See `quickshell/PLUGINS/AttachedPanelExample` and `quickshell/PLUGINS/SURFACE_WIDGETS.md`.\n\n## Popout Namespace\n\nFor plugins with multiple popout instances, use `layerNamespacePlugin` to isolate popout state:\n\n```qml\nPluginComponent {\n    layerNamespacePlugin: \"my-plugin\"\n}\n```\n\nThe shell prefixes it, so the popout's namespace becomes `dms:plugins:my-plugin`.\n\n## Reading Plugin Data\n\nAccess saved settings reactively via the injected `pluginData`:\n\n```qml\nPluginComponent {\n    property string displayText: pluginData?.text || \"Default\"\n\n    Connections {\n        target: pluginService\n        function onPluginDataChanged(changedId) {\n            if (changedId === pluginId)\n                displayText = pluginService.loadPluginData(pluginId, \"text\", \"Default\")\n        }\n    }\n}\n```\n\n## Complete Example\n\nBased on the ExampleEmojiPlugin pattern:\n\n```qml\nimport QtQuick\nimport Quickshell\nimport qs.Common\nimport qs.Widgets\nimport qs.DCommon.Widgets\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    id: root\n    property var popoutService: null\n\n    property var emojis: [\"star\", \"heart\", \"smile\"]\n    property int currentIndex: 0\n\n    Timer {\n        interval: 2000\n        running: true\n        repeat: true\n        onTriggered: currentIndex = (currentIndex + 1) % emojis.length\n    }\n\n    popoutWidth: 350\n    popoutHeight: 400\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeLarge\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: root.emojis[root.currentIndex]\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"Emoji Picker\"\n\n            DGridView {\n                width: parent.width\n                height: 300\n                cellWidth: 50\n                cellHeight: 50\n                model: root.emojis\n\n                delegate: Rectangle {\n                    width: 48\n                    height: 48\n                    radius: Theme.cornerRadius\n                    color: mouseArea.containsMouse ? Theme.surfaceContainerHighest : \"transparent\"\n\n                    Text {\n                        anchors.centerIn: parent\n                        text: modelData\n                        font.pixelSize: 24\n                    }\n\n                    MouseArea {\n                        id: mouseArea\n                        anchors.fill: parent\n                        hoverEnabled: true\n                        onClicked: {\n                            Quickshell.execDetached([\"dms\", \"cl\", \"copy\", modelData])\n                            ToastService?.showInfo(\"Copied \" + modelData)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n<!-- File: .agents/skills/dms-plugin-dev/SKILL.md (Tokens: ~5979 | Category: anthropic-skill) -->\n---\nname: dms-plugin-dev\ndescription: >\n  Develop plugins for Dux Shell (DMS), a QML-based Linux desktop shell built on\n  Quickshell. Supports widget (bar, dock + Control Center), daemon (background service),\n  launcher (search + actions), desktop (draggable desktop widgets), dash (dash tab),\n  dashCard (dash overview card), and composite (multi-surface) plugins. Covers manifest\n  creation, QML component development, startup checks,\n  settings UI, data persistence, theme integration, PopoutService usage, IPC runtime\n  discovery, and external command execution. Use when the user wants to create, modify,\n  or debug a DMS plugin, or asks about the DMS plugin API.\ncompatibility: Designed for Claude Code (or similar products)\nmetadata:\n  author: Dux Shell\n  version: \"1.2\"\n  domain: qml-desktop-development\n  framework: Dux Shell\n  languages: qml, javascript\nallowed-tools: Bash Read Write Edit\n---\n\n# Dux Shell Plugin Development\n\n## Overview\n\nDMS plugins extend the desktop shell with bar and dock widgets, background services, launcher\nintegrations, desktop widgets, and dash tabs and cards. Plugins are QML components discovered from\n`~/.config/dms/plugins/`.\n\n**Minimum plugin structure:**\n\n```\n~/.config/dms/plugins/YourPlugin/\n  plugin.json        # Required: manifest with metadata\n  YourComponent.qml  # Required: main QML component\n  YourSettings.qml   # Optional: settings UI\n  *.js               # Optional: JavaScript utilities\n```\n\n**Plugin registry:** Community plugins are available at https://plugins.duxlabs.dev/\n\n**Plugin types:**\n\n| Type        | Purpose                        | Base Component             | Bar/dock pills | CC integration |\n|-------------|--------------------------------|----------------------------|----------------|----------------|\n| `widget`    | Bar or dock widget + popout    | `PluginComponent`          | Yes            | Yes            |\n| `daemon`    | Background service             | `PluginComponent` (no UI)  | No             | Optional       |\n| `launcher`  | Searchable items in launcher   | `Item`                     | No             | No             |\n| `desktop`   | Draggable desktop widget       | `DesktopPluginComponent`   | No             | No             |\n| `dash`      | Tab in the dash popout         | `DashTabComponent`         | No             | No             |\n| `dashCard`  | Card in the dash overview grid | `DashCardComponent`        | No             | No             |\n| `composite` | Multi-surface plugin           | One component per surface  | Optional       | Optional       |\n\n## Step 1: Determine Plugin Type\n\nChoose the type based on what the plugin does:\n\n- **Shows in the bar or a dock?** - Use `widget`. Displays a pill in DankBar or a dock (same\n  component), optionally opens a popout or an attached dock panel, optionally integrates with\n  Control Center.\n- **Runs in background only?** - Use `daemon`. No visible UI, reacts to events (wallpaper\n  changes, notifications, battery level, etc.).\n- **Provides searchable/actionable items?** - Use `launcher`. Items appear in the DMS launcher\n  with trigger-based filtering (e.g., type `=` for calculator, `:` for emoji).\n- **Shows on the desktop background?** - Use `desktop`. Draggable, resizable widget on the\n  desktop layer.\n- **Full page in the dash?** - Use `dash`. A tab in the dash popout, id `plugin_<pluginId>`.\n- **Small tile on the dash overview?** - Use `dashCard`. A resizable card in the overview grid.\n- **Needs multiple surfaces?** - Use `composite`. A single plugin that registers any combination\n  of the above (e.g., a daemon + bar widget + desktop widget). Each surface gets its own\n  QML component file.\n\n## Step 2: Create the Manifest\n\nCreate `plugin.json` in your plugin directory. See [plugin-manifest-reference.md](references/plugin-manifest-reference.md) for the full schema.\n\n**Minimal manifest:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description of what your plugin does\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\"\n}\n```\n\n**With settings, startup check, and permissions:**\n\n```json\n{\n    \"id\": \"yourPlugin\",\n    \"name\": \"Your Plugin Name\",\n    \"description\": \"Brief description\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"your-capability\"],\n    \"component\": \"./YourWidget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"requires_dms\": \">=0.1.0\",\n    \"dependencies\": [\"mytool\"],\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Composite plugin (multi-surface):**\n\n```json\n{\n    \"id\": \"myComposite\",\n    \"name\": \"My Composite Plugin\",\n    \"description\": \"Daemon + widget + desktop from one plugin\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"daemon\", \"dankbar-widget\", \"desktop-widget\"],\n    \"icon\": \"extension\",\n    \"components\": {\n        \"daemon\": \"./MyDaemon.qml\",\n        \"widget\": \"./MyBarWidget.qml\",\n        \"desktop\": \"./MyDesktopWidget.qml\"\n    },\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n```\n\n**Standalone dash tab or card:** same single file form, `\"type\": \"dash\"` or `\"type\": \"dashCard\"`\nplus `\"component\"`. A tab and a card together (or with a bar widget) use `components` with the\nkeys `dash` and `dashCard`. Both accept an optional `dash` block for labels, card size and\noptions. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n**Key rules:**\n- `id` must be camelCase, matching pattern `^[a-zA-Z][a-zA-Z0-9]*$`\n- `version` must be semver (e.g., `1.0.0`)\n- Provide either `component` (single-surface) or `components` (multi-surface), not both\n- `component` / component paths must start with `./` and end with `.qml`\n- `type: \"launcher\"` (or a `components` object with a `launcher` key) requires a `trigger` field\n- `settings_write` permission is **required** if the plugin has a settings component\n- `dependencies` replaces the deprecated `requires` field\n\n## Step 3: Create the Main Component\n\n### Widget\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    horizontalBarPill: Component {\n        StyledRect {\n            width: label.implicitWidth + Theme.spacingM * 2\n            height: parent.widgetThickness\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hello\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeMedium\n            }\n        }\n    }\n\n    verticalBarPill: Component {\n        StyledRect {\n            width: parent.widgetThickness\n            height: label.implicitHeight + Theme.spacingM * 2\n            radius: Theme.cornerRadius\n            color: Theme.surfaceContainerHigh\n\n            StyledText {\n                id: label\n                anchors.centerIn: parent\n                text: \"Hi\"\n                color: Theme.surfaceText\n                font.pixelSize: Theme.fontSizeSmall\n                rotation: 90\n            }\n        }\n    }\n}\n```\n\nSee [widget-plugin-guide.md](references/widget-plugin-guide.md) for popouts, CC integration, and advanced features.\n\n### Launcher\n\n```qml\nimport QtQuick\nimport qs.Services\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string trigger: \"#\"\n\n    signal itemsChanged()\n\n    function getItems(query) {\n        const items = [\n            { name: \"Item One\", icon: \"material:star\", comment: \"Description\",\n              action: \"toast:Hello!\", categories: [\"MyPlugin\"] }\n        ]\n        if (!query) return items\n        const q = query.toLowerCase()\n        return items.filter(i => i.name.toLowerCase().includes(q))\n    }\n\n    function executeItem(item) {\n        const [type, ...rest] = item.action.split(\":\")\n        const data = rest.join(\":\")\n        if (type === \"toast\") ToastService?.showInfo(data)\n        else if (type === \"copy\") Quickshell.execDetached([\"dms\", \"cl\", \"copy\", data])\n    }\n}\n```\n\nSee [launcher-plugin-guide.md](references/launcher-plugin-guide.md) for triggers, icon types, context menus, and image tiles.\n\n### Desktop\n\n```qml\nimport QtQuick\nimport qs.Common\n\nItem {\n    id: root\n\n    property var pluginService: null\n    property string pluginId: \"\"\n    property bool editMode: false\n    property real widgetWidth: 200\n    property real widgetHeight: 200\n    property real minWidth: 150\n    property real minHeight: 150\n\n    Rectangle {\n        anchors.fill: parent\n        radius: Theme.cornerRadius\n        color: Theme.surfaceContainer\n        opacity: 0.85\n        border.color: root.editMode ? Theme.primary : \"transparent\"\n        border.width: root.editMode ? 2 : 0\n\n        Text {\n            anchors.centerIn: parent\n            text: \"Desktop Widget\"\n            color: Theme.surfaceText\n        }\n    }\n}\n```\n\nSee [desktop-plugin-guide.md](references/desktop-plugin-guide.md) for sizing, persistence, and edit mode.\n\n### Daemon\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Services\nimport qs.Modules.Plugins\n\nPluginComponent {\n    property var popoutService: null\n\n    Connections {\n        target: SessionData\n        function onSomeSignal() {\n            console.log(\"Event received\")\n        }\n    }\n}\n```\n\nSee [daemon-plugin-guide.md](references/daemon-plugin-guide.md) for event-driven patterns and process execution.\n\n### Dash Tab\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash\n\nDashTabComponent {\n    id: root\n\n    property date now: new Date()\n\n    implicitHeight: DashMetrics.tabMinHeight\n\n    Timer {\n        interval: 1000\n        repeat: true\n        running: root.live\n        onTriggered: root.now = new Date()\n    }\n\n    StyledText {\n        anchors.centerIn: parent\n        text: root.now.toLocaleTimeString(I18n.locale())\n        color: Theme.surfaceText\n    }\n}\n```\n\n### Dash Card\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nDashCardComponent {\n    id: root\n\n    tone: options.tone ?? \"\"\n\n    StyledText {\n        anchors.centerIn: parent\n        text: I18n.trFor(\"myPlugin\", \"Hello\")\n        color: root.contentColor\n    }\n}\n```\n\nGate timers and animations on `live`; the dash content stays alive after the popout closes.\nCards must lay out for every size in their manifest range. Use the card colors (`accentColor`,\n`contentColor`, `mutedColor`) so tones work. See [dash-plugin-guide.md](references/dash-plugin-guide.md).\n\n### Composite\n\nFor composite plugins, create a separate QML file per surface. Each surface uses the same\nbase component as the corresponding single-surface type (PluginComponent for widget/daemon,\nItem for launcher, etc.). All surfaces share the same `pluginId` and `pluginService`.\n\n```\nMyCompositePlugin/\n  plugin.json\n  MyBarWidget.qml      # PluginComponent (widget surface)\n  MyDaemon.qml         # PluginComponent (daemon surface)\n  MyDesktopWidget.qml  # Item with desktop widget properties\n  Settings.qml         # Shared settings for all surfaces\n```\n\n`pluginService.availablePlugins[pluginId].surfaces` lists the surfaces a plugin registered.\n\n## Step 4: Add Startup Check (Optional)\n\nGate plugin activation on dependency checks by providing a `startupCheck` component. This\nruns before the plugin loads and blocks activation if a required tool or condition is missing.\n\nCreate a `StartupCheck.qml` (non-visual QtObject):\n\n```qml\nimport QtQuick\nimport qs.Common\n\nQtObject {\n    function check(done) {\n        Proc.runCommand(\"myPlugin.depCheck\", [\"sh\", \"-c\", \"command -v mytool\"], (stdout, exitCode) => {\n            if (exitCode === 0) {\n                done(null);\n                return;\n            }\n            done({\n                \"title\": I18n.tr(\"mytool is required\"),\n                \"details\": I18n.tr(\"Install 'mytool' and re-enable this plugin.\")\n            });\n        });\n    }\n}\n```\n\nThe `done` callback accepts:\n- `null` - allow activation\n- A string - block with a short error message\n- `{ title, details }` - block with a title and expandable details body\n\nA synchronous variant (no `done` parameter, return the result directly) is also supported.\n\nFailed checks show a toast error and store the error in `pluginService.pluginLoadErrors`.\n\nAdd to your manifest:\n```json\n{\n    \"startupCheck\": \"./StartupCheck.qml\",\n    \"dependencies\": [\"mytool\"]\n}\n```\n\n## Step 5: Add Settings (Optional)\n\nWrap settings in `PluginSettings` with your `pluginId`. All settings auto-save and auto-load.\n\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n\nPluginSettings {\n    pluginId: \"yourPlugin\"\n\n    StringSetting {\n        settingKey: \"apiKey\"\n        label: \"API Key\"\n        description: \"Your API key\"\n        placeholder: \"sk-...\"\n    }\n\n    ToggleSetting {\n        settingKey: \"enabled\"\n        label: \"Enable Feature\"\n        defaultValue: true\n    }\n\n    SelectionSetting {\n        settingKey: \"interval\"\n        label: \"Refresh Interval\"\n        options: [\n            { label: \"1 min\", value: \"60\" },\n            { label: \"5 min\", value: \"300\" }\n        ]\n        defaultValue: \"300\"\n    }\n}\n```\n\n**Available setting components:** StringSetting, ToggleSetting, SelectionSetting, SliderSetting, ColorSetting, ListSetting, ListSettingWithInput.\n\nSee [settings-components-reference.md](references/settings-components-reference.md) for full property lists.\n\n**Important:** Your plugin must declare `\"permissions\": [\"settings_write\"]` in plugin.json, or the settings UI will show an error.\n\n## Step 6: Use Data Persistence\n\nThree tiers of persistence:\n\n| API | Persisted | Use case |\n|-----|-----------|----------|\n| `pluginService.savePluginData(id, key, val)` / `loadPluginData(id, key, default)` | Yes (settings.json) | User preferences, config |\n| `pluginService.savePluginState(id, key, val)` / `loadPluginState(id, key, default)` | Yes (separate state file) | Runtime state, history, cache |\n| `PluginGlobalVar { varName; defaultValue; value; set() }` | No (runtime only) | Cross-instance shared state |\n| `pluginService.getPluginPath(id)` | N/A | Get the plugin's installation directory path |\n\n- `pluginData` is a reactive property on PluginComponent, auto-loaded from settings\n- React to settings changes with `Connections { target: pluginService; function onPluginDataChanged(id) { ... } }`\n- Global vars sync across all instances (multi-monitor, multiple bar sections)\n\nSee [data-persistence-guide.md](references/data-persistence-guide.md) for details and examples.\n\n## Step 7: Theme Integration\n\nAlways use `Theme.*` properties from `qs.Common` - never hardcode colors or sizes.\n\n**Essential properties:**\n- Colors: `Theme.surfaceContainerHigh`, `Theme.surfaceText`, `Theme.primary`, `Theme.onPrimary`\n- Fonts: `Theme.fontSizeSmall` (12), `Theme.fontSizeMedium` (14), `Theme.fontSizeLarge` (16), `Theme.fontSizeXLarge` (20)\n- Spacing: `Theme.spacingXS`, `Theme.spacingS`, `Theme.spacingM`, `Theme.spacingL`, `Theme.spacingXL`\n- Radius: `Theme.cornerRadius`, `Theme.cornerRadiusSmall`, `Theme.cornerRadiusLarge`\n- Icons: `Theme.iconSizeSmall` (16), `Theme.iconSize` (24), `Theme.iconSizeLarge` (32)\n\n**Common widgets from `qs.DCommon.Widgets`:** `StyledText`, `StyledRect`, `DIcon`, `DButton`, `DToggle`, `DTextField`, `DSlider`, `DGridView`, `CachingImage`. The old `Dank*` names in `qs.Widgets` are deprecated.\n\nSee [theme-reference.md](references/theme-reference.md) for the complete property list.\n\n## Step 8: Add Popout Content (Widgets Only)\n\nAdd a popout that opens when the bar pill is clicked:\n\n```qml\nPluginComponent {\n    popoutWidth: 400\n    popoutHeight: 300\n\n    popoutContent: Component {\n        PopoutComponent {\n            headerText: \"My Plugin\"\n            detailsText: \"Optional subtitle\"\n\n            Column {\n                width: parent.width\n                spacing: Theme.spacingM\n\n                StyledText {\n                    text: \"Content here\"\n                    color: Theme.surfaceText\n                }\n            }\n        }\n    }\n\n    horizontalBarPill: Component { /* ... */ }\n    verticalBarPill: Component { /* ... */ }\n}\n```\n\n**PopoutComponent properties:** `headerText`, `detailsText`, `closePopout()` (auto-injected), `headerHeight` (readonly), `detailsHeight` (readonly).\n\nCalculate available content height: `popoutHeight - headerHeight - detailsHeight - spacing`\n\n## Step 9: Control Center Integration (Widgets Only)\n\nAdd your widget to the Control Center grid:\n\n```qml\nPluginComponent {\n    ccWidgetIcon: \"toggle_on\"\n    ccWidgetPrimaryText: \"My Feature\"\n    ccWidgetSecondaryText: isActive ? \"On\" : \"Off\"\n    ccWidgetIsActive: isActive\n\n    onCcWidgetToggled: {\n        isActive = !isActive\n        pluginService?.savePluginData(pluginId, \"active\", isActive)\n    }\n\n    // Optional: expandable detail panel (for CompoundPill)\n    ccDetailContent: Component {\n        Rectangle {\n            implicitHeight: 200\n            color: Theme.surfaceContainerHigh\n            radius: Theme.cornerRadius\n        }\n    }\n}\n```\n\n**CC sizing:** 25% width = SmallToggleButton (icon only), 50% width = ToggleButton or CompoundPill (if ccDetailContent is defined).\n\n## Step 10: External Commands and Clipboard\n\n**Run commands and capture output:**\n\n```qml\nimport qs.Common\n\nProc.runCommand(\n    \"myPlugin.fetch\",\n    [\"curl\", \"-s\", \"https://api.example.com/data\"],\n    (stdout, exitCode) => {\n        if (exitCode === 0) processData(stdout)\n    },\n    500  // debounce ms\n)\n```\n\n**Fire-and-forget (clipboard, notifications):**\n\n```qml\nimport Quickshell\n\nQuickshell.execDetached([\"dms\", \"cl\", \"copy\", textToCopy])\n```\n\n**Long-running processes:** Use the `Process` QML component from `Quickshell.Io` with `StdioCollector`.\n\n**Shell commands with pipes:** `[\"sh\", \"-c\", \"ps aux | grep foo\"]`\n\n**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.\n\n## Translations (Optional)\n\nShip per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:\n\n```\nMyPlugin/\n└── translations/\n    ├── es.json\n    └── zh_CN.json\n```\n\nFile names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.\n\nEach file maps the English term to its translation, using the term as its own context bucket:\n\n```json\n{\n  \"Cycle Speed\": {\n    \"Cycle Speed\": \"Velocidad de ciclo\"\n  }\n}\n```\n\nIn QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:\n\n```qml\ntext: I18n.trFor(\"myPlugin\", \"Cycle Speed\")\n```\n\nLookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.\n\n## Step 11: Validate and Test\n\n1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)\n2. Run the shell with verbose output: `qs -v -p $CONFIGPATH/quickshell/dms/shell.qml`\n3. Open Settings > Plugins > Scan for Plugins\n4. Enable your plugin and add it to the DankBar layout, a dock, or the dash (tabs show up on their own; add cards from the dash edit mode)\n\n**Runtime plugin discovery via IPC:**\n\nPlugins can be scanned, rescanned, and reloaded at runtime without restarting the shell:\n\n```bash\ndms ipc plugin-scan scan          # Trigger a full rescan of all plugin directories\ndms ipc plugin-scan rescan <id>   # Force rescan of a specific plugin\ndms ipc plugin-scan reload <id>   # Force reload of a loaded plugin\ndms ipc plugin-scan list          # List all known plugins (TSV: id, loaded, type, name)\ndms ipc plugin-scan status <id>   # Get status of a specific plugin (TSV: loaded, type, error)\n```\n\nPlugin IDs are validated against `^[a-zA-Z0-9_\\-:]{1,64}$`.\n\n**Common issues:**\n- Plugin not detected: check plugin.json syntax with `jq . plugin.json`\n- Widget not showing: ensure it's enabled AND added to a DankBar section\n- Settings error: verify `settings_write` permission is declared\n- Data not persisting: check pluginService injection and permissions\n- Startup check failing: check `pluginService.pluginLoadErrors` or run `dms ipc plugin-scan status <id>`\n\n## Common Mistakes\n\n1. **Missing `settings_write` permission** - Settings UI shows error without it\n2. **Missing `property var popoutService: null`** - Must declare for injection to work\n3. **Missing vertical bar pill** - Widget disappears when bar is on left/right edge\n4. **Hardcoded colors** - Use `Theme.*` properties, not hex values\n5. **Using `globalThis.clipboard`** - Does not exist; use `Quickshell.execDetached([\"dms\", \"cl\", \"copy\", text])`\n6. **Wrong Theme property names** - `Theme.fontSizeS` does not exist, use `Theme.fontSizeSmall`\n7. **Wrong import for Quickshell** - Use `import Quickshell` (not `import QtQuick` for execDetached)\n8. **Forgetting `categories` in launcher items** - Items won't display without it\n9. **Not handling null pluginService** - Always use optional chaining or null checks\n10. **Using `PluginComponent` for launchers** - Launchers use plain `Item`, not `PluginComponent`\n11. **Using `requires` instead of `dependencies`** - `requires` is deprecated; use `dependencies`\n12. **Providing both `component` and `components`** - Use one or the other, not both\n13. **Missing `trigger` on composite with launcher surface** - Still required when `components` has a `launcher` key\n14. **Using `\"type\": \"widget\"` for a dash tab** - Dash surfaces need `\"type\": \"dash\"` / `\"dashCard\"` or a `components` key\n15. **Timers in dash tabs or cards that ignore `live`** - The dash content outlives the popout; gate on `live`\n16. **Reading `dash.options` from a bar widget** - `options` only reach dash surfaces; share values through `settings` and `pluginData`\n17. **Raw theme colors in a dash card** - Use `accentColor` / `contentColor` / `mutedColor` so the card follows its tone\n\n## Quick Reference: Imports\n\n**Widget / Daemon:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\n```\n\n**Launcher:**\n```qml\nimport QtQuick\nimport qs.Services\n```\n\n**Desktop:**\n```qml\nimport QtQuick\nimport qs.Common\n```\n\n**Dash tab / card:**\n```qml\nimport QtQuick\nimport qs.Common\nimport qs.Widgets\nimport qs.Modules.Plugins\nimport qs.Modules.DankDash          // DashMetrics, DashWidgetGrid\nimport qs.Modules.DankDash.Overview // Card, for tiles inside a tab\n```\n\n**For clipboard/exec:** `import Quickshell`\n**For processes:** `import Quickshell.Io`\n**For networking:** `import Quickshell.Networking`\n**For toast notifications:** access `ToastService` from `qs.Services`\n\n## Quick Reference: File Naming\n\n- **Directory name:** PascalCase (e.g., `MyAwesomePlugin/`)\n- **Plugin ID:** camelCase (e.g., `myAwesomePlugin`)\n- **QML files:** PascalCase (e.g., `MyWidget.qml`, `Settings.qml`)\n- **Component paths in manifest:** relative with `./` prefix (e.g., `\"./MyWidget.qml\"`)\n- **JS utility files:** camelCase (e.g., `utils.js`, `apiAdapter.js`)\n\n## Reference Files\n\nLoad these on demand for detailed API documentation:\n\n- [plugin-manifest-reference.md](references/plugin-manifest-reference.md) - Complete plugin.json field reference and JSON schema\n- [widget-plugin-guide.md](references/widget-plugin-guide.md) - PluginComponent, bar and dock pills, attached dock panels, popouts, click actions, CC integration\n- [dash-plugin-guide.md](references/dash-plugin-guide.md) - DashTabComponent, DashCardComponent, DashWidgetGrid, dash options, card sizing\n- [launcher-plugin-guide.md](references/launcher-plugin-guide.md) - getItems/executeItem, triggers, icon types, context menus, tile view\n- [desktop-plugin-guide.md](references/desktop-plugin-guide.md) - DesktopPluginComponent, sizing, edit mode, position persistence\n- [daemon-plugin-guide.md](references/daemon-plugin-guide.md) - Event-driven background services, process execution\n- [settings-components-reference.md](references/settings-components-reference.md) - All 7 setting components with complete property lists\n- [theme-reference.md](references/theme-reference.md) - Theme colors, spacing, fonts, radii, common patterns\n- [data-persistence-guide.md](references/data-persistence-guide.md) - pluginData, state API, global variables\n- [popout-service-reference.md](references/popout-service-reference.md) - PopoutService API for controlling shell popouts and modals\n- [advanced-patterns.md](references/advanced-patterns.md) - Variants, JS utilities, qmldir, IPC, multi-file plugins\n\n<!-- File: .agents/skills/README.md (Tokens: ~1001 | Category: anthropic-skill) -->\n# Agent Skills\n\nThis directory contains agent skills following the [Agent Skills](https://agentskills.io) open standard - a portable, version-controlled format for giving AI agents specialized capabilities.\n\nEach 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.\n\n## Available Skills\n\n| Skill | Description |\n|-------|-------------|\n| [dms-plugin-dev](dms-plugin-dev/) | Develop plugins for Dux Shell - covers all 4 plugin types (widget, daemon, launcher, desktop), manifest creation, QML components, settings UI, data persistence, theme integration, and PopoutService usage. |\n\n## Installation\n\nThe `.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.\n\n### Claude Code\n\nClaude 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:\n\n**Project-level** (this repo only):\n\n```bash\nmkdir -p .claude/skills\nln -s ../../.agents/skills/dms-plugin-dev .claude/skills/dms-plugin-dev\n```\n\n**Personal** (all your projects):\n\n```bash\nln -s /path/to/dms/.agents/skills/dms-plugin-dev ~/.claude/skills/dms-plugin-dev\n```\n\nAfter 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.\n\nSee the [Claude Code skills docs](https://code.claude.com/docs/en/skills) for more on skill configuration, invocation control, and frontmatter options.\n\n### Cursor\n\nCursor discovers skills from `.cursor/skills/` in the project root:\n\n```bash\nmkdir -p .cursor/skills\nln -s ../../.agents/skills/dms-plugin-dev .cursor/skills/dms-plugin-dev\n```\n\nSee [Cursor skills docs](https://cursor.com/docs/context/skills) for details.\n\n### VS Code (Copilot)\n\nVS Code Copilot discovers skills from `.github/skills/` or `.vscode/skills/`:\n\n```bash\nmkdir -p .github/skills\nln -s ../../.agents/skills/dms-plugin-dev .github/skills/dms-plugin-dev\n```\n\nSee [VS Code skills docs](https://code.visualstudio.com/docs/copilot/customization/agent-skills) for details.\n\n### Gemini CLI\n\nGemini CLI discovers skills from `.gemini/skills/` in the project root:\n\n```bash\nmkdir -p .gemini/skills\nln -s ../../.agents/skills/dms-plugin-dev .gemini/skills/dms-plugin-dev\n```\n\nSee [Gemini CLI skills docs](https://geminicli.com/docs/cli/skills/) for details.\n\n### OpenAI Codex\n\nCodex discovers skills from `.codex/skills/` in the project root:\n\n```bash\nmkdir -p .codex/skills\nln -s ../../.agents/skills/dms-plugin-dev .codex/skills/dms-plugin-dev\n```\n\nSee [Codex skills docs](https://developers.openai.com/codex/skills/) for details.\n\n### Other Agents\n\nThe 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.\n\nCheck the [Agent Skills client showcase](https://agentskills.io/clients) for setup instructions specific to your agent.\n\nThe general pattern is:\n\n```bash\nmkdir -p .<agent>/skills\nln -s ../../.agents/skills/dms-plugin-dev .<agent>/skills/dms-plugin-dev\n```\n\n## Adding New Skills\n\nTo add a new skill to this directory:\n\n1. Create a subdirectory named with lowercase letters, numbers, and hyphens (e.g., `my-new-skill/`)\n2. Add a `SKILL.md` file with YAML frontmatter (`name`, `description`) and markdown instructions\n3. Optionally add `references/`, `scripts/`, and `assets/` subdirectories\n4. Keep `SKILL.md` under 500 lines - move detailed content to reference files\n\nSee the [Agent Skills specification](https://agentskills.io/specification) for the full format.\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/daemon/plugin.json (Tokens: ~103 | Category: plugin-manifest) -->\n{\n    \"id\": \"myDaemon\",\n    \"name\": \"My Daemon\",\n    \"description\": \"A background service that reacts to events\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"daemon\",\n    \"capabilities\": [\"background-service\"],\n    \"component\": \"./Daemon.qml\",\n    \"icon\": \"settings\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\", \"process\"]\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/dash-card/plugin.json (Tokens: ~220 | Category: plugin-manifest) -->\n{\n    \"id\": \"myDashCard\",\n    \"name\": \"My Dash Card\",\n    \"description\": \"A custom dash overview card\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./Card.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"card\": { \"w\": 2, \"h\": 1, \"minW\": 1, \"minH\": 1, \"maxW\": 3, \"maxH\": 2 },\n        \"options\": [\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/dash-tab/plugin.json (Tokens: ~141 | Category: plugin-manifest) -->\n{\n    \"id\": \"myDashTab\",\n    \"name\": \"My Dash Tab\",\n    \"description\": \"A custom dash tab\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"dash\",\n    \"capabilities\": [\"dash-tab\"],\n    \"component\": \"./Tab.qml\",\n    \"icon\": \"extension\",\n    \"dash\": {\n        \"title\": \"My Tab\",\n        \"options\": [\n            { \"key\": \"compact\", \"text\": \"Compact layout\", \"type\": \"toggle\", \"def\": false }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/desktop/plugin.json (Tokens: ~99 | Category: plugin-manifest) -->\n{\n    \"id\": \"myDesktopWidget\",\n    \"name\": \"My Desktop Widget\",\n    \"description\": \"A custom desktop widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"desktop\",\n    \"capabilities\": [\"desktop-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"widgets\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/launcher/plugin.json (Tokens: ~104 | Category: plugin-manifest) -->\n{\n    \"id\": \"myLauncher\",\n    \"name\": \"My Launcher\",\n    \"description\": \"Custom launcher plugin with searchable items\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"launcher\",\n    \"capabilities\": [\"launcher\"],\n    \"component\": \"./Launcher.qml\",\n    \"trigger\": \"#\",\n    \"icon\": \"search\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: .agents/skills/dms-plugin-dev/assets/templates/widget/plugin.json (Tokens: ~99 | Category: plugin-manifest) -->\n{\n    \"id\": \"myWidget\",\n    \"name\": \"My Widget\",\n    \"description\": \"A custom bar widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Your Name\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./Widget.qml\",\n    \"icon\": \"extension\",\n    \"settings\": \"./Settings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: quickshell/PLUGINS/AttachedPanelExample/plugin.json (Tokens: ~111 | Category: plugin-manifest) -->\n{\n    \"id\": \"attachedPanelExample\",\n    \"name\": \"Attached Panel Example\",\n    \"description\": \"A widget with an optional attached dock panel and a normal bar popout\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS contributors\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"dankbar-widget\"],\n    \"component\": \"./AttachedPanelExample.qml\",\n    \"icon\": \"sticky_note_2\",\n    \"requires_dms\": \">=1.7.0\"\n}\n\n<!-- File: quickshell/PLUGINS/AttachedPanelExample/README.md (Tokens: ~158 | Category: plugin-manifest) -->\n# Attached panel example\n\nA normal widget plugin that also sets `attachedContent`. Enable it in Settings → Plugins\nand add it to a bar, a dock, or both.\n\n- On a bar, a click opens the regular popout.\n- On a dock, what a click does is the dock's choice. With Settings → Dock → Apps & widgets\n  → Open widgets set to Inline, the dock grows and shows the attached panel next to the\n  apps. With Popout (the default) it opens the same popout the bar would.\n\nEscape or Close dismisses the panel and hands focus back to the widget. Opening another\nattached widget replaces the panel. No background process, no timer.\n\nSee [the surface widget contract](../SURFACE_WIDGETS.md).\n\n<!-- File: quickshell/PLUGINS/ColorDemoPlugin/plugin.json (Tokens: ~96 | Category: plugin-manifest) -->\n{\n    \"id\": \"colorDemo\",\n    \"name\": \"Color Demo\",\n    \"description\": \"Demonstrates color picker plugin setting\",\n    \"version\": \"1.0.0\",\n    \"author\": \"DMS\",\n    \"icon\": \"palette\",\n    \"type\": \"widget\",\n    \"component\": \"./ColorDemoWidget.qml\",\n    \"settings\": \"./ColorDemoSettings.qml\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: quickshell/PLUGINS/ControlCenterDetailExample/plugin.json (Tokens: ~98 | Category: plugin-manifest) -->\n{\n    \"id\": \"controlCenterDetailExample\",\n    \"name\": \"CC Detail Example\",\n    \"description\": \"Example plugin with Control Center detail dropdown\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./DetailExampleWidget.qml\",\n    \"icon\": \"settings\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: quickshell/PLUGINS/ControlCenterExample/plugin.json (Tokens: ~99 | Category: plugin-manifest) -->\n{\n    \"id\": \"controlCenterExample\",\n    \"name\": \"CC Toggle Example\",\n    \"description\": \"Example plugin with Control Center toggle widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"widget\",\n    \"capabilities\": [\"control-center\"],\n    \"component\": \"./ControlCenterExampleWidget.qml\",\n    \"icon\": \"toggle_on\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: quickshell/PLUGINS/DashCardExample/plugin.json (Tokens: ~416 | Category: plugin-manifest) -->\n{\n    \"id\": \"dashCardExample\",\n    \"name\": \"Progress Example\",\n    \"description\": \"A standalone overview card showing how much of the day, week, month or year has passed\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"dashCard\",\n    \"capabilities\": [\"dash-card\"],\n    \"component\": \"./ProgressCard.qml\",\n    \"icon\": \"hourglass_bottom\",\n    \"dash\": {\n        \"card\": {\n            \"title\": \"Progress\",\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 4,\n            \"maxH\": 2\n        },\n        \"options\": [\n            {\n                \"key\": \"period\",\n                \"text\": \"Period\",\n                \"type\": \"choice\",\n                \"def\": \"day\",\n                \"choices\": [\n                    { \"value\": \"day\", \"text\": \"Day\" },\n                    { \"value\": \"week\", \"text\": \"Week\" },\n                    { \"value\": \"month\", \"text\": \"Month\" },\n                    { \"value\": \"year\", \"text\": \"Year\" }\n                ]\n            },\n            {\n                \"key\": \"remaining\",\n                \"text\": \"Show time left\",\n                \"type\": \"toggle\",\n                \"def\": true\n            },\n            {\n                \"key\": \"tone\",\n                \"text\": \"Tone\",\n                \"type\": \"choice\",\n                \"def\": \"\",\n                \"choices\": [\n                    { \"value\": \"\", \"text\": \"Surface\" },\n                    { \"value\": \"primary\", \"text\": \"Primary\" },\n                    { \"value\": \"secondary\", \"text\": \"Secondary\" },\n                    { \"value\": \"tertiary\", \"text\": \"Tertiary\" }\n                ]\n            }\n        ]\n    },\n    \"requires_dms\": \">=1.7.0\"\n}\n\n<!-- File: quickshell/PLUGINS/DashCardExample/README.md (Tokens: ~248 | Category: plugin-manifest) -->\n# Progress Example\n\nA standalone overview card: how much of today, this week, this month or this year has passed. The manifest uses the single file form, `\"type\": \"dashCard\"` plus `\"component\"`.\n\nWhat it shows:\n\n- `DashCardComponent` as the root. It has no tab, so it is not clickable and keyboard navigation skips it.\n- A layout for every size in its range (1x1 up to 4x2). `wide` and `tall` come from the card's pixel size against `DashMetrics.gridRowUnit`.\n- `live` gating. `SystemClock` is disabled while the dash is hidden.\n- `tone` and the card colors (`accentColor`, `contentColor`, `mutedColor`, `chipColor`), so the card follows the Surface, Primary, Secondary and Tertiary tones like the built-in cards.\n- `dash.options`: a choice for the period, a toggle for the time left and a choice for the tone. They show up in the card's options sheet in edit mode and under Settings → Dashboard.\n\nAdd it from the dash: open the overview, hover the selected page icon, click the pencil, then Add widget.\n\n<!-- File: quickshell/PLUGINS/DashCounterExample/plugin.json (Tokens: ~224 | Category: plugin-manifest) -->\n{\n    \"id\": \"dashCounterExample\",\n    \"name\": \"Counter Example\",\n    \"description\": \"One counter shared by a dash tab, an overview card and a bar or dock widget\",\n    \"version\": \"1.0.0\",\n    \"author\": \"Dux Shell\",\n    \"type\": \"composite\",\n    \"capabilities\": [\"dash-tab\", \"dash-card\", \"dankbar-widget\"],\n    \"icon\": \"counter_1\",\n    \"components\": {\n        \"dash\": \"./CounterTab.qml\",\n        \"dashCard\": \"./CounterCard.qml\",\n        \"widget\": \"./CounterWidget.qml\"\n    },\n    \"dash\": {\n        \"title\": \"Counter\",\n        \"card\": {\n            \"w\": 2,\n            \"h\": 1,\n            \"minW\": 1,\n            \"minH\": 1,\n            \"maxW\": 3,\n            \"maxH\": 2\n        }\n    },\n    \"requires_dms\": \">=1.7.0\",\n    \"permissions\": [\"settings_read\", \"settings_write\"]\n}\n\n<!-- File: quickshell/PLUGINS/DashCounterExample/README.md (Tokens: ~255 | Category: plugin-manifest) -->\n# Counter Example\n\nA composite plugin with three surfaces that share one counter:\n\n- `components.dash` (`CounterTab.qml`): a dash tab built on `DashTabComponent`. It hosts a `DashWidgetGrid`, so the dash Edit, Add widget, Reset and Clear All actions work on it. The Reset widget starts in the Add menu (`enabled: false`). `+` and `-` change the count.\n- `components.dashCard` (`CounterCard.qml`): an overview card built on `DashCardComponent`. Clicking it opens the tab. Left and Right change the count while it has focus.\n- `components.widget` (`CounterWidget.qml`): a bar pill. Left click toggles the dash on the counter tab, right click adds one. The same file works in a dock with no extra code.\n\nThe count is stored with `setData(\"count\", value)` / `savePluginData` and read back through `pluginData.count`. Every surface reloads `pluginData` on `pluginDataChanged`, so they stay in sync.\n\nOpen the tab from a terminal with `dms ipc call dash open plugin_dashCounterExample`. If the tab is hidden from the tab bar it opens as a detail page with a back button.\n\n<!-- ... +20 more modular skills available via /api/skills/AvengeMedia/DankMaterialShell?path={path} -->\n</yakaai_skills>"}