When using a single external monitor with multiple HDMI sources, things can get awkward quickly. In my setup, the monitor is connected to two machines:
- my Arch Linux workstation running Hyprland/Wayland
- a customer laptop running Windows 11
Switching the monitor input from Arch to the Windows laptop worked perfectly. Windows immediately used the external monitor. But switching back to the Arch workstation often resulted in a black screen or a “no source” message. The monitor LED briefly changed from standby to active, then went back again.
Interestingly, the Arch session was still alive. I could recover the desktop manually by switching virtual terminals:
Alt + F2
Alt + F1
After that, Hyprland displayed the desktop again.
This blog post documents the investigation, the failed approaches, and the final working solution: listening for DRM hotplug events and performing a controlled VT refresh using chvt.
The Problem
The symptoms were:
Switch monitor input from Arch Linux to Windows laptop: works
Switch monitor input back to Arch Linux: monitor stays dark / no source
Press Alt+F2, then Alt+F1: Arch desktop appears again
The Arch workstation was not frozen. Hyprland was still running. The monitor was also still known to Hyprland.
Running:
hyprctl monitors
showed that Hyprland believed everything was fine:
Monitor HDMI-A-2 (ID 0):
3840x2160@60.00000 at 0x0
focused: yes
dpmsStatus: 1
disabled: false
active workspace: 1 (1)
So from Hyprland’s perspective:
- the monitor was active
- DPMS was on
- the workspace was assigned
- the monitor was focused
- the output was not disabled
Still, the physical monitor showed no usable image until a VT switch happened.
What Did Not Work
The first attempt was a simple systemd timer that periodically ran a monitor recovery script.
The early version tried both wlr-randr and xrandr:
wlr-randr --output HDMI-A-1 --on
xrandr --auto
This was wrong for several reasons.
First, xrandr is for X11. Under Hyprland/Wayland, it is not the correct tool for managing real outputs. It also produced errors like:
Authorization required, but no authorization protocol specified
Can't open display :0
Second, the service was originally configured with:
Restart=always
RestartSec=10
That caused the script to restart continuously, even though it was supposed to be run by a timer.
The correct systemd service type for a one-shot script is:
Type=oneshot
But even after cleaning that up, polling every few seconds was not ideal. Some recovery attempts caused the screen to briefly go dark and come back, which is unacceptable during normal desktop usage.
Correct Monitor Name
Another important discovery was that the monitor was not called HDMI-A-1.
Hyprland reported:
hyprctl monitors
Output:
Monitor HDMI-A-2 (ID 0):
3840x2160@60.00000 at 0x0
description: Iiyama North America PL3288UH
So all scripts had to use:
MONITOR="HDMI-A-2"
instead of:
MONITOR="HDMI-A-1"
Why a Simple Hyprland Recovery Was Not Enough
The next attempt was to use hyprctl:
hyprctl dispatch dpms on HDMI-A-2
hyprctl dispatch focusmonitor HDMI-A-2
hyprctl keyword monitor "HDMI-A-2,3840x2160@60,0x0,1"
hyprctl reload
All of these commands succeeded:
ok
But the monitor still did not recover automatically.
The logs showed:
focused: yes
dpmsStatus: 1
disabled: false
active workspace: 1
That told us something important:
Hyprland already believed the monitor was active and correctly configured.
The problem was not a missing Hyprland monitor rule. It was deeper in the graphics stack, somewhere around the HDMI/DRM/VT state.
Hyprland Events Were Not Enough Either
Hyprland exposes an event socket:
$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock
We listened to it using:
socat -U - UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" \
| grep --line-buffered -E '^(monitoradded|monitoraddedv2|monitorremoved|monitorremovedv2|focusedmon|focusedmonv2|workspace|workspacev2)>>'
But switching monitor inputs did not produce useful Hyprland monitor events.
There were no events like:
monitorremoved>>HDMI-A-2
monitoradded>>HDMI-A-2
So Hyprland itself did not notice the input switch in a useful way.
The Useful Signal: DRM Hotplug Events
The breakthrough came from monitoring DRM events:
udevadm monitor --kernel --property --subsystem-match=drm
When switching the monitor input, the kernel emitted events like:
KERNEL[...] change /devices/.../drm/card1 (drm)
ACTION=change
SUBSYSTEM=drm
HOTPLUG=1
DEVNAME=/dev/dri/card1
DEVTYPE=drm_minor
That was the missing trigger.
Hyprland did not emit a usable monitor event, but the kernel did report a DRM hotplug event.
So the final solution became:
DRM HOTPLUG=1 event
-> wait a few seconds for HDMI sync
-> run a controlled recovery script
-> perform VT switch away and back
-> return to Hyprland
Final Architecture
The final setup consists of:
- a recovery script
- a DRM hotplug watcher script
- a systemd user service
- a restricted sudoers rule for
chvt
The old timer is no longer needed.
Step 1: Disable the Old Timer
If you previously used a timer, disable it:
systemctl --user disable --now revive-monitor.timer
systemctl --user stop revive-monitor.service
systemctl --user reset-failed revive-monitor.service
Verify:
systemctl --user list-timers --all | grep revive
There should be no active timer for the monitor recovery anymore.
Step 2: Create the Recovery Script
Create:
nano ~/Scripts/revive-monitor-fix.sh
Content:
#!/usr/bin/env bash
MONITOR="HDMI-A-2"
LOGTAG="revive-monitor-fix"
# Adjust these if your Hyprland session runs on another VT.
HYPRLAND_VT="1"
TEMP_VT="2"
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
LOCK_FILE="$STATE_DIR/fix.lock"
mkdir -p "$STATE_DIR"
log() {
logger -t "$LOGTAG" "$1"
}
run() {
"$@" >/dev/null 2>&1
}
# Prevent overlapping recovery runs.
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "Recovery already running - skipping"
exit 0
fi
log "Recovery started for $MONITOR"
# Give the HDMI handshake some time.
sleep 5
# Make sure Hyprland still sees and enables the output.
run hyprctl dispatch dpms on "$MONITOR"
run hyprctl dispatch focusmonitor "$MONITOR"
sleep 1
log "VT refresh: $TEMP_VT -> $HYPRLAND_VT"
if sudo -n /usr/bin/chvt "$TEMP_VT"; then
sleep 1
sudo -n /usr/bin/chvt "$HYPRLAND_VT"
sleep 2
else
log "Error: sudo chvt $TEMP_VT failed"
exit 1
fi
# After returning to Hyprland, make sure the monitor is active again.
run hyprctl dispatch dpms on "$MONITOR"
run hyprctl dispatch focusmonitor "$MONITOR"
log "Recovery completed for $MONITOR"
Make it executable:
chmod +x ~/Scripts/revive-monitor-fix.sh
Finding the Correct VT
To check which VT your Hyprland session uses:
loginctl show-session "$XDG_SESSION_ID" -p VTNr -p Type -p State
Example:
VTNr=1
Type=wayland
State=active
If your Hyprland session runs on VT 1, keep:
HYPRLAND_VT="1"
TEMP_VT="2"
If it runs on VT 2, use something like:
HYPRLAND_VT="2"
TEMP_VT="3"
Step 3: Allow chvt Safely via sudoers
The recovery script needs to switch virtual terminals. That requires root privileges.
Instead of giving broad passwordless sudo access, allow only /usr/bin/chvt.
Create a dedicated sudoers file:
sudo visudo -f /etc/sudoers.d/revive-monitor-chvt
Add:
johannes ALL=(root) NOPASSWD: /usr/bin/chvt
Replace johannes with your username if needed.
Test it:
sudo -n /usr/bin/chvt 2
sleep 1
sudo -n /usr/bin/chvt 1
The -n flag is important. It makes sudo fail immediately if a password would be required. This is exactly what we want for a non-interactive systemd user service.
This sudoers rule is intentionally narrow:
Only this user may run only /usr/bin/chvt as root without a password.
It does not grant general passwordless sudo access.
Step 4: Create the DRM Hotplug Watcher
Create:
nano ~/Scripts/revive-monitor-drm-watch.sh
Content:
#!/usr/bin/env bash
LOGTAG="revive-monitor-drm-watch"
FIX_SCRIPT="$HOME/Scripts/revive-monitor-fix.sh"
STATE_DIR="${XDG_RUNTIME_DIR:-/tmp}/revive-monitor"
LAST_RUN_FILE="$STATE_DIR/last-drm-hotplug-run"
DEBOUNCE_SECONDS=30
mkdir -p "$STATE_DIR"
log() {
logger -t "$LOGTAG" "$1"
}
run_recovery() {
local now
local last
local diff
now="$(date +%s)"
last="0"
if [ -f "$LAST_RUN_FILE" ]; then
last="$(cat "$LAST_RUN_FILE" 2>/dev/null || echo 0)"
fi
diff=$((now - last))
if [ "$diff" -lt "$DEBOUNCE_SECONDS" ]; then
log "DRM hotplug ignored because of debounce (${diff}s)"
return
fi
echo "$now" > "$LAST_RUN_FILE"
log "DRM hotplug detected - starting recovery"
"$FIX_SCRIPT"
log "Recovery after DRM hotplug finished"
}
if ! command -v udevadm >/dev/null 2>&1; then
log "udevadm not found"
exit 1
fi
if [ ! -x "$FIX_SCRIPT" ]; then
log "Fix script is not executable: $FIX_SCRIPT"
exit 1
fi
log "Listening for DRM hotplug events"
stdbuf -oL udevadm monitor --kernel --property --subsystem-match=drm | while IFS= read -r line; do
case "$line" in
HOTPLUG=1)
run_recovery
;;
esac
done
Make it executable:
chmod +x ~/Scripts/revive-monitor-drm-watch.sh
The debounce is important because one physical input switch can produce multiple DRM events.
Start with:
DEBOUNCE_SECONDS=30
If you still see duplicate recoveries, increase it to:
DEBOUNCE_SECONDS=60
Step 5: Create the systemd User Service
Create:
nano ~/.config/systemd/user/revive-monitor-drm-watch.service
Content:
[Unit]
Description=Watch DRM hotplug events and revive HDMI monitor
After=graphical-session.target
[Service]
Type=simple
ExecStart=%h/Scripts/revive-monitor-drm-watch.sh
Restart=always
RestartSec=2
Environment=XDG_RUNTIME_DIR=%t
[Install]
WantedBy=default.target
Enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now revive-monitor-drm-watch.service
Check status:
systemctl --user status revive-monitor-drm-watch.service
Check whether it is enabled:
systemctl --user is-enabled revive-monitor-drm-watch.service
Expected result:
enabled
Step 6: Test the Setup
Follow this test flow:
- Make sure Arch/Hyprland is visible on the monitor.
- Switch the monitor input to the Windows laptop.
- Wait a few seconds.
- Switch the monitor input back to Arch.
- Wait for the DRM watcher and recovery script to run.
Watch logs:
journalctl --user -u revive-monitor-drm-watch.service -f
Or:
journalctl --user -t revive-monitor-fix -f
A successful run looks similar to this:
DRM hotplug detected - starting recovery
Recovery started for HDMI-A-2
VT refresh: 2 -> 1
Recovery completed for HDMI-A-2
Recovery after DRM hotplug finished
You may also see sudo log entries like:
COMMAND=/usr/bin/chvt 2
COMMAND=/usr/bin/chvt 1
That is expected.
Why This Works
The important finding was that Hyprland was not actually “missing” the monitor.
Hyprland reported:
focused: yes
dpmsStatus: 1
disabled: false
active workspace: 1
So the compositor believed the output was fine.
The physical monitor, however, did not show the signal correctly after switching HDMI inputs. A manual VT switch fixed the problem, which indicated that the graphics stack needed a deeper refresh than a normal Hyprland workspace or DPMS command.
The final solution therefore does not poll constantly and does not toggle the display every few seconds. Instead, it waits for a real kernel DRM hotplug event:
HOTPLUG=1
Then it runs a controlled recovery:
wait for HDMI sync
enable/focus monitor in Hyprland
switch VT away
switch VT back to Hyprland
enable/focus monitor again
This reproduces the manual workaround automatically.
Security Considerations
The only privileged operation is chvt.
The sudoers rule is intentionally limited:
johannes ALL=(root) NOPASSWD: /usr/bin/chvt
This avoids giving the user broad passwordless sudo access.
The script also uses:
sudo -n /usr/bin/chvt
so it never waits for a password prompt inside systemd.
Additionally, the recovery script uses a lock file:
flock -n
to avoid overlapping recovery runs.
The watcher also uses debounce logic to avoid running the recovery repeatedly for a burst of DRM hotplug events.
Summary
The issue was caused by switching a physical monitor input between an Arch Linux Hyprland workstation and a Windows laptop. After switching back to Arch, the monitor sometimes stayed black or reported no source, even though Hyprland still believed the monitor was active.
Several approaches did not fully solve the problem:
xrandrwas not suitable under Wayland/Hyprland.wlr-randrandhyprctl dpms onwere not enough.- Hyprland monitor events did not fire reliably for this input switch.
- Polling with a systemd timer caused unwanted display interruptions.
The working solution was:
- listen for kernel DRM hotplug events using
udevadm monitor - trigger recovery only on
HOTPLUG=1 - wait for HDMI sync
- perform a controlled VT switch using
chvt - return to the Hyprland VT
- restrict passwordless sudo to
/usr/bin/chvtonly
The result is a reliable automatic recovery without periodic screen flickering and without broad sudo permissions.
Views: 0
