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.

Fixing a black external monitor after suspend on Omarchy

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
  • hyprmoncfg 1.18.4, the one actually running as a user daemon
  • hyprmon-bin 0.0.17, an older profile manager I’d stopped using, but whose generated file was still required
  • HyprDynamicMonitors, 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.

More Articles

Fix the grayed-out volume slider on your USB DAC

Your DAC reports no volume control, so macOS grays out the volume keys. A lightweight virtual audio driver and menu bar app give them back.

From issue to PR: AI workflow with Overvy and Hermes

Overvy and Hermes turn GitHub issues into implemented pull requests without a human touching the code.

Trading from your terminal: setting up Hermes Agent with Montrose

How I connected my Hermes Agent to Montrose for terminal-based portfolio tracking, trade tickets, and automated daily summaries.

Generate Commit Messages with Ollama in Neovim

Generate conventional commit messages from staged diffs using Ollama. Run it locally for privacy and offline, or use cloud models for speed.

Adding Umami analytics to the OpenClaw morning brief

An agent skill that fetches traffic data from Umami, and how it fits into a daily automated briefing.

Setting up Google Calendar sync for OpenClaw

How I set up read-only Google Calendar sync for my personal AI assistant running on a home server VM.

Better Clipboard Handling in Claude Code

A plugin that makes clipboard operations in Claude Code more reliable and natural to use.

Get notified when Claude Code needs your input

Stop constantly checking your terminal. Set up notifications that alert you when Claude Code is ready for your input.

Auto-format generated Code with Claude Code Hooks

How to set up a PostToolUse hook in Claude Code to automatically run prettier after every file edit or write operation.