Remove unreleased Noctalia shell compatibility

This commit is contained in:
Ryan Hughes
2026-05-17 12:06:18 -04:00
parent 7517c8b624
commit 5faee9cf76
42 changed files with 14 additions and 1979 deletions
@@ -58,76 +58,11 @@ QtObject {
property color unselectedBorder: root.pick("image-picker.unselected-border", root.foreground)
}
// Noctalia palette tokens used by the compat widgets. Mapped onto the
// foundational palette; not exposed in shell.toml.
readonly property color mPrimary: accent
readonly property color mSecondary: Qt.darker(accent, 1.2)
readonly property color mTertiary: Qt.lighter(accent, 1.3)
readonly property color mSurface: background
readonly property color mSurfaceVariant: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.06)
readonly property color mOnSurface: foreground
readonly property color mOnSurfaceVariant: Qt.darker(foreground, 1.4)
readonly property color mOutline: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
readonly property color mHover: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.14)
readonly property color mOnHover: foreground
readonly property color mError: urgent
readonly property color mOnError: background
// Plugins read this to know whether to skip mid-flight transitions. We
// don't ship theme transitions ourselves, so it's always false.
readonly property bool isTransitioning: false
function alpha(c, opacity) {
if (!c) return Qt.rgba(0, 0, 0, opacity)
return Qt.rgba(c.r, c.g, c.b, opacity)
}
// Noctalia's smartAlpha picks an alpha based on the host theme's perceived
// contrast. A flat 0.6 reads acceptably across our themes; cheap, no math.
function smartAlpha(c) {
return alpha(c, 0.6)
}
// adaptiveOpacity takes a 0..1 ratio and clamps it. Plugins use it for
// fade animations relative to a "full" opacity value.
function adaptiveOpacity(value) {
if (value === undefined || value === null) return 1.0
return Math.max(0, Math.min(1, Number(value)))
}
// Plugins occasionally pass a color key like "accent" or "primary" via
// their own settings. resolveColorKey returns a color; resolveColorKeyOptional
// returns null/undefined-ish for "none".
function resolveColorKey(key) {
var resolved = resolveColorKeyOptional(key)
return resolved === null ? foreground : resolved
}
function resolveColorKeyOptional(key) {
var k = String(key || "").toLowerCase()
if (!k || k === "none") return null
switch (k) {
case "primary": return mPrimary
case "secondary": return mSecondary
case "tertiary": return mTertiary
case "surface": return mSurface
case "surfacevariant": return mSurfaceVariant
case "onsurface": return mOnSurface
case "onsurfacevariant": return mOnSurfaceVariant
case "outline": return mOutline
case "hover": return mHover
case "onhover": return mOnHover
case "error": return mError
case "onerror": return mOnError
case "accent": return accent
case "foreground": return foreground
case "background": return background
case "urgent":
case "red": return urgent
}
return Qt.color(k)
}
function loadColors(raw) {
var lines = String(raw || "").split("\n")
var foundAccent = false
@@ -1,15 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Their plugins normally route translations through
// pluginApi.tr(), not this singleton, so the stub here mostly exists to
// satisfy `import qs.Commons` style usage.
QtObject {
readonly property string langCode: "en"
function tr(key, interp) { return String(key === undefined ? "" : key) }
function trp(key, count, interp) { return String(key === undefined ? "" : key) }
function hasTranslation(key) { return false }
signal translationsLoaded()
}
@@ -1,88 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Their plugins resolve Tabler icon names to glyphs via
// this singleton. The original ships ~3000 mappings; for v1 we cover the
// names most commonly used by the plugins in their repo and fall back to
// "?" for unknowns so missing glyphs are visually obvious.
QtObject {
// Tabler icon names → Nerd Font glyphs. Most entries are mdi-* substitutes
// since Omarchy installs JetBrains Mono Nerd Font which includes Material
// Design Icons. Glyph codepoints embedded as actual chars (Python helper
// when the write tool strips multi-byte codepoints in some positions).
readonly property var glyphs: ({
"bell": "\udb80\udc9a", // mdi-bell
"bell-off": "\udb80\udc9b", // mdi-bell_off
"bell-ring": "\udb80\udd6b", // mdi-bell_ring
"clock": "\udb80\udc50", // mdi-clock
"clock-outline": "\udb80\udc51", // mdi-clock_outline
"wifi": "\udb81\udda9", // mdi-wifi
"wifi-off": "\udb81\uddaa", // mdi-wifi_off
"ethernet": "\udb80\udc02", // mdi-ethernet
"bluetooth": "\udb80\udcaf", // mdi-bluetooth
"bluetooth-off": "\udb80\udcb2", // mdi-bluetooth_off
"volume": "\udb81\udd7e", // mdi-volume_high
"volume-up": "\udb81\udd7e",
"volume-down": "\udb81\udd7f",
"volume-off": "\udb83\udc08", // mdi-volume_variant_off
"volume-mute": "\udb83\udc08",
"headphones": "\udb80\udecb", // mdi-headphones
"microphone": "\udb80\udf6c", // mdi-microphone
"microphone-off": "\udb80\udf6d", // mdi-microphone_off
"play": "\udb81\udc0a", // mdi-play
"pause": "\udb80\udfe4", // mdi-pause
"skip-back": "\udb81\udcae", // mdi-skip_previous
"skip-forward": "\udb81\udcad", // mdi-skip_next
"music": "\udb81\udd5a", // mdi-music
"weather": "\udb81\udd99", // mdi-weather_sunny
"weather-sun": "\udb81\udd99",
"weather-moon": "\udb81\udd94", // mdi-weather_night
"weather-cloud": "\udb81\udd90", // mdi-weather_cloudy
"weather-rain": "\udb81\udd96", // mdi-weather_pouring
"battery": "\udb80\udc83", // mdi-battery
"battery-charging": "\udb80\udc84",
"cpu": "\udb83\udee0", // mdi-cpu_64_bit
"memory": "\udb80\udd5b", // mdi-memory
"tools": "\udb80\udd64", // mdi-tools
"settings": "\udb80\udc93", // mdi-cog
"settings-outline":"\udb80\udcbb",
"power": "\udb80\udc25", // mdi-power
"lock": "\udb80\udd3e", // mdi-lock
"unlock": "\udb80\udd3f",
"refresh": "\udb81\udc50", // mdi-refresh
"ai": "󰚩", // mdi-robot
"robot": "󰚩",
"x": "\udb80\udd56", // mdi-close
"check": "\udb80\udc12", // mdi-check
"chevron-down": "\udb80\udd40",
"chevron-up": "\udb80\udd43",
"chevron-left": "\udb80\udd41",
"chevron-right": "\udb80\udd42",
"plus": "\udb80\udc15",
"minus": "\udb80\udc16",
"leaf": "\udb80\udf2a",
"rocket": "\udb81\udc63",
"balance": "\udb81\uddd1",
"moon": "\udb81\udd94",
"sun": "\udb81\udd99",
"trash": "\udb81\udcd7", // mdi-delete
"edit": "\udb80\udd66",
"search": "\udb80\udd6f",
"menu": "\udb80\udd6c",
"home": "\udb80\udf0c",
"folder": "\udb80\udd99",
"file": "\udb80\udd97",
"calendar": "\udb80\udcf7", // mdi-calendar
"circle": "\udb80\udd2f"
})
function get(name) {
var key = String(name || "").toLowerCase()
if (glyphs[key]) return glyphs[key]
return "?"
}
function has(name) {
return !!glyphs[String(name || "").toLowerCase()]
}
}
@@ -1,19 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Plugins call Logger.d/i/w/e routinely.
QtObject {
function format(args) {
var out = []
for (var i = 0; i < args.length; i++) {
var a = args[i]
out.push(a === undefined ? "undefined" : (a === null ? "null" : String(a)))
}
return out.join(" ")
}
function d() { console.debug(format(arguments)) }
function i() { console.info(format(arguments)) }
function w() { console.warn(format(arguments)) }
function e() { console.error(format(arguments)) }
}
@@ -1,62 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Noctalia compat shim. Their plugins read Settings.data.* and call
// Settings.getBarPositionForScreen / getBarWidgetsForScreen. We expose a
// read-only view of our shell.json so reads succeed; writes are best routed
// through pluginApi.saveSettings() instead.
QtObject {
id: root
property var data: ({
bar: {
position: "top",
widgets: { left: [], center: [], right: [] }
},
general: {},
colorSchemes: { darkMode: true }
})
// Plugins use this gate around verbose debug logging. Defaulting to false
// keeps their Logger.d calls silent unless we explicitly flip it.
property bool isDebug: false
// shellConfig is wired by shell.qml at startup; reading it keeps Settings.data
// in lockstep with the live shell config.
property var shellConfig: null
onShellConfigChanged: rebuildData()
function rebuildData() {
var sc = shellConfig || {}
var bar = (sc && sc.bar) ? sc.bar : {}
var layout = (bar && bar.layout) ? bar.layout : {}
data = {
bar: {
position: bar.position || "top",
centerAnchor: bar.centerAnchor || "",
fontFamily: bar.fontFamily || "JetBrainsMono Nerd Font",
widgets: {
left: Array.isArray(layout.left) ? layout.left : [],
center: Array.isArray(layout.center) ? layout.center : [],
right: Array.isArray(layout.right) ? layout.right : []
}
},
general: {},
colorSchemes: { darkMode: true }
}
}
function getBarPositionForScreen(name) {
return data && data.bar ? data.bar.position : "top"
}
function getBarWidgetsForScreen(name) {
return data && data.bar ? data.bar.widgets : { left: [], center: [], right: [] }
}
function getScreenOverrideEntry(name) { return null }
function hasScreenOverride(name, key) { return false }
function setScreenOverride(name, key, value) { /* not supported */ }
}
@@ -1,12 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Their plugins occasionally introspect global UI
// state through this singleton. Returning safe defaults lets reads succeed
// without us tracking the state ourselves.
QtObject {
readonly property bool isLoaded: true
property var activeScreen: null
property bool launcherOpen: false
property bool settingsOpen: false
}
@@ -1,104 +0,0 @@
pragma Singleton
import QtQuick
import "." as Commons
// Noctalia compat shim. Sizing/spacing/typography tokens that plugins
// reference unconditionally. Real values picked to roughly match what
// Omarchy renders today — close enough that plugin layouts don't look
// jarring next to native widgets.
QtObject {
id: root
// Radii. Omarchy uses sharp corners; all radius tokens collapse to 0 so
// Noctalia plugins inherit the same look as native widgets.
readonly property real radiusXXS: 0
readonly property real radiusXS: 0
readonly property real radiusS: 0
readonly property real radiusM: 0
readonly property real radiusL: 0
readonly property real radiusXL: 0
readonly property real iRadiusXS: 0
readonly property real iRadiusS: 0
readonly property real iRadiusM: 0
readonly property real iRadiusL: 0
// Margins / paddings. Noctalia uses two parallel scales for margins (one
// tighter, one looser); we map them to the same values.
readonly property real marginXXS: 1
readonly property real marginXS: 2
readonly property real marginS: 4
readonly property real marginM: 6
readonly property real marginL: 10
readonly property real marginXL: 14
readonly property real margin2XXS: 1
readonly property real margin2XS: 2
readonly property real margin2S: 3
readonly property real margin2M: 5
readonly property real margin2L: 8
readonly property real margin2XL: 12
// Border widths.
readonly property real borderS: 1
readonly property real borderM: 1
readonly property real borderL: 2
// Font sizes.
readonly property real fontSizeXXS: 9
readonly property real fontSizeXS: 10
readonly property real fontSizeS: 11
readonly property real fontSizeM: 12
readonly property real fontSizeL: 14
readonly property real fontSizeXL: 16
readonly property real fontSizeXXL: 20
readonly property real fontSizeXXXL: 24
// Font weights.
readonly property int fontWeightLight: 300
readonly property int fontWeightRegular: 400
readonly property int fontWeightMedium: 500
readonly property int fontWeightSemiBold: 600
readonly property int fontWeightBold: 700
// Opacities.
readonly property real opacityLight: 0.4
readonly property real opacityMedium: 0.6
readonly property real opacityHeavy: 0.8
readonly property real opacityFull: 1.0
// Animation durations (ms).
readonly property int animationFast: 100
readonly property int animationNormal: 160
readonly property int animationSlow: 250
readonly property int animationSlower: 400
readonly property int animationSlowest: 600
// Tooltip delay (ms).
readonly property int tooltipDelay: 400
// Capsule (bar pill) tokens. The Omarchy bar is flat, no capsule background,
// so we route these to transparent + the foreground outline for visual hint.
readonly property color capsuleColor: Qt.rgba(0, 0, 0, 0)
readonly property color capsuleBorderColor: Qt.rgba(0, 0, 0, 0)
readonly property real capsuleBorderWidth: 0
readonly property real baseWidgetSize: 22
// UI scale ratio. Noctalia plugins multiply lengths by this when
// applyUiScale is true; we leave it at 1.
readonly property real uiScaleRatio: 1.0
// Helpers. Plugins pass screen names but in practice we ignore them and
// return our shell-wide values.
function getBarHeightForScreen(name) { return 28 }
function getCapsuleHeightForScreen(name) { return 22 }
function getBarFontSizeForScreen(name) { return fontSizeM }
function getBarRadiusForScreen(name) { return radiusM }
function pixelAlignCenter(parentSize, childSize) {
return Math.round((Number(parentSize) - Number(childSize)) / 2)
}
function toOdd(n) {
var i = Math.floor(Number(n))
return i | 1
}
}
@@ -1,10 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Their plugins resolve app icons through XDG icon
// themes; we don't ship that machinery, so iconForAppId returns empty and
// callers fall back to their default glyph.
QtObject {
function iconFromName(name) { return "" }
function iconForAppId(appId) { return "" }
}
@@ -1,18 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim.
QtObject {
function getFormattedTimestamp() {
return new Date().toISOString()
}
function nowMs() {
return Date.now()
}
function formatDate(date, fmt) {
if (!date) date = new Date()
return Qt.formatDateTime(date, fmt || "yyyy-MM-dd HH:mm:ss")
}
}
@@ -1,10 +1,2 @@
module qs.Commons
singleton Color 1.0 Color.qml
singleton Style 1.0 Style.qml
singleton Logger 1.0 Logger.qml
singleton Settings 1.0 Settings.qml
singleton I18n 1.0 I18n.qml
singleton Time 1.0 Time.qml
singleton Icons 1.0 Icons.qml
singleton ThemeIcons 1.0 ThemeIcons.qml
singleton ShellState 1.0 ShellState.qml
@@ -1,9 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. The "noctaliaPerformanceMode" flag was a v3 internal
// state Noctalia plugins occasionally check. We hardcode false; plugins that
// branch on it will fall into the non-performance code path.
QtObject {
readonly property bool noctaliaPerformanceMode: false
}
@@ -1,2 +0,0 @@
module qs.Services.Power
singleton PowerProfileService 1.0 PowerProfileService.qml
@@ -1,36 +0,0 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
// Noctalia compat shim. activate-linux and a few other plugins display
// distro/host info. We parse /etc/os-release lazily.
QtObject {
id: root
property string osPretty: ""
property string osName: ""
property string osVersion: ""
property string hostname: ""
property Process osReleaseProc: Process {
command: ["bash", "-c",
"( . /etc/os-release && printf '%s\\t%s\\t%s\\n' \"$PRETTY_NAME\" \"$NAME\" \"$VERSION_ID\" ); hostname"]
onExited: {
var text = String(osReleaseStdout.text || "").trim().split("\n")
if (text.length >= 1) {
var fields = text[0].split("\t")
root.osPretty = fields[0] || ""
root.osName = fields[1] || ""
root.osVersion = fields[2] || ""
}
if (text.length >= 2) root.hostname = text[1].trim()
}
stdout: StdioCollector {
id: osReleaseStdout
waitForEnd: true
}
}
Component.onCompleted: osReleaseProc.running = true
}
@@ -1,2 +0,0 @@
module qs.Services.System
singleton HostService 1.0 HostService.qml
@@ -1,80 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Plugin bar widgets call into BarService for
// registration bookkeeping and to ask the host where their tooltip should
// pop. We track the bar reference (set by the host at startup) and expose
// the helpers plugins actually use.
QtObject {
id: root
// Wired by omarchy-shell's Bar.qml on construction so we can reach back
// for things like position and the tooltip popup.
property var bar: null
// Plugins read this when computing their own layout-revision markers.
property int widgetsRevision: 0
// Bookkeeping for plugin widgets. Not consulted by Omarchy itself; some
// plugins call lookupWidget() to coordinate across instances.
property var registered: ({})
function registerWidget(screen, section, widgetId, index, item) {
var key = (screen && screen.name ? screen.name : "_") + ":" + section + ":" + widgetId + ":" + index
var next = {}
for (var k in registered) next[k] = registered[k]
next[key] = { screen: screen, section: section, widgetId: widgetId, index: index, item: item }
registered = next
widgetsRevision++
}
function unregisterWidget(screen, section, widgetId, index) {
var key = (screen && screen.name ? screen.name : "_") + ":" + section + ":" + widgetId + ":" + index
if (!registered[key]) return
var next = {}
for (var k in registered) if (k !== key) next[k] = registered[k]
registered = next
widgetsRevision++
}
// Plugins occasionally search for a sibling widget by id. We do a linear
// scan since the registry is small in practice.
function lookupWidget(widgetId) {
for (var k in registered) {
if (registered[k].widgetId === widgetId) return registered[k]
}
return undefined
}
// Translate the bar's edge to a tooltip direction string. Noctalia expects
// strings; the Omarchy bar drives its own tooltip popup off bar.position.
function getTooltipDirection(screenName) {
var pos = bar ? bar.position : "top"
if (pos === "top") return "down"
if (pos === "bottom") return "up"
if (pos === "left") return "right"
if (pos === "right") return "left"
return "down"
}
function getPillDirection(item) {
var pos = bar ? bar.position : "top"
return pos === "left" || pos === "right" ? "horizontal" : "vertical"
}
// Forward into the omarchy-shell host. Most Noctalia widgets call this
// from a right-click handler.
function openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings) {
if (bar && bar.shell && typeof bar.shell.summon === "function") {
bar.shell.summon("omarchy.settings",
JSON.stringify({ focusWidgetId: widgetId, section: section, index: sectionWidgetIndex }))
}
}
function openPluginSettings(screen, manifest) {
if (bar && bar.shell && typeof bar.shell.summon === "function") {
bar.shell.summon("omarchy.settings",
JSON.stringify({ focusPluginId: manifest ? manifest.id : "" }))
}
}
}
@@ -1,34 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Plugins use this for context menus and to reach
// the host's panels. We forward into the omarchy-shell host where there's
// a matching surface, and log a warning for anything we don't ship in v1.
QtObject {
id: root
property var bar: null
property var shell: null
// Plugins call this on right-click with a Menu component. The Noctalia
// implementation parents the menu to the screen's overlay; we simply
// call `open()` on whatever the plugin passes — most plugin context menus
// are plain QtQuick `Menu` items that know how to show themselves.
function showContextMenu(menu, item, screen) {
if (!menu) return
if (typeof menu.popup === "function") menu.popup()
else if (typeof menu.open === "function") menu.open()
}
function closeContextMenu(screen) {
// No-op; the Menu / PopupWindow closes itself on outside click.
}
function openLauncherWithSearch(screen, prefix) {
console.warn("PanelService.openLauncherWithSearch is not supported in the Omarchy compat layer")
}
function getPanel(name, screen) {
return null
}
}
@@ -1,30 +0,0 @@
pragma Singleton
import QtQuick
// Noctalia compat shim. Their plugins call TooltipService.show(item, text)
// on hover; we route that into the Omarchy bar's shared tooltip popup.
QtObject {
id: root
// Wired by Bar.qml on construction.
property var bar: null
function show(item, text, direction) {
if (!bar || !item) return
if (typeof bar.showTooltip === "function") {
bar.showTooltip(item, text || "")
}
}
function hide(item) {
if (!bar) return
if (item && typeof bar.hideTooltip === "function") {
bar.hideTooltip(item)
} else if (bar.tooltipTarget) {
// Noctalia plugins often call TooltipService.hide() without the source
// item on click. Native bar.hideTooltip(target) ignores null targets, so
// clear the currently visible tooltip explicitly.
bar.hideTooltip(bar.tooltipTarget)
}
}
}
@@ -1,4 +0,0 @@
module qs.Services.UI
singleton BarService 1.0 BarService.qml
singleton TooltipService 1.0 TooltipService.qml
singleton PanelService 1.0 PanelService.qml
@@ -1,9 +0,0 @@
import QtQuick
import qs.Commons
Rectangle {
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
radius: Style.radiusM
}
@@ -1,26 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
Button {
id: root
property string label: ""
property color colorBg: Color.mSurfaceVariant
property color colorFg: Color.mOnSurface
text: label || ""
background: Rectangle {
color: root.hovered ? Color.mHover : root.colorBg
radius: Style.radiusM
border.color: Color.mOutline
border.width: Style.borderS
}
contentItem: Text {
text: root.text
color: root.colorFg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeM
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
@@ -1,11 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
CheckBox {
id: root
property string label: ""
text: label || ""
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
}
@@ -1,138 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
ComboBox {
id: root
property string label: ""
// Noctalia's NComboBox API commonly uses a model of { key, name } objects,
// a currentKey property, and an onSelected(key, item) handler. Qt's ComboBox
// only knows currentIndex/currentText, so bridge the small API surface here.
property string currentKey: ""
signal selected(var key, var item)
textRole: "name"
valueRole: "key"
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
implicitHeight: 32
leftPadding: 10
rightPadding: 28
topPadding: 4
bottomPadding: 4
function itemAt(index) {
if (index < 0) return null
if (Array.isArray(root.model)) return root.model[index] || null
return null
}
function keyAt(index) {
var item = itemAt(index)
if (!item) return ""
if (item.key !== undefined && item.key !== null) return String(item.key)
if (item.value !== undefined && item.value !== null) return String(item.value)
if (item.name !== undefined && item.name !== null) return String(item.name)
return String(item)
}
function syncCurrentIndex() {
if (!Array.isArray(root.model)) return
for (var i = 0; i < root.model.length; i++) {
if (keyAt(i) === String(root.currentKey || "")) {
if (root.currentIndex !== i) root.currentIndex = i
return
}
}
if (root.model.length > 0 && root.currentIndex < 0) root.currentIndex = 0
}
Component.onCompleted: syncCurrentIndex()
onCurrentKeyChanged: syncCurrentIndex()
onModelChanged: syncCurrentIndex()
onActivated: function(index) {
var item = itemAt(index)
var key = keyAt(index)
root.currentKey = key
root.selected(key, item)
}
contentItem: Text {
text: root.displayText
color: Color.mOnSurface
font: root.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: Color.mSurfaceVariant
border.color: root.activeFocus ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
radius: 0
}
indicator: Text {
x: root.width - width - 10
y: (root.height - height) / 2
text: "▾"
color: Color.mOnSurfaceVariant
font.family: root.font.family
font.pixelSize: 11
}
delegate: ItemDelegate {
id: del
required property int index
required property var modelData
width: ListView.view ? ListView.view.width : root.width
height: 28
highlighted: root.highlightedIndex === del.index
contentItem: Text {
text: {
var m = del.modelData
if (m === null || m === undefined) return ""
if (root.textRole && m[root.textRole] !== undefined) return String(m[root.textRole])
return String(m)
}
color: Color.mOnSurface
font: root.font
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
background: Rectangle {
color: del.highlighted || del.hovered
? Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.14)
: "transparent"
radius: 0
}
}
popup: Popup {
y: root.height
width: root.width
implicitHeight: Math.min(contentItem.implicitHeight + 2, 280)
padding: 1
background: Rectangle {
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS
radius: 0
}
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: root.popup.visible ? root.delegateModel : null
currentIndex: root.highlightedIndex
boundsBehavior: Flickable.StopAtBounds
ScrollIndicator.vertical: ScrollIndicator { }
}
}
}
@@ -1,8 +0,0 @@
import QtQuick
import qs.Commons
Rectangle {
implicitHeight: 1
color: Color.mOutline
opacity: 0.4
}
@@ -1,18 +0,0 @@
import QtQuick
import qs.Commons
// Noctalia compat shim. Renders an icon by name through the Icons map.
Text {
id: root
property string icon: ""
property real pointSize: 14
property bool applyUiScale: true
text: Icons.get(icon)
color: Color.mOnSurface
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Math.round(pointSize * (applyUiScale ? Style.uiScaleRatio : 1))
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
}
@@ -1,67 +0,0 @@
import QtQuick
import qs.Commons
import qs.Services.UI
// Noctalia compat shim. The widget plugins lean on this heavily; it's the
// primary clickable bar element. We mimic Noctalia's surface area
// (icon/text/tooltip/colors/border) but skip the long tail of properties
// no observed plugin actually sets.
Rectangle {
id: root
property string icon: ""
property string text: ""
property string tooltipText: ""
property string tooltipDirection: ""
property real baseSize: Style.baseWidgetSize
property bool applyUiScale: false
property real customRadius: -1
property color colorBg: Style.capsuleColor
property color colorFg: Color.mOnSurface
property color colorBgHover: Color.mHover
property color colorFgHover: Color.mOnHover
property color colorBorder: Style.capsuleBorderColor
property color colorBorderHover: Style.capsuleBorderColor
property bool flat: false
signal clicked()
signal rightClicked()
signal middleClicked()
signal wheel(int delta)
readonly property real effectiveSize: Math.round(baseSize * (applyUiScale ? Style.uiScaleRatio : 1))
implicitWidth: effectiveSize
implicitHeight: effectiveSize
radius: customRadius >= 0 ? customRadius : Style.radiusM
color: mouse.containsMouse ? colorBgHover : colorBg
border.color: mouse.containsMouse ? colorBorderHover : colorBorder
border.width: Style.capsuleBorderWidth
Behavior on color { ColorAnimation { duration: Style.animationFast } }
Text {
anchors.centerIn: parent
text: root.icon ? Icons.get(root.icon) : root.text
color: mouse.containsMouse ? root.colorFgHover : root.colorFg
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Math.round(Style.fontSizeM * (root.applyUiScale ? Style.uiScaleRatio : 1))
Behavior on color { ColorAnimation { duration: Style.animationFast } }
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: if (root.tooltipText) TooltipService.show(root, root.tooltipText, root.tooltipDirection)
onExited: TooltipService.hide(root)
onClicked: function(m) {
if (m.button === Qt.RightButton) root.rightClicked()
else if (m.button === Qt.MiddleButton) root.middleClicked()
else root.clicked()
}
onWheel: function(w) { root.wheel(w.angleDelta.y) }
}
}
@@ -1,27 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
// Noctalia compat shim. Plugins instantiate this with a model of action
// objects and expect `triggered(action)` when a row is picked.
Menu {
id: root
property var model: []
// Noctalia passes the owning ShellScreen through here. Qt Quick Controls
// Menu doesn't need it, but accepting the property keeps plugin QML from
// failing at load time.
property var screen: null
signal triggered(var action, var item)
Instantiator {
model: root.model
delegate: MenuItem {
required property var modelData
text: modelData ? String(modelData.label || modelData.action || "") : ""
onTriggered: root.triggered(modelData ? String(modelData.action || "") : "", modelData)
}
onObjectAdded: function(index, object) { root.insertItem(index, object) }
onObjectRemoved: function(index, object) { root.removeItem(object) }
}
}
@@ -1,55 +0,0 @@
import QtQuick
import qs.Commons
// Noctalia compat shim. Auto-scrolling label.
Item {
id: root
property string text: ""
property real maxWidth: 200
property real pointSize: Style.fontSizeM
property int scrollMode: NScrollText.ScrollMode.Hover
property bool forcedHover: false
property real fadeExtent: 0.1
property real fadeCornerRadius: 0
property bool fadeRoundLeftCorners: false
property bool fadeRoundRightCorners: false
property color textColor: Color.mOnSurface
enum ScrollMode { Always, Hover, Never }
implicitHeight: label.implicitHeight
implicitWidth: Math.min(maxWidth, label.implicitWidth)
clip: true
readonly property bool overflowing: label.implicitWidth > width
readonly property bool shouldScroll: {
if (!overflowing) return false
if (scrollMode === NScrollText.ScrollMode.Always) return true
if (scrollMode === NScrollText.ScrollMode.Hover) return forcedHover
return false
}
Text {
id: label
text: root.text
color: root.textColor
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: root.pointSize
verticalAlignment: Text.AlignVCenter
height: parent.height
anchors.verticalCenter: parent.verticalCenter
NumberAnimation on x {
id: scrollAnim
running: root.shouldScroll
loops: Animation.Infinite
duration: Math.max(5000, label.implicitWidth * 22)
from: root.width
to: -label.implicitWidth
easing.type: Easing.Linear
}
onShouldScrollChanged: if (!root.shouldScroll) x = 0
}
}
@@ -1,8 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
Slider {
id: root
property string label: ""
}
@@ -1,75 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
SpinBox {
id: root
property string label: ""
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
editable: true
implicitHeight: 32
leftPadding: 10
rightPadding: 24
topPadding: 4
bottomPadding: 4
background: Rectangle {
color: Color.mSurfaceVariant
border.color: root.activeFocus ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
}
contentItem: TextInput {
text: root.displayText
font: root.font
color: Color.mOnSurface
selectionColor: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.35)
selectedTextColor: Color.mOnSurface
horizontalAlignment: Qt.AlignLeft
verticalAlignment: Qt.AlignVCenter
readOnly: !root.editable
validator: root.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
}
up.indicator: Rectangle {
x: root.mirrored ? 0 : root.width - width
width: 20
height: root.height / 2
color: root.up.pressed
? Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.16)
: root.up.hovered
? Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.08)
: "transparent"
radius: Style.radiusS
Text {
anchors.centerIn: parent
text: "▲"
font.family: root.font.family
font.pixelSize: 7
color: root.up.hovered ? Color.mOnSurface : Color.mOnSurfaceVariant
}
}
down.indicator: Rectangle {
x: root.mirrored ? 0 : root.width - width
y: root.height / 2
width: 20
height: root.height / 2
color: root.down.pressed
? Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.16)
: root.down.hovered
? Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.08)
: "transparent"
radius: Style.radiusS
Text {
anchors.centerIn: parent
text: "▼"
font.family: root.font.family
font.pixelSize: 7
color: root.down.hovered ? Color.mOnSurface : Color.mOnSurfaceVariant
}
}
}
@@ -1,26 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
Rectangle {
id: root
property int currentIndex: 0
property int margins: Style.marginXS
property bool distributeEvenly: false
implicitHeight: 32
radius: Style.radiusS
color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Style.borderS
default property alias content: row.children
RowLayout {
id: row
anchors.fill: parent
anchors.margins: root.margins
spacing: Style.marginXS
}
}
@@ -1,46 +0,0 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
Rectangle {
id: root
property string text: ""
property int tabIndex: -1
property bool checked: false
signal clicked()
Layout.fillWidth: true
implicitWidth: label.implicitWidth + Style.marginL * 2
implicitHeight: 26
radius: Style.radiusXS
color: area.containsMouse
? Color.mHover
: (checked ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.18) : "transparent")
border.color: checked ? Color.mPrimary : "transparent"
border.width: checked ? 1 : 0
Behavior on color { ColorAnimation { duration: Style.animationFast } }
Text {
id: label
anchors.centerIn: parent
text: root.text
color: checked ? Color.mPrimary : Color.mOnSurface
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
font.bold: checked
elide: Text.ElideRight
width: Math.max(0, parent.width - Style.marginM * 2)
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}
@@ -1,19 +0,0 @@
import QtQuick
import qs.Commons
// Noctalia compat shim. The original supports pointSize (interpreted as
// pixel size in Noctalia) plus an applyUiScale flag. We translate to
// font.pixelSize directly; uiScaleRatio comes from Style.
Text {
id: root
property real pointSize: 12
property bool applyUiScale: true
property var customFont: null
color: Color.mOnSurface
font.family: customFont || "JetBrainsMono Nerd Font"
font.pixelSize: Math.round(pointSize * (applyUiScale ? Style.uiScaleRatio : 1))
font.weight: Style.fontWeightRegular
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
@@ -1,20 +0,0 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
TextField {
id: root
property string label: ""
property string description: ""
property string defaultValue: ""
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
color: Color.mOnSurface
background: Rectangle {
color: Color.mSurfaceVariant
border.color: root.focus ? Color.mPrimary : Color.mOutline
border.width: Style.borderS
radius: Style.radiusS
}
}
@@ -1,68 +0,0 @@
import QtQuick
import qs.Commons
Rectangle {
id: root
property string label: ""
property string text: ""
property bool checked: false
signal toggled(var value)
signal clicked()
implicitWidth: labelItem.visible ? indicator.width + 8 + labelItem.implicitWidth : indicator.width
implicitHeight: Math.max(indicator.height, labelItem.implicitHeight)
color: "transparent"
opacity: enabled ? 1 : 0.4
Rectangle {
id: indicator
width: 48
height: 22
anchors.verticalCenter: parent.verticalCenter
radius: 0
color: root.checked
? Color.mPrimary
: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.10)
border.color: root.checked
? Color.mPrimary
: Qt.rgba(Color.mOnSurface.r, Color.mOnSurface.g, Color.mOnSurface.b, 0.4)
border.width: Style.borderS
Rectangle {
width: 16
height: 16
radius: 0
anchors.verticalCenter: parent.verticalCenter
x: root.checked ? parent.width - width - 3 : 3
color: root.checked ? Color.mSurface : Color.mOnSurface
Behavior on x { NumberAnimation { duration: Style.animationFast } }
}
}
Text {
id: labelItem
visible: (root.label || root.text) !== ""
anchors.left: indicator.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: root.label || root.text
color: Color.mOnSurface
font.family: "JetBrainsMono Nerd Font"
font.pixelSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
enabled: root.enabled
cursorShape: Qt.PointingHandCursor
onClicked: {
root.checked = !root.checked
root.clicked()
root.toggled(root.checked)
}
}
}
@@ -1,17 +0,0 @@
module qs.Widgets
NText 1.0 NText.qml
NIcon 1.0 NIcon.qml
NIconButton 1.0 NIconButton.qml
NBox 1.0 NBox.qml
NButton 1.0 NButton.qml
NPopupContextMenu 1.0 NPopupContextMenu.qml
NScrollText 1.0 NScrollText.qml
NToggle 1.0 NToggle.qml
NSpinBox 1.0 NSpinBox.qml
NSlider 1.0 NSlider.qml
NTextInput 1.0 NTextInput.qml
NComboBox 1.0 NComboBox.qml
NCheckbox 1.0 NCheckbox.qml
NDivider 1.0 NDivider.qml
NTabBar 1.0 NTabBar.qml
NTabButton 1.0 NTabButton.qml
@@ -1,146 +0,0 @@
import QtQuick
import Quickshell
import qs.Commons
// Wraps Noctalia panel entry points, which are plain Item content intended to
// be hosted inside Noctalia's SmartPanel. We host them as anchored PopupWindows
// so clicking a Noctalia bar widget behaves like native Omarchy popups instead
// of opening a tiled app window.
Item {
id: root
property string panelSource: ""
property var pluginApi: null
property var manifest: ({})
property string omarchyPath: ""
property var shell: null
property var pluginRegistry: null
property var barWidgetRegistry: null
property bool popupOpen: false
property bool closingFromShell: false
readonly property var anchorItem: pluginApi ? pluginApi.panelAnchorItem : null
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property string barPosition: {
if (shell && shell.barConfig && shell.barConfig.position) return String(shell.barConfig.position)
return "top"
}
function open(payloadJson) {
popupOpen = true
}
function close() {
closingFromShell = true
popupOpen = false
if (pluginApi) {
pluginApi.panelOpenScreen = null
pluginApi.panelAnchorItem = null
}
closingFromShell = false
}
function dismiss() {
if (!popupOpen) return
if (pluginApi && typeof pluginApi.closePanel === "function") pluginApi.closePanel(null)
else if (shell && manifest && manifest.id && typeof shell.hide === "function") shell.hide(manifest.id)
else close()
}
PopupWindow {
id: popup
visible: root.popupOpen
color: "transparent"
implicitWidth: Math.max(320, panelLoader.item && panelLoader.item.contentPreferredWidth
? panelLoader.item.contentPreferredWidth : 420)
implicitHeight: Math.max(260, panelLoader.item && panelLoader.item.contentPreferredHeight
? panelLoader.item.contentPreferredHeight : 520)
onVisibleChanged: {
if (!visible && root.popupOpen && !root.closingFromShell) root.dismiss()
}
// Do not use HyprlandFocusGrab here. Noctalia panels are larger,
// interactive surfaces with tabs and nested Flickables; focus grabs made
// internal clicks (notably tab switches) look like outside clicks and also
// interfered with wheel scrolling. Native Omarchy micro-popups can keep
// focus-grab dismissal, but compat panels behave more like pinned panels.
anchor {
id: popupAnchor
window: root.anchorWindow
adjustment: PopupAdjustment.Slide
edges: Edges.Top | Edges.Left
gravity: Edges.Bottom | Edges.Right
rect.width: 1
rect.height: 1
onAnchoring: {
var target = root.anchorItem
var window = root.anchorWindow
if (!target || !window) {
popupAnchor.rect.x = 0
popupAnchor.rect.y = 0
return
}
var popupWidth = popup.implicitWidth
var popupHeight = popup.implicitHeight
var margin = 8
var localX = target.width / 2 - popupWidth / 2
var localY = target.height + margin
if (root.barPosition === "bottom") {
localY = -popupHeight - margin
} else if (root.barPosition === "left") {
localX = target.width + margin
localY = target.height / 2 - popupHeight / 2
} else if (root.barPosition === "right") {
localX = -popupWidth - margin
localY = target.height / 2 - popupHeight / 2
}
var point = window.contentItem.mapFromItem(target, localX, localY)
popupAnchor.rect.x = Math.round(point.x)
popupAnchor.rect.y = Math.round(point.y)
}
}
Rectangle {
anchors.fill: parent
color: Color.mSurface
border.color: Color.mOutline
border.width: 1
radius: 0
Loader {
id: panelLoader
anchors.fill: parent
source: root.panelSource
onLoaded: root.injectPanelProps()
onStatusChanged: {
if (status === Loader.Error) {
console.warn("noctalia panel failed for " + (root.manifest ? root.manifest.id : "") + ":", errorString())
}
}
}
}
}
onPluginApiChanged: injectPanelProps()
onManifestChanged: injectPanelProps()
function injectPanelProps() {
var item = panelLoader.item
if (!item) return
if ("pluginApi" in item) item.pluginApi = root.pluginApi
if ("manifest" in item) item.manifest = root.manifest
if ("omarchyPath" in item) item.omarchyPath = root.omarchyPath
if ("shell" in item) item.shell = root.shell
if ("pluginRegistry" in item) item.pluginRegistry = root.pluginRegistry
if ("barWidgetRegistry" in item) item.barWidgetRegistry = root.barWidgetRegistry
if ("screen" in item) {
var screens = Quickshell.screens
item.screen = screens && screens.length > 0 ? screens[0] : null
}
}
}
@@ -1,114 +0,0 @@
import QtQuick
import Quickshell
// Builds the `pluginApi` QtObject that Noctalia plugins expect. The host
// (omarchy-shell or the bar) calls create() once per plugin and caches the
// returned object so the same handle is reused across the plugin's bar
// widget, panel, settings form, etc.
Item {
// create(pluginId, manifest, settingsProvider, hostBridge) -> QtObject
//
// settingsProvider: function() that returns the plugin's current settings
// merged with defaults. Called fresh on every read so the plugin always
// sees the live shell.json entry, not a stale snapshot from when create()
// was first called.
// hostBridge: object with these methods (all optional):
// - persistSettings(pluginId, settings): write back to shell.json via
// the shell's updateEntryInline helper.
// - openPanel(pluginId, screen, buttonItem): summon the plugin's panel
// entry point through the shell's plugin host.
// - closePanel(pluginId, screen): hide a previously-summoned panel.
// - currentScreen(): return the screen the plugin should anchor to.
// togglePanel is provided by the factory itself and routes through the
// open/closePanel bridge methods. Main.qml service instances are wired
// separately by the shell and set on api.mainInstance once instantiated.
function create(pluginId, manifest, settingsProvider, hostBridge) {
var api = pluginApiFactory.createObject(null, {
pluginId: pluginId,
pluginDir: manifest && manifest.__sourceDir ? manifest.__sourceDir : "",
manifest: manifest || ({}),
_settingsProvider: settingsProvider || (function() { return {} }),
_hostBridge: hostBridge || ({})
})
return api
}
Component {
id: pluginApiFactory
QtObject {
id: api
property string pluginId: ""
property string pluginDir: ""
property var manifest: ({})
property var _settingsProvider
property var _hostBridge: ({})
property var mainInstance: null
// Writable because Noctalia Settings.qml components commonly stage edits
// with `pluginApi.pluginSettings = ...; pluginApi.saveSettings()`.
// The initial binding resolves shell.json + defaults; assignment during
// save intentionally replaces it with the staged value to persist.
property var pluginSettings: _settingsProvider ? _settingsProvider() : ({})
property var panelOpenScreen: null
property var panelAnchorItem: null
property var ipcHandlers: ({})
// Noctalia i18n surface — v1 returns the key as-is.
readonly property string currentLanguage: "en"
readonly property var pluginTranslations: ({})
readonly property var pluginFallbackTranslations: ({})
readonly property int translationVersion: 0
function tr(key, interp) { return String(key === undefined ? "" : key) }
function trp(key, count, interp) { return String(key === undefined ? "" : key) }
function hasTranslation(key) { return false }
function saveSettings(settings) {
if (settings !== undefined) pluginSettings = settings
if (_hostBridge && typeof _hostBridge.persistSettings === "function")
_hostBridge.persistSettings(pluginId, pluginSettings)
// Re-read after persisting so mainInstance/bar widgets see the
// canonical merged settings (defaults + saved overrides) immediately.
if (_settingsProvider) pluginSettings = _settingsProvider()
if (mainInstance && typeof mainInstance.refresh === "function") mainInstance.refresh()
}
function openPanel(screen, buttonItem) {
panelOpenScreen = screen || true
panelAnchorItem = buttonItem || null
if (_hostBridge && _hostBridge.tooltipService && typeof _hostBridge.tooltipService.hide === "function")
_hostBridge.tooltipService.hide(buttonItem)
if (_hostBridge && typeof _hostBridge.openPanel === "function")
_hostBridge.openPanel(pluginId, screen, buttonItem)
}
function closePanel(screen) {
panelOpenScreen = null
panelAnchorItem = null
if (_hostBridge && typeof _hostBridge.closePanel === "function")
_hostBridge.closePanel(pluginId, screen)
}
function togglePanel(screen, buttonItem) {
if (panelOpenScreen) closePanel(screen)
else openPanel(screen, buttonItem)
}
function withCurrentScreen(cb) {
if (typeof cb !== "function") return
var s = (_hostBridge && typeof _hostBridge.currentScreen === "function")
? _hostBridge.currentScreen() : null
cb(s)
}
function openLauncher(screen) {
console.warn("pluginApi.openLauncher is not supported in the Omarchy compat layer (plugin=" + pluginId + ")")
}
function closeLauncher(screen) { /* no-op */ }
function toggleLauncher(screen) { openLauncher(screen) }
}
}
}
@@ -1,72 +0,0 @@
# Noctalia plugin compatibility (omarchy-shell)
The omarchy-shell can load most bar widget plugins from the
[noctalia-dev/noctalia-plugins](https://github.com/noctalia-dev/noctalia-plugins)
ecosystem without any modification to the plugin code.
## Install a Noctalia plugin
```sh
git clone https://github.com/noctalia-dev/noctalia-plugins /tmp/noctalia-plugins
ln -s /tmp/noctalia-plugins/asus-um5606-fan-state \
~/.config/omarchy/plugins/asus-um5606-fan-state
omarchy-shell-ipc shell rescanPlugins
```
The plugin id you see inside Omarchy is prefixed: `noctalia.asus-um5606-fan-state`.
Add it via the bar customizer or by editing `~/.config/omarchy/shell.json` directly.
## What's supported in v1
| Noctalia concept | Omarchy support |
|---|---|
| `entryPoints.barWidget` | yes — registered through `BarWidgetRegistry` |
| `entryPoints.panel` | yes — opened via `pluginApi.openPanel()` |
| `entryPoints.settings` | yes — embedded in the settings dialog |
| `entryPoints.main` | yes — instantiated as a hidden service, exposed via `pluginApi.mainInstance` |
| `entryPoints.desktopWidget` | **no** — skipped with a console warning |
| `entryPoints.launcherProvider` | **no** — skipped with a console warning |
| Plugin i18n (`i18n/<lang>.json`) | **no**`tr()` returns the raw key |
| Plugin install from git URL | **no** — manual drop into `~/.config/omarchy/plugins/` |
| Hot reload on plugin change | **no** — call `omarchy-shell-ipc shell rescanPlugins` |
## Shim surface
We ship just enough of Noctalia's QML namespace to render typical bar widgets.
| Module | Symbols |
|---|---|
| `qs.Commons` | `Color`, `Style`, `Logger`, `Settings` (read-only), `I18n` (stub), `Time`, `Icons` (~50 entries), `ThemeIcons` (stub), `ShellState` (stub) |
| `qs.Widgets` | `NText`, `NIcon`, `NIconButton`, `NBox`, `NButton`, `NPopupContextMenu`, `NScrollText`, `NToggle`, `NSpinBox`, `NSlider`, `NTextInput`, `NComboBox`, `NCheckbox`, `NDivider`, `NTabBar`, `NTabButton` |
| `qs.Services.UI` | `BarService`, `TooltipService`, `PanelService` |
| `qs.Services.System` | `HostService` (reads `/etc/os-release`) |
| `qs.Services.Power` | `PowerProfileService` (returns `noctaliaPerformanceMode: false`) |
Anything outside this surface logs a `console.warn` instead of crashing the
shell, but the plugin may not render correctly.
## `pluginApi` surface
A Noctalia plugin's `pluginApi` is built per-plugin and injected onto the bar
widget / panel / settings entry points. The implementation lives at
`compat/noctalia/PluginApiFactory.qml`.
| Property / method | Behaviour |
|---|---|
| `pluginId`, `pluginDir`, `manifest` | Provided as expected. |
| `pluginSettings` | Live read of the plugin's entry in `~/.config/omarchy/shell.json`, merged with the manifest's `metadata.defaultSettings`. |
| `mainInstance` | Live instance of `Main.qml` when the plugin declares one. |
| `saveSettings()` | Persists the merged settings into the plugin's entry in `shell.json`. |
| `openPanel(screen, btn)` / `closePanel(screen)` / `togglePanel(screen, btn)` | Routes through `omarchy-shell-ipc shell summon/hide` against the plugin's panel entry point. |
| `withCurrentScreen(cb)` | Calls `cb` with the bar's currently-rendering screen. |
| `tr/trp/hasTranslation` | Returns the key as-is; no i18n in v1. |
| `openLauncher(...)` family | Stub — logs a warning. |
## Known limitations
- Plugin labels show raw translation keys for non-English locales.
- Icon names not present in our compact `Icons` map render as `?`.
- Plugins that declare only `desktopWidget` or `launcherProvider` and no
bar/panel/main do not appear in the Omarchy catalog.
- Some plugins call `Color.mPrimary` for accent colors that don't perfectly
match Omarchy's theme palette; results are close but not pixel-identical.
@@ -26,11 +26,8 @@ Item {
// the bar just renders whatever it's handed. The bar font follows the
// OS-level fontconfig monospace binding it is not stored in shell.json.
required property var barConfig
// Injected by the host shell so the bar can detect Noctalia-compat plugins
// and look up manifests when wiring per-widget Noctalia pluginApi.
property var pluginRegistry: null
// Injected by the host shell. Used so Noctalia plugins that reach for
// shell-wide APIs (openPanel, currentScreen) can do so via pluginApi.
// Injected by the host shell. Used for shell-wide actions such as opening
// settings and persisting inline widget state.
property var shell: null
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
// killing the entire shell. Wired to BarPanel.visible below; updated by the
@@ -93,40 +90,6 @@ Item {
if (activePopout === owner) activePopout = null
}
// -------------------------------------------------- Noctalia compat helpers
//
// The shell owns pluginApi creation and Main.qml service instantiation now;
// bar widgets just look up their api by moduleName. Keeping section/index
// helpers here because they're needed for Noctalia bar-widget injection
// (widgetId, section, sectionWidgetIndex, sectionWidgetsCount).
function sectionOfEntry(entry) {
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
var list = layoutConfig[sections[s]] || []
for (var i = 0; i < list.length; i++) {
if (list[i] === entry) return sections[s]
}
}
return ""
}
function indexOfEntry(entry) {
var section = sectionOfEntry(entry)
var list = section ? (layoutConfig[section] || []) : []
for (var i = 0; i < list.length; i++) if (list[i] === entry) return i
return -1
}
function entriesOfSection(section) {
return Array.isArray(layoutConfig[section]) ? layoutConfig[section] : []
}
function noctaliaPluginApiFor(moduleName) {
if (!shell || typeof shell.noctaliaPluginApiFor !== "function") return null
return shell.noctaliaPluginApiFor(moduleName)
}
readonly property bool vertical: position === "left" || position === "right"
readonly property int barSize: vertical ? 28 : 26
@@ -1269,20 +1232,6 @@ Item {
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = moduleName
if ("settings" in target) target.settings = moduleSettings
// Noctalia compat injection. Only kicks in for plugins whose manifest
// was translated from Noctalia shape; for our first-party widgets these
// properties don't exist on the target item so nothing happens.
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[moduleName] : null
if (manifest && manifest.__noctaliaCompat) {
if ("pluginApi" in target) target.pluginApi = root.noctaliaPluginApiFor(moduleName)
if ("widgetId" in target) target.widgetId = moduleName
if ("section" in target) target.section = root.sectionOfEntry(entry)
if ("sectionWidgetIndex" in target) target.sectionWidgetIndex = root.indexOfEntry(entry)
if ("sectionWidgetsCount" in target) target.sectionWidgetsCount = root.entriesOfSection(root.sectionOfEntry(entry)).length
if ("screen" in target && root.QsWindow && root.QsWindow.window)
target.screen = root.QsWindow.window.screen
}
}
Component {
@@ -370,7 +370,7 @@ Item {
displayName: meta.displayName || manifest.name || key,
name: meta.displayName || manifest.name || key,
description: meta.description || manifest.description || "",
category: meta.category || (manifest.__noctaliaCompat ? "Noctalia" : "Plugin"),
category: meta.category || "Plugin",
allowMultiple: meta.allowMultiple === true,
settingsForm: meta.settingsForm || "",
schema: Array.isArray(meta.schema) ? meta.schema : [],
@@ -402,17 +402,9 @@ Item {
var meta = widgetMetadata(id)
if (meta.settingsForm) return true
if (widgetSchema(id).length > 0) return true
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
if (manifest && manifest.__noctaliaCompat && manifest.entryPoints && manifest.entryPoints.settings)
return true
return false
}
function widgetIsNoctaliaPlugin(id) {
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
return !!(manifest && manifest.__noctaliaCompat)
}
function widgetAllowsMultiple(id) {
var meta = widgetMetadata(id)
if (meta.allowMultiple === true) return true
@@ -465,8 +457,7 @@ Item {
var allowsMultiple = widgetAllowsMultiple(id)
if (!allowsMultiple && existingInBar[id]) continue
result.push({ id: id, name: widgetName(id), description: widgetDescription(id),
elsewhere: allowsMultiple && !!existingInBar[id] && !existsHere,
isNoctalia: widgetIsNoctaliaPlugin(id) })
elsewhere: allowsMultiple && !!existingInBar[id] && !existsHere })
}
return result
}
@@ -986,7 +977,6 @@ Item {
Text {
text: modelData.name
+ (modelData.isNoctalia ? " (Noctalia)" : "")
+ (modelData.elsewhere ? " (elsewhere)" : "")
color: root.foreground
font.family: root.fontFamily
@@ -1246,9 +1236,6 @@ Item {
case "weatherSettings": return weatherSettingsComponent
}
}
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
if (manifest && manifest.__noctaliaCompat && manifest.entryPoints && manifest.entryPoints.settings)
return noctaliaSettingsComponent
if (widgetSchema(id).length > 0) return dynamicSettingsComponent
return null
}
@@ -1262,53 +1249,6 @@ Item {
}
}
Component {
id: noctaliaSettingsComponent
Item {
id: noctaliaForm
property var entry: ({})
property string pluginId: entry && entry.id ? String(entry.id) : ""
property var manifest: pluginId && root.pluginRegistry
? root.pluginRegistry.installedPlugins[pluginId] : null
function saveSettings() {
if (settingsLoader.item && typeof settingsLoader.item.saveSettings === "function") {
settingsLoader.item.saveSettings()
} else {
console.warn("Noctalia settings form has no saveSettings():", pluginId)
}
}
implicitHeight: settingsLoader.item ? settingsLoader.item.implicitHeight : 0
implicitWidth: settingsLoader.item ? settingsLoader.item.implicitWidth : 0
Loader {
id: settingsLoader
anchors.fill: parent
source: {
if (!noctaliaForm.manifest) return ""
return root.pluginRegistry.entryPointUrl(noctaliaForm.manifest, "settings")
}
asynchronous: false
onLoaded: {
if (!item) return
var api = (root.shell && typeof root.shell.noctaliaPluginApiFor === "function")
? root.shell.noctaliaPluginApiFor(noctaliaForm.pluginId) : null
if (api && "pluginApi" in item) item.pluginApi = api
if ("screen" in item && root.QsWindow && root.QsWindow.window)
item.screen = root.QsWindow.window.screen
}
onStatusChanged: {
if (status === Loader.Error) {
console.warn("noctalia Settings.qml failed for " + noctaliaForm.pluginId + ":",
sourceComponent ? sourceComponent.errorString() : "")
}
}
}
}
}
Component {
id: spacerSettingsComponent
@@ -44,98 +44,11 @@ QtObject {
return true
}
// Detect a Noctalia-shape manifest (https://github.com/noctalia-dev/noctalia-plugins).
// Noctalia manifests don't carry our schemaVersion; we recognise them by the
// presence of `minNoctaliaVersion` or `metadata.defaultSettings`, or an
// `entryPoints` map that names roles Noctalia owns (main/barWidget/panel).
function isNoctaliaShape(manifest) {
if (!isPlainObject(manifest)) return false
if (manifest.schemaVersion !== undefined) return false
if (manifest.minNoctaliaVersion !== undefined) return true
if (isPlainObject(manifest.metadata) && isPlainObject(manifest.metadata.defaultSettings)) return true
if (isPlainObject(manifest.entryPoints)) {
var ep = manifest.entryPoints
if (ep.barWidget || ep.main || ep.panel || ep.desktopWidget
|| ep.launcherProvider) return true
}
return false
}
// Translate a Noctalia manifest into the shape we validate normally. We do
// not modify the on-disk file; the in-memory copy gets a __noctaliaCompat
// marker so downstream code can branch where the semantics diverge.
function translateNoctaliaManifest(src) {
var ep = isPlainObject(src.entryPoints) ? src.entryPoints : {}
var kinds = []
var unsupported = []
if (ep.main) kinds.push("service")
if (ep.barWidget) kinds.push("bar-widget")
if (ep.panel) kinds.push("panel")
// Out of scope for v1 warn loudly so we don't silently strand the plugin.
if (ep.desktopWidget) unsupported.push("desktopWidget")
if (ep.launcherProvider) unsupported.push("launcherProvider")
if (kinds.length === 0) {
console.warn("PluginRegistry: noctalia plugin '" + src.id + "' has no supported entryPoints"
+ (unsupported.length ? " (unsupported in v1: " + unsupported.join(", ") + ")" : ""))
return null
}
if (unsupported.length) {
console.warn("PluginRegistry: noctalia plugin '" + src.id
+ "' uses entryPoints not supported in v1, ignoring: " + unsupported.join(", "))
}
var translated = {
schemaVersion: 1,
id: "noctalia." + String(src.id),
name: src.name || src.id,
version: src.version || "0.0.0",
author: src.author || "",
description: src.description || "",
kinds: kinds,
activation: kinds.indexOf("service") !== -1 ? "persistent" : "on-demand",
entryPoints: {},
__noctaliaCompat: true,
__noctaliaOriginal: src
}
if (ep.barWidget) translated.entryPoints.barWidget = ep.barWidget
if (ep.main) translated.entryPoints.service = ep.main
if (ep.panel) translated.entryPoints.panel = ep.panel
if (ep.settings) translated.entryPoints.settings = ep.settings
var defaults = (isPlainObject(src.metadata) && isPlainObject(src.metadata.defaultSettings))
? src.metadata.defaultSettings : {}
if (translated.entryPoints.barWidget) {
translated.barWidget = {
displayName: translated.name,
description: translated.description,
category: "Noctalia",
allowMultiple: false,
defaults: defaults,
schema: []
}
}
if (defaults && Object.keys(defaults).length > 0) translated.defaults = defaults
return translated
}
function validateManifest(manifest, sourcePath) {
if (!isPlainObject(manifest)) {
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
return null
}
// Noctalia shape translate before validation. The translated manifest
// goes through the standard schemaVersion=1 path below.
if (isNoctaliaShape(manifest)) {
var preservedSourceDir = manifest.__sourceDir
var preservedFirstParty = manifest.__isFirstParty
manifest = translateNoctaliaManifest(manifest)
if (!manifest) return null
manifest.__sourceDir = preservedSourceDir
manifest.__isFirstParty = preservedFirstParty
}
if (manifest.schemaVersion !== 1) {
console.warn("PluginRegistry: unsupported schemaVersion at " + sourcePath)
return null
+10 -209
View File
@@ -5,9 +5,6 @@ import Quickshell.Io
import "plugins/bar"
import "services"
import "compat/noctalia" as Compat
import qs.Services.UI as NoctaliaUI
import qs.Commons as NoctaliaCommons
ShellRoot {
id: shell
@@ -161,20 +158,8 @@ ShellRoot {
// case the user dir already existed at startup.
pluginRegistry.rescan()
shell._syncFirstPartyServices()
// Wire Noctalia compat singletons. Plugins read state from these globals;
// we hand them references to the host so they can route into the right
// popup, tooltip, etc.
NoctaliaUI.BarService.bar = bar
NoctaliaUI.TooltipService.bar = bar
NoctaliaUI.PanelService.bar = bar
NoctaliaUI.PanelService.shell = shell
NoctaliaCommons.Settings.shellConfig = shell.shellConfig
}
// Keep Noctalia.Settings.data in sync with whatever shell.json currently is.
onShellConfigChanged: NoctaliaCommons.Settings.shellConfig = shell.shellConfig
function mutateShellConfig(mutator) {
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
mutator(copy)
@@ -190,181 +175,14 @@ ShellRoot {
omarchyPath: shell.omarchyPath
barWidgetRegistry: shell.barWidgetRegistry
barConfig: shell.barConfig
pluginRegistry: shell.pluginRegistry
shell: shell
}
// ------------------------------------------------- Noctalia plugin API
//
// One pluginApi instance per plugin, cached for the life of the shell. The
// factory is host-owned (shell, not bar) so non-bar Noctalia plugins
// (panels, services) can call into it too. Settings lookups walk the live
// shell.json on every read so reorders/edits never leave a stale entry
// captured in the closure.
Compat.PluginApiFactory { id: noctaliaApiFactory }
Item {
id: noctaliaServiceHost
visible: false
}
property var _noctaliaApis: ({})
property var _noctaliaServices: ({})
function findShellEntry(pluginId) {
var config = shellConfig
if (!isPlainObject(config)) return null
if (isPlainObject(config.bar) && isPlainObject(config.bar.layout)) {
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
var arr = config.bar.layout[sections[s]]
if (!Array.isArray(arr)) continue
for (var i = 0; i < arr.length; i++) {
if (arr[i] && arr[i].id === pluginId) return arr[i]
}
}
}
if (Array.isArray(config.plugins)) {
for (var p = 0; p < config.plugins.length; p++) {
if (config.plugins[p] && config.plugins[p].id === pluginId) return config.plugins[p]
}
}
return null
}
function noctaliaPluginApiFor(pluginId) {
var key = String(pluginId)
if (!key) return null
var existing = _noctaliaApis[key]
if (existing) return existing
var manifest = pluginRegistry && pluginRegistry.installedPlugins
? pluginRegistry.installedPlugins[key] : null
if (!manifest || !manifest.__noctaliaCompat) return null
var api = noctaliaApiFactory.create(
key,
manifest,
function() {
// Compute effective settings on every read. We can't capture an entry
// reference because mutateSection rebuilds the layout objects and the
// captured reference would point at a snapshot.
var defaults = (manifest.barWidget && manifest.barWidget.defaults) || manifest.defaults || {}
var merged = {}
for (var k in defaults) merged[k] = defaults[k]
var entry = findShellEntry(key)
if (entry) {
for (var ek in entry) if (ek !== "id") merged[ek] = entry[ek]
}
return merged
},
{
persistSettings: function(_pluginId, settings) {
if (typeof shell.updateEntryInline === "function")
shell.updateEntryInline(key, settings)
},
openPanel: function(_pluginId, _screen, _btn) {
shell.summon(key, JSON.stringify({ source: "noctalia" }))
},
closePanel: function(_pluginId, _screen) { shell.hide(key) },
tooltipService: NoctaliaUI.TooltipService,
currentScreen: function() {
var screens = Quickshell.screens
return screens && screens.length > 0 ? screens[0] : null
}
}
)
var next = ({})
for (var existingKey in _noctaliaApis) next[existingKey] = _noctaliaApis[existingKey]
next[key] = api
_noctaliaApis = next
return api
}
// -------------------------------------------------- noctalia service main
//
// Noctalia plugins with `entryPoints.main` (translated to kind="service")
// are instantiated once per shell session when enabled. The Main.qml is
// typically headless a data source the plugin's bar widget reads from.
// We rely on the shared pluginApi so the service and any bar widgets see
// the same handle.
function ensureNoctaliaService(pluginId) {
var key = String(pluginId)
if (_noctaliaServices[key]) return _noctaliaServices[key]
var manifest = pluginRegistry && pluginRegistry.installedPlugins
? pluginRegistry.installedPlugins[key] : null
if (!manifest || !manifest.__noctaliaCompat) return null
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
var url = pluginRegistry.entryPointUrl(manifest, "service")
if (!url) return null
var api = noctaliaPluginApiFor(key)
if (!api) return null
var comp = Qt.createComponent(url, Component.PreferSynchronous)
function finalize() {
if (comp.status !== Component.Ready) {
console.warn("noctalia service load failed for " + key + ": " + comp.errorString())
return
}
var inst = comp.createObject(noctaliaServiceHost, { pluginApi: api })
if (!inst) {
console.warn("noctalia service createObject returned null for", key)
return
}
var snext = ({})
for (var sk in _noctaliaServices) snext[sk] = _noctaliaServices[sk]
snext[key] = inst
_noctaliaServices = snext
api.mainInstance = inst
}
if (comp.status === Component.Loading) {
comp.statusChanged.connect(finalize)
return null
}
finalize()
return _noctaliaServices[key] || null
}
function _syncNoctaliaServices() {
if (!pluginRegistry || !pluginRegistry.installedPlugins) return
var plugins = pluginRegistry.installedPlugins
for (var id in plugins) {
var m = plugins[id]
if (!m || !m.__noctaliaCompat) continue
if (!m.entryPoints || !m.entryPoints.service) continue
if (!pluginRegistry.isEnabled(id)) continue
if (_noctaliaServices[id]) continue
ensureNoctaliaService(id)
}
// Drop services for plugins that have been disabled or removed.
for (var existingId in _noctaliaServices) {
var stillThere = plugins[existingId]
var stillEnabled = stillThere && pluginRegistry.isEnabled(existingId)
if (stillThere && stillEnabled) continue
var inst = _noctaliaServices[existingId]
if (inst && typeof inst.destroy === "function") inst.destroy()
var next = ({})
for (var k in _noctaliaServices) if (k !== existingId) next[k] = _noctaliaServices[k]
_noctaliaServices = next
var apis = ({})
for (var ak in _noctaliaApis) apis[ak] = _noctaliaApis[ak]
if (apis[existingId]) apis[existingId].mainInstance = null
_noctaliaApis = apis
}
}
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { shell._syncNoctaliaServices() }
}
// ---------------------------------------------------- first-party services
//
// Generic loader for any first-party plugin that declares kind "service".
// The notification daemon is the first user; future first-party services
// (background updaters, idle inhibitor, etc.) plug in here. Mirrors
// ensureNoctaliaService but skips the noctalia-specific pluginApi handoff
// and gates on `__isFirstParty` instead of `__noctaliaCompat`.
// (background updaters, idle inhibitor, etc.) plug in here.
Item {
id: firstPartyServiceHost
visible: false
@@ -382,7 +200,6 @@ ShellRoot {
var manifest = pluginRegistry && pluginRegistry.installedPlugins
? pluginRegistry.installedPlugins[key] : null
if (!manifest || !manifest.__isFirstParty) return null
if (manifest.__noctaliaCompat) return null
if (!Array.isArray(manifest.kinds) || manifest.kinds.indexOf("service") === -1) return null
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
var url = pluginRegistry.entryPointUrl(manifest, "service")
@@ -421,7 +238,7 @@ ShellRoot {
var plugins = pluginRegistry.installedPlugins
for (var id in plugins) {
var m = plugins[id]
if (!m || !m.__isFirstParty || m.__noctaliaCompat) continue
if (!m || !m.__isFirstParty) continue
if (!Array.isArray(m.kinds) || m.kinds.indexOf("service") === -1) continue
if (!m.entryPoints || !m.entryPoints.service) continue
if (!pluginRegistry.isEnabled(id)) continue
@@ -446,14 +263,11 @@ ShellRoot {
function onPluginsChanged() { shell._syncFirstPartyServices() }
}
// Used by Noctalia compat: pluginApi.saveSettings() ends up writing inline
// settings to the plugin's entry in shell.json. moduleName is the entry id
// (the bare manifest id, e.g. "noctalia.air-quality"); settings is the
// merged plugin state. Returns true if anything actually changed.
// Compute the proposed new shellConfig in a local clone, and only persist
// if anything actually changed. Lets Noctalia plugins call saveSettings()
// repeatedly with identical values (common for reactive QML bindings)
// without dirtying shell.json or thrashing the file watcher.
// Writes inline settings to a bar layout entry or top-level plugin entry in
// shell.json. moduleName is the entry id; settings is the merged plugin
// state. Returns true if anything actually changed. Compute the proposed
// new shellConfig in a local clone, and only persist if anything actually
// changed so reactive bindings do not dirty shell.json unnecessarily.
function updateEntryInline(moduleName, settings) {
var stripped = String(moduleName)
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
@@ -647,9 +461,7 @@ ShellRoot {
readonly property string sourceUrl: shell.pluginRegistry.entryPointUrl(manifest, entryKind)
property Loader panelLoader: Loader {
readonly property bool wrapNoctaliaPanel: !!(panelEntry.manifest && panelEntry.manifest.__noctaliaCompat && panelEntry.entryKind === "panel")
source: wrapNoctaliaPanel ? "" : panelEntry.sourceUrl
sourceComponent: wrapNoctaliaPanel ? noctaliaPanelWrapperComponent : null
source: panelEntry.sourceUrl
active: panelEntry.sourceUrl !== "" && (panelEntry.keepLoaded || shell.openPanelIds[panelEntry.pluginId] === true)
asynchronous: true
onLoaded: {
@@ -663,11 +475,6 @@ ShellRoot {
// (e.g. omarchy.notifications) read shared state off `service`. We
// hand them the matching service instance if one was loaded.
if ("service" in item) item.service = shell.firstPartyServiceFor(panelEntry.pluginId)
// Noctalia panel/overlay/menu plugins expect a pluginApi just like
// their bar widgets do. First-party Omarchy plugins don't declare
// the property so the `in target` check skips them.
if (panelEntry.manifest && panelEntry.manifest.__noctaliaCompat && "pluginApi" in item)
item.pluginApi = shell.noctaliaPluginApiFor(panelEntry.pluginId)
shell.registerPanelLoader(panelEntry.pluginId, this)
}
onStatusChanged: {
@@ -683,12 +490,6 @@ ShellRoot {
}
Component.onDestruction: shell.unregisterPanelLoader(panelEntry.pluginId)
}
property Component noctaliaPanelWrapperComponent: Component {
Compat.PanelWrapper {
panelSource: panelEntry.sourceUrl
}
}
}
}
@@ -698,8 +499,8 @@ ShellRoot {
// Each enabled plugin with kind "bar-widget" gets a Component created from
// its manifest entry point and registered under its plain manifest id.
// First-party widget ids (calendar, weather, etc.) are short and don't
// collide with namespaced plugin ids like noctalia.air-quality, so we don't
// need a separate "plugin:" namespace anymore.
// collide with namespaced plugin ids, so we don't need a separate
// "plugin:" namespace anymore.
Connections {
target: shell.pluginRegistry
function onPluginsChanged() { shell.syncPluginWidgets() }