Build, register and publish a LunaDash plugin.
Plugin SDK 2 separates implementation type from desktop target. Create a package, let the CMake SDK validate/install it, enable it through extension configuration, then optionally submit source to the reviewed LunaDash-Plugins registry.
Plugin SDK 2 model
A package has one identity and one or more target implementations. type answers how it is implemented; target answers which feature it attaches to. Valid types are quickshell, effect and opengl; data/plugins/targets.json is authoritative for valid pairs.
| Type | Runtime | Use |
|---|---|---|
quickshell | QML/JS Item | Panels/pages/widgets/visual slots. |
effect | Native C11/C++20 API 2 module | Layout/rules/animation data hooks. |
opengl | Qt Quick ShaderEffect/qsb | Shader replacement/augmentation in visual slots. |
replace replaces the built-in only after the plugin is valid/ready; augment keeps the built-in. Almost every current target is selection: "single", so only one plugin implementation may be enabled there regardless of mode. desktop-widgets is multi-selection.
Create, build and install
lunadash-create-plugin --list
lunadash-create-plugin --type quickshell --target panel \\
--id org.example.panel --output my-panel
# From a source checkout
python3 scripts/create-plugin.py --type effect --target window-layout \\
--id org.example.layout --output my-layoutIds match ^[A-Za-z0-9][A-Za-z0-9._-]+$. The generator rejects unsupported type/target pairs and requires a new output directory.
cmake_minimum_required(VERSION 3.21)
project(MyLunaDashPlugin LANGUAGES C CXX)
find_package(LunaDashPlugin 2 CONFIG REQUIRED)
lunadash_add_plugin(my_plugin
METADATA "metadata.json"
# SOURCES Effect.cpp
# FILES Helper.qml icon.svg
)cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX="$HOME/.local"
cmake --build build
cmake --install build
lunadashctl open-settings pluginsFor an uninstalled LunaDash build use -DLunaDashPlugin_DIR=/absolute/path/to/build/sdk-build. Relocated installed SDKs can use CMAKE_PREFIX_PATH.
metadata.json
{
"schemaVersion":2,
"sdk":{"name":"LunaDash","apiVersion":2},
"id":"org.example.panel","name":"Example panel","version":"1.0.0",
"author":{"name":"Your name"},"icon":"applications-system","tags":["Panel"],
"type":"quickshell","target":"panel","mode":"replace","entry":"Main.qml",
"enabledByDefault":false,
"settings":{"compact":{"type":"boolean","default":false,"control":"toggle"}}
}Schema 2 requires sdk.name="LunaDash" and apiVersion=2. id/name/version are required. A local image icon is one leaf png/jpg/jpeg/webp/svg filename inside the package; theme icon names are also valid.
A package can declare up to 16 unique implementations with targets. Each target has its own type, target, mode, settings and entry/shaders. Native implementations in one package must share one .so entry.
{
"schemaVersion":2,"sdk":{"name":"LunaDash","apiVersion":2},
"id":"org.example.desktop-pack","name":"Desktop pack","version":"1.0.0","enabledByDefault":false,
"targets":[
{"id":"panel","type":"quickshell","target":"panel","mode":"replace","entry":"Panel.qml","settings":{}},
{"id":"wallpaper","type":"opengl","target":"wallpaper","mode":"augment","shaders":{"vertex":"Effect.vert","fragment":"Effect.frag"},"settings":{}}
]
}Registered targets
Visual quickshell/opengl targets
| Target | Feature/context |
|---|---|
| panel | Taskbar/workspaces/tray |
| taskbar-windows | Window list; groups context |
| wallpaper | Desktop wallpaper |
| wallpaper-transition | imageSource/previous/incoming/progress/transitioning |
| launcher | App launcher |
| overview | Dashboard |
| desktop-menu | Desktop menu |
| media | controller/media |
| calendar | Calendar |
| audio | Audio controls |
| network | Network controls |
| devices | Removable devices |
| session | Session controls |
| settings | Settings container; recovery pages stay built in |
| setup | Welcome |
| compatibility | Compatibility launcher |
| clipboard | Clipboard history |
| feedback | Messages |
| notifications | Notifications |
| screenshot | Screenshot feedback |
| workspace-transition | Workspace animation |
| startup | Startup animation |
| window-decoration | Borders; interaction |
| window-switcher | Switcher; interaction |
| tiling-hint | Drop hint |
| blur | Backdrop; image/reveal |
| image-picker | picker/purpose |
These expose built-in opacity 0..1 and are single-selection.
Settings pages
settings.general, settings.appearance, settings.windows, settings.shortcuts, settings.display, settings.input, settings.input-method, settings.sound, settings.network, settings.bluetooth, settings.power, settings.applications, settings.privacy, settings.system, settings.devices, settings.about, settings.dashboard
These are single-selection quickshell/opengl slots. settings.modules and settings.plugins remain built-in recovery pages.
desktop-widgets
desktop-widgets is Quickshell-only, has no built-in settings and is selection: "multiple".
Native hooks
| Target | Response | Validation |
|---|---|---|
| window-animation | duration, enterOffset, focusOpacity, exitScale, easing | Complete animation profile; desktop animations disabled still wins. |
| shell-animation | { duration } | Exactly one duration integer 0..600 ms. |
| window-rules | { workspace, maximized } | Initial ordinary-window rule; workspace must exist. |
| window-layout | { windows:[{id,x,y,width,height},...] } | Same non-minimized ids once each, positive sizes, inside work area; overlap only when allowed. |
window-animation settings: duration -1..600, enterOffset -100..100, focusOpacity 0..1, exitScale 0.5..1, easing linear/outCubic/outQuint/inOutCubic. shell-animation: duration -1..600. window-layout: gap -1..64.
Quickshell contract
import QtQuick
Item {
required property var shell
required property var settings
required property var context
readonly property bool pluginReady: width > 0 && height > 0
Text { anchors.centerIn: parent; text: shell.tr("Workspace") + " " + String((shell.state.workspace ?? 0) + 1) }
TapHandler { onTapped: shell.command("open-settings", "plugins") }
}The host injects all three required properties before construction. Every visual context adds target, built-in source, builtinSettings, effective mode and reportError(message). Do not destroy/re-parent source. Fatal runtime conditions should call context.reportError(...); built-in fallback stays available.
See the QML API table for shell helpers.
Native effect contract
The library exports SDK-generated ludash_plugin_entry_v2; authors implement ludash_plugin_process(request,response,capacity). Return UTF-8 JSON byte length excluding NUL or a negative value. Hooks run synchronously on the compositor thread: never block, retain host pointers, run nested event loops or start lifetime-escaping work.
{
"target":"window-layout","mode":"replace",
"context":{"workspace":0,"area":{"x":0,"y":40,"width":1920,"height":1040},"newWindows":[23],"windowTemplate":"tiling","allowOverlap":false},
"builtin":{"windows":[...]},"current":{},"settings":{"gap":12}
}builtin is always available. Replace gets empty current; augment gets the previous valid result. Return a complete object, not a patch. Invalid output preserves the previous valid result and quarantines the instance.
window-layout output ids exactly match all non-minimized inputs once each, with positive sizes inside area. Overlap is rejected unless allowed. Manifest windowTemplate:"stacking" is legal only on an effect/window-layout replacement and selects the host's persistent freeform interaction policy.
Native effects are in-process. ABI/receipt checks are compatibility checks, not signing or sandboxing; a crash/hang can take down the compositor.
OpenGL packages
"type":"opengl",
"target":"wallpaper",
"mode":"augment",
"shaders":{"vertex":"Effect.vert","fragment":"Effect.frag"}The SDK runs Qt 6 qsb and installs source plus baked .qsb; runtime requires both stages and both baked outputs. Host uniforms include source, strength, resolution and parameters (parameter0..parameter3), plus Qt's matrix/opacity uniforms. Software rendering leaves the built-in visual. These shaders affect only their Quickshell slot, not arbitrary app buffers or the wlroots framebuffer.
Plugin settings
Plugin schemas allow only toggle boolean, select enum-backed scalar, number integer/number and bounded slider. Third-party plugins cannot expose free-form text/array controls. Each field requires type/default; values are strictly validated.
"settings":{
"enabledGlow":{"type":"boolean","default":true,"control":"toggle"},
"quality":{"type":"string","default":"medium","control":"select","enum":["low","medium","high"]},
"strength":{"type":"number","default":0.5,"control":"slider","minimum":0,"maximum":1},
"samples":{"type":"integer","default":4,"control":"number","minimum":1,"maximum":16}
}Local registration = installation
There is no separate database. A complete SDK-built package is discovered under standard data locations at lunadash/plugins/<id>. With $HOME/.local this is normally ~/.local/share/lunadash/plugins/<id>. Development builds also scan executable-adjacent plugins/. The dev branch no longer discovers old lunadash/shell/plugins or ludash/plugins.
~/.local/share/lunadash/plugins/org.example.panel/
├── metadata.json
├── .lunadash-sdk.json
├── Main.qml
└── icon.svgThe SDK generates .lunadash-sdk.json; schema-2 discovery verifies its metadataSha256. Do not copy only source files. Native .so files are staged into a private temp directory before loading and stay loaded while active.
Enable/configure
Use Settings → Plugins or extension-save. The document is $XDG_CONFIG_HOME/LuDash/extensions.json, max 24 KiB, atomically written with owner-only permissions and symlink refusal.
{"schemaVersion":1,"builtins":{},"plugins":{"org.example.panel":{"enabled":true,"mode":"replace","settings":{"compact":true}}}}{
"schemaVersion":1,"builtins":{},
"plugins":{"org.example.desktop-pack":{"enabled":true,"targets":{
"panel":{"enabled":true,"mode":"replace","settings":{}},
"wallpaper":{"enabled":true,"mode":"augment","settings":{}}
}}}
}Package enabled gates all multi-target entries, then each target owns enabled/mode/settings. Single-selection targets reject a second enabled plugin. Settings editors should prefer settings-describe/settings-update.
Reload behavior
Discovery refreshes at startup and during plugin/configuration operations; there is no filesystem watcher. Disable a native plugin before replacing its binary, install the new build, then enable/retry. Visual slots retain built-in fallback on load failure.
Publish to LunaDash-Plugins
Local install and Store publication are separate. Community plugins go through LunaDash-Plugins, the PR gate and generated catalogue consumed by Settings → Plugins → Store.
plugins/<id>/
├── metadata.json
├── store.json
├── CMakeLists.txt
├── LICENSE
└── source/assets declared by the pluginDirectory name exactly matches metadata id. Submit source only: no .so/object/generated .qsb/SDK receipt/archive. Published ids are stable; deprecate instead of rename. Updates bump version. Community/native plugins default disabled.
{"storeSchemaVersion":1,"license":"GPL-3.0-or-later","repository":"https://github.com/example/org.example.panel","homepage":"https://example.com/plugin"}storeSchemaVersion, license and HTTPS repository are required. Optional: homepage, upstream, screenshots, featured, deprecated, replacement. Plugin PRs do not edit generated index.json.
python3 scripts/validate.py
python3 scripts/check_safety.py
python3 scripts/build_index.py --check
cd site
npm install
npm run check
npm run buildAfter merge the registry generates index.json. LunaDash validates ids, target/type pairs, tags and HTTPS URLs, with a bundled fallback. LUNADASH_PLUGIN_CATALOG_URL can point to another HTTPS index or off. Installable entries contain 1–32 HTTPS files with SHA-256 hashes and must include metadata.json plus CMakeLists.txt; downloads are staged/verified/built/installed and never auto-enable native code.
Debug checklist
- Confirm the type/target with
lunadash-create-plugin --list. - Reconfigure/build with LunaDashPlugin 2 after metadata edits.
- Inspect
status.extensions.installedfor status/error/entry/settings. - Use
context.reportError(), then clear through emptyextension-errorafter fixing. - Native metadata library name must match the installed file and export SDK-generated
ludash_plugin_entry_v2. - Store failures are in
extensions.remote[].installError/installPhase.