From 64cc9f58d03d5b7d022264afba3b3ae929b5a720 Mon Sep 17 00:00:00 2001 From: Harshith Nadig Date: Sun, 30 Aug 2026 12:49:30 +0530 Subject: [PATCH] fix(monitor): detect stranded outputs with no active scanout in omarchy-hyprland-monitor-modeless 'omarchy-hyprland-monitor-modeless' only checked whether an enabled monitor had .width == 0 or .height == 0. When an output drops link during an atomic commit (or loses its CRTC after DPMS off), it retains a valid mode in 'hyprctl monitors all' but is unassigned and absent from active scanout ('hyprctl monitors'), causing the recovery loop in 'monitor-watch' to abort prematurely and leave the screen black. Compare active scanout outputs with all connected outputs to detect unassigned/stranded enabled monitors alongside 0x0 mode monitors. Fixes #9078 --- bin/omarchy-hyprland-monitor-modeless | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/bin/omarchy-hyprland-monitor-modeless b/bin/omarchy-hyprland-monitor-modeless index cb55e3b8f07..463863cf5da 100755 --- a/bin/omarchy-hyprland-monitor-modeless +++ b/bin/omarchy-hyprland-monitor-modeless @@ -1,19 +1,30 @@ #!/bin/bash -# omarchy:summary=Returns true when Hyprland has an enabled monitor with no mode +# omarchy:summary=Returns true when Hyprland has an enabled monitor with no mode or no active scanout # omarchy:hidden=true # A monitor powered off at boot answers with a partial EDID carrying no video -# modes, so Hyprland brings it up at 0x0 and the screen stays black. Mirrors are -# absent from plain `monitors`, hence `all` plus an explicit disabled filter. +# modes (0x0), or drops link during atomic commit landing without a CRTC +# (disabled != true, present in `monitors all` but absent from active `monitors`). # -# Exits 0 modeless, 1 not, 2 when the compositor cannot say. Nothing fires an -# event for this state, so a caller that gave up on an unanswered query would -# leave the screen black for good. -monitors=$(hyprctl monitors all -j 2>/dev/null) || exit 2 +# Exits 0 modeless/stranded, 1 active/recovered, 2 when the compositor cannot say. -state=$(jq 'if any(.[]; .disabled != true and (.width == 0 or .height == 0)) then 0 else 1 end' \ - <<<"$monitors" 2>/dev/null) +active_monitors=$(hyprctl monitors -j 2>/dev/null) || exit 2 +all_monitors=$(hyprctl monitors all -j 2>/dev/null) || exit 2 + +state=$(jq -n \ + --argjson active "$active_monitors" \ + --argjson all "$all_monitors" \ + ' + ($active | map(.name)) as $active_names | + if any($all[]; + .disabled != true and ( + .width == 0 or + .height == 0 or + (.name as $n | ($active_names | index($n) | not) and (.mirrorOf == "" or .mirrorOf == null)) + ) + ) then 0 else 1 end + ' 2>/dev/null) case $state in 0 | 1) exit "$state" ;;