From bf0223304d3ad1fb39332b9b5d428e8d0104e056 Mon Sep 17 00:00:00 2001 From: dxvid Date: Mon, 29 Jun 2026 19:17:18 -0500 Subject: [PATCH 1/2] fix(brightness): guard against null screen reference during disconnect --- services/Brightness.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index c7bb6b9e1..c117545ed 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -79,11 +79,11 @@ Singleton { required property ShellScreen screen readonly property bool isDdc: { - const match = root.ddcMonitors.find(m => screen.model?.includes(m.model) && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); + const match = root.ddcMonitors.find(m => screen?.model?.includes(m.model) && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); return !!match; } readonly property string busNum: { - const match = root.ddcMonitors.find(m => screen.model?.includes(m.model) && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); + const match = root.ddcMonitors.find(m => screen?.model?.includes(m.model) && !root.monitors.slice(0, root.monitors.indexOf(this)).some(mon => mon.busNum === m.busNum)); return match?.busNum ?? ""; } property int rawMaxBrightness: 100 From c21434841c60425add3181a0bd48c29ae13ad8fc Mon Sep 17 00:00:00 2001 From: dxvid Date: Wed, 22 Jul 2026 14:01:06 -0500 Subject: [PATCH 2/2] fix(brightness): destroy stale monitor objects on screen disconnect The optional-chaining guard stops the TypeError flood, but doesn't address why it happened: monitors was a binding that recreated a BrightnessMonitor for every screen on each Quickshell.screens change, leaking the previous set (never destroyed, still parented to root, Timers/Behaviors still running) since none of them get destroy()ed. Each subsequent screen change added more zombies reacting to a dead screen reference, which is what actually drove CPU/memory up. Reconcile against the live screen list instead: reuse existing monitors for screens that are still connected, and destroy() the ones whose screen went away. --- services/Brightness.qml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index c117545ed..ce5b416a2 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -20,9 +20,31 @@ Singleton { signal brightnessChanged() property var ddcMonitors: [] - readonly property list monitors: Quickshell.screens.map(screen => monitorComp.createObject(root, { - screen - })) + property list monitors: [] + + // Reconciles `monitors` against the live screen list instead of + // recreating every entry on each change: still-connected screens keep + // their existing BrightnessMonitor (and its running Timers/Processes), + // while ones whose screen disconnected are explicitly destroyed so they + // don't linger as zombie objects reacting to a dead `screen` reference. + function _syncMonitors() { + const next = Quickshell.screens.map(screen => root.monitors.find(m => m.screen === screen) ?? monitorComp.createObject(root, { + screen + })); + for (const m of root.monitors) { + if (!next.includes(m)) m.destroy(); + } + root.monitors = next; + } + + Component.onCompleted: root._syncMonitors() + + Connections { + target: Quickshell + function onScreensChanged() { + root._syncMonitors(); + } + } function getMonitorForScreen(screen: ShellScreen): var { return monitors.find(m => m.screen === screen);