From c69d1c46cda95d01de28680df3fbb8f309d76eb4 Mon Sep 17 00:00:00 2001 From: Kiril Staikov Date: Fri, 23 Oct 2026 11:56:51 -0400 Subject: [PATCH 1/4] Sleep external monitors over DDC/CI instead of DPMS behind a ddc-blank toggle Some HDMI 2.1 monitors drop hotplug when the output is turned off. Behind the lock screen that removes and re-adds the display every ~40 s, each time relighting it, and after a real standby the link needs a long FRL training to come back (40-50 s on a Dell U3224KB with amdgpu 7.2, which gives up after one attempt). Keeping the link up avoids both. With `omarchy toggle ddc-blank` on and only external monitors enabled, `omarchy-brightness-display off|on` now writes the monitor's VCP power mode (D6 = 04/01) through omarchy-brightness-display-ddc instead of dispatching DPMS. The write is unverified, since a monitor that is off no longer answers the read-back, and wakes are throttled to one a second because the lock screen requests one on every key press and pointer move. A built-in panel, a monitor that refuses the command, or a monitor that will not wake falls back to DPMS, so nothing is left lit or dark. Co-Authored-By: Claude Fable 5.1 --- bin/omarchy-brightness-display | 54 ++++++++++++++++- bin/omarchy-brightness-display-ddc | 37 +++++++++++- bin/omarchy-toggle-ddc-blank | 17 ++++++ default/i18n/en/cli.json | 2 + default/i18n/en/menu.json | 6 ++ default/omarchy/omarchy-menu.jsonc | 2 + test/shell.d/brightness-display-test.sh | 78 +++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 5 deletions(-) create mode 100755 bin/omarchy-toggle-ddc-blank diff --git a/bin/omarchy-brightness-display b/bin/omarchy-brightness-display index 5a3e534e27d..8e15f443721 100755 --- a/bin/omarchy-brightness-display +++ b/bin/omarchy-brightness-display @@ -3,6 +3,13 @@ # 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 +# +# `off` and `on` normally blank and relight every display with DPMS. With the +# ddc-blank toggle on (omarchy toggle ddc-blank) and only external monitors +# attached, they instead put each monitor to sleep and wake it over DDC/CI, +# so the video link stays up. Some HDMI 2.1 monitors drop hotplug on DPMS off, +# which removes and re-adds the output every few seconds while locked, and +# need a long link training to come back; keeping the link up avoids both. no_osd=0 monitor="" @@ -48,18 +55,51 @@ use_ddc_display() { # I2C buses, slowly, so only a connector with a ddc node is probed. Everywhere # else every external monitor is. monitor_may_have_ddc() { + local name=${1:-$monitor} local displays_conf=/usr/share/omarchy-platform/displays.conf directive argument [[ -r $displays_conf ]] || return 0 while IFS=$' \t\r' read -r directive argument || [[ -n $directive ]]; do if [[ $directive == "ddc-require-connector-ddc" && -z $argument ]]; then - compgen -G "/sys/class/drm/card*-$monitor/ddc" >/dev/null + compgen -G "/sys/class/drm/card*-$name/ddc" >/dev/null return fi done <"$displays_conf" return 0 } +# The enabled monitors when every one of them can be slept over DDC/CI: all +# external and, on a platform that says so, all with a ddc node. A built-in +# panel has no DDC, so a laptop with its lid open keeps DPMS for everything; +# mixing the two would leave one display lit. +ddc_blank_monitors() { + omarchy-toggle-enabled ddc-blank || return 1 + local names name + names=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.disabled == false) | .name') || return 1 + [[ -n $names ]] || return 1 + for name in $names; do + [[ $name =~ ^(eDP|LVDS|DSI)- ]] && return 1 + monitor_may_have_ddc "$name" || return 1 + done + printf '%s\n' "$names" +} + +# Sleep or wake every DDC-blank monitor at once; one slow monitor must not +# delay the others. Fails if any monitor did not take the command. +ddc_blank_all() { + local action="$1" names name pid status=0 + names=$(ddc_blank_monitors) || return 1 + local -a pids=() + for name in $names; do + omarchy-brightness-display-ddc "$name" "$action" & + pids+=("$!") + done + for pid in "${pids[@]}"; do + wait "$pid" || status=1 + done + return "$status" +} + # The kernel backlight is the built-in panel's, so an external monitor without # DDC fails rather than dimming the panel. use_backlight_display() { @@ -85,9 +125,19 @@ fi step="$1" if [[ $step == "off" ]]; then - hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 + # A monitor that did not take the DDC command still has to go dark: DPMS + # then blanks everything, link drop and all, rather than leave it lit. + if ! ddc_blank_all off; then + hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 + fi exit 0 elif [[ $step == "on" ]]; then + if ddc_blank_monitors >/dev/null && ! ddc_blank_all on; then + # A monitor that ignores the DDC wake only comes back on a fresh signal, + # so give it the modeset it would have had with DPMS. + hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 + sleep 1 + fi # Skip the dispatch when every active display is already lit: a redundant # DPMS enable right after system resume forces another modeset, which blanks # the panel for a beat (visible flash at the unlock screen). diff --git a/bin/omarchy-brightness-display-ddc b/bin/omarchy-brightness-display-ddc index 9bb6f82d357..09e8a8fb691 100755 --- a/bin/omarchy-brightness-display-ddc +++ b/bin/omarchy-brightness-display-ddc @@ -1,8 +1,8 @@ #!/bin/bash -# omarchy:summary=Show or adjust DDC/CI display brightness for a Hyprland monitor. -# omarchy:args= [+N%|N%-|N%] -# omarchy:examples=omarchy-brightness-display-ddc DP-1 | omarchy-brightness-display-ddc DP-1 50% +# omarchy:summary=Show or adjust DDC/CI display brightness, or sleep and wake a Hyprland monitor. +# omarchy:args= [+N%|N%-|N%|off|on] +# omarchy:examples=omarchy-brightness-display-ddc DP-1 | omarchy-brightness-display-ddc DP-1 50% | omarchy-brightness-display-ddc HDMI-A-1 off monitor="${1:-}" step="${2:-}" @@ -126,6 +126,37 @@ if [[ -z $step ]]; then exit 0 fi +# VCP D6 is the monitor's power mode: 01 on, 04 off. Written without +# verification: a monitor that is off no longer answers the read-back, so a +# verified write reports failure for a command it applied. Wake requests arrive +# in bursts (the lock screen asks on every key press and pointer move), and each +# DDC write holds the bus for a few hundred milliseconds, so one wake a second is +# plenty. +if [[ $step == "off" || $step == "on" ]]; then + bus="$(find_bus)" || exit 1 + wake_file="$cache_dir/$cache_name.wake" + if [[ $step == "on" ]]; then + now=$(date +%s) + if [[ -r $wake_file ]]; then + read -r woke_at <"$wake_file" || true + if [[ $woke_at =~ ^[0-9]+$ ]] && (( now - woke_at < 1 )); then + exit 0 + fi + fi + mkdir -p "$cache_dir" 2>/dev/null || true + printf '%s\n' "$now" >"$wake_file" 2>/dev/null || true + value=01 + else + rm -f "$wake_file" + value=04 + fi + if ! ddcutil --bus "$bus" --skip-ddc-checks --noverify setvcp D6 "$value" >/dev/null 2>&1; then + rm -f "$cache_file" + exit 1 + fi + exit 0 +fi + if [[ $step =~ ^([0-9]+)%$ ]]; then target="${BASH_REMATCH[1]}" range_cache_fresh=0 diff --git a/bin/omarchy-toggle-ddc-blank b/bin/omarchy-toggle-ddc-blank new file mode 100755 index 00000000000..b4129c43728 --- /dev/null +++ b/bin/omarchy-toggle-ddc-blank @@ -0,0 +1,17 @@ +#!/bin/bash + +# omarchy:summary=Toggle sleeping external monitors over DDC/CI instead of DPMS when the screen blanks + +# Some HDMI 2.1 monitors drop hotplug when the output is turned off, which +# removes and re-adds the display every few seconds behind the lock screen, +# and then need a long link training to come back. Sleeping the monitor over +# DDC/CI keeps the link up on both counts. Applies only while every enabled +# monitor is external; a built-in panel keeps DPMS. + +omarchy-toggle ddc-blank + +if omarchy-toggle-enabled ddc-blank; then + omarchy-notification-send -g 󰍹 "$(omarchy-i18n ddcBlank.enabled)" +else + omarchy-notification-send -g 󰍹 "$(omarchy-i18n ddcBlank.disabled)" +fi diff --git a/default/i18n/en/cli.json b/default/i18n/en/cli.json index 9db37a3acee..e6a65ad83bb 100644 --- a/default/i18n/en/cli.json +++ b/default/i18n/en/cli.json @@ -103,6 +103,8 @@ "crash.captureEnabled": "Crash capture enabled", "crash.diagnose": "Click to diagnose with AI", "crash.processCrashed": "Process crashed: %1", + "ddcBlank.disabled": "Monitors now sleep with DPMS", + "ddcBlank.enabled": "Monitors now sleep over DDC/CI", "default.browserSet": "%1 is now the default browser", "default.editorSet": "%1 is now the default editor", "default.terminalSet": "%1 is now the default terminal", diff --git a/default/i18n/en/menu.json b/default/i18n/en/menu.json index 64177c4cfa2..5f3c8ce2a40 100644 --- a/default/i18n/en/menu.json +++ b/default/i18n/en/menu.json @@ -318,6 +318,12 @@ "trigger.toggle.screensaver-off": { "label": "Screensaver Off" }, + "trigger.toggle.ddc-blank-on": { + "label": "DDC Monitor Sleep On" + }, + "trigger.toggle.ddc-blank-off": { + "label": "DDC Monitor Sleep Off" + }, "trigger.toggle.crash-capture-on": { "label": "Crash Capture On" }, diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 2aed5cae8e7..5fce9f47376 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -134,6 +134,8 @@ "trigger.toggle.nightlight-off": {"icon":"󰔎","label":"Nightlight Off","when":"[[ $(omarchy-toggle-nightlight --status) == *'\"enabled\":true'* ]]","action":"omarchy-toggle-nightlight"}, "trigger.toggle.screensaver-on": {"icon":"󱄄","label":"Screensaver On","when":"omarchy-toggle-enabled screensaver-off","action":"omarchy-toggle-screensaver"}, "trigger.toggle.screensaver-off": {"icon":"󱄄","label":"Screensaver Off","when":"! omarchy-toggle-enabled screensaver-off","action":"omarchy-toggle-screensaver"}, + "trigger.toggle.ddc-blank-on": {"icon":"󰍹","label":"DDC Monitor Sleep On","when":"! omarchy-toggle-enabled ddc-blank","action":"omarchy-toggle-ddc-blank"}, + "trigger.toggle.ddc-blank-off": {"icon":"󰍹","label":"DDC Monitor Sleep Off","when":"omarchy-toggle-enabled ddc-blank","action":"omarchy-toggle-ddc-blank"}, "trigger.toggle.crash-capture-on": {"icon":"󱚡","label":"Crash Capture On","when":"omarchy-toggle-enabled crash-capture-off","action":"omarchy-toggle-crash-capture"}, "trigger.toggle.crash-capture-off": {"icon":"󱚡","label":"Crash Capture Off","when":"! omarchy-toggle-enabled crash-capture-off","action":"omarchy-toggle-crash-capture"}, "trigger.emoji": {"icon":"","label":"Emoji","aliases":["emoji","emojis"],"action":"omarchy-menu-emoji"}, diff --git a/test/shell.d/brightness-display-test.sh b/test/shell.d/brightness-display-test.sh index 22586c1fe67..a0947ea4c53 100755 --- a/test/shell.d/brightness-display-test.sh +++ b/test/shell.d/brightness-display-test.sh @@ -220,3 +220,81 @@ for conf in "" "ddc-require-connector-ddc extra" "ddc-future"; do done rm -f "$platform_root/displays.conf" pass "every external monitor is probed without the directive" + +# `off` and `on` blank with DPMS unless the ddc-blank toggle is on and every +# enabled display is external: then the monitor sleeps over DDC/CI and the +# video link stays up (some HDMI 2.1 monitors drop hotplug on DPMS off). +home_dir="$test_tmp/home" +mkdir -p "$home_dir" + +cat >"$mock_bin/hyprctl" <<'SH' +#!/bin/bash +if [[ $1 == "dispatch" ]]; then + printf 'hyprctl dispatch %s\n' "$2" >>"$CALL_LOG" + exit 0 +fi +monitors=${MONITORS_JSON:-} +[[ -n $monitors ]] || monitors='[{"name":"HDMI-A-1","disabled":false,"dpmsStatus":true}]' +printf '%s\n' "$monitors" +SH + +cat >"$mock_bin/ddcutil" <<'SH' +#!/bin/bash +printf 'ddcutil %s\n' "$*" >>"$CALL_LOG" +if [[ $* == *" detect --brief"* ]]; then + cat <"$call_log" +run_power off +grep -F 'hyprctl dispatch hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ + fail "off blanks with DPMS while the toggle is off" +! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "off sends no DDC power command while the toggle is off" +pass "off blanks with DPMS while the toggle is off" + +mkdir -p "$home_dir/.local/state/omarchy/toggles" +: >"$home_dir/.local/state/omarchy/toggles/ddc-blank" +rm -f "$runtime_dir/omarchy-brightness-display-ddc/"* +: >"$call_log" +run_power off +grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp D6 04' "$call_log" >/dev/null || \ + fail "the ddc-blank toggle sleeps an external monitor over DDC/CI" +! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "a monitor asleep over DDC/CI is not also turned off with DPMS" +pass "the ddc-blank toggle sleeps an external monitor over DDC/CI instead of DPMS" + +: >"$call_log" +MONITORS_JSON='[{"name":"eDP-1","disabled":false,"dpmsStatus":true},{"name":"HDMI-A-1","disabled":false,"dpmsStatus":true}]' run_power off +grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ + fail "a built-in panel keeps DPMS for every display" +! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "no display sleeps over DDC/CI beside a built-in panel" +pass "a built-in panel keeps DPMS for every display" + +: >"$call_log" +run_power on +run_power on +(( $(grep -c 'setvcp D6 01' "$call_log") == 1 )) || \ + fail "a burst of wake requests writes DDC once" "$(<"$call_log")" +! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "waking over DDC/CI leaves a lit display's DPMS alone" +pass "a burst of wake requests writes DDC once and leaves lit displays alone" + +rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*.wake +: >"$call_log" +DDC_SET_FAIL=1 run_power off +grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ + fail "a monitor that refuses the DDC command is blanked with DPMS" +pass "a monitor that refuses the DDC command is blanked with DPMS" From d74449848e0b7e03be38c0b4475156a5ad9b895f Mon Sep 17 00:00:00 2001 From: Kiril Staikov Date: Sat, 24 Oct 2026 00:17:12 -0400 Subject: [PATCH 2/4] Wake the monitors that were slept over DDC/CI and serialize blank against wake `on` picked the monitors to wake from the current monitor set, so a laptop whose lid reopened between the blank and the wake found a built-in panel, sent no DDC wake, and left the external monitors dark: DPMS still read them as on. Each monitor that takes the sleep is now marked asleep under $XDG_RUNTIME_DIR/omarchy-ddc-blank/ and `on` wakes exactly those, which also makes a burst of wake requests write once per monitor without a clock-based throttle, and removes the test that depended on two runs sharing a second. The lock screen starts blank and wake without waiting for one another, so a blank slow to fail over to DPMS could turn the displays off after a newer wake had lit them. Blank and wake now share a lock; the wake waits and has the last word. The test's mock compositor remembers its DPMS state, so a wake after a DPMS blank has something to turn back on. Co-Authored-By: Claude Fable 5.1 --- bin/omarchy-brightness-display | 55 +++++++++++++++++---- bin/omarchy-brightness-display-ddc | 16 +----- test/shell.d/brightness-display-test.sh | 65 ++++++++++++++++++++++--- 3 files changed, 106 insertions(+), 30 deletions(-) diff --git a/bin/omarchy-brightness-display b/bin/omarchy-brightness-display index 8e15f443721..e7be48a3451 100755 --- a/bin/omarchy-brightness-display +++ b/bin/omarchy-brightness-display @@ -84,18 +84,52 @@ ddc_blank_monitors() { printf '%s\n' "$names" } -# Sleep or wake every DDC-blank monitor at once; one slow monitor must not -# delay the others. Fails if any monitor did not take the command. +# A marker per monitor asleep over DDC/CI, so a wake reaches the monitors that +# were slept even after the monitor set changed in between (a laptop lid +# reopened before the wake would otherwise leave them dark: DPMS still reads +# them as on). Blank and wake share one lock: the lock screen starts them +# without waiting for one another, and a slow blank that fails over to DPMS +# must not turn the displays off after a newer wake has lit them. +blank_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-ddc-blank" + +ddc_blank_lock() { + mkdir -p "$blank_dir" 2>/dev/null || return 0 + exec {blank_fd}>"$blank_dir/.lock" || return 0 + flock -w 10 "$blank_fd" || true +} + +ddc_blank_marker() { + printf '%s/%s\n' "$blank_dir" "${1//[^[:alnum:]_.-]/_}" +} + +ddc_blank_asleep() { + local marker + for marker in "$blank_dir"/*; do + [[ -f $marker ]] && cat "$marker" + done +} + +# Sleep or wake the named monitors at once; one slow monitor must not delay +# the others. A monitor that took the sleep is marked asleep until a wake has +# been tried on it. Fails if any monitor did not take the command. ddc_blank_all() { - local action="$1" names name pid status=0 - names=$(ddc_blank_monitors) || return 1 - local -a pids=() + local action="$1" names="$2" name status=0 i + local -a monitors=() pids=() for name in $names; do omarchy-brightness-display-ddc "$name" "$action" & + monitors+=("$name") pids+=("$!") done - for pid in "${pids[@]}"; do - wait "$pid" || status=1 + for i in "${!pids[@]}"; do + name=${monitors[i]} + if ! wait "${pids[i]}"; then + status=1 + rm -f "$(ddc_blank_marker "$name")" + elif [[ $action == "off" ]]; then + printf '%s\n' "$name" >"$(ddc_blank_marker "$name")" 2>/dev/null || true + else + rm -f "$(ddc_blank_marker "$name")" + fi done return "$status" } @@ -125,14 +159,17 @@ fi step="$1" if [[ $step == "off" ]]; then + ddc_blank_lock # A monitor that did not take the DDC command still has to go dark: DPMS # then blanks everything, link drop and all, rather than leave it lit. - if ! ddc_blank_all off; then + if ! names=$(ddc_blank_monitors) || ! ddc_blank_all off "$names"; then hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 fi exit 0 elif [[ $step == "on" ]]; then - if ddc_blank_monitors >/dev/null && ! ddc_blank_all on; then + ddc_blank_lock + asleep=$(ddc_blank_asleep) + if [[ -n $asleep ]] && ! ddc_blank_all on "$asleep"; then # A monitor that ignores the DDC wake only comes back on a fresh signal, # so give it the modeset it would have had with DPMS. hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 diff --git a/bin/omarchy-brightness-display-ddc b/bin/omarchy-brightness-display-ddc index 09e8a8fb691..95e555c877d 100755 --- a/bin/omarchy-brightness-display-ddc +++ b/bin/omarchy-brightness-display-ddc @@ -128,26 +128,12 @@ fi # VCP D6 is the monitor's power mode: 01 on, 04 off. Written without # verification: a monitor that is off no longer answers the read-back, so a -# verified write reports failure for a command it applied. Wake requests arrive -# in bursts (the lock screen asks on every key press and pointer move), and each -# DDC write holds the bus for a few hundred milliseconds, so one wake a second is -# plenty. +# verified write reports failure for a command it applied. if [[ $step == "off" || $step == "on" ]]; then bus="$(find_bus)" || exit 1 - wake_file="$cache_dir/$cache_name.wake" if [[ $step == "on" ]]; then - now=$(date +%s) - if [[ -r $wake_file ]]; then - read -r woke_at <"$wake_file" || true - if [[ $woke_at =~ ^[0-9]+$ ]] && (( now - woke_at < 1 )); then - exit 0 - fi - fi - mkdir -p "$cache_dir" 2>/dev/null || true - printf '%s\n' "$now" >"$wake_file" 2>/dev/null || true value=01 else - rm -f "$wake_file" value=04 fi if ! ddcutil --bus "$bus" --skip-ddc-checks --noverify setvcp D6 "$value" >/dev/null 2>&1; then diff --git a/test/shell.d/brightness-display-test.sh b/test/shell.d/brightness-display-test.sh index a0947ea4c53..c00f8c0212c 100755 --- a/test/shell.d/brightness-display-test.sh +++ b/test/shell.d/brightness-display-test.sh @@ -227,14 +227,26 @@ pass "every external monitor is probed without the directive" home_dir="$test_tmp/home" mkdir -p "$home_dir" +# The mock compositor remembers its DPMS state, so a wake after a DPMS blank +# has something to turn back on. +dpms_state="$test_tmp/dpms" cat >"$mock_bin/hyprctl" <<'SH' #!/bin/bash if [[ $1 == "dispatch" ]]; then printf 'hyprctl dispatch %s\n' "$2" >>"$CALL_LOG" + if [[ $2 == *disable* ]]; then + printf 'false\n' >"$DPMS_STATE" + else + printf 'true\n' >"$DPMS_STATE" + fi exit 0 fi monitors=${MONITORS_JSON:-} -[[ -n $monitors ]] || monitors='[{"name":"HDMI-A-1","disabled":false,"dpmsStatus":true}]' +if [[ -z $monitors ]]; then + dpms=true + [[ -r $DPMS_STATE ]] && read -r dpms <"$DPMS_STATE" + monitors='[{"name":"HDMI-A-1","disabled":false,"dpmsStatus":'$dpms'}]' +fi printf '%s\n' "$monitors" SH @@ -248,6 +260,7 @@ Display 1 DRM connector: card1-HDMI-A-1 EOT elif [[ $* == *" setvcp D6 "* ]]; then + sleep "${DDC_SET_DELAY:-0}" if [[ ${DDC_SET_FAIL:-0} == "1" ]]; then exit 1 fi @@ -256,7 +269,7 @@ SH chmod +x "$mock_bin/hyprctl" "$mock_bin/ddcutil" run_power() { - CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" HOME="$home_dir" \ + CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" HOME="$home_dir" DPMS_STATE="$dpms_state" \ PATH="$mock_bin:$ROOT/bin:$PATH" "$brightness_display" "$@" } @@ -284,17 +297,57 @@ grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ ! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "no display sleeps over DDC/CI beside a built-in panel" pass "a built-in panel keeps DPMS for every display" +# The external monitor is still asleep over DDC/CI from the earlier blank, and +# a built-in panel that came back since (lid reopened) does not stand in the +# way of waking it: DPMS would read it as on and leave it dark. +: >"$call_log" +rm -f "$dpms_state" +MONITORS_JSON='[{"name":"eDP-1","disabled":false,"dpmsStatus":true},{"name":"HDMI-A-1","disabled":false,"dpmsStatus":true}]' run_power on +grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp D6 01' "$call_log" >/dev/null || \ + fail "a monitor slept over DDC/CI is woken over DDC/CI whatever the monitor set has become" "$(<"$call_log")" +! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "waking over DDC/CI leaves a lit display's DPMS alone" +pass "a monitor slept over DDC/CI is woken whatever the monitor set has become" + +# Every key press and pointer move asks for a wake; only the monitors still +# asleep get one. : >"$call_log" run_power on run_power on -(( $(grep -c 'setvcp D6 01' "$call_log") == 1 )) || \ - fail "a burst of wake requests writes DDC once" "$(<"$call_log")" -! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "waking over DDC/CI leaves a lit display's DPMS alone" +! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "a monitor already woken gets no second DDC wake" "$(<"$call_log")" +! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "a burst of wake requests leaves lit displays alone" pass "a burst of wake requests writes DDC once and leaves lit displays alone" -rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*.wake : >"$call_log" DDC_SET_FAIL=1 run_power off grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ fail "a monitor that refuses the DDC command is blanked with DPMS" pass "a monitor that refuses the DDC command is blanked with DPMS" + +: >"$call_log" +run_power on +! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "a monitor blanked with DPMS gets no DDC wake" "$(<"$call_log")" +grep -F 'hl.dsp.dpms({ action = "enable" })' "$call_log" >/dev/null || \ + fail "a monitor blanked with DPMS is woken with DPMS" "$(<"$call_log")" +pass "a monitor blanked with DPMS is woken with DPMS" + +# A monitor that takes the sleep but ignores the wake only comes back on a +# fresh signal: a DPMS cycle. +: >"$call_log" +run_power off +DDC_SET_FAIL=1 run_power on +[[ $(grep -F 'hl.dsp.dpms' "$call_log" | tr -d '\n') == *disable*enable* ]] || \ + fail "a monitor that ignores the DDC wake gets a DPMS cycle" "$(<"$call_log")" +pass "a monitor that ignores the DDC wake gets a DPMS cycle" + +# The lock screen starts blank and wake without waiting for one another. A +# blank that is slow to fail must not fall back to DPMS after a newer wake has +# lit the screen: the wake waits for it and has the last word. +: >"$call_log" +DDC_SET_DELAY=1 DDC_SET_FAIL=1 run_power off & +blank_pid=$! +sleep 0.3 +run_power on +wait "$blank_pid" +[[ $(grep -F 'hl.dsp.dpms' "$call_log" | tail -n 1) == *enable* ]] || \ + fail "a wake that arrives during a slow blank still ends with the display lit" "$(<"$call_log")" +pass "a wake that arrives during a slow blank still ends with the display lit" From 29da7915f6ce1f9d1fd5fb88fc03cf5c014a51ea Mon Sep 17 00:00:00 2001 From: Kiril Staikov Date: Sun, 25 Oct 2026 11:43:08 -0400 Subject: [PATCH 3/4] Keep the DDC blank lock in the runtime directory and wait for it without a timeout The lock and the asleep markers were falling back to a shared /tmp path, where another user could plant a symlink for the truncating open. They now live in $XDG_RUNTIME_DIR only; without one the toggle is ignored and DPMS blanks as before. The lock file is opened for append, and the lock is taken without a timeout: the timeout path went ahead unserialized, which was the race the lock exists to prevent. Co-Authored-By: Claude Fable 5.1 --- bin/omarchy-brightness-display | 29 +++++++++++++++---------- test/shell.d/brightness-display-test.sh | 11 ++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/bin/omarchy-brightness-display b/bin/omarchy-brightness-display index e7be48a3451..9ab166e9205 100755 --- a/bin/omarchy-brightness-display +++ b/bin/omarchy-brightness-display @@ -89,13 +89,22 @@ ddc_blank_monitors() { # reopened before the wake would otherwise leave them dark: DPMS still reads # them as on). Blank and wake share one lock: the lock screen starts them # without waiting for one another, and a slow blank that fails over to DPMS -# must not turn the displays off after a newer wake has lit them. -blank_dir="${XDG_RUNTIME_DIR:-/tmp}/omarchy-ddc-blank" - +# must not turn the displays off after a newer wake has lit them. Both live +# in the private runtime directory: a shared directory would let another +# user plant a symlink where the lock is opened. Without one there is nowhere +# safe to put them and the displays keep DPMS. +blank_dir="" +[[ -n ${XDG_RUNTIME_DIR:-} ]] && blank_dir="$XDG_RUNTIME_DIR/omarchy-ddc-blank" + +# Waits for the lock however long the other side holds it. A wake must run +# after the blank in flight, and giving up after a timeout would mean acting +# without the ordering the lock is for; nothing holds it longer than its DDC +# writes, which the I2C layer bounds. ddc_blank_lock() { - mkdir -p "$blank_dir" 2>/dev/null || return 0 - exec {blank_fd}>"$blank_dir/.lock" || return 0 - flock -w 10 "$blank_fd" || true + [[ -n $blank_dir ]] || return 1 + mkdir -m 700 -p "$blank_dir" 2>/dev/null || return 1 + exec {blank_fd}>>"$blank_dir/.lock" || return 1 + flock "$blank_fd" } ddc_blank_marker() { @@ -104,6 +113,7 @@ ddc_blank_marker() { ddc_blank_asleep() { local marker + [[ -n $blank_dir ]] || return 0 for marker in "$blank_dir"/*; do [[ -f $marker ]] && cat "$marker" done @@ -159,17 +169,14 @@ fi step="$1" if [[ $step == "off" ]]; then - ddc_blank_lock # A monitor that did not take the DDC command still has to go dark: DPMS # then blanks everything, link drop and all, rather than leave it lit. - if ! names=$(ddc_blank_monitors) || ! ddc_blank_all off "$names"; then + if ! ddc_blank_lock || ! names=$(ddc_blank_monitors) || ! ddc_blank_all off "$names"; then hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 fi exit 0 elif [[ $step == "on" ]]; then - ddc_blank_lock - asleep=$(ddc_blank_asleep) - if [[ -n $asleep ]] && ! ddc_blank_all on "$asleep"; then + if ddc_blank_lock && asleep=$(ddc_blank_asleep) && [[ -n $asleep ]] && ! ddc_blank_all on "$asleep"; then # A monitor that ignores the DDC wake only comes back on a fresh signal, # so give it the modeset it would have had with DPMS. hyprctl dispatch 'hl.dsp.dpms({ action = "disable" })' >/dev/null 2>&1 diff --git a/test/shell.d/brightness-display-test.sh b/test/shell.d/brightness-display-test.sh index c00f8c0212c..d8d8bbf9213 100755 --- a/test/shell.d/brightness-display-test.sh +++ b/test/shell.d/brightness-display-test.sh @@ -290,6 +290,17 @@ grep -F 'ddcutil --bus 7 --skip-ddc-checks --noverify setvcp D6 04' "$call_log" ! grep -F 'hl.dsp.dpms' "$call_log" >/dev/null || fail "a monitor asleep over DDC/CI is not also turned off with DPMS" pass "the ddc-blank toggle sleeps an external monitor over DDC/CI instead of DPMS" +# The lock and the asleep markers live in the private runtime directory; a +# shared one would let another user plant a symlink where the lock is opened. +# Without it the toggle is ignored and DPMS blanks as before. +: >"$call_log" +CALL_LOG="$call_log" HOME="$home_dir" DPMS_STATE="$dpms_state" PATH="$mock_bin:$ROOT/bin:$PATH" \ + env -u XDG_RUNTIME_DIR "$brightness_display" off +grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ + fail "without a runtime directory the toggle is ignored and DPMS blanks" "$(<"$call_log")" +! grep -F ' setvcp D6 ' "$call_log" >/dev/null || fail "no DDC sleep without a runtime directory to track it in" +pass "without a private runtime directory the toggle is ignored and DPMS blanks" + : >"$call_log" MONITORS_JSON='[{"name":"eDP-1","disabled":false,"dpmsStatus":true},{"name":"HDMI-A-1","disabled":false,"dpmsStatus":true}]' run_power off grep -F 'hl.dsp.dpms({ action = "disable" })' "$call_log" >/dev/null || \ From 1147f0cef55b0983b12f4f18c471f4afadffc9e6 Mon Sep 17 00:00:00 2001 From: Kiril Staikov Date: Mon, 2 Nov 2026 11:28:54 -0500 Subject: [PATCH 4/4] Start the wake in the ordering test only once the slow blank holds the lock The test slept 0.3 s and assumed the background blank had taken the lock by then; on a loaded runner the wake could finish first and the assertion fail with the lock working. The ddcutil mock now marks when its delayed write begins, and the test waits for that mark before sending the wake. Co-Authored-By: Claude Fable 5.1 --- test/shell.d/brightness-display-test.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/shell.d/brightness-display-test.sh b/test/shell.d/brightness-display-test.sh index d8d8bbf9213..5cdbfd959a4 100755 --- a/test/shell.d/brightness-display-test.sh +++ b/test/shell.d/brightness-display-test.sh @@ -260,6 +260,7 @@ Display 1 DRM connector: card1-HDMI-A-1 EOT elif [[ $* == *" setvcp D6 "* ]]; then + [[ -n ${DDC_SET_STARTED:-} ]] && : >"$DDC_SET_STARTED" sleep "${DDC_SET_DELAY:-0}" if [[ ${DDC_SET_FAIL:-0} == "1" ]]; then exit 1 @@ -352,11 +353,19 @@ pass "a monitor that ignores the DDC wake gets a DPMS cycle" # The lock screen starts blank and wake without waiting for one another. A # blank that is slow to fail must not fall back to DPMS after a newer wake has -# lit the screen: the wake waits for it and has the last word. +# lit the screen: the wake waits for it and has the last word. The wake is +# sent only once the blank's DDC write is under way, so the blank holds the +# lock by then whatever the runner's load. : >"$call_log" -DDC_SET_DELAY=1 DDC_SET_FAIL=1 run_power off & +ddc_started="$runtime_dir/ddc-set-started" +rm -f "$ddc_started" +DDC_SET_STARTED="$ddc_started" DDC_SET_DELAY=1 DDC_SET_FAIL=1 run_power off & blank_pid=$! -sleep 0.3 +for _ in $(seq 1 100); do + [[ -e $ddc_started ]] && break + sleep 0.05 +done +[[ -e $ddc_started ]] || fail "the slow blank never reached its DDC write" "$(<"$call_log")" run_power on wait "$blank_pid" [[ $(grep -F 'hl.dsp.dpms' "$call_log" | tail -n 1) == *enable* ]] || \