Support external monitor brightness in Quattro (#6490)

* Support external monitor brightness

Route brightness through the focused Hyprland monitor so internal panels keep using the kernel backlight while compatible external displays use DDC/CI. Preserve the Apple Display backend and leave brightness unavailable when the focused display cannot be controlled.

Add cached DDC bus and VCP range handling, install ddcutil for new and existing systems, and cover backend selection and brightness conversion with shell tests.

* Harden external brightness caching

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
This commit is contained in:
ssupt
2026-08-01 15:35:34 -05:00
committed by GitHub
co-authored by David Heinemeier Hansson
parent fa3d28ca61
commit c0d4037237
8 changed files with 379 additions and 19 deletions
+47 -13
View File
@@ -1,28 +1,59 @@
#!/bin/bash
# omarchy:summary=Show or adjust brightness on the most likely display device.
# omarchy:args=[--no-osd] [+N%|N%-|N%|off|on]
# omarchy:examples=omarchy brightness display | omarchy brightness display +5% | omarchy brightness display --no-osd 50% | omarchy brightness display off | omarchy brightness display on
# omarchy:summary=Show or adjust brightness on the focused display.
# omarchy:args=[--no-osd] [--monitor name] [+N%|N%-|N%|off|on]
# omarchy:examples=omarchy brightness display | omarchy brightness display +5% | omarchy brightness display --monitor DP-1 50% | omarchy brightness display off | omarchy brightness display on
no_osd=0
if [[ ${1:-} == "--no-osd" ]]; then
no_osd=1
shift
fi
monitor=""
while (( $# > 0 )); do
case "$1" in
--no-osd)
no_osd=1
shift
;;
--monitor)
(( $# >= 2 )) || exit 1
monitor="$2"
shift 2
;;
*)
break
;;
esac
done
# Get the brightness of the passed display
display_brightness() {
backlight_brightness() {
brightnessctl -d "$1" -m 2>/dev/null | awk -F, '{ gsub("%", "", $4); print $4; found=1 } END{ exit !found }'
}
[[ -n $monitor ]] || monitor="$(omarchy-hyprland-monitor-focused 2>/dev/null || true)"
monitor_is_internal() {
[[ $monitor =~ ^(eDP|LVDS|DSI)- ]]
}
use_apple_display() {
omarchy-hyprland-monitor-focused-apple "$monitor"
}
use_ddc_display() {
[[ -n $monitor ]] && ! monitor_is_internal
}
if (( $# == 0 )); then
if omarchy-hyprland-monitor-focused-apple; then
if use_apple_display; then
omarchy-brightness-display-apple
exit
elif use_ddc_display; then
omarchy-brightness-display-ddc "$monitor"
exit
fi
device="$(omarchy-hw-display)" || exit 1
display_brightness "$device"
backlight_brightness "$device"
exit
fi
@@ -45,18 +76,21 @@ fi
exec {lock_fd}>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display.lock"
flock -n "$lock_fd" || exit 0
if omarchy-hyprland-monitor-focused-apple; then
if use_apple_display; then
if (( no_osd )); then
omarchy-brightness-display-apple --no-osd "$step"
else
omarchy-brightness-display-apple "$step"
fi
elif use_ddc_display; then
brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1
(( no_osd )) || omarchy-osd -i brightness -p "$brightness"
else
# Current device highlighted
device="$(omarchy-hw-display)" || exit 1
# Current brightness percentage
current=$(display_brightness "$device") || exit 1
current=$(backlight_brightness "$device") || exit 1
# Apply non-uniform step size: 1% steps if at or below 5%, otherwise set an
# absolute target percentage to avoid raw backlight rounding causing uneven OSD steps.
@@ -84,5 +118,5 @@ else
brightnessctl -d "$device" set "$step" >/dev/null
# Show the new brightness in OSD
(( no_osd )) || omarchy-osd -i brightness -p "$(display_brightness "$device")"
(( no_osd )) || omarchy-osd -i brightness -p "$(backlight_brightness "$device")"
fi
+168
View File
@@ -0,0 +1,168 @@
#!/bin/bash
# omarchy:summary=Show or adjust DDC/CI display brightness for a Hyprland monitor.
# omarchy:args=<monitor> [+N%|N%-|N%]
# omarchy:examples=omarchy-brightness-display-ddc DP-1 | omarchy-brightness-display-ddc DP-1 50%
monitor="${1:-}"
step="${2:-}"
[[ -n $monitor ]] || exit 1
cache_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-ddc"
cache_name="${monitor//[^[:alnum:]_.-]/_}"
cache_file="$cache_dir/$cache_name.bus"
unavailable_cache_seconds=60
range_cache_seconds=10
cache_unavailable() {
mkdir -p "$cache_dir" 2>/dev/null || true
printf 'unavailable %s\n' "$(date +%s)" >"$cache_file" 2>/dev/null || true
}
detect_bus() {
ddcutil --skip-ddc-checks detect --brief 2>/dev/null | awk -v monitor="$monitor" '
/I2C bus:/ {
bus = $NF
sub(/^.*\/i2c-/, "", bus)
}
/DRM connector:/ {
connector = $NF
sub(/^card[0-9]+-/, "", connector)
if (connector == monitor && bus != "") {
print bus
exit
}
bus = ""
}
'
}
find_bus() {
local bus=""
local cached_value=""
local now=0
if [[ -r $cache_file ]]; then
read -r bus cached_value <"$cache_file" || true
fi
if [[ $bus == "unavailable" ]]; then
now=$(date +%s)
if [[ $cached_value =~ ^[0-9]+$ ]] && (( now - cached_value < unavailable_cache_seconds )); then
return 1
fi
bus=""
rm -f "$cache_file"
fi
if [[ -z $bus ]]; then
bus="$(detect_bus)" || return 1
if [[ ! $bus =~ ^[0-9]+$ ]]; then
cache_unavailable
return 1
fi
mkdir -p "$cache_dir" 2>/dev/null || true
printf '%s\n' "$bus" >"$cache_file" 2>/dev/null || true
fi
printf '%s\n' "$bus"
}
read_vcp() {
local bus="$1"
ddcutil --bus "$bus" --skip-ddc-checks getvcp 10 --brief 2>/dev/null | awk '
$1 == "VCP" && toupper($2) == "10" && $3 == "C" && $4 ~ /^[0-9]+$/ && $5 ~ /^[0-9]+$/ && $5 > 0 {
print $4, $5
found = 1
exit
}
END { exit !found }
'
}
read_brightness() {
local bus=""
local now=0
local values=""
bus="$(find_bus)" || return 1
values="$(read_vcp "$bus")" || {
rm -f "$cache_file"
return 1
}
now=$(date +%s)
mkdir -p "$cache_dir" 2>/dev/null || true
printf '%s %s %s\n' "$bus" "${values##* }" "$now" >"$cache_file" 2>/dev/null || true
printf '%s %s\n' "$bus" "$values"
}
bus=""
current=""
maximum=""
percent=""
range_cached_at=""
if [[ -z $step ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
printf '%s\n' "$percent"
exit 0
fi
if [[ $step =~ ^([0-9]+)%$ ]]; then
target="${BASH_REMATCH[1]}"
range_cache_fresh=0
if [[ -r $cache_file ]]; then
read -r bus maximum range_cached_at <"$cache_file" || true
fi
if [[ $bus =~ ^[0-9]+$ && $maximum =~ ^[0-9]+$ && $range_cached_at =~ ^[0-9]+$ ]] && (( maximum > 0 )); then
now=$(date +%s)
if (( range_cached_at <= now && now - range_cached_at < range_cache_seconds )); then
range_cache_fresh=1
fi
fi
if (( ! range_cache_fresh )); then
read -r bus current maximum < <(read_brightness) || exit 1
fi
elif [[ $step =~ ^\+([0-9]+)%$ ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
amount="${BASH_REMATCH[1]}"
if (( amount == 5 && percent < 5 )); then
(( target = percent + 1 ))
else
(( target = percent + amount ))
fi
elif [[ $step =~ ^([0-9]+)%-$ ]]; then
read -r bus current maximum < <(read_brightness) || exit 1
[[ -n ${bus:-} && -n ${current:-} && -n ${maximum:-} ]] || exit 1
(( percent = (current * 100 + maximum / 2) / maximum ))
amount="${BASH_REMATCH[1]}"
if (( amount == 5 && percent <= 5 )); then
(( target = percent - 1 ))
else
(( target = percent - amount ))
fi
else
exit 1
fi
(( target < 1 )) && target=1
(( target > 100 )) && target=100
(( raw_target = (target * maximum + 50) / 100 ))
if ! ddcutil --bus "$bus" --skip-ddc-checks --noverify setvcp 10 "$raw_target" >/dev/null 2>&1; then
rm -f "$cache_file"
exit 1
fi
printf '%s\n' "$target"
+9 -2
View File
@@ -1,5 +1,12 @@
#!/bin/bash
# omarchy:summary=Return success if the focused Hyprland monitor is an Apple display.
# omarchy:summary=Return success if the focused or named Hyprland monitor is an Apple display.
# omarchy:args=[monitor]
hyprctl monitors -j | jq -e '.[] | select(.focused == true) | select(.make == "Apple Computer Inc" and (.model | test("StudioDisplay|ProDisplayXDR|Studio XDR")))' >/dev/null
monitor="${1:-}"
hyprctl monitors -j | jq -e --arg monitor "$monitor" '
.[]
| select(if $monitor == "" then .focused == true else .name == $monitor end)
| select(.make == "Apple Computer Inc" and (.model | test("StudioDisplay|ProDisplayXDR|Studio XDR")))
' >/dev/null
+4 -3
View File
@@ -3,9 +3,10 @@
# omarchy:summary=Print monitor panel state for the shell
# omarchy:group=monitor
{ omarchy-brightness-display 2>/dev/null; echo; } | head -n 1
monitors_json=$(hyprctl monitors all -j)
focused_monitor=$(printf '%s\n' "$monitors_json" | jq -r '[.[] | select(.focused == true)][0].name // ""')
{ omarchy-brightness-display --monitor "$focused_monitor" 2>/dev/null; echo; } | head -n 1
printf '%s\n' "$monitors_json" | jq -r '
def internal: test("^(eDP|LVDS|DSI)-");
@@ -15,7 +16,7 @@ printf '%s\n' "$monitors_json" | jq -r '
([.[] | select((.name | internal) and .mirrorOf != "none")][0].mirrorOf // "")
'
omarchy-hyprland-monitor-focused 2>/dev/null || echo
printf '%s\n' "$focused_monitor"
omarchy-hyprland-monitor-scaling 2>/dev/null || echo
printf '%s\n' "$monitors_json" | jq -c \
+1
View File
@@ -20,6 +20,7 @@ cups
cups-browsed
cups-filters
cups-pdf
ddcutil
docker
docker-buildx
docker-compose
+3
View File
@@ -0,0 +1,3 @@
echo "Install ddcutil for external monitor brightness control"
omarchy-pkg-add ddcutil
+1 -1
View File
@@ -244,7 +244,7 @@ Panel {
}
root.brightnessSetQueued = false
setBrightnessProc.command = ["bash", "-c", "omarchy-brightness-display --no-osd " + percent + "%"]
setBrightnessProc.command = ["omarchy-brightness-display", "--no-osd", "--monitor", root.focusedMonitor, percent + "%"]
setBrightnessProc.running = true
}
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
call_log="$test_tmp/calls"
runtime_dir="$test_tmp/runtime"
mkdir -p "$mock_bin" "$runtime_dir"
cat >"$mock_bin/omarchy-hyprland-monitor-focused-apple" <<'SH'
#!/bin/bash
exit 1
SH
cat >"$mock_bin/omarchy-hyprland-monitor-focused" <<'SH'
#!/bin/bash
printf '%s\n' "${FOCUSED_MONITOR:-eDP-1}"
SH
cat >"$mock_bin/omarchy-hw-display" <<'SH'
#!/bin/bash
printf 'mock_backlight\n'
SH
cat >"$mock_bin/brightnessctl" <<'SH'
#!/bin/bash
printf 'brightnessctl %s\n' "$*" >>"$CALL_LOG"
if [[ $* == *" -m"* ]]; then
printf 'mock_backlight,backlight,40,40%%\n'
fi
SH
cat >"$mock_bin/ddcutil" <<'SH'
#!/bin/bash
printf 'ddcutil %s\n' "$*" >>"$CALL_LOG"
if [[ $* == *" detect --brief"* ]]; then
cat <<EOF
Display 1
I2C bus: /dev/i2c-${DDC_BUS:-7}
DRM connector: card1-${DDC_CONNECTOR:-DP-1}
EOF
elif [[ $* == *" getvcp 10 "* ]]; then
[[ ${DDC_READ_FAIL:-0} == "1" ]] && exit 1
printf 'VCP 10 C %s %s\n' "${DDC_CURRENT:-40}" "${DDC_MAXIMUM:-80}"
fi
SH
chmod +x "$mock_bin"/*
run_brightness() {
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" PATH="$mock_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-brightness-display" "$@"
}
brightness=$(run_brightness --monitor DP-1)
[[ $brightness == "50" ]] || fail "external brightness is converted to a percentage" "actual: $brightness"
pass "external brightness is converted to a percentage"
(( $(grep -c '^ddcutil --skip-ddc-checks detect --brief$' "$call_log") == 1 )) || fail "DDC bus is detected once"
run_brightness --monitor DP-1 >/dev/null
(( $(grep -c '^ddcutil --skip-ddc-checks detect --brief$' "$call_log") == 1 )) || fail "DDC bus mapping is cached"
pass "DDC bus mapping is cached"
run_brightness --no-osd --monitor DP-1 25%
grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 20' "$call_log" >/dev/null || \
fail "external percentage is converted to the monitor VCP range"
pass "external percentage is converted to the monitor VCP range"
get_count=$(grep -c ' getvcp 10 ' "$call_log")
run_brightness --no-osd --monitor DP-1 30%
(( $(grep -c ' getvcp 10 ' "$call_log") == get_count )) || \
fail "absolute external brightness reuses the cached VCP range"
grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 24' "$call_log" >/dev/null || \
fail "absolute external brightness skips write verification"
pass "absolute external brightness reuses the cached VCP range"
brightness=$(run_brightness --monitor eDP-1)
[[ $brightness == "40" ]] || fail "internal monitor uses the kernel backlight" "actual: $brightness"
grep -F 'brightnessctl -d mock_backlight -m' "$call_log" >/dev/null || \
fail "internal monitor queries brightnessctl"
pass "internal monitor uses the kernel backlight"
brightness=$(FOCUSED_MONITOR=DP-1 run_brightness)
[[ $brightness == "50" ]] || fail "brightness follows the focused external monitor" "actual: $brightness"
pass "brightness follows the focused external monitor"
detect_count=$(grep -c ' detect --brief' "$call_log")
if DDC_CONNECTOR=DP-1 run_brightness --monitor DP-2 >/dev/null 2>&1; then
fail "unsupported external monitor has no brightness backend"
fi
if DDC_CONNECTOR=DP-1 run_brightness --monitor DP-2 >/dev/null 2>&1; then
fail "cached unsupported external monitor has no brightness backend"
fi
(( $(grep -c ' detect --brief' "$call_log") == detect_count + 1 )) || \
fail "unsupported external monitor detection is temporarily cached"
pass "unsupported external monitor has no brightness backend"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/DP-1.bus"
detect_count=$(grep -c ' detect --brief' "$call_log")
if DDC_READ_FAIL=1 run_brightness --monitor DP-1 >/dev/null 2>&1; then
fail "transient DDC read failure is reported"
fi
(( $(grep -c ' detect --brief' "$call_log") == detect_count + 1 )) || \
fail "transient DDC read failure is not retried immediately"
brightness=$(run_brightness --monitor DP-1)
[[ $brightness == "50" ]] || fail "transient DDC read failure is retried on the next invocation" "actual: $brightness"
(( $(grep -c ' detect --brief' "$call_log") == detect_count + 2 )) || \
fail "transient DDC read failure does not create a negative cache entry"
pass "transient DDC read failure is retried on the next invocation"
printf '7 80 0\n' >"$runtime_dir/omarchy-brightness-display-ddc/DP-1.bus"
get_count=$(grep -c ' getvcp 10 ' "$call_log")
DDC_MAXIMUM=100 run_brightness --no-osd --monitor DP-1 50%
(( $(grep -c ' getvcp 10 ' "$call_log") == get_count + 1 )) || \
fail "expired external brightness range is refreshed"
grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 50' "$call_log" >/dev/null || \
fail "expired external brightness range uses the refreshed maximum"
pass "expired external brightness range is refreshed"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/DP-1.bus"
DDC_CURRENT=4 DDC_MAXIMUM=100 run_brightness --no-osd --monitor DP-1 +5%
grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 5' "$call_log" >/dev/null || \
fail "external low brightness writes the one-percent target"
pass "external low brightness uses a one-percent step"
cat >"$mock_bin/hyprctl" <<'SH'
#!/bin/bash
printf '%s\n' '[
{"name":"DP-1","focused":true,"make":"HPN","model":"OMEN X 25f"},
{"name":"DP-2","focused":false,"make":"Apple Computer Inc","model":"StudioDisplay"}
]'
SH
chmod +x "$mock_bin/hyprctl"
PATH="$mock_bin:$PATH" "$ROOT/bin/omarchy-hyprland-monitor-focused-apple" DP-2 || \
fail "named Apple display is detected independently of focus"
if PATH="$mock_bin:$PATH" "$ROOT/bin/omarchy-hyprland-monitor-focused-apple"; then
fail "focused non-Apple display is not detected as Apple"
fi
pass "named Apple display is detected independently of focus"