Skip to content

Instantly share code, notes, and snippets.

@kjanat
Last active September 11, 2025 21:21
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
echo "Error: ddcutil is not installed or not in PATH" >&2
echo "Installation instructions:" >&2
echo " Ubuntu/Debian: sudo apt install ddcutil" >&2
echo " Arch Linux: sudo pacman -S ddcutil" >&2
echo " Fedora: sudo dnf install ddcutil" >&2
echo " Or visit: https://www.ddcutil.com/install/" >&2
exit 1
fi
# Check for common i2c setup issues (non-fatal warnings)
if ! lsmod 2>/dev/null | grep -q "i2c_dev" && [ ! -d "/sys/module/i2c_dev" ]; then
echo "Warning: i2c-dev kernel module may not be loaded" >&2
echo " Try: sudo modprobe i2c-dev" >&2
echo " Or add 'i2c-dev' to /etc/modules for auto-loading" >&2
fi
if ! groups 2>/dev/null | grep -q "i2c" && ! ls /dev/i2c-* >/dev/null 2>&1; then
echo "Warning: No i2c devices found or user may lack permissions" >&2
echo " Try: sudo usermod -a -G i2c \$(whoami)" >&2
echo " Then logout/login, or run: newgrp i2c" >&2
elif ! groups 2>/dev/null | grep -q "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
echo "Error: No monitors detected or ddcutil failed" >&2
echo "Troubleshooting tips:" >&2
echo " - Ensure ddcutil is installed: sudo apt install ddcutil" >&2
echo " - Check user permissions: sudo usermod -a -G i2c \$(whoami)" >&2
echo " - Ensure kernel module 'i2c-dev' is enabled/loaded" >&2
echo " - Test DDC communication: ddcutil detect" >&2
exit 1
fi
# Parse and cache the output
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
sort -t'|' -k1,1n | 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
brightness=$(ddcutil getvcp 10 --display "$display" --brief 2>/dev/null | 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 2>/dev/null; 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 2>/dev/null; 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