Skip to content

Instantly share code, notes, and snippets.

@kjanat
Last active February 25, 2026 03:44
Show Gist options
  • Select an option

  • Save kjanat/0efbcfef3026498bb9f21cd1518dd001 to your computer and use it in GitHub Desktop.

Select an option

Save kjanat/0efbcfef3026498bb9f21cd1518dd001 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# brightness.sh - DDC/CI Monitor Brightness Control Utility
#
# Intelligent monitor brightness control with sophisticated caching system.
# Supports multiple monitors, relative adjustments, and atomic cache operations.
#
# Installation:
# curl -o ~/.local/bin/brightness https://gist.github.com/kjanat/0efbcfef3026498bb9f21cd1518dd001/raw/brightness.sh
# chmod +x ~/.local/bin/brightness
# (Ensure ~/.local/bin is in your PATH)
#
# Dependencies: ddcutil (for DDC/CI communication)
# Platform: Requires GNU coreutils (GNU/Linux); macOS/BSD users may need gstat or alternative flags
# Permissions: User must be in 'i2c' group for DDC access
# Performance: Uses 1-hour monitor cache and 2-second brightness cache
# Cache file for monitor detection (valid for 1 hour)
CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/brightness"
CACHE_FILE="${CACHE_DIR}/monitors.cache"
CACHE_MAX_AGE=3600 # 1 hour in seconds
# Create cache directory if needed
mkdir -p "${CACHE_DIR}"
# Check for ddcutil dependency
if ! command -v ddcutil >/dev/null 2>&1; then
cat >&2 <<-'EOF'
Error: ddcutil is not installed or not in PATH
Installation instructions:
Ubuntu/Debian: sudo apt install ddcutil
Arch Linux: sudo pacman -S ddcutil
Fedora: sudo dnf install ddcutil
Or visit: https://www.ddcutil.com/install/
EOF
exit 1
fi
# Check for common i2c setup issues (non-fatal warnings)
i2c_dev_loaded=false
lsmod_output=$(lsmod 2>/dev/null || true)
if echo "${lsmod_output}" | grep -q "i2c_dev"; then
i2c_dev_loaded=true
fi
if ! ${i2c_dev_loaded} && [[ ! -d "/sys/module/i2c_dev" ]]; then
cat >&2 <<-'EOF'
Warning: i2c-dev kernel module may not be loaded
Try: sudo modprobe i2c-dev
Or add 'i2c-dev' to /etc/modules for auto-loading
EOF
fi
user_in_i2c=false
groups_output=$(groups 2>/dev/null || true)
if echo "${groups_output}" | grep -q "i2c"; then
user_in_i2c=true
fi
if ! ${user_in_i2c} && ! ls /dev/i2c-* >/dev/null 2>&1; then
cat >&2 <<-'EOF'
Warning: No i2c devices found or user may lack permissions
Try: sudo usermod -a -G i2c $(whoami)
Then logout/login, or run: newgrp i2c
EOF
elif ! ${user_in_i2c} && ls /dev/i2c-* >/dev/null 2>&1; then
# Check if any i2c device is readable
device_readable=false
for device in /dev/i2c-*; do
if [[ -r "${device}" ]]; then
device_readable=true
break
fi
done
if [[ "${device_readable}" = "false" ]]; then
echo "Warning: User not in i2c group, DDC access may fail" >&2
echo " Try: sudo usermod -a -G i2c \$(whoami) && newgrp i2c" >&2
fi
fi
# Function to get cached or fresh monitor data
# Args: $1 - force_refresh (true/false)
# Returns: Pipe-delimited monitor data (display|mfg|model|serial)
# Uses 1-hour cache to avoid expensive ddcutil detect calls
get_monitors() {
local force_refresh=${1:-false}
# Check if cache exists and is fresh
if [[ "${force_refresh}" = "false" ]] && [[ -f "${CACHE_FILE}" ]]; then
local cache_age=$(($(date +%s) - $(stat -c %Y "${CACHE_FILE}" 2>/dev/null || echo 0)))
if [[ "${cache_age}" -lt "${CACHE_MAX_AGE}" ]]; then
cat "${CACHE_FILE}"
return
fi
fi
# Use --terse for faster detection (0.7s vs 0.9s)
local detect_output
detect_output=$(ddcutil detect --terse 2>/dev/null)
if [[ -z "${detect_output}" ]]; then
cat >&2 <<-'EOF'
Error: No monitors detected or ddcutil failed
Troubleshooting tips:
- Ensure ddcutil is installed: sudo apt install ddcutil
- Check user permissions: sudo usermod -a -G i2c $(whoami)
- Ensure kernel module 'i2c-dev' is enabled/loaded
- Test DDC communication: ddcutil detect
EOF
exit 1
fi
# Parse and cache the output
local parsed
parsed=$(echo "${detect_output}" | awk '
/^Display [0-9]/ {
display = $2
}
/Monitor:/ {
# Extract substring after "Monitor:" regardless of spacing/tokenization
monitor_pos = index($0, "Monitor:")
if (monitor_pos > 0) {
monitor_info = substr($0, monitor_pos + 8) # Skip "Monitor:"
gsub(/^[ \t]+/, "", monitor_info) # Trim leading whitespace
split(monitor_info, parts, ":")
mfg = parts[1]
model = parts[2]
serial = parts[3]
printf "%s|%s|%s|%s\n", display, mfg, model, serial
}
}
')
# Atomic write with exclusive lock to prevent concurrent writers
(
flock 200
local sorted
sorted=$(echo "${parsed}" | sort -t'|' -k1,1n)
echo "${sorted}" | tee "${CACHE_FILE}.tmp" && mv "${CACHE_FILE}.tmp" "${CACHE_FILE}"
) 200>"${CACHE_FILE}.lock"
}
# Function to validate brightness value
# Args: $1 - brightness value to validate
# Returns: 0 if valid (0-100), 1 if invalid
validate_brightness() {
local val=$1
if ! [[ "${val}" =~ ^[0-9]+$ ]] || [[ "${val}" -lt 0 ]] || [[ "${val}" -gt 100 ]]; then
return 1
fi
return 0
}
# Function to get current brightness with caching
# Args: $1 - display number
# Returns: Current brightness value (0-100), or empty if unable to read
# Uses 2-second cache for responsive queries, validates cached values
get_brightness() {
local display=$1
local cache_key="${CACHE_DIR}/brightness_${display}.tmp"
# Use cached value if it's less than 2 seconds old
if [[ -f "${cache_key}" ]]; then
local cache_age=$(($(date +%s) - $(stat -c %Y "${cache_key}" 2>/dev/null || echo 0)))
if [[ "${cache_age}" -lt 2 ]]; then
local cached_value
cached_value=$(cat "${cache_key}")
if validate_brightness "${cached_value}"; then
echo "${cached_value}"
return
else
# Invalid cached value, remove it
rm -f "${cache_key}"
fi
fi
fi
# Get fresh value and cache it
local brightness
local getvcp_output
getvcp_output=$(ddcutil getvcp 10 --display "${display}" --brief 2>/dev/null)
brightness=$(echo "${getvcp_output}" | awk '{print $4}')
if [[ -n "${brightness}" ]] && validate_brightness "${brightness}"; then
# Atomic cache write
echo "${brightness}" >"${cache_key}.tmp" && mv "${cache_key}.tmp" "${cache_key}"
echo "${brightness}"
elif [[ -n "${brightness}" ]]; then
# Invalid brightness value, don't cache
echo "Warning: Invalid brightness value '${brightness}' from display ${display}" >&2
rm -f "${cache_key}"
fi
}
# Function to set brightness with retry logic
# Args: $1 - display number, $2 - brightness value (0-100)
# Returns: 0 on success, 1 on failure (including invalid input)
# Uses input validation, optional 2s timeout, 2-attempt retry with 0.1s delay
set_brightness() {
local display=$1
local value=$2
local retries=2
# Defensive input validation
if ! validate_brightness "${value}"; then
return 1
fi
while [[ "${retries}" -gt 0 ]]; do
# Use timeout to prevent hangs on flaky DDC links
if command -v timeout >/dev/null 2>&1; then
if timeout 2s ddcutil setvcp 10 "${value}" --display "${display}" --noverify >/dev/null 2>&1; then
# Clear brightness cache for this display
rm -f "${CACHE_DIR}/brightness_${display}.tmp"
return 0
fi
else
if ddcutil setvcp 10 "${value}" --display "${display}" --noverify >/dev/null 2>&1; then
# Clear brightness cache for this display
rm -f "${CACHE_DIR}/brightness_${display}.tmp"
return 0
fi
fi
retries=$((retries - 1))
[[ "${retries}" -gt 0 ]] && sleep 0.1
done
return 1
}
# Parse command line arguments
FORCE_REFRESH=false
while [[ $# -gt 0 ]]; do
case $1 in
--refresh | -r)
FORCE_REFRESH=true
shift
;;
--clear-cache)
CLEAR_CACHE=true
shift
;;
--help)
cat <<EOF
Usage: brightness [OPTIONS] [VALUE|+VALUE|-VALUE]
Control monitor brightness via DDC/CI
Options:
--refresh, -r Force refresh monitor detection cache
--clear-cache Clear all cached data and start fresh
--help Show this help message
Arguments:
(no args) Show current brightness for all monitors
VALUE Set brightness to VALUE (0-100)
+VALUE Increase brightness by VALUE
-VALUE Decrease brightness by VALUE
Examples:
brightness # Show current brightness
brightness 70 # Set all monitors to 70%
brightness +10 # Increase by 10%
brightness -5 # Decrease by 5%
Cache is stored in: ${CACHE_DIR}
EOF
exit 0
;;
--*)
echo "Error: Unknown option '$1'" >&2
echo "Try 'brightness --help' for usage information" >&2
exit 1
;;
*)
break
;;
esac
done
# Clear cache if requested
if [[ "${CLEAR_CACHE}" = "true" ]]; then
echo "Clearing cache directory: ${CACHE_DIR}"
rm -rf "${CACHE_DIR}"/*.cache "${CACHE_DIR}"/*.tmp "${CACHE_DIR}"/*.lock 2>/dev/null || true
echo "Cache cleared!"
[[ -z "${1:-}" ]] && exit 0
fi
# Get monitor information
MONITORS=$(get_monitors "${FORCE_REFRESH}")
if [[ -z "${MONITORS}" ]]; then
echo "Error: No monitors found"
exit 1
fi
# Count monitors
COUNT=$(echo "${MONITORS}" | wc -l)
# Main logic based on argument
case "${1:-}" in
"")
# Show current brightness
echo "Brightness levels (${COUNT} monitor(s)):"
echo ""
# Process monitors in parallel for faster response with stable ordering
temp_dir=$(mktemp -d)
trap 'rm -rf "${temp_dir}"' EXIT
index=0
while IFS='|' read -r display mfg model _; do
{
brightness=$(get_brightness "${display}")
if [[ -n "${brightness}" ]]; then
echo "Display ${display} (${mfg} ${model}): ${brightness}%" >"${temp_dir}/${index}"
else
echo "Display ${display} (${mfg} ${model}): Unable to read" >"${temp_dir}/${index}"
fi
} &
index=$((index + 1))
done <<<"${MONITORS}"
wait
# Output results in original order
for ((i = 0; i < index; i++)); do
cat "${temp_dir}/${i}"
done
rm -rf "${temp_dir}"
;;
+*)
# Increase brightness
VAL=${1#+}
if ! validate_brightness "${VAL}"; then
echo "Error: Invalid increment value. Must be 0-100"
exit 1
fi
echo "Increasing brightness by ${VAL}%..."
success=0
failed=0
while IFS='|' read -r display mfg model _; do
current=$(get_brightness "${display}")
if [[ -n "${current}" ]]; then
new=$((current + VAL))
[[ "${new}" -gt 100 ]] && new=100
if set_brightness "${display}" "${new}"; then
echo "Display ${display}: ${current}% -> ${new}%"
success=$((success + 1))
else
echo "Display ${display}: Failed to set brightness"
failed=$((failed + 1))
fi
else
echo "Display ${display}: Cannot read current value"
failed=$((failed + 1))
fi
done <<<"${MONITORS}"
echo "Brightness update: ${success} succeeded, ${failed} failed"
[[ "${failed}" -gt 0 ]] && exit 1
;;
-*)
# Decrease brightness
VAL=${1#-}
if ! validate_brightness "${VAL}"; then
echo "Error: Invalid decrement value. Must be 0-100"
exit 1
fi
echo "Decreasing brightness by ${VAL}%..."
success=0
failed=0
while IFS='|' read -r display mfg model _; do
current=$(get_brightness "${display}")
if [[ -n "${current}" ]]; then
new=$((current - VAL))
[[ "${new}" -lt 0 ]] && new=0
if set_brightness "${display}" "${new}"; then
echo "Display ${display}: ${current}% -> ${new}%"
success=$((success + 1))
else
echo "Display ${display}: Failed to set brightness"
failed=$((failed + 1))
fi
else
echo "Display ${display}: Cannot read current value"
failed=$((failed + 1))
fi
done <<<"${MONITORS}"
echo "Brightness update: ${success} succeeded, ${failed} failed"
[[ "${failed}" -gt 0 ]] && exit 1
;;
*)
# Set absolute brightness
if ! validate_brightness "$1"; then
echo "Error: Brightness must be between 0-100, got: '$1'" >&2
echo "Examples: brightness 70, brightness +10, brightness -5" >&2
echo "Try 'brightness --help' for full usage information" >&2
exit 1
fi
echo "Setting brightness to $1%..."
# Set all monitors in sequence (more reliable than parallel)
success=0
failed=0
while IFS='|' read -r display mfg model _; do
if set_brightness "${display}" "$1"; then
echo "Display ${display}: Set to $1%"
success=$((success + 1))
else
echo "Display ${display}: Failed to set brightness"
failed=$((failed + 1))
fi
done <<<"${MONITORS}"
if [[ "${failed}" -gt 0 ]]; then
echo "Error: ${failed} monitor(s) failed to update"
exit 1
else
echo "All monitors updated successfully!"
fi
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment