Skip to content

Instantly share code, notes, and snippets.

@maietta
Created October 17, 2025 16:39
Show Gist options
  • Save maietta/3349faa4a4e09743a07aaffc5e3709a7 to your computer and use it in GitHub Desktop.
Save maietta/3349faa4a4e09743a07aaffc5e3709a7 to your computer and use it in GitHub Desktop.
# Per-Monitor Brightness Control for Hyprland + Waybar (Tested with Omarchy)

Per-Monitor Brightness Control for Hyprland + Waybar

A comprehensive solution for controlling brightness on multiple monitors in Hyprland, with per-monitor Waybar widgets and keyboard shortcuts.

Features

  • 🖥️ Per-Monitor Control: Each Waybar instance shows and controls its own monitor's brightness
  • ⌨️ Keyboard Shortcuts: Control focused monitor brightness with Right Ctrl + Arrow keys
  • 🖱️ Waybar Widget: Scroll to adjust, click to set 100% or 0%
  • 🔄 Smart Detection: Automatically handles different monitor types:
    • External monitors via DDC/CI (ddcutil)
    • Laptop displays via brightnessctl
    • Unsupported monitors show "N/A"
  • Fast Updates: Async DDC/CI commands prevent UI lag

Requirements

Dependencies

Install the following packages on Arch Linux:

sudo pacman -S ddcutil i2c-tools brightnessctl
  • ddcutil: Controls external monitors via DDC/CI protocol
  • i2c-tools: I2C device utilities
  • brightnessctl: Controls laptop backlight

System Configuration

1. Load i2c Kernel Module

# Load the module immediately
sudo modprobe i2c-dev

# Make it load on boot
echo "i2c-dev" | sudo tee /etc/modules-load.d/i2c.conf

2. Set Up i2c Group Permissions

# Create i2c group (if it doesn't exist)
getent group i2c || sudo groupadd i2c

# Add your user to the i2c group
sudo usermod -aG i2c $USER

# Create udev rule for i2c device permissions
sudo sh -c 'echo "KERNEL==\"i2c-[0-9]*\", GROUP=\"i2c\", MODE=\"0660\"" > /etc/udev/rules.d/99-i2c.rules'

# Reload udev rules
sudo udevadm control --reload-rules && sudo udevadm trigger

⚠️ Important: Log out and log back in (or reboot) for group membership to take effect.

3. Detect Your Monitors

After installing ddcutil, check which monitors support DDC/CI:

ddcutil detect

Example output:

Display 1
   I2C bus:  /dev/i2c-14
   DRM_connector:           card1-DP-7
   EDID synopsis:
      Mfg id:               GSM - Goldstar Company Ltd (LG)
      Model:                LG ULTRAWIDE
      Product code:         30655  (0x77bf)
   VCP version:         2.1

Invalid display
   I2C bus:  /dev/i2c-9
   DRM_connector:           card1-eDP-1
   This is a laptop display.  Laptop displays do not support DDC/CI.

Note the display numbers and connector names (e.g., DP-7, eDP-1).

Installation

1. Create the Brightness Control Script

Create ~/.config/waybar/scripts/brightness-per-monitor.sh:

#!/bin/bash

# --- CONFIGURATION ---
STEP=5
STATE_FILE_PREFIX="/tmp/waybar_brightness"
# ---------------------

# Get monitor name - either from argument, env var, or focused monitor
get_monitor_name() {
    if [ -n "$1" ]; then
        echo "$1"
    elif [ -n "$WAYBAR_OUTPUT_NAME" ]; then
        echo "$WAYBAR_OUTPUT_NAME"
    else
        hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
    fi
}

# Get ddcutil display number for a monitor
get_ddcutil_display() {
    local monitor_name="$1"
    case "$monitor_name" in
        "DP-7")
            echo "1"  # LG ULTRAWIDE - ADJUST THIS FOR YOUR SETUP
            ;;
        # Add more monitors here as needed:
        # "DP-4")
        #     echo "2"
        #     ;;
        *)
            echo ""  # No DDC/CI support
            ;;
    esac
}

# Get state file for this monitor
get_state_file() {
    local monitor="$1"
    echo "${STATE_FILE_PREFIX}_${monitor}.tmp"
}

# Function to send DDC/CI command in background
set_brightness_ddcutil() {
    local display_num="$1"
    local brightness="$2"
    pkill -f "ddcutil.*setvcp 10"
    (ddcutil --display "$display_num" setvcp 10 "$brightness") &
}

# Function to set laptop brightness
set_brightness_laptop() {
    local brightness="$1"
    brightnessctl set "${brightness}%" > /dev/null 2>&1
}

# Get current brightness for a specific monitor
get_current_brightness() {
    local monitor="$1"
    local state_file=$(get_state_file "$monitor")
    local display_num=$(get_ddcutil_display "$monitor")
    
    if [ "$monitor" = "eDP-1" ]; then
        # Laptop display
        if [ ! -f "$state_file" ]; then
            local current=$(brightnessctl get 2>/dev/null || echo "0")
            local max=$(brightnessctl max 2>/dev/null || echo "1")
            local percent=$((current * 100 / max))
            echo "$percent" > "$state_file"
        fi
        cat "$state_file"
    elif [ -n "$display_num" ]; then
        # External monitor with DDC/CI
        if [ ! -f "$state_file" ]; then
            local brightness=$(ddcutil --display "$display_num" getvcp 10 -t 2>/dev/null | awk '{print $4}' || echo "100")
            echo "$brightness" > "$state_file"
        fi
        cat "$state_file"
    else
        echo "?"
    fi
}

# Set brightness for a specific monitor
set_brightness() {
    local monitor="$1"
    local new_brightness="$2"
    local state_file=$(get_state_file "$monitor")
    local display_num=$(get_ddcutil_display "$monitor")
    
    if [ "$monitor" = "eDP-1" ]; then
        # Laptop display
        echo "$new_brightness" > "$state_file"
        set_brightness_laptop "$new_brightness"
    elif [ -n "$display_num" ]; then
        # External monitor with DDC/CI
        echo "$new_brightness" > "$state_file"
        set_brightness_ddcutil "$display_num" "$new_brightness"
    else
        # Unsupported monitor
        return
    fi
    
    # Signal all waybar instances
    pkill -RTMIN+8 waybar
}

# Parse command - format: "command [monitor]"
# If monitor not specified, use WAYBAR_OUTPUT_NAME or focused monitor
COMMAND="$1"
MONITOR=$(get_monitor_name "$2")
current=$(get_current_brightness "$MONITOR")

case "$COMMAND" in
    "get")
        echo " $current"
        ;;
    "up")
        if [ "$current" = "?" ]; then
            echo " N/A"
        else
            new_brightness=$((current + STEP > 100 ? 100 : current + STEP))
            if [ "$current" -ne "$new_brightness" ]; then
                set_brightness "$MONITOR" "$new_brightness"
            fi
        fi
        ;;
    "down")
        if [ "$current" = "?" ]; then
            echo " N/A"
        else
            new_brightness=$((current - STEP < 0 ? 0 : current - STEP))
            if [ "$current" -ne "$new_brightness" ]; then
                set_brightness "$MONITOR" "$new_brightness"
            fi
        fi
        ;;
    "right_click")
        if [ "$current" != "?" ]; then
            new_brightness=0
            set_brightness "$MONITOR" "$new_brightness"
        fi
        ;;
    "left_click")
        if [ "$current" != "?" ]; then
            new_brightness=100
            set_brightness "$MONITOR" "$new_brightness"
        fi
        ;;
esac

Make it executable:

chmod +x ~/.config/waybar/scripts/brightness-per-monitor.sh

⚠️ Important: Edit the get_ddcutil_display() function to match your monitor setup. Use the display numbers and connector names from ddcutil detect.

2. Configure Waybar

Add this to your ~/.config/waybar/config.jsonc:

Add to modules-right array:

"modules-right": [
  "group/tray-expander",
  "bluetooth",
  "network",
  "pulseaudio",
  "custom/brightness",  // Add this line
  "cpu",
  "battery"
],

Add the brightness widget configuration:

"custom/brightness": {
  "format": "☀ {}%",
  "signal": 8,
  "exec": "~/.config/waybar/scripts/brightness-per-monitor.sh get",
  "on-scroll-up": "~/.config/waybar/scripts/brightness-per-monitor.sh up",
  "on-scroll-down": "~/.config/waybar/scripts/brightness-per-monitor.sh down",
  "on-click": "~/.config/waybar/scripts/brightness-per-monitor.sh left_click",
  "on-click-right": "~/.config/waybar/scripts/brightness-per-monitor.sh right_click",
  "tooltip-format": "Monitor Brightness\\nScroll: adjust\\nLeft click: 100%\\nRight click: 0%"
}

Reload Waybar:

pkill -SIGUSR2 waybar

3. Configure Hyprland Keybindings

Add to your ~/.config/hypr/bindings.conf:

# Monitor brightness control (controls focused monitor)
bindeld = RCTRL, Up, Brightness up, exec, ~/.config/waybar/scripts/brightness-per-monitor.sh up
bindeld = RCTRL, Down, Brightness down, exec, ~/.config/waybar/scripts/brightness-per-monitor.sh down

Reload Hyprland:

hyprctl reload

Usage

Keyboard Shortcuts

  • Right Ctrl + Up Arrow: Increase brightness of focused monitor by 5%
  • Right Ctrl + Down Arrow: Decrease brightness of focused monitor by 5%

Waybar Widget

Each Waybar instance (one per monitor) shows the brightness for its specific monitor:

  • Scroll Up/Down: Adjust brightness by 5%
  • Left Click: Set to 100%
  • Right Click: Set to 0%

Manual Control

You can also control brightness from the command line:

# Get brightness for current focused monitor
~/.config/waybar/scripts/brightness-per-monitor.sh get

# Adjust brightness for focused monitor
~/.config/waybar/scripts/brightness-per-monitor.sh up
~/.config/waybar/scripts/brightness-per-monitor.sh down

# Control specific monitor
~/.config/waybar/scripts/brightness-per-monitor.sh up DP-7
~/.config/waybar/scripts/brightness-per-monitor.sh down eDP-1

Configuration Options

Adjust Step Size

Edit the STEP variable in brightness-per-monitor.sh:

STEP=5  # Change to 10 for larger steps

Add More External Monitors

If you have multiple external monitors, add them to the get_ddcutil_display() function:

get_ddcutil_display() {
    local monitor_name="$1"
    case "$monitor_name" in
        "DP-7")
            echo "1"  # First external monitor
            ;;
        "DP-4")
            echo "2"  # Second external monitor
            ;;
        "HDMI-A-1")
            echo "3"  # HDMI monitor
            ;;
        *)
            echo ""  # No DDC/CI support
            ;;
    esac
}

Find your display numbers with:

ddcutil detect

Troubleshooting

Monitor Not Detected

# Check if i2c devices exist
ls -la /dev/i2c-*

# Verify i2c module is loaded
lsmod | grep i2c

# Check group membership
groups $USER

Permission Denied Errors

# Check if you're in the i2c group
groups | grep i2c

# If not, add yourself and log out/in
sudo usermod -aG i2c $USER

DDC/CI Not Working

Some monitors may need to enable DDC/CI in their OSD (On-Screen Display) settings. Look for options like:

  • DDC/CI
  • External Control
  • PC Communication

Brightness Changes Are Slow

This is normal for DDC/CI. The script runs ddcutil commands in the background to prevent UI lag. The Waybar display updates immediately using cached values.

Test DDC/CI Manually

# Get current brightness
ddcutil --display 1 getvcp 10

# Set brightness to 50%
ddcutil --display 1 setvcp 10 50

How It Works

  1. Monitor Detection: The script uses WAYBAR_OUTPUT_NAME (set by Waybar for each instance) or queries the focused monitor via hyprctl

  2. Per-Monitor State: Each monitor's brightness is cached in /tmp/waybar_brightness_<monitor>.tmp for fast reads

  3. Backend Selection:

    • Laptop displays (eDP-1) use brightnessctl
    • External monitors use ddcutil with DDC/CI
    • Unsupported monitors show "?"
  4. Async Updates: DDC/CI commands run in the background to prevent blocking. The UI updates immediately from the cache, while the actual hardware change happens asynchronously.

  5. Waybar Signals: Uses RTMIN+8 signal to refresh all Waybar instances after brightness changes

License

This configuration is provided as-is for personal use. Feel free to modify and share.

Credits

Based on the brightness control gist by negoro26, adapted for per-monitor control in Hyprland with Waybar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment