Guidelines for AI tools contributing to the Noctalia Plugins repository. Study the official plugins before writing code — especially hello-world (minimal reference) and timer (complex example with shared state). Official plugins have "official": true in their manifest.
Every plugin component receives a pluginApi property. This is the core interface:
Item {
property var pluginApi: null
// Settings access pattern — always use this fallback chain
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
property string message: cfg.message ?? defaults.message ?? "fallback"
}Key pluginApi members:
pluginSettings— mutable settings object, persisted viasaveSettings()manifest— read-only manifest.json datapluginId,pluginDir— plugin identity and pathmainInstance— reference to Main.qml instance (for shared state)tr(key, interpolations)— translate a key, e.g.pluginApi?.tr("widget.label")trp(key, count, singular, plural)— plural translationopenPanel(screen, widget),togglePanel(screen, widget)— panel controlsaveSettings()— persistpluginSettingsto diskwithCurrentScreen(callback)— get current screen in IPC handlerspanelOpenScreen— the screen where this plugin's panel is open
Only include the entry points your plugin uses. Available types:
| Entry Point | File | Purpose |
|---|---|---|
main |
Main.qml | Shared state, IPC handlers |
barWidget |
BarWidget.qml | Bar widget |
panel |
Panel.qml | Overlay panel |
controlCenterWidget |
ControlCenterWidget.qml | Control center button |
settings |
Settings.qml | Plugin settings UI |
desktopWidget |
DesktopWidget.qml | Draggable desktop widget |
desktopWidgetSettings |
DesktopWidgetSettings.qml | Desktop widget settings |
launcherProvider |
LauncherProvider.qml | Launcher search provider |
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Item {
id: root
property var pluginApi: null
// Shared state accessible from other components via pluginApi.mainInstance
property bool isActive: false
// IPC handler for CLI control (qs ipc call plugin:my-plugin commandName)
IpcHandler {
target: "plugin:my-plugin"
function toggle() {
if (pluginApi) {
pluginApi.withCurrentScreen(screen => {
pluginApi.togglePanel(screen);
});
}
}
}
}import Quickshell
import qs.Commons
import qs.Services.UI
import qs.Widgets
Item {
id: root
// Injected properties
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Settings
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
// Bar layout awareness
readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name)
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screen?.name)
implicitWidth: isVertical ? capsuleHeight : contentWidth
implicitHeight: isVertical ? contentHeight : capsuleHeight
// Context menu (right-click)
NPopupContextMenu {
id: contextMenu
model: [
{ "label": pluginApi?.tr("menu.settings"), "action": "settings", "icon": "settings" }
]
onTriggered: action => {
contextMenu.close();
PanelService.closeContextMenu(screen);
if (action === "settings") {
BarService.openPluginSettings(screen, pluginApi.manifest);
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (pluginApi) pluginApi.togglePanel(root.screen, root);
} else if (mouse.button === Qt.RightButton) {
PanelService.showContextMenu(contextMenu, root, screen);
}
}
}
}import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Services.UI
import qs.Widgets
Item {
id: root
property var pluginApi: null
// Required for background rendering
readonly property var geometryPlaceholder: panelContainer
// Panel dimensions (always scale with uiScaleRatio)
property real contentPreferredWidth: 400 * Style.uiScaleRatio
property real contentPreferredHeight: 500 * Style.uiScaleRatio
// Enable panel attach/detach UI
readonly property bool allowAttach: true
anchors.fill: parent
Rectangle {
id: panelContainer
anchors.fill: parent
color: "transparent"
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL
spacing: Style.marginL
// Panel content using N* widgets
}
}
}import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
ColumnLayout {
id: root
property var pluginApi: null
property var cfg: pluginApi?.pluginSettings || ({})
property var defaults: pluginApi?.manifest?.metadata?.defaultSettings || ({})
// Edit copies of settings (don't modify pluginSettings directly in bindings)
property string editMessage: cfg.message ?? defaults.message ?? ""
spacing: Style.marginL
NTextInput {
Layout.fillWidth: true
label: pluginApi?.tr("settings.message.label")
description: pluginApi?.tr("settings.message.desc")
text: root.editMessage
onTextChanged: root.editMessage = text
}
// Required — called by the shell when user saves
function saveSettings() {
if (!pluginApi) return;
pluginApi.pluginSettings.message = root.editMessage;
pluginApi.saveSettings();
}
}import Quickshell
import qs.Widgets
NIconButtonHot {
property ShellScreen screen
property var pluginApi: null
icon: "my-icon"
tooltipText: pluginApi?.tr("widget.tooltip")
onClicked: {
if (pluginApi) pluginApi.togglePanel(screen, this);
}
}{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"minNoctaliaVersion": "4.4.1",
"author": "Author Name",
"license": "MIT",
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
"description": "Concise description of what the plugin does",
"tags": ["Bar", "Panel"],
"entryPoints": {
"barWidget": "BarWidget.qml",
"panel": "Panel.qml",
"settings": "Settings.qml"
},
"dependencies": {
"plugins": []
},
"metadata": {
"defaultSettings": {
"message": "Hello",
"iconColor": "none"
}
}
}Field rules:
idmust match the folder nameversionstarts at1.0.0; bump appropriately on updatesminNoctaliaVersion— verify the features you use exist in that versionrepository— alwayshttps://github.com/noctalia-dev/noctalia-pluginsfor PRs to this repotags— use only tags from README.md; include compositor tags if compositor-specificentryPoints— only include the ones your plugin providesmetadata.defaultSettings— must contain defaults for every setting your plugin uses
Plugin translations live in i18n/*.json (one file per language):
{
"widget": {
"tooltip": "My Widget"
},
"menu": {
"settings": "Widget settings"
},
"settings": {
"message": {
"label": "Message",
"desc": "Custom message to display"
}
}
}Access via dot notation: pluginApi?.tr("settings.message.label").
Do not add fallback text after tr() calls — the translation system handles missing keys.
- Use Noctalia widgets (
NButton,NLabel,NBox,NSlider, etc.) instead of raw Qt types (Text,Rectangle,Button). This ensures correct theming. - Use
Styleconstants for margins, radii, colors:Style.marginL,Style.radiusM,Color.mPrimary - Use
Loggerfor logging (Logger.i,Logger.d,Logger.w,Logger.e), notconsole.log - Always null-coalesce pluginApi access:
pluginApi?.tr(...),pluginApi?.pluginSettings || ({}) - camelCase for variables/functions, PascalCase for component files
- No fallback values after
I18n.tr()orpluginApi?.tr()— the translation system returns the key on miss
import QtQuick
import QtQuick.Layouts
import Quickshell // ShellScreen, IpcHandler
import Quickshell.Io // FileView, Process
import qs.Commons // Settings, Style, Color, Logger, I18n, Icons
import qs.Services.UI // PanelService, BarService
import qs.Widgets // N* componentsThese are the most frequent issues in AI-generated plugin PRs:
- Hallucinated APIs — inventing functions or properties that don't exist in Quickshell, Qt, or the plugin API. Always verify against official plugins before using any API.
- Using raw Qt types —
Text,Rectangle,Buttoninstead ofNLabel,NBox,NButton. The N* widgets handle theming automatically. - Hardcoded strings — all user-facing text must go through
pluginApi?.tr()with translations ini18n/. - Wrong settings pattern — modifying
pluginApi.pluginSettingsdirectly in bindings instead of using edit-copy properties and saving insaveSettings(). - Missing
saveSettings()function in Settings.qml — the shell calls this; without it, settings won't persist. - Incorrect manifest fields —
idnot matching folder name, missingdefaultSettingsfor settings the plugin uses, wrongminNoctaliaVersion. - Using
console.loginstead ofLogger.i/Logger.d/Logger.w/Logger.e.
- Avoid expensive property bindings — complex calculations should be in functions, not inline bindings. Simple ternaries and property reads are fine.
- Use
Loaderfor heavy content that isn't always visible — panels already do this, but apply it within your own components too. - Debounce rapid updates with
Timer— e.g. if reacting to slider changes that trigger expensive operations. - Prefer signals/bindings over polling — don't use
Timerto repeatedly check state when a signal or binding would work.
- Plugin loads and runs with
qs -c noctalia-shell - Test with both light and dark themes
- Test on target compositors (Niri, Hyprland, Sway, Labwc, MangoWC) — especially if using compositor-specific features
- Verify settings persist across restarts
- Check edge cases: empty states, missing data, rapid interactions
- Plugin tested with Noctalia Shell (
qs -c noctalia-shell) -
manifest.jsonis valid with all required fields -
idmatches folder name -
registry.jsonis not included in the PR (auto-generated) - All user-facing strings use
pluginApi?.tr()with translations ini18n/ - Settings use the
cfg → defaults → hardcodedfallback chain -
Settings.qmlexposes asaveSettings()function - No hallucinated APIs — all functions and properties verified against official plugins
- No
console.log— useLoggerinstead - Uses N* widgets, not raw Qt types
-
preview.pngincluded (16:9, 960x540) -
README.mdincluded with description and features
Title format: type(plugin-name): short description
feat(my-plugin): add brightness control panel
fix(timer): handle zero-duration edge case
Types: feat, fix, docs, style, refactor, perf, chore
Description should include:
- What was changed and why
- How to test the changes
- References to related issues (e.g. "Closes #123")
- Official Plugins — study
hello-worldandtimerfirst - Plugin Documentation
- Development Guidelines
- Noctalia Widgets — all N* components