feat(display): blackout rescue — re-enable a disabled panel on undock
Some checks failed
Check / eval (push) Failing after 1m40s

Bernardo's question: with "Laptop screen off" active, does the panel
come back when the dock is yanked? VM answer (new maintainer harness
tools/monitor-fallback.nix, softGL desktop): no — Hyprland 0.55.4
leaves the session with zero active outputs and never re-enables a
soft-disabled monitor. rescue_blackout in the display hotplug watcher
now re-enables every disabled output (immediate, retried, idempotent
keyword + toast) whenever a monitorremoved leaves nothing active;
independent of the profile auto-switch and its settings gate.

Sharp edge documented rather than hidden: the zero-output state itself
crashes Hyprland in the VM ~4/5 runs (aquamarine CBackend::dispatchIdle
ABRT within ~0.5s — faster than any userland rescue; a control run
removing the external with the panel ACTIVE survives 5/5, so the state
is the trigger, not headless removal; crash reproduced with the rescue
provably inert, so not caused by it). Rescue ships as defense-in-depth:
it recovers every surviving case, and the worst case on real DRM is
session-to-greeter on a re-lit panel instead of a black brick. Rider
fix: `grep -c .` exits 1 at count zero — the ||-guard on that pipeline
silently disabled the rescue exactly when it mattered.

Verified: hazard V2 (VM reproduces the dead state); control
discriminator 5/5; flake check green. V3 pending: real-dock
undock-while-off in HARDWARE-QUEUE (run with nothing important open;
crash on real DRM = rework the row to mirror or wait for a Hyprland
bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:09:17 +01:00
parent 6439105d38
commit c840202018
6 changed files with 208 additions and 2 deletions

View File

@@ -215,7 +215,43 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
}
outputs() { hyprctl monitors all -j 2>/dev/null | jq -r '.[].name' | sort; }
# Blackout rescue the session must never end up with ZERO active
# outputs. Hyprland does NOT re-enable a soft-disabled monitor when
# the last active one disconnects (VM-verified, tools/
# monitor-fallback.nix): menu "Laptop screen off" + sudden undock
# left a dead session. On every monitorremoved, if nothing is
# active, re-enable whatever is disabled (preferred/auto the
# declared rule returns at the next reload/relogin). Independent of
# the profile auto-switch and its settings gate: this is a safety
# invariant, not a layout preference.
active_count() {
# NB: `grep -c .` exits 1 when the count is 0 the case this whole
# rescue exists for so never `||`-guard on the pipeline status.
hyprctl monitors -j 2>/dev/null | jq -r '.[].name' | grep -c . || true
}
rescue_blackout() {
# Act IMMEDIATELY and retry: the zero-output state is not just a
# black screen, it can kill the compositor outright in the softGL
# VM harness aquamarine ABRT'd (CBackend::dispatchIdle) within
# ~0.5s of the last output going while it persisted, so shrinking
# the window is the priority; the keyword is idempotent and the
# retries cover a first attempt racing the removal teardown.
tries=0
while [ "$(active_count)" = 0 ] && [ "$tries" -lt 4 ]; do
hyprctl monitors all -j 2>/dev/null | jq -r '.[] | select(.disabled) | .name' \
| while read -r m; do
[ -n "$m" ] || continue
hyprctl keyword monitor "$m,preferred,auto,auto" >/dev/null 2>&1
done
tries=$((tries+1))
sleep 1
done
if [ "$tries" -gt 0 ] && [ "$(active_count)" != 0 ]; then
notify-send "Display" "Screen re-enabled the active output disappeared." 2>/dev/null || true
fi
}
check_monitors() {
cur=$(outputs)
[ "$cur" = "$1" ] && return
@@ -239,7 +275,12 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
# Listen to Hyprland's IPC socket for instant hotplug reaction
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do
case "$line" in
monitoradded*|monitorremoved*)
monitorremoved*)
rescue_blackout
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;
monitoradded*)
res=$(check_monitors "$prev")
[ -n "$res" ] && prev="$res"
;;