diff --git a/bin/omarchy-brightness-display b/bin/omarchy-brightness-display index a6a1e8304bf..11d899476bf 100755 --- a/bin/omarchy-brightness-display +++ b/bin/omarchy-brightness-display @@ -35,6 +35,42 @@ monitor_is_internal() { [[ $monitor =~ ^(eDP|LVDS|DSI)- ]] } +# Hyprland monitor names are DRM connector names, so a backlight on the monitor's connected external +# connector is its own. Another card can expose a connector with the same name; when more than one is +# connected the name can't tell them apart, so neither backlight is used. Internal panels stay on +# omarchy-hw-display: GPU drivers can register an eDP backlight that doesn't drive the panel. +connector_backlight() { + local status connector="" subsystem + + if [[ -z $monitor ]] || monitor_is_internal; then + return 1 + fi + + for status in "${OMARCHY_DRM_PATH:-/sys/class/drm}"/card*-"$monitor"/status; do + [[ -e $status ]] || continue + + if [[ $(<"$status") == "connected" ]]; then + [[ -z $connector ]] || return 1 + connector="${status%/status}" + fi + done + + [[ -n $connector ]] || return 1 + + for subsystem in "$connector"/*/subsystem; do + if [[ $(readlink "$subsystem") == */class/backlight ]]; then + subsystem="${subsystem%/subsystem}" + printf '%s\n' "${subsystem##*/}" + return 0 + fi + done + + return 1 +} + +# A connector backlight that can't be read or written falls back to the display's other backends +device="$(connector_backlight)" + use_apple_display() { omarchy-hyprland-monitor-focused-apple "$monitor" } @@ -43,8 +79,47 @@ use_ddc_display() { [[ -n $monitor ]] && ! monitor_is_internal } +# Step a kernel backlight and show its new level, failing when it can't be read or written +step_backlight() { + local device="$1" step="$2" current target + + # Current brightness percentage + current=$(backlight_brightness "$device") || return 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. + if [[ $step == "+5%" ]]; then + if (( current < 5 )); then + (( target = current + 1 )) + else + (( target = current + 5 )) + fi + + (( target > 100 )) && target=100 + step="$target%" + elif [[ $step == "5%-" ]]; then + if (( current <= 5 )); then + (( target = current - 1 )) + else + (( target = current - 5 )) + fi + + (( target < 1 )) && target=1 + step="$target%" + fi + + # Set brightness of the display device. + brightnessctl -d "$device" set "$step" >/dev/null || return 1 + + # Show the new brightness in OSD + (( no_osd )) || omarchy-osd -i brightness -p "$(backlight_brightness "$device")" + return 0 +} + if (( $# == 0 )); then - if use_apple_display; then + if [[ -n $device ]] && backlight_brightness "$device"; then + exit + elif use_apple_display; then omarchy-brightness-display-apple exit elif use_ddc_display; then @@ -76,47 +151,19 @@ fi exec {lock_fd}>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display.lock" flock -n "$lock_fd" || exit 0 -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=$(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. - if [[ $step == "+5%" ]]; then - if (( current < 5 )); then - (( target = current + 1 )) - else - (( target = current + 5 )) - fi - - (( target > 100 )) && target=100 - step="$target%" - elif [[ $step == "5%-" ]]; then - if (( current <= 5 )); then - (( target = current - 1 )) +if [[ -z $device ]] || ! step_backlight "$device" "$step"; then + if use_apple_display; then + if (( no_osd )); then + omarchy-brightness-display-apple --no-osd "$step" else - (( target = current - 5 )) + omarchy-brightness-display-apple "$step" fi - - (( target < 1 )) && target=1 - step="$target%" + elif use_ddc_display; then + brightness="$(omarchy-brightness-display-ddc "$monitor" "$step")" || exit 1 + (( no_osd )) || omarchy-osd -i brightness -p "$brightness" + else + # The most likely panel backlight + device="$(omarchy-hw-display)" || exit 1 + step_backlight "$device" "$step" || exit 1 fi - - # Set brightness of the display device. - brightnessctl -d "$device" set "$step" >/dev/null - - # Show the new brightness in OSD - (( no_osd )) || omarchy-osd -i brightness -p "$(backlight_brightness "$device")" fi diff --git a/manual/33-monitors.md b/manual/33-monitors.md index e6a5e30e021..d84ab9c6ea3 100644 --- a/manual/33-monitors.md +++ b/manual/33-monitors.md @@ -44,11 +44,11 @@ You can also checkout [Hyprmon](https://github.com/erans/hyprmon/), if you'd lik ### Controlling brightness -Monitor brightness is controlled by the dedicated function keys for brightness up/down. If you hold down shift while pressing these, you'll go to maximum or minimum brightness. The keys control the display you're focused on, so external monitors that speak DDC/CI are adjusted the same way as the laptop screen. +Monitor brightness is controlled by the dedicated function keys for brightness up/down. If you hold down shift while pressing these, you'll go to maximum or minimum brightness. The keys control the display you're focused on, so external monitors are adjusted the same way as the laptop screen, either through a kernel backlight the monitor exposes or over DDC/CI. ### Apple Displays -If you're using an Apple display, the regular keyboard brightness keys will also automatically work, if you're focused on the Apple display. This is done through the `asdcontrol` command. +If you're using an Apple display, the regular keyboard brightness keys will also automatically work, if you're focused on the Apple display. This is done through the kernel backlight when the display exposes one, and through the `asdcontrol` command otherwise. Note that if you're using an Apple 6K XDR display, you may see a phantom screen in your `hyprctl monitors` listing. You can turn this off with something like `hl.monitor({ output = "DP-2", disabled = true })` via _Setup > Monitors_. diff --git a/shell/services/BrightnessKeys.qml b/shell/services/BrightnessKeys.qml index 0f14b141af0..276cfc3696d 100644 --- a/shell/services/BrightnessKeys.qml +++ b/shell/services/BrightnessKeys.qml @@ -11,7 +11,8 @@ import "BrightnessModel.js" as BrightnessModel // device, reading, writing and reading back, then an IPC client for the OSD. // They step, clamp and read back the way that script does, so either path // lands on the same level and OSD. External and Apple displays go through the -// script, which drives them over DDC or their own helper. +// script, which drives them through their connector's kernel backlight when +// they have one, else over DDC or their own helper. Item { id: root diff --git a/test/shell.d/brightness-display-test.sh b/test/shell.d/brightness-display-test.sh index 3e41c511ae0..a4c025e27c3 100755 --- a/test/shell.d/brightness-display-test.sh +++ b/test/shell.d/brightness-display-test.sh @@ -10,11 +10,38 @@ trap 'rm -rf "$test_tmp"' EXIT mock_bin="$test_tmp/bin" call_log="$test_tmp/calls" runtime_dir="$test_tmp/runtime" +drm_dir="$test_tmp/drm" mkdir -p "$mock_bin" "$runtime_dir" +# DP-3 has a connector backlight and DP-1 only a non-backlight child. eDP-1's i915-style backlight must +# not override omarchy-hw-display, and card1's disconnected DP-3 backlight must be ignored. DP-4 is +# connected on two cards, so card2's backlight can't be told apart from card1's plain monitor. +mkdir -p "$drm_dir/card2-DP-3/apple-DP-3-bl" "$drm_dir/card2-DP-1/power" "$drm_dir/card0-eDP-1/intel_backlight" \ + "$drm_dir/card1-DP-3/other-DP-3-bl" "$drm_dir/card2-DP-4/apple-DP-4-bl" "$drm_dir/card1-DP-4" +ln -s ../../../../class/backlight "$drm_dir/card2-DP-3/apple-DP-3-bl/subsystem" +ln -s ../../../../class/backlight "$drm_dir/card0-eDP-1/intel_backlight/subsystem" +ln -s ../../../../bus/platform "$drm_dir/card2-DP-1/power/subsystem" +ln -s ../../../../class/backlight "$drm_dir/card1-DP-3/other-DP-3-bl/subsystem" +ln -s ../../../../class/backlight "$drm_dir/card2-DP-4/apple-DP-4-bl/subsystem" +for connector in card2-DP-3 card2-DP-1 card0-eDP-1 card2-DP-4 card1-DP-4; do + printf 'connected\n' >"$drm_dir/$connector/status" +done +printf 'disconnected\n' >"$drm_dir/card1-DP-3/status" + cat >"$mock_bin/omarchy-hyprland-monitor-focused-apple" <<'SH' #!/bin/bash -exit 1 +printf 'omarchy-hyprland-monitor-focused-apple %s\n' "$*" >>"$CALL_LOG" +[[ ${APPLE_DISPLAY:-0} == "1" ]] +SH + +cat >"$mock_bin/omarchy-brightness-display-apple" <<'SH' +#!/bin/bash +printf 'omarchy-brightness-display-apple %s\n' "$*" >>"$CALL_LOG" +SH + +cat >"$mock_bin/omarchy-osd" <<'SH' +#!/bin/bash +printf 'omarchy-osd %s\n' "$*" >>"$CALL_LOG" SH cat >"$mock_bin/omarchy-hyprland-monitor-focused" <<'SH' @@ -31,7 +58,10 @@ cat >"$mock_bin/brightnessctl" <<'SH' #!/bin/bash printf 'brightnessctl %s\n' "$*" >>"$CALL_LOG" if [[ $* == *" -m"* ]]; then + [[ -n ${BRIGHTNESSCTL_READ_FAIL:-} && $* == "-d $BRIGHTNESSCTL_READ_FAIL "* ]] && exit 1 printf 'mock_backlight,backlight,40,40%%\n' +elif [[ -n ${BRIGHTNESSCTL_WRITE_FAIL:-} && $* == "-d $BRIGHTNESSCTL_WRITE_FAIL set "* ]]; then + exit 1 fi SH @@ -54,7 +84,7 @@ SH chmod +x "$mock_bin"/* run_brightness() { - CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" PATH="$mock_bin:$ROOT/bin:$PATH" \ + CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" OMARCHY_DRM_PATH="$drm_dir" PATH="$mock_bin:$ROOT/bin:$PATH" \ "$ROOT/bin/omarchy-brightness-display" "$@" } @@ -129,6 +159,72 @@ grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 5' "$call_log" > fail "external low brightness writes the one-percent target" pass "external low brightness uses a one-percent step" +: >"$call_log" +brightness=$(APPLE_DISPLAY=1 run_brightness --monitor DP-3) +[[ $brightness == "40" ]] || fail "connector backlight reports brightness" "actual: $brightness" +grep -F 'brightnessctl -d apple-DP-3-bl -m' "$call_log" >/dev/null || \ + fail "connector backlight is queried through brightnessctl" +pass "connector backlight reports brightness" + +: >"$call_log" +APPLE_DISPLAY=1 run_brightness --monitor DP-3 +5% +grep -Fx 'brightnessctl -d apple-DP-3-bl set 45%' "$call_log" >/dev/null || \ + fail "connector backlight steps through brightnessctl" "$(cat "$call_log")" +grep -Fx 'omarchy-osd -i brightness -p 40' "$call_log" >/dev/null || \ + fail "connector backlight shows the OSD" "$(cat "$call_log")" +if grep -E 'omarchy-brightness-display-apple|omarchy-hyprland-monitor-focused-apple|ddcutil' "$call_log"; then + fail "connector backlight skips the Apple and DDC backends" +fi +pass "connector backlight wins over the Apple and DDC backends" + +: >"$call_log" +FOCUSED_MONITOR=DP-3 run_brightness --no-osd 1%- +grep -Fx 'brightnessctl -d apple-DP-3-bl set 1%-' "$call_log" >/dev/null || \ + fail "focused monitor's connector backlight takes precise steps" "$(cat "$call_log")" +pass "focused monitor's connector backlight takes precise steps" + +: >"$call_log" +APPLE_DISPLAY=1 run_brightness --no-osd --monitor DP-1 +5% +grep -Fx 'omarchy-brightness-display-apple --no-osd +5%' "$call_log" >/dev/null || \ + fail "Apple display without a connector backlight keeps asdcontrol" "$(cat "$call_log")" +pass "Apple display without a connector backlight keeps asdcontrol" + +: >"$call_log" +APPLE_DISPLAY=1 BRIGHTNESSCTL_WRITE_FAIL=apple-DP-3-bl run_brightness --monitor DP-3 +5% +grep -Fx 'omarchy-brightness-display-apple +5%' "$call_log" >/dev/null || \ + fail "unwritable connector backlight falls back to asdcontrol" "$(cat "$call_log")" +if grep -F 'omarchy-osd' "$call_log"; then + fail "unwritable connector backlight shows no OSD of its own" +fi +pass "unwritable connector backlight falls back to asdcontrol" + +: >"$call_log" +rm -f "$runtime_dir/omarchy-brightness-display-ddc/DP-3.bus" +DDC_CONNECTOR=DP-3 DDC_CURRENT=40 DDC_MAXIMUM=100 BRIGHTNESSCTL_WRITE_FAIL=apple-DP-3-bl \ + run_brightness --no-osd --monitor DP-3 30% +grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp 10 30' "$call_log" >/dev/null || \ + fail "unwritable connector backlight falls back to DDC with the requested step" "$(cat "$call_log")" +pass "unwritable connector backlight falls back to DDC with the requested step" + +: >"$call_log" +APPLE_DISPLAY=1 BRIGHTNESSCTL_READ_FAIL=apple-DP-3-bl run_brightness --monitor DP-3 >/dev/null +grep -Fx 'omarchy-brightness-display-apple ' "$call_log" >/dev/null || \ + fail "unreadable connector backlight falls back to asdcontrol for its level" "$(cat "$call_log")" +pass "unreadable connector backlight falls back to asdcontrol for its level" + +stderr=$(APPLE_DISPLAY=1 run_brightness --no-osd --monitor HEADLESS-1 +5% 2>&1 >/dev/null) +[[ -z $stderr ]] || fail "monitor without a DRM connector looks up its backlight quietly" "$stderr" +pass "monitor without a DRM connector looks up its backlight quietly" + +: >"$call_log" +APPLE_DISPLAY=1 run_brightness --no-osd --monitor DP-4 +5% +if grep -F 'apple-DP-4-bl' "$call_log"; then + fail "connector name shared by two connected cards uses neither backlight" +fi +grep -Fx 'omarchy-brightness-display-apple --no-osd +5%' "$call_log" >/dev/null || \ + fail "connector name shared by two connected cards keeps the other backends" "$(cat "$call_log")" +pass "connector name shared by two connected cards uses neither backlight" + cat >"$mock_bin/hyprctl" <<'SH' #!/bin/bash printf '%s\n' '[