Fixing a black external monitor after suspend on Omarchy
My external monitor was detected after waking from sleep, but the screen stayed black. The monitor daemon was applying its profile before Hyprland knew which modes the display supported. Two flags fixed it.
If I boot my ThinkPad with the monitor plugged in, everything works. If I close the lid, open it again and plug the monitor back in, the screen stays black. A reboot brings it back. On a laptop that goes in and out of sleep all day, that got old fast.
The monitor was being detected fine. My monitor daemon, hyprmoncfgd, was just too quick. It applied its profile about a second after the monitor showed up, before Hyprland knew which modes the display supported. The mode didn’t resolve, the output was left enabled at 0x0, and the daemon rolled its change back. So Hyprland knew about the monitor and the screen was still black.
The fix turned out to be two command-line flags.
The setup
| Component | Detail |
|---|---|
| Laptop | Lenovo ThinkPad T480s |
| GPU | Intel UHD Graphics 620, i915 |
| OS | Omarchy (Arch Linux) |
| Compositor | Hyprland 0.56.2 |
| Monitor manager | hyprmoncfg 1.18.4 (auto-switching profile daemon) |
| Monitor | ASUS VG27A, 2560x1440, over a USB-C to DisplayPort cable |
It worked after a cold boot and never after sleep or replugging the cable. I should have taken that hint sooner than I did.
First, figure out who’s in charge
Part of why this took so long is that I had config for four different monitor tools lying around:
- Omarchy’s defaults in
~/.config/hypr/monitors.lua hyprmoncfg1.18.4, the one actually running as a user daemonhyprmon-bin0.0.17, an older profile manager I’d stopped using, but whose generated file was stillrequiredHyprDynamicMonitors, installed and disabled, with its config still there
If you’ve tried a few monitor tools on Omarchy, yours might look similar. Check which one is actually running before you edit anything:
$ systemctl --user list-units | grep -i monitor
Rule out the Hyprland config
Next I checked that the config itself was valid:
$ hyprctl configerrors
$ hyprctl reload
ok
No errors, and the same config gives me a working layout at boot. The rules were fine, so the problem had to be when they got applied.
Read the daemon’s log
I plugged the monitor in after a resume and looked at the daemon’s journal:
$ journalctl --user -u hyprmoncfgd -b
hyprmoncfgd: monitoradded:1,DP-2,ASUSTek COMPUTER INC VG27A M1LMQS003124
hyprmoncfgd: best profile "bed" score=150 lid=open
hyprmoncfgd: apply failed: DP-2 mode mismatch: wanted 2560x1440, got 0x0
Look at the first line. Hyprland saw the monitor and read its make, model and serial number, so detection was working. The failure comes later, when the daemon applies the profile.
Why only the mode failed
hyprmoncfg writes a Hyprland monitor rule and reloads. Then it compares the result with the profile it wanted, and reverts if they don’t match. The checks are visible in the binary’s strings:
%s mode mismatch: wanted %dx%d, got %dx%d
%s scale mismatch: wanted %s, got %s
%s position mismatch: wanted %dx%d, got %dx%d
Scale and position passed. Only the mode check failed, and that makes sense once you think about how each one is applied. Position and scale are plain values that take effect straight away. A mode like 2560x1440@59.95 has to be matched against the list of modes the monitor supports, and if Hyprland hasn’t filled in that list yet, the mode gets dropped. The output stays enabled with no mode, which gives you a black screen that Hyprland still counts as connected.
hyprmoncfgd --help lists two flags that control the timing:
--debounce duration Debounce duration before applying profile (default 1.2s)
--wake-settle duration Quiet period after display wake before applying profile (default 2s)
On my machine, the defaults weren’t long enough for the mode list to show up. Boot was never affected because the daemon starts well after the monitor has been detected.
The fix
I added a systemd drop-in at ~/.config/systemd/user/hyprmoncfgd.service.d/zz-settle.conf to give the daemon more time:
[Service]
ExecStart=
ExecStart=/usr/bin/hyprmoncfgd --monitors-conf %h/.config/hypr/hyprmoncfg-monitors.lua --debounce 3s --wake-settle 8s
The empty ExecStart= clears the original command, so the new line replaces it instead of adding a second one. Before you copy this, run systemctl --user cat hyprmoncfgd to see which --monitors-conf path your unit uses, and keep that.
Reload systemd, restart the daemon and check that the new flags stuck:
$ systemctl --user daemon-reload
$ systemctl --user restart hyprmoncfgd
$ systemctl --user show hyprmoncfgd -p ExecStart --value
Eight seconds after wake sounds like a long time. In practice the monitor takes a few seconds to wake up anyway, and I’d much rather wait than reboot.
Did it work?
The journal has the same scenario from before and after the change.
Before, the daemon picked a profile 1.4 seconds after the monitor appeared, and the apply was rejected:
1595.15 monitoradded:1,DP-2,ASUSTek COMPUTER INC VG27A M1LMQS003124
1596.55 best profile "bed" score=150
1602.38 apply failed: DP-2 mode mismatch: wanted 2560x1440, got 0x0
After, following a suspend and resume, it waited 10.2 seconds and the apply went through:
[ 66.79] resumed from sleep; waking displays
[ 74.40] monitoradded:1,DP-1,ASUSTek COMPUTER INC VG27A M1LMQS003124
[ 84.56] best profile "bed" score=150
[ 86.38] applied profile: bed
No mode mismatches since. The only apply failed left in the log is no running Hyprland instances found, which happens once at login when the daemon starts slightly before Hyprland. It’s harmless.
What I’d do differently next time
“Works after boot, breaks after replugging” should have pointed me at timing right away. At boot everything is ready before the daemon starts. On a hotplug or wake, the daemon races Hyprland. I spent too long reading monitor rules that were never wrong.
I’d also read the tool’s own output more carefully, and earlier. “mode mismatch: wanted 2560x1440, got 0x0” pretty much says what’s wrong once you notice that scale and position didn’t complain. And the flags that fixed it were right there in --help the whole time.