Smart Brightness Control in Linux: Auto-Detect and Manage Backlight Like a Pro
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.