A monitor powered off when the machine boots — a smart strip cutting AC, the PC coming back on its own — still answers DDC, but with a partial EDID that carries no video modes. Hyprland takes the connector as present and brings the monitor up at 0x0. Powering it on afterwards changes nothing: the connector never dropped at DRM level, so no hotplug fires, nothing re-reads the EDID, and the screen stays black until a reboot. Only a reload re-reads it. Forcing a DRM re-probe would work too but needs root, and the kernel's cached mode list stays empty without one, so there is nothing cheaper to poll: the reload is both the fix and the only way to learn whether it was needed. Poll only while a monitor is in that state, back off from three seconds to a minute, and stop as soon as one reports a mode — the machine can sit black all night, and powering the monitor on fires no event to stop on. Nothing will ask again if this loop gives up, so an unreadable answer is not taken for a healthy monitor. It is also not waited on forever: a compositor that stays silent has gone, and with it the session and any reason to keep asking. Reloading on our own schedule means minding the reload guard, which exists to keep Hyprland out of package-owned config mid-transaction, and which only disables the automatic reloads. The guard can now be asked, and recovery holds off while a transaction is in flight. A reload lands a monitor at 0x0 the same way a boot does, so configreloaded is watched alongside the hotplug events. The recovery's own reload comes back through it, and a lock keeps that from stacking a second loop. Contention waits rather than drops: a trigger arriving while a loop is exiting is the last one that will come, and one arriving while a loop is running is answered by its next pass anyway. Mirrors are dropped from `hyprctl monitors`, so the check asks for all of them and filters the disabled ones itself. Monitors turned off on purpose sit at 0x0 too, and re-applying config would fight the user over those. The existing poll here recovers internal panels on docked laptops and never runs on a desktop, which is where this happens. Reported in #6668. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
115 lines
3.6 KiB
Bash
Executable File
115 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# omarchy:summary=Watch Hyprland monitor events and recover monitor toggles when a monitor is removed
|
|
|
|
SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock"
|
|
LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-clamshell.lock"
|
|
MODELESS_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-monitor-modeless.lock"
|
|
|
|
sync_clamshell() {
|
|
(
|
|
flock -n 9 || exit 0
|
|
omarchy-hyprland-monitor-clamshell
|
|
) 9>"$LOCK"
|
|
}
|
|
|
|
sync_clamshell_after_monitor_change() {
|
|
sync_clamshell
|
|
|
|
(
|
|
for delay in 1 3 7; do
|
|
sleep "$delay"
|
|
sync_clamshell
|
|
done
|
|
) &
|
|
}
|
|
|
|
# Powering the monitor on fires no DRM hotplug, and forcing a re-probe needs
|
|
# root, so only a reload re-reads the EDID and only a reload reveals whether it
|
|
# worked. Back off: the machine can sit like this all night. The lock keeps one
|
|
# loop running across the events that call this; the wait is for an event landing
|
|
# in the moment one is exiting, which would otherwise be the last one to come.
|
|
recover_modeless() {
|
|
(
|
|
flock -w 1 9 || exit 0
|
|
|
|
local delay=3 state reloaded unanswered=0
|
|
|
|
while true; do
|
|
omarchy-hyprland-monitor-modeless
|
|
state=$?
|
|
|
|
# A monitor reporting a mode is recovered. One the compositor cannot speak
|
|
# for is not an answer, and nothing else will ask again -- but a compositor
|
|
# that stays silent has gone, taking the session and this loop's reason
|
|
# with it.
|
|
(( state == 1 )) && break
|
|
(( state == 2 )) && (( ++unanswered > 20 )) && break
|
|
(( state == 0 )) && unanswered=0
|
|
|
|
reloaded=0
|
|
# Reloading into half-replaced package config is what the guard prevents.
|
|
if (( state == 0 )) && ! omarchy-hyprland-reload-guard paused; then
|
|
hyprctl reload >/dev/null 2>&1 || true
|
|
reloaded=1
|
|
fi
|
|
|
|
sleep "$delay"
|
|
(( reloaded )) && (( delay = delay * 2 > 60 ? 60 : delay * 2 ))
|
|
done
|
|
) 9>"$MODELESS_LOCK" &
|
|
}
|
|
|
|
poll_clamshell_state() {
|
|
while true; do
|
|
sleep 2
|
|
sync_clamshell
|
|
done
|
|
}
|
|
|
|
# The internal panel is only ever disabled while a laptop is docked (lid shut
|
|
# with an external monitor active), so the reconciliation poll only has anything
|
|
# to reconcile in that window. Run it while docked, stop it when undocked, and
|
|
# never on a machine without a lid. Lid open/close itself is handled by the
|
|
# Hyprland "switch:*:Lid Switch" binds; this poll is the recovery backstop for
|
|
# drift those binds can miss (e.g. across suspend/resume).
|
|
poll_pid=""
|
|
sync_poll_state() {
|
|
if omarchy-hw-laptop && omarchy-hyprland-monitor-external-active; then
|
|
if [[ -z $poll_pid ]] || ! kill -0 "$poll_pid" 2>/dev/null; then
|
|
poll_clamshell_state &
|
|
poll_pid=$!
|
|
fi
|
|
elif [[ -n $poll_pid ]]; then
|
|
kill "$poll_pid" 2>/dev/null
|
|
poll_pid=""
|
|
fi
|
|
}
|
|
|
|
sync_clamshell_after_monitor_change
|
|
sync_poll_state
|
|
recover_modeless
|
|
|
|
# Process substitution (not a pipe) keeps this loop in the main shell, so
|
|
# sync_poll_state can start and stop the background poll as monitors come and go.
|
|
while read -r event; do
|
|
case "$event" in
|
|
monitoradded\>\>*|monitoraddedv2\>\>*)
|
|
sync_clamshell_after_monitor_change
|
|
sync_poll_state
|
|
recover_modeless
|
|
;;
|
|
monitorremoved\>\>*|monitorremovedv2\>\>*)
|
|
sync_clamshell_after_monitor_change
|
|
sync_poll_state
|
|
recover_modeless
|
|
;;
|
|
# A reload while the monitor is unpowered leaves it at 0x0 with no hotplug
|
|
# to notice, same as at boot. Our own recovery reload lands here too, and is
|
|
# a no-op while its loop still holds the pid.
|
|
configreloaded\>\>*)
|
|
recover_modeless
|
|
;;
|
|
esac
|
|
done < <(socat -U - "UNIX-CONNECT:$SOCKET")
|