Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions bin/omarchy-brightness-display
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down Expand Up @@ -48,18 +55,95 @@ 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"
}

# 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. 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() {
[[ -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() {
printf '%s/%s\n' "$blank_dir" "${1//[^[:alnum:]_.-]/_}"
}

ddc_blank_asleep() {
local marker
[[ -n $blank_dir ]] || return 0
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="$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 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"
}

# The kernel backlight is the built-in panel's, so an external monitor without
# DDC fails rather than dimming the panel.
use_backlight_display() {
Expand All @@ -85,9 +169,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_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
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
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).
Expand Down
23 changes: 20 additions & 3 deletions bin/omarchy-brightness-display-ddc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/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%
# omarchy:summary=Show or adjust DDC/CI display brightness, or sleep and wake a Hyprland monitor.
# omarchy:args=<monitor> [+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:-}"
Expand Down Expand Up @@ -126,6 +126,23 @@ 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.
if [[ $step == "off" || $step == "on" ]]; then
bus="$(find_bus)" || exit 1
if [[ $step == "on" ]]; then
value=01
else
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
Expand Down
17 changes: 17 additions & 0 deletions bin/omarchy-toggle-ddc-blank
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions default/i18n/en/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions default/i18n/en/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 2 additions & 0 deletions default/omarchy/omarchy-menu.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
142 changes: 142 additions & 0 deletions test/shell.d/brightness-display-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,145 @@ 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"

# 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:-}
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

cat >"$mock_bin/ddcutil" <<'SH'
#!/bin/bash
printf 'ddcutil %s\n' "$*" >>"$CALL_LOG"
if [[ $* == *" detect --brief"* ]]; then
cat <<EOT
Display 1
I2C bus: /dev/i2c-7
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
fi
SH
chmod +x "$mock_bin/hyprctl" "$mock_bin/ddcutil"

run_power() {
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" HOME="$home_dir" DPMS_STATE="$dpms_state" \
PATH="$mock_bin:$ROOT/bin:$PATH" "$brightness_display" "$@"
}

: >"$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"

# 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 || \
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"

# 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 -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"

: >"$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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
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"