Skip to content

In short: Autorandr for Hyprland. Manage Hyprland configuration based on connected displays and power state.

License

Notifications You must be signed in to change notification settings

fiffeek/hyprdynamicmonitors

Repository files navigation

hyprdynamicmonitors logo

HyprDynamicMonitors


An event-driven service that automatically manages Hyprland monitor configurations based on connected displays and power state.

Documentation

Features

  • 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)

Design Philosophy

HyprDynamicMonitors follows a fail-fast architecture designed for reliability and simplicity.

Reliability Through Restarts

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.

Hot Reloading With Graceful Restart

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.

Hyprland-Native Integration

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.

Installation

Binary Release

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

AUR

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

Build from Source

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

Command Line

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

Minimal Example

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.

Examples

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.

Runtime requirements

  • 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)

Configuration

Monitor Matching

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.

Configuration File Types

  • Static: Creates symlinks to existing configuration files
  • Template: Processes Go templates with dynamic monitor and power state data

Template Variables

.PowerState          // "AC" or "BAT"
.Monitors           // Array of connected monitors
.MonitorsByTag      // Map of tagged monitors (monitor_tag -> monitor)

Static Template Values

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.

Template Functions

isOnBattery         // Returns true if on battery power
isOnAC              // Returns true if on AC power
powerState          // Returns current power state string

Fallback Profile

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.

User Callbacks (Exec Commands)

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 applied
  • post_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.

Notifications

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

Hyprland Integration

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 Events

Power state monitoring uses D-Bus to listen for UPower events. This feature is optional and can be completely disabled.

Disabling power events

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.

Default power event configuration

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)

Querying

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

Receive Filters

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.

Custom D-Bus Configuration

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!

Leave Empty Token

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"

Signals

  • 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

Hot Reloading

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.

Tests

Live Testing

Live tested on:

  • Hyprland v0.50.1
  • UPower v1.90.9

You can see my configuration here.

Running with systemd

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.

Hyprland under 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

Run on boot and let restarts do the job

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.

Custom systemd target

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

Alternative: Wrapper script

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

Development

Setup Development Environment

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

Development Commands

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

Development Workflow

  1. Initial setup: make dev (one-time setup)
  2. Development cycle: Make changes, then run make pre-push before committing
  3. Testing: Use make test for full test suite, or specific test targets for focused testing
  4. Pre-commit hooks: Automatically run on commit (installed by make dev)

Alternative software

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

About

In short: Autorandr for Hyprland. Manage Hyprland configuration based on connected displays and power state.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published