Banner of Smart Brightness Control in Linux: Auto-Detect and Manage Backlight Like a Pro

Smart Brightness Control in Linux: Auto-Detect and Manage Backlight Like a Pro


Category: Linux

📅 April 28, 2026   |   👁️ Views: 116

Author:   mosaid

Managing screen brightness on Linux sounds trivial… until you realize your system exposes multiple brightness interfaces and only one of them actually works correctly.

I’ve personally run into systems where writing to /sys/class/backlight/acpi_video0/brightness does absolutely nothing, while another hidden interface works perfectly.

So instead of guessing every time, I built a smart brightness script that:

•Automatically detects the correct device

•Ignores broken or misleading interfaces

•Provides verbose debugging output

•Supports get / set / increment / decrement


🧠 Understanding the Problem

Linux exposes brightness controls through /sys, typically under:


/sys/class/backlight/
/sys/class/leds/

The issue? You might see something like:


/sys/class/backlight/intel_backlight
/sys/class/backlight/acpi_video0

But only one of them actually controls your screen.

•intel_backlight → usually correct (GPU native)

•acpi_video0 → often fake or limited

This is why hardcoding paths is a bad idea.


⚙️ The Smart Brightness Script

Here’s the full script I use. It automatically detects the best interface and gives you detailed output when needed.


#!/usr/bin/env bash

set -euo pipefail

STEP=5
VERBOSE=0

log() {
    [[ $VERBOSE -eq 1 ]] && echo "[brightness] $*" >&2
}

percent_to_raw() {
    local percent=$1 max=$2
    echo $(( percent * max / 100 ))
}

raw_to_percent() {
    local val=$1 max=$2
    echo $(( val * 100 / max ))
}

get_candidates() {
    local d

    for d in /sys/class/backlight/*; do
        [[ -d "$d" && -f "$d/brightness" && -f "$d/max_brightness" ]] && echo "$d"
    done

    for d in /sys/class/leds/*; do
        [[ -f "$d/brightness" && -f "$d/max_brightness" ]] && echo "$d"
    done
}

detect_device() {
    mapfile -t candidates < <(get_candidates)

    [[ ${#candidates[@]} -eq 0 ]] && return 1

    log "Found ${#candidates[@]} candidates:"
    for d in "${candidates[@]}"; do
        log "  - $d"
    done

    for d in "${candidates[@]}"; do
        if [[ "$d" == *intel_backlight* || "$d" == *amdgpu* ]]; then
            log "Selected (GPU native): $d"
            echo "$d"
            return
        fi
    done

    for d in "${candidates[@]}"; do
        if [[ "$d" != *acpi_video* ]]; then
            log "Selected (non-ACPI fallback): $d"
            echo "$d"
            return
        fi
    done

    log "Selected (last resort): ${candidates[0]}"
    echo "${candidates[0]}"
}

get_brightness() {
    local dev=$1
    local val max percent

    val=$(<"$dev/brightness")
    max=$(<"$dev/max_brightness")
    percent=$(raw_to_percent "$val" "$max")

    echo "Device: $dev"
    echo "Raw: $val / $max"
    echo "Brightness: ${percent}%"
}

set_brightness() {
    local dev=$1 percent=$2
    local max raw

    max=$(<"$dev/max_brightness")
    raw=$(percent_to_raw "$percent" "$max")

    log "Writing $raw to $dev/brightness (=${percent}%)"
    echo "$raw" | sudo tee "$dev/brightness" >/dev/null

    [[ $VERBOSE -eq 1 ]] && get_brightness "$dev"
}

list_devices() {
    local d val max percent

    mapfile -t candidates < <(get_candidates)

    [[ ${#candidates[@]} -eq 0 ]] && {
        echo "No brightness devices found"
        exit 1
    }

    for d in "${candidates[@]}"; do
        val=$(<"$d/brightness")
        max=$(<"$d/max_brightness")
        percent=$(raw_to_percent "$val" "$max")

        echo "$d"
        echo "  Raw: $val / $max"
        echo "  Brightness: ${percent}%"
        echo
    done
}

main() {
    local cmd="get"

    if [[ "${1:-}" == "-v" ]]; then
        VERBOSE=1
        shift
    fi

    cmd=${1:-get}
    shift || true

    case "$cmd" in
        list)
            list_devices
            return
            ;;
    esac

    local dev
    dev=$(detect_device) || {
        echo "No brightness device found"
        exit 1
    }

    case "$cmd" in
        get)
            get_brightness "$dev"
            ;;
        set)
            [[ $# -lt 1 ]] && { echo "Usage: $0 set <percent>"; exit 1; }
            set_brightness "$dev" "$1"
            ;;
        inc)
            local cur
            cur=$(raw_to_percent "$(<"$dev/brightness")" "$(<"$dev/max_brightness")")
            set_brightness "$dev" $(( cur + STEP ))
            ;;
        dec)
            local cur
            cur=$(raw_to_percent "$(<"$dev/brightness")" "$(<"$dev/max_brightness")")
            set_brightness "$dev" $(( cur - STEP ))
            ;;
        *)
            echo "Usage: $0 [-v] {get|set <n>|inc|dec|list}"
            exit 1
            ;;
    esac
}

main "$@"

⚡ Usage Examples

•Check current brightness


./brightness.sh get

•Enable verbose debugging


./brightness.sh -v get

•Set brightness to 50%


./brightness.sh set 50

•Increment / decrement


./brightness.sh inc
./brightness.sh dec

•List all detected devices


./brightness.sh list

🔐 Optional: Remove the Need for sudo

You can allow brightness control without sudo using a udev rule:


echo 'ACTION=="add", SUBSYSTEM=="backlight", RUN+="/bin/chmod 666 /sys/class/backlight/%k/brightness"' | sudo tee /etc/udev/rules.d/90-backlight.rules

🧠 Final Thoughts

What I like about this approach is that it’s portable, predictable, and transparent. Instead of relying on desktop environments or external tools, you get direct control over the kernel interface.

And more importantly—you always know which device is actually being used.

If you’re the kind of user who lives in the terminal or runs minimal setups like i3 or sway, this kind of script becomes essential.


🚀 Next Steps

If you want to push this further, here are some ideas:

•Bind it to XF86 brightness keys

•Add smooth fade transitions

•Cache the detected device for faster execution

•Add external monitor support via ddcutil

Once you start owning your system at this level, going back to GUI sliders feels… limiting.


← Supercharging Zsh: Practical Functions and Keybindings for Power Users Automating Beautiful Quote Images with LaTeX and Bash →

Leave a comment

5000 characters max.