Daemon Behavior
Why a daemon
You save profiles with the TUI. But who applies them when you’re not looking?
That’s what hyprmoncfgd does. It runs in the background, watches for monitor hotplug and lid events, and applies the best matching profile automatically. Plug in a monitor, close the lid, undock your laptop, connect to a projector – the daemon handles it.
On Omarchy releases that start omarchy-hyprland-monitor-watch, the daemon takes exclusive ownership of monitor state. It stops the watcher’s exact transient user scope, keeps later copies suppressed, and starts the watcher again when hyprmoncfgd exits while Hyprland is still running. This prevents Omarchy’s clamshell reconciliation from overwriting an active hyprmoncfg profile.
Static Configs On Omarchy
A generated monitor config cannot provide runtime process ownership by itself. If you use a generated monitors.lua without running hyprmoncfgd, Omarchy’s monitor watcher remains active and may reconcile the laptop panel using Omarchy’s own settings. Disable that watcher separately or run the daemon.
This is especially useful if you move between setups regularly. A conference projector, a coworking space monitor, your desk at home – each one has different resolution, position, and scale requirements. Save a profile once, and the daemon takes care of it from then on.
Setup
AUR, Fedora COPR, Nixpkgs, and Gentoo GURU:
systemctl --user enable --now hyprmoncfgd
Void Linux with Blackhole-vl:
exec-once = hyprmoncfgd
Manual install:
mkdir -p ~/.config/systemd/user
cp packaging/systemd/hyprmoncfgd.local.service ~/.config/systemd/user/hyprmoncfgd.service
systemctl --user daemon-reload
systemctl --user enable --now hyprmoncfgd
That’s it. The daemon is running. The rest of this page explains how it decides which profile to apply and how to troubleshoot it.
How it works
When the daemon detects a monitor or lid-state change, it runs through these steps:
- Read the current monitor set from Hyprland
- Score every saved profile against the connected hardware (see Profile matching below for how scoring works)
- Pick the highest-scoring profile
- If the lid is closed and an external monitor is already usable and will remain enabled, force internal laptop-panel outputs off for this apply
- Write the active generated monitor file atomically (temp file + rename, so a crash mid-write can’t corrupt your config)
- Tell Hyprland to reload
- Re-read monitor state and verify the result matches what was intended
If the winning profile is the same one that’s already applied, the daemon skips re-applying it. If it does reapply, for example after the displays wake, and the generated file would come out identical while the displays already match, it skips steps 5 and 6. You won’t see unnecessary reloads.
An output absent from a non-strict profile is added to its right edge, vertically centered against the adjacent independent display using logical dimensions. For simultaneous arrivals, each new display becomes the next neighbor. The backend chooses the greatest advertised pixel area, then the highest refresh at that resolution, with VRR off and 8-bit sRGB. Existing output settings and an enabled workspace plan are preserved; the saved profile is not overwritten. Physical-size scale recommendations and explicit failed-wake/cold-start rescue remain separate follow-up work.
When no saved profile matches, usable live displays keep their mode, position,
rotation, scale and color settings. VRR and bit depth use the same values as a
newly added display (VRR off, 8-bit), because Hyprland reports whether VRR is
active and the current buffer format rather than the configured settings.
Existing workspace assignments are imported. Only modeless or disabled displays
need an automatic extended layout; an overlapping initial arrangement is rebuilt
rather than preserved. Hyprland’s synthetic FALLBACK output is never part of it.
If verification rejects a layout and rollback restores the previous configuration
and leaves every intended display usable, the daemon pauses automatic attempts for that same request and restored
state. This prevents repeatedly switching between a rejected mode and a working
one. Editing the profile, changing the display state or workspace rules, or
explicitly running hyprmoncfg manage or turning automatic profile selection on permits another attempt. Confirmed previews
also clear the pause. VRR activity and scanout buffer format changes do not clear
it. The pause is held for the running daemon session and logged as
automatic retry paused; it does not rewrite saved profiles. A rejection whose
rollback failed keeps retrying. A compositor that stops answering after it has
already reported a mismatch still counts as a rejection, not a busy compositor.
Attempts to restore saved settings from a recovery fallback are exempt from
this pause: they return to the fallback and schedule another attempt.
Other failed automatic applies retry independently of monitor-change events, starting after 2 seconds with capped exponential backoff up to 30 seconds. Transient busy reads and writer contention retry after the normal debounce interval. Successful application stops retries. Preview ownership defers retries; suspend, intentional display sleep and unmanaged mode stop them. Resume or a later wake/topology event restarts reconciliation. Verification reports all failed outputs, not just the first. Monitor/workspace discovery queries have a 750ms timeout. This does not prove physical projector readiness.
Resuming or opening the lid is a request for light. When displays still report DPMS off right afterwards, the daemon treats it as a wake that did not take and repeats it up to three times before treating the displays as asleep by choice. It stops early once every enabled display in the layout is awake, so a lit laptop panel doesn’t end the wake while an external display is still off. After three retries it stops, and the awake displays stay usable. If the built-in panel is off while the lid is open (it was switched off with the lid shut and the external stayed asleep), the daemon applies the profile anyway, so the panel comes back without waiting for the external. The wake command uses the Lua or legacy syntax Hyprland last reported, even when Hyprland is too busy to answer right after resume.
Workspaces when a display comes back
When a display turns off, Hyprland moves its workspaces to the others. When it comes back, Hyprland would show its default workspace. If the profile plans workspaces, the daemon shows the workspace that display last showed instead, for example workspace 2 rather than 1, after applying the layout. Focus and the cursor go back to the display and position they were on.
It remembers the last workspace of each display by hardware identity, while the daemon runs. A workspace that another display is showing, or that the profile assigns to another display, stays where it is. Displays with identical descriptions can’t be told apart, so they keep Hyprland’s choice. With the workspace planner set to Off, the daemon doesn’t touch workspaces at all.
Displays that won’t stay on
Some displays can’t wake straight into a demanding mode such as 4K at 144 Hz. They drop off the link a second or two after connecting. When they come back, they show “No signal” and go back to sleep. Woken at about 60 Hz and switched to the saved mode once awake, they work.
So when a display drops within 4 seconds of connecting at its saved settings, the daemon remembers that it needs a gentle wake:
- While the display is disconnected, its rule in the generated config wakes it at the refresh rate closest to 60 Hz, at the same resolution, with VRR off. This also happens immediately after the last display disconnects, so the first display can reconnect using its wake rule without waiting for recovery.
- Once it has stayed connected with a usable mode for 10 seconds, the daemon switches it to its saved settings.
Displays that come up cleanly never drop, so they keep coming up in one step.
If that switch makes the display drop again within 8 seconds, the daemon steps its settings down one at a time to obtain a working fallback:
- VRR off.
- The next lower refresh rate at the same resolution, for example 144 Hz to 120 Hz.
- The refresh rate closest to 60 Hz.
The same steps apply to a display that comes back from sleep enabled but without a mode twice in a row.
After a display at reduced settings reports a usable, awake mode for 10 seconds, the daemon tries its saved refresh rate and VRR again. If that attempt is rejected or the display drops right after it, recovery returns to the fallback and waits twice as long before the next attempt (20 seconds, then 40, up to 5 minutes), because each attempt blanks a working display. The wait resets once the saved settings hold, or when the daemon restarts. This also applies to fallbacks caused by a display dropping after a switch. Each attempt finishes before another begins, and recovery stops once the requested settings work. Sleep, suspend, removal, unmanaged mode and previews defer recovery.
Resolution, scale and position never change, so other displays and windows stay
where they are. The saved profile never changes either. hyprmoncfg status
names each stepped-down display and how it runs now, and the daemon log says
why. The gentle wake and the steps are remembered per display in
~/.config/hyprmoncfg/display-fallbacks.json, so a restart doesn’t repeat the
struggle.
Confirming any layout clears the steps, so the next switch tries the saved settings again. It keeps the gentle wake, because that still ends at the saved settings. Editing the saved mode or VRR of a display also clears its steps. To forget a gentle wake, delete the file and restart the daemon.
Displays with the same description can’t be told apart here, so they are never changed. The daemon also can’t see a display that stays connected but shows nothing, because Hyprland and the kernel report that the same as a working one. The gentle wake exists because the drop before that silence is visible.
Displays stuck without a mode
aquamarine 0.15.0, the display library under Hyprland 0.56, never releases the
kernel’s display pipe (CRTC) of a monitor that disconnects. When a monitor
comes back and is handed that pipe, the kernel rejects every mode: Hyprland
lists it at 0x0 and it stays black or frozen. It typically happens when every
monitor was turned off and one comes back alone, or when two monitors come back
close together and each is handed the other’s pipe. aquamarine 0.15.1 fixes it
(hyprwm/aquamarine#386,
#428).
The daemon recognises it: an awake monitor stays at 0x0 although it lists
modes, while /sys/class/drm/card*-* shows a disconnected connector as still
enabled. It logs the cause, does not step the monitor’s settings down, and
switches to a free console and back through
systemd-logind, then reloads Hyprland. The console switch makes Hyprland reset
the kernel’s display state. You see a text console for about two seconds.
If the monitor is still stuck, the next switch waits 30 seconds, then twice as
long each time, up to 10 minutes.
When two monitors come back close together and each is handed the other’s
pipe, both stay at 0x0 while both are connected. A console switch doesn’t
clear that: Hyprland keeps the swapped pipes, and the screens go on showing the
console’s last image. The daemon logs it instead, and doesn’t step the
monitors’ settings down. Turn one of the two monitors off, wait 5 seconds, and
turn it on again: with it off, the daemon recovers the other one, and the one
you turn back on gets a free pipe.
logind allows this without root for the active local session. If it refuses,
the log says so; switch by hand with sudo chvt 2, then sudo chvt 1 (use the
console your session runs on, loginctl show-session $XDG_SESSION_ID -p VTNr),
or update aquamarine.
Profile matching
Profiles are matched by hardware identity (make, model, serial) – not connector name. This means your layout survives when monitors swap between DP-1 and DP-2 across reboots. Each profile is scored against the currently connected monitors:
| Condition | Points |
|---|---|
| Monitor enabled in profile and connected | +100 |
| Monitor disabled in profile but connected | +50 |
| Connected monitor not in the profile | −20 |
| Monitor enabled in profile but not connected | −30 |
| Monitor disabled in profile and not connected | −10 |
With the lid closed and an external display connected, the built-in panel scores the other way round, because the closed-lid policy turns it off whatever the profile says:
| Condition, lid closed | Points |
|---|---|
| Built-in panel kept off in the profile | +100 |
| Built-in panel turned on in the profile | +50 |
So a profile you saved for working with the lid closed wins over an otherwise identical profile that turns the panel on. Without an external display, the panel is scored as usual.
For automatic switching, a profile must enable at least one connected display and have a positive score. A partial match can provide the base for a temporary extended layout: unfamiliar displays are added unless the profile explicitly sets disable_unknown_outputs: true. Deliberately disabled known displays remain off. Missing saved displays remain allowed, so undocking can still restore the laptop layout.
Among eligible profiles, the highest score wins. Ties break alphabetically by profile name. A profile that mentions a monitor which is not plugged in pays for it either way, so the profile that describes exactly the connected displays beats a larger profile that happens to include them. The profiles tab shows every score, along with this breakdown for the selected profile.
Another physical unit of the same model may have a different serial number and therefore a different identity. Extension treats it as a new display without copying another unit’s calibration.
On laptops, the daemon also reads lid state. UPower is optional, but recommended: with UPower available, lid changes arrive as D-Bus events and the daemon can react immediately. Without UPower, the daemon falls back to polling /proc/acpi/button/lid/*/state at --lid-poll-interval, which defaults to 1s and is not available on every system. If neither source exists, lid-aware switching is disabled and monitor hotplug still works.
Lid state is not a separate profile type. Save the profile for the monitor setup you actually have attached. When the lid is closed and an external output has an awake, nonzero mode and remains enabled in the target profile, hyprmoncfg treats internal laptop-panel outputs like eDP-1, LVDS-1, or DSI-1 as forced off for that apply. Modeless, disabled, sleeping, and synthetic fallback outputs do not qualify. Saved profiles are not rewritten. If workspace rules target the forced-off internal panel, those workspaces are moved to the first enabled external output in the selected profile.
The built-in panel stays off only while an external display actually shows a picture. When every external the profile keeps on is modeless, asleep, or missing, and a connected built-in panel would be off, whether because the lid is closed or because the profile turns it off, the daemon turns the panel on, to the right of the other displays. It logs no external display shows a picture when it does. If the reload turns the panel on without a mode, as i915 does with the lid closed, the daemon sets the panel’s mode directly, and the apply only counts once the panel has one. Some drivers only give a modeless external its mode back once another output is lit, so without this a clamshell setup could stay dark until the lid is opened. Once an external has a mode, the next pass applies the profile as saved and the panel goes off again. While nothing is lit, a missing mode also doesn’t count toward stepping the external’s settings down.
Remove Throwaway Profiles
The daemon does not know which profiles are “real” and which were temporary experiments. Any profile with a positive hardware match can be eligible. An old throwaway profile with a high enough score can win over the one you actually want.
If you want reliable auto-switching:
- Save profiles for every real monitor setup you want the daemon to handle
- Keep one profile per setup – don’t accumulate near-duplicates
- Delete experimental profiles when you’re done experimenting
- If two profiles tie, the one whose name comes first alphabetically wins
- When auto-switching picks the wrong profile, start by listing the files in
~/.config/hyprmoncfg/profiles/– a forgotten profile is almost always the answer
Run manually
For testing or one-off use:
hyprmoncfgd
Useful flags
hyprmoncfgd --hotplug-debounce 1s # wait longer before applying after a plug event (default 300ms)
hyprmoncfgd --debounce 1500ms # wait longer on every other trigger, and on hotplug too unless set above
hyprmoncfgd --wake-settle 2s # quiet period after displays wake
hyprmoncfgd --poll-interval 5s # how often to run fallback monitor checks
hyprmoncfgd --lid-poll-interval 1s # how often to run fallback lid checks
hyprmoncfgd --profile desk # always apply this specific profile
hyprmoncfgd --quiet # suppress log output
Forcing a specific profile
Use --profile <name> to bypass automatic matching entirely. The daemon applies this one profile every time, regardless of what’s connected. This is useful when you know exactly which setup you’re on and want to eliminate any chance of a wrong match.
Stop the running daemon first, then start it with the flag:
systemctl --user stop hyprmoncfgd
hyprmoncfgd --profile conference-projector
The daemon owns a per-session writer lock. If a direct editor or another daemon already owns it, a starting daemon logs the contention and waits without changing monitor configuration or repeatedly exiting under systemd. Close the direct editor to let the daemon start serving IPC. Stopping the service also cancels the wait. Direct writers still use the lock rather than racing another writer.
Starting with monitor management off preserves the handoff: it does not restore
the generated include. Read-only dialect discovery still runs, so a later wake
uses the correct Lua or legacy syntax even if Hyprland is temporarily too busy
to report its version. Use hyprmoncfg manage to opt back in.
Logs
journalctl --user -u hyprmoncfgd -f
The log shows every step: which profiles were scored, what each one scored, which one won, what generated monitor config was written, and whether verification passed. This is the first place to look when you want to understand why the daemon picked a particular profile.
Raw monitor-event arrival, slow or failed compositor queries, and reconciliation duration are logged separately. Compare these timestamps to distinguish time spent before an event arrives from time spent querying or applying a profile. Query timing messages include the operation and elapsed time without adding monitor serial numbers.
Separate Matching From Applying
If you’re not sure whether the daemon picked the wrong profile or failed to apply the right one, test the profile directly with hyprmoncfg apply <name>. If the layout looks correct, the problem is matching, not applying – check the logs and your profile directory.
Filing A Matching Bug
If you think the daemon selected the wrong profile, open an issue and include all profiles from ~/.config/hyprmoncfg/profiles/, not just the one you expected to win. Matching depends on the full candidate set.