Develop plugins for noctalia-shell (https://github.com/noctalia-dev/noctalia-plugins). Use this skill whenever the user wants to create, edit, or extend a noctalia-shell plugin, write QML for the noctalia bar widget, panel, desktop widget, settings UI, launcher provider, or any component that integrates with noctalia-shell's Style/Color/Widgets system. Always apply noctalia-shell's design tokens (Style.*, Color.m*) and reuse its built-in Widgets (NIcon, NText, NButton, NIconButton, NScrollView, NTextInput, NComboBox, etc.) rather than rolling custom alternatives.
Noctalia is a Wayland desktop shell built on Quickshell (Qt6/QML).
Plugins extend the shell by contributing bar widgets, panels, desktop widgets, launcher
providers, settings UI, or background IPC logic — all written in QML.
import QtQuick
import QtQuick.Layouts
import Quickshell // ShellScreen, IpcHandler, etc.
import qs.Commons // Style, Color, Settings, Logger, I18n
import qs.Widgets // NIcon, NText, NButton, NIconButton, NTextInput,
// NComboBox, NScrollView, NPopupContextMenu, etc.
import qs.Services.UI // ToastService, TooltipService, PanelService, BarService
import qs.Services.System // (optional, for compositor-level data)
plugin-name/
├── manifest.json ← required, see references/manifest.md
├── BarWidget.qml ← optional, adds a capsule to the bar
├── Panel.qml ← optional, overlay panel opened from the bar widget
├── Settings.qml ← optional, settings tab shown in Noctalia's Settings panel
├── Main.qml ← optional, IPC target / background singleton logic
├── preview.png ← 960×540 16:9 screenshot for the plugin registry
└── README.md
Always import qs.Commons and use Style.* / Color.m* — never hardcode
pixel values, colours, or font sizes. Using literals instead of tokens will break
theme switching and per-monitor scaling.
Reuse qs.Widgets components — NIcon, NText, NButton, NIconButton,
NScrollView, NTextInput, NComboBox, NPopupContextMenu. Only create custom
components when no built-in widget fits.
Scale all explicit dimensions by Style.uiScaleRatio (panels) or use the
per-screen helpers for bar widgets (see BarWidget section).
Use Color.m* tokens (Color.mPrimary, Color.mOnSurface,
Color.mSurfaceVariant, Color.mHover, …) so the widget reacts to dark-mode and
colour-scheme changes automatically.
Hover effects via binding, not imperative handlers:
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
This skill ships with detailed reference files alongside this SKILL.md.
Before writing any QML or JSON for a plugin component, you MUST read the
corresponding reference file in full. The cheat-sheets below are summaries only;
the reference files contain complete property lists, required patterns, checklists,
and edge-case guidance that are essential for correct output.
| Task | File to read FIRST |
|---|---|
Writing or editing BarWidget.qml | references/bar-widget.md |
Writing or editing Panel.qml | references/panel.md |
Writing or editing Settings.qml | references/settings-ui.md |
Writing or editing manifest.json | references/manifest.md |
| Any question about Style/Color tokens | references/style-tokens.md |
| Any question about built-in widgets | references/widgets.md |
These files are located next to this
SKILL.mdin thereferences/subdirectory. Read them with your file-reading tool before generating code — do not rely on the cheat-sheets below alone.
// Spacing
Style.marginXS Style.marginS Style.marginM Style.marginL Style.marginXL
// Corner radius
Style.radiusS Style.radiusM Style.radiusL
// Bar capsule (use per-screen variants in BarWidget!)
Style.capsuleColor // bar widget background
Style.capsuleBorderColor // bar widget border
Style.capsuleBorderWidth // typically 1
// Font sizes
Style.fontSizeXS Style.fontSizeS Style.fontSizeM Style.fontSizeL
Style.fontSizeXL Style.fontSizeXXL
// UI scale (multiply explicit panel dimensions by this)
Style.uiScaleRatio
// Helpers (per-monitor)
Style.getCapsuleHeightForScreen(screenName)
Style.getBarFontSizeForScreen(screenName)
Style.pixelAlignCenter(parentSize, childSize)
Color.mPrimary // accent/brand colour
Color.mOnPrimary // text on primary backgrounds
Color.mSurface // main card/panel background
Color.mSurfaceVariant // secondary container background
Color.mOnSurface // primary text colour
Color.mOnSurfaceVariant // secondary/dim text colour
Color.mHover // hover overlay for interactive elements
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Widgets
Item {
id: root
property var pluginApi: null
property ShellScreen screen
property string widgetId: ""
property string section: ""
readonly property string screenName: screen?.name ?? ""
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
readonly property real contentWidth: row.implicitWidth + Style.marginM * 2
readonly property real contentHeight: capsuleHeight
implicitWidth: contentWidth
implicitHeight: contentHeight
Rectangle {
id: visualCapsule
x: Style.pixelAlignCenter(parent.width, width)
y: Style.pixelAlignCenter(parent.height, height)
width: root.contentWidth
height: root.contentHeight
color: mouseArea.containsMouse ? Color.mHover : Style.capsuleColor
radius: Style.radiusL
border.color: Style.capsuleBorderColor
border.width: Style.capsuleBorderWidth
RowLayout {
id: row
anchors.centerIn: parent
spacing: Style.marginS
NIcon { icon: "plug"; color: Color.mPrimary }
NText { text: "My Plugin"; color: Color.mOnSurface; pointSize: barFontSize }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: pluginApi?.openPanel(root.screen, root)
}
}
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
Item {
id: root
property var pluginApi: null
readonly property var geometryPlaceholder: panelContainer
readonly property bool allowAttach: true
property real contentPreferredWidth: 500 * Style.uiScaleRatio
property real contentPreferredHeight: 400 * Style.uiScaleRatio
anchors.fill: parent
Rectangle {
id: panelContainer
anchors.fill: parent
color: "transparent"
ColumnLayout {
anchors { fill: parent; margins: Style.marginL }
spacing: Style.marginL
NText {
text: "My Plugin"
pointSize: Style.fontSizeL
font.weight: Font.Bold
color: Color.mOnSurface
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusL
// panel body content here
}
}
}
}
// Toasts
import qs.Services.UI
ToastService.showNotice("Done!")
ToastService.showError("Something went wrong")
// Tooltips (in MouseArea handlers)
onEntered: TooltipService.show(root, "Tooltip text", BarService.getTooltipDirection())
onExited: TooltipService.hide()
// Context menus — always use PanelService, not plain popups
PanelService.showContextMenu(contextMenu, root, screen)
// in NPopupContextMenu.onTriggered:
contextMenu.close()
PanelService.closeContextMenu(screen)
// Logging
Logger.i("PluginId", "message", optionalValue)
Logger.d("PluginId", "debug", optionalValue)
Logger.w("PluginId", "warn", optionalValue)
Logger.e("PluginId", "error", optionalValue)
// Read with fallback chain
readonly property string myValue:
pluginApi?.pluginSettings?.myKey ||
pluginApi?.manifest?.metadata?.defaultSettings?.myKey ||
"fallback"
// Write and persist
pluginApi.pluginSettings.myKey = newValue
pluginApi.saveSettings()