
An event-driven service that automatically manages Hyprland monitor configurations based on connected displays and power state.
- HyprDynamicMonitors
- Documentation
- Features
- Design Philosophy
- Installation
- Usage
- Minimal Example
- Examples
- Runtime requirements
- Configuration
- Tests
- Running with systemd
- Development
- Alternative software
- Event-driven architecture responding to monitor and power state changes in real-time
- Profile-based configuration with different settings for different monitor setups
- Template support for dynamic configuration generation
- Hot reloading: automatically detects and applies configuration changes without restart by watching config files (optional)
- Configurable UPower queries for custom power management systems
- Desktop notifications for configuration changes (optional)
HyprDynamicMonitors follows a fail-fast architecture designed for reliability and simplicity.
The service intentionally fails quickly on critical issues rather than attempting complex recovery. This design expects the service to run under systemd or a wrapper script that provides automatic restarts. Since configuration is applied on startup, restarts ensure the service remains operational even after encountering errors.
For configuration changes, the service provides automatic hot reloading by watching configuration files. When hot reloading encounters issues, it gracefully falls back to the fail-fast behavior, prioritizing reliability over attempting risky recovery scenarios.
The service leverages Hyprland's native abstractions rather than working directly with Wayland protocols. It detects the desired configuration based on current monitor state and power supply, then either:
- Generates a templated Hyprland config file at the specified destination
- Or creates a symlink to a user-provided static configuration file
Hyprland automatically detects and applies these configuration changes (granted it's not explicitly turned off, if so you have to use
the callbacks to hyprctl reload
), ensuring seamless integration with the compositor's built-in configuration system.
Download the latest binary from GitHub releases:
# optionally override the destination directory, defaults to ~/.local/bin/
export DESTDIR="$HOME/.bin"
curl -o- https://raw.githubusercontent.com/fiffeek/hyprdynamicmonitors/refs/heads/main/scripts/install.sh | bash
For Arch Linux users, install from the AUR:
# Using your preferred AUR helper (replace 'aurHelper' with your choice)
aurHelper="yay" # or paru, trizen, etc.
$aurHelper -S hyprdynamicmonitors-bin
# Or using makepkg:
git clone https://aur.archlinux.org/hyprdynamicmonitors-bin.git
cd hyprdynamicmonitors-bin
makepkg -si
Requires asdf to manage the Go toolchain:
# Build the binary (output goes to ./dest/)
make
# Install to custom location
make DESTDIR=$HOME/binaries install
# Uninstall from custom location
make DESTDIR=$HOME/binaries uninstall
# Install system-wide (may require sudo)
sudo make DESTDIR=/usr/bin install
Usage: hyprdynamicmonitors [options] [command]
Commands:
run Run the service (default)
validate Validate configuration file and exit
Options:
-config string
Path to configuration file (default "$HOME/.config/hyprdynamicmonitors/config.toml")
-connect-to-session-bus
Connect to session bus instead of system bus for power events: https://wiki.archlinux.org/title/D-Bus. You can switch as long as you expose power line events in your user session bus.
-debug
Enable debug logging
-disable-auto-hot-reload
Disable automatic hot reload (no file watchers)
-disable-power-events
Disable power events (dbus)
-dry-run
Show what would be done without making changes
-enable-json-logs-format
Enable structured logging
-run-once
Run once and exit immediately
-verbose
Enable verbose logging
-version
Show version information
Validate configuration:
# Validate default config file
hyprdynamicmonitors validate
# Validate specific config file
hyprdynamicmonitors -config /path/to/config.toml validate
# Validate with debug output
hyprdynamicmonitors -debug validate
This example sets up basic laptop-only monitor configuration. First, check your display name with hyprctl monitors
.
Configuration file (~/.config/hyprdynamicmonitors/config.toml
):
[general]
destination = "$HOME/.config/hypr/monitors.conf"
[power_events]
[power_events.dbus_query_object]
path = "/org/freedesktop/UPower/devices/line_power_ACAD"
[[power_events.dbus_signal_match_rules]]
object_path = "/org/freedesktop/UPower/devices/line_power_ACAD"
[profiles.laptop_only]
config_file = "hyprconfigs/laptop.conf"
config_file_type = "static"
[[profiles.laptop_only.conditions.required_monitors]]
name = "eDP-1" # Replace with your display name from hyprctl monitors
Monitor configuration (~/.config/hyprdynamicmonitors/hyprconfigs/laptop.conf
):
monitor=eDP-1,[email protected],0x0,2.0,vrr,1
Run the service using systemd (recommended - see Running with systemd) or add to Hyprland config:
exec-once = hyprdynamicmonitors
Ensure you source the linked destination
config file (in ~/.config/hypr/hyprland.conf
):
source = ~/.config/hypr/monitors.conf
How it works: When only the eDP-1
monitor is detected, the service symlinks hyprconfigs/laptop.conf
to $HOME/.config/hypr/monitors.conf
, and Hyprland automatically applies the new configuration.
See examples/
directory for complete configuration
examples including basic setups and comprehensive configurations with all features.
Most notably, examples/full/config.toml
contains all available configuration options reference.
An example for disabling a monitor depending on the
detected setup is in examples/disable-monitors
.
- Hyprland with IPC support
- UPower (optional, for power state monitoring)
- Read-only access to system D-Bus (optional for power state monitoring; should already be your default)
- Write access to system D-Bus for notifications (optional; should already be your default)
Profiles match monitors based on their properties. You can match by:
- Name: The monitor's connector name (e.g.,
eDP-1
,DP-1
) - Description: The monitor's model/manufacturer string
You also optionally set (required for templating):
- Tags: Custom labels you assign to monitors for easier reference
[[profiles.laptop_only.conditions.required_monitors]]
name = "eDP-1" # Match by connector name
[[profiles.external_4k.conditions.required_monitors]]
description = "Dell U2720Q" # Match by monitor model
[[profiles.dual_setup.conditions.required_monitors]]
name = "eDP-1"
monitor_tag = "laptop" # Assign a tag for template use
Use hyprctl monitors
to see available monitors and their properties.
To understand scoring and profile matching more see examples/scoring
.
- Static: Creates symlinks to existing configuration files
- Template: Processes Go templates with dynamic monitor and power state data
.PowerState // "AC" or "BAT"
.Monitors // Array of connected monitors
.MonitorsByTag // Map of tagged monitors (monitor_tag -> monitor)
You can define custom static values that are available in templates. These can be defined globally or per-profile:
Global static values (available in all templates):
[static_template_values]
default_vrr = "1"
default_res = "2880x1920"
refresh_rate_high = "120.00000"
refresh_rate_low = "60.00000"
Per-profile static values (override global values):
[profiles.laptop_only.static_template_values]
default_vrr = "0" # Override global value
battery_scaling = "1.5" # Profile-specific value
Template usage:
# Use static values in templates
monitor=eDP-1,{{.default_res}}@{{if isOnAC}}{{.refresh_rate_high}}{{else}}{{.refresh_rate_low}}{{end}},0x0,1,vrr,{{.default_vrr}}
To understand template variables more see examples/template-variables
.
isOnBattery // Returns true if on battery power
isOnAC // Returns true if on AC power
powerState // Returns current power state string
When no regular profile matches the current monitor setup and power state, you can define a fallback profile that will be used as a last resort. This is particularly useful for handling unexpected monitor configurations or providing a safe default configuration.
# Regular profiles with specific conditions
[profiles.laptop_only]
config_file = "hyprconfigs/laptop.conf"
[[profiles.laptop_only.conditions.required_monitors]]
name = "eDP-1"
[profiles.dual_monitor]
config_file = "hyprconfigs/dual.conf"
[[profiles.dual_monitor.conditions.required_monitors]]
name = "eDP-1"
[[profiles.dual_monitor.conditions.required_monitors]]
description = "External Monitor"
# Fallback profile - used when no other profile matches
[fallback_profile]
config_file = "hyprconfigs/fallback.conf"
config_file_type = "static"
The fallback profile in hyprconfigs/fallback.conf
might contain a safe default configuration:
# Generic fallback: configure all connected monitors with preferred settings
monitor=,preferred,auto,1
Key characteristics of fallback profiles:
- Cannot define conditions (since they're used when no conditions match)
- Supports both static and template configuration types
- Only used when no regular profile matches the current setup
- Regular matching profiles always take precedence over the fallback
To understand fallback profiles more see examples/fallback
.
HyprDynamicMonitors supports custom user commands that are executed before and after profile configuration changes. These commands can be defined globally or per-profile, allowing for custom actions like notifications, script execution, or system adjustments.
[general]
# Global exec commands - run for all profile changes
pre_apply_exec = "notify-send 'HyprDynamicMonitors' 'Switching monitor profile...'"
post_apply_exec = "notify-send 'HyprDynamicMonitors' 'Profile applied successfully'"
# Profile-specific exec commands override global settings
[profiles.gaming_setup]
config_file = "hyprconfigs/gaming.conf"
pre_apply_exec = "notify-send 'Gaming Mode' 'Activating high-performance profile'"
post_apply_exec = "/usr/local/bin/gaming-mode-on.sh"
Key characteristics:
pre_apply_exec
: Executed before the new monitor configuration is appliedpost_apply_exec
: Executed after the new monitor configuration is successfully applied- Profile-specific commands override global commands for that profile
- Failure handling: If exec commands fail, the service continues operating normally (no interruption to monitor configuration)
- Shell execution: Commands are executed through
bash -c
, supporting shell features like pipes and environment variables
To understand callbacks more see examples/callbacks
.
HyprDynamicMonitors can show desktop notifications when configuration changes occur. Notifications are sent via D-Bus using the standard org.freedesktop.Notifications
interface.
[notifications]
disabled = false # Enable/disable notifications (default: false)
timeout_ms = 10000 # Notification timeout in milliseconds (default: 10000)
To disable notifications completely:
[notifications]
disabled = true
To show brief notifications:
[notifications]
timeout_ms = 3000 # 3 seconds
Add to your Hyprland config (assuming ~/.config/hypr/monitors.conf
is your destination):
source = ~/.config/hypr/monitors.conf
Important: Do not set disable_autoreload = true
in Hyprland settings, or you'll have to reload Hyprland manually after configuration changes.
Power state monitoring uses D-Bus to listen for UPower events. This feature is optional and can be completely disabled.
To disable power state monitoring entirely, start with the flag:
hyprdynamicmonitors --disable-power-events
When disabled, the system defaults to AC
power state.
No power events will be delivered, no dbus connection will be made.
By default, the service listens for D-Bus signals:
- Signal:
org.freedesktop.DBus.Properties.PropertiesChanged
- Interface:
org.freedesktop.DBus.Properties
- Member:
PropertiesChanged
- Path:
/org/freedesktop/UPower/devices/line_power_ACAD
These defaults can be overridden in the configuration (or left empty).
You can monitor these events with:
gdbus monitor -y -d org.freedesktop.UPower | grep -E "PropertiesChanged|Device(Added|Removed)"
Example output:
/org/freedesktop/UPower/devices/line_power_ACAD: org.freedesktop.DBus.Properties.PropertiesChanged ('org.freedesktop.UPower.Device', {'UpdateTime': <uint64 1756242314>, 'Online': <true>}, @as [])
# Format: PATH INTERFACE.MEMBER (INFO)
On each event, the current power status is queried. Here's the equivalent command:
dbus-send --system --print-reply --dest=org.freedesktop.UPower \
/org/freedesktop/UPower/devices/line_power_ACAD \
org.freedesktop.DBus.Properties.Get string:org.freedesktop.UPower.Device string:Online
You can filter received events by name. By default, only org.freedesktop.DBus.Properties.PropertiesChanged
is matched. This prevents noisy signals. Additionally, power status changes are only propagated when the state actually changes, and template/link replacement only occurs when file contents differ.
You can customize which D-Bus signals to monitor:
[power_events]
# Custom D-Bus signal match rules
[[power_events.dbus_signal_match_rules]]
interface = "org.freedesktop.DBus.Properties"
member = "PropertiesChanged"
object_path = "/org/freedesktop/UPower/devices/line_power_ACAD"
# Custom signal filters
[[power_events.dbus_signal_receive_filters]]
name = "org.freedesktop.DBus.Properties.PropertiesChanged"
# Custom UPower query for non-standard power managers
[power_events.dbus_query_object]
destination = "org.freedesktop.UPower"
path = "/org/freedesktop/UPower"
method = "org.freedesktop.DBus.Properties.Get"
expected_discharging_value = "true"
[[power_events.dbus_query_object.args]]
arg = "org.freedesktop.UPower"
[[power_events.dbus_query_object.args]]
arg = "OnBattery"
The above query settings are equivalent to:
dbus-send --system --print-reply --dest=org.freedesktop.UPower \
/org/freedesktop/UPower org.freedesktop.DBus.Properties.Get string:org.freedesktop.UPower string:OnBattery
Note: This particular query is not recommended for production use!
To explicitly remove default values from D-Bus match rules, use the leaveEmptyToken
:
[[power_events.dbus_signal_match_rules]]
interface = "leaveEmptyToken" # Removes interface match
member = "PropertiesChanged"
object_path = "/custom/path"
- SIGHUP: Instantly reloads configuration and reapplies monitor setup
- SIGUSR1: Reapplies the monitor setup without reloading the service configuration
- SIGTERM/SIGINT: Graceful shutdown
You can trigger an instant reload with:
kill -SIGHUP $(pidof hyprdynamicmonitors)
# or just restart if running as systemd service
# since the configuration is applied on the startup
systemctl --user reload hyprdynamicmonitors
HyprDynamicMonitors automatically watches for changes to configuration files and applies them without requiring a restart. This includes:
- Configuration file changes: Modifications to
config.toml
are detected and applied automatically - Profile config changes: Updates to individual profile configuration files (both static and template files)
- New profile files: Adding new configuration files referenced by profiles
The service uses file system watching with debounced updates to avoid excessive reloading during rapid changes (e.g., when editors create temporary files).
Hot reload behavior:
- Configuration changes are debounced by default (1000ms delay; configurable)
- Only actual content changes trigger reloads (identical content is ignored)
- Service continues running normally during hot reloads
Disabling hot reload:
hyprdynamicmonitors --disable-auto-hot-reload
When disabled, you can still use SIGHUP
signal for manual reloading.
Live tested on:
- Hyprland v0.50.1
- UPower v1.90.9
You can see my configuration here.
For production use, it's recommended to run HyprDynamicMonitors as a systemd user service. This ensures automatic restart on failures and proper integration with session management.
Important: Ensure you're properly pushing environment variables to systemd.
If you run Hyprland under systemd, setup is straightforward.
Create ~/.config/systemd/user/hyprdynamicmonitors.service
:
[Unit]
Description=HyprDynamicMonitors - Dynamic monitor configuration for Hyprland
After=graphical-session.target
Wants=graphical-session.target
PartOf=hyprland-session.target
[Service]
Type=exec
ExecStart=/usr/bin/hyprdynamicmonitors
Restart=on-failure
RestartSec=5
[Install]
WantedBy=hyprland-session.target
Enable and start the service:
systemctl --user daemon-reload
systemctl --user enable hyprdynamicmonitors
systemctl --user start hyprdynamicmonitors
You can essentially just run it on boot and add restarts, e.g.:
[Unit]
Description=HyprDynamicMonitors - Dynamic monitor configuration for Hyprland
After=default.target
[Service]
Type=exec
ExecStart=/usr/bin/hyprdynamicmonitors
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
It will keep failing until Hyprland is ready/launched and environment variables are propagated.
You can also add a custom systemd target that would be started by Hyprland, e.g.
exec-once = systemctl --user start hyprland-custom-session.target
bind = $mainMod, X, exec, systemctl --user stop hyprland-session.target
Then:
❯ cat ~/.config/systemd/user/hyprland-custom-session.target
[Unit]
Description=A target for other services when hyprland becomes ready
After=graphical-session-pre.target
Wants=graphical-session-pre.target
BindsTo=graphical-session.target
And:
❯ cat ~/.config/systemd/user/hyprdynamicmonitors.service
[Unit]
Description=Run hyprdynamicmonitors daemon
After=hyprland-custom-session.target
After=dbus.socket
Requires=dbus.socket
PartOf=hyprland-custom-session.target
[Service]
Type=exec
ExecStart=/usr/bin/hyprdynamicmonitors
Restart=on-failure
RestartSec=5
[Install]
WantedBy=hyprland-custom-session.target
If you prefer a wrapper script approach, create a simple restart loop:
#!/bin/bash
while true; do
/usr/bin/hyprdynamicmonitors
echo "HyprDynamicMonitors exited with code $?, restarting in 5 seconds..."
sleep 5
done
Then execute it from Hyprland:
exec-once = /path/to/the/script.sh
Set up the complete development environment with all dependencies:
make dev
This installs:
- asdf version manager with required tool versions
- Go toolchain and dependencies
- Python virtual environment for pre-commit hooks
- Node.js dependencies for commit linting
- Pre-commit hooks configuration
- Documentation generation tools
Code quality and testing:
make fmt # Format code and tidy modules
make lint # Run linting checks
make test # Run all tests (unit + integration)
make pre-push # Run complete CI pipeline (fmt + lint + test)
Testing specific areas:
make test/unit # Run only unit tests
make test/integration # Run only integration tests
make test/integration/regenerate # Regenerate test fixtures
Running selected tests (runs with -debug
for log output):
# Run subset of integration tests
make TEST_SELECTOR=Test__Run_Binary/power_events_triggers test/integration/selected
# Run subset of unit tests
make TEST_SELECTOR="TestIPC_Run/happy_path$" PACKAGE_SELECTOR=hypr/... test/unit/selected
Building:
make release/local # Build release binaries for all platforms
make build/test # Build test binary for integration tests
Documentation:
make help/generate # Generate help documentation from binary
- Initial setup:
make dev
(one-time setup) - Development cycle: Make changes, then run
make pre-push
before committing - Testing: Use
make test
for full test suite, or specific test targets for focused testing - Pre-commit hooks: Automatically run on commit (installed by
make dev
)
Most similar tools are more generic, working with any Wayland compositor. In contrast, hyprdynamicmonitors
is specifically designed for Hyprland (using its IPC) but provides several advantages:
Advantages of HyprDynamicMonitors:
- Full configuration control: Instead of introducing another configuration format, you work directly with Hyprland's native config syntax
- Template system: Dynamic configuration generation based on connected monitors and power state
- Power state awareness: Built-in AC/battery detection for laptop users
Trade-offs:
- Hyprland-specific (not generic Wayland)
- Requires systemd or wrapper script for production use (fail-fast design)
- More complex setup compared to simpler tools
Similar Tools:
- kanshi - Generic Wayland output management
- shikane - Another Wayland output manager
- nwg-displays - GUI-based display configuration tool for Sway/Hyprland
- hyprmon - TUI-based display configuration tool for Hyprland
hyprdynamicmonitors
can be used side-by-side with nwg-displays
or hyprmon
:
- Tweak the configuration in either tool
- Let
hyprdynamicmonitors
automatically write/link it to your hyprland's configuration directory