LUDASH / PLUGINS

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.

TypeRuntimeUse
quickshellQML/JS ItemPanels/pages/widgets/visual slots.
effectNative C11/C++20 API 2 moduleLayout/rules/animation data hooks.
openglQt Quick ShaderEffect/qsbShader 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

GENERATOR
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-layout

Ids match ^[A-Za-z0-9][A-Za-z0-9._-]+$. The generator rejects unsupported type/target pairs and requires a new output directory.

CMAKELISTS.TXT
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
)
ONE-USER INSTALL
cmake -S . -B build -G Ninja -DCMAKE_INSTALL_PREFIX="$HOME/.local"
cmake --build build
cmake --install build
lunadashctl open-settings plugins

For an uninstalled LunaDash build use -DLunaDashPlugin_DIR=/absolute/path/to/build/sdk-build. Relocated installed SDKs can use CMAKE_PREFIX_PATH.

metadata.json

SINGLE TARGET
{
 "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.

MULTI TARGET
{
 "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

TargetFeature/context
panelTaskbar/workspaces/tray
taskbar-windowsWindow list; groups context
wallpaperDesktop wallpaper
wallpaper-transitionimageSource/previous/incoming/progress/transitioning
launcherApp launcher
overviewDashboard
desktop-menuDesktop menu
mediacontroller/media
calendarCalendar
audioAudio controls
networkNetwork controls
devicesRemovable devices
sessionSession controls
settingsSettings container; recovery pages stay built in
setupWelcome
compatibilityCompatibility launcher
clipboardClipboard history
feedbackMessages
notificationsNotifications
screenshotScreenshot feedback
workspace-transitionWorkspace animation
startupStartup animation
window-decorationBorders; interaction
window-switcherSwitcher; interaction
tiling-hintDrop hint
blurBackdrop; image/reveal
image-pickerpicker/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

TargetResponseValidation
window-animationduration, enterOffset, focusOpacity, exitScale, easingComplete 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

MAIN.QML
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.

WINDOW-LAYOUT REQUEST
{
 "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

METADATA
"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
"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.

INSTALLED PACKAGE
~/.local/share/lunadash/plugins/org.example.panel/
├── metadata.json
├── .lunadash-sdk.json
├── Main.qml
└── icon.svg

The 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.

SINGLE TARGET
{"schemaVersion":1,"builtins":{},"plugins":{"org.example.panel":{"enabled":true,"mode":"replace","settings":{"compact":true}}}}
MULTI TARGET
{
 "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.

SUBMISSION LAYOUT
plugins/<id>/
├── metadata.json
├── store.json
├── CMakeLists.txt
├── LICENSE
└── source/assets declared by the plugin

Directory 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.

STORE.JSON
{"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.

REGISTRY CHECKS
python3 scripts/validate.py
python3 scripts/check_safety.py
python3 scripts/build_index.py --check
cd site
npm install
npm run check
npm run build

After 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

  1. Confirm the type/target with lunadash-create-plugin --list.
  2. Reconfigure/build with LunaDashPlugin 2 after metadata edits.
  3. Inspect status.extensions.installed for status/error/entry/settings.
  4. Use context.reportError(), then clear through empty extension-error after fixing.
  5. Native metadata library name must match the installed file and export SDK-generated ludash_plugin_entry_v2.
  6. Store failures are in extensions.remote[].installError/installPhase.