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
+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() }