420 lines
16 KiB
Bash
Executable File
420 lines
16 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-osd"
|
|
scratch="$(mktemp -d)"
|
|
trap 'rm -rf "$scratch"' EXIT
|
|
|
|
mkdir -p "$scratch/bin"
|
|
log="$scratch/calls"
|
|
|
|
cat >"$scratch/bin/wpctl" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='wpctl'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
if [[ $1 == "get-volume" ]]; then
|
|
printf '%s\n' "${WPCTL_OUTPUT:-Volume: 0.58}"
|
|
fi
|
|
SH
|
|
|
|
cat >"$scratch/bin/brightnessctl" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='brightnessctl'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
if [[ " $* " == *" -m "* && " $* " != *" set "* ]]; then
|
|
[[ ${BACKLIGHT_AVAILABLE:-true} == true ]] || exit 1
|
|
printf '%s\n' "${BRIGHTNESS_OUTPUT:-intel_backlight,backlight,500,50%,1000}"
|
|
fi
|
|
SH
|
|
|
|
cat >"$scratch/bin/panama-brightness" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='panama-brightness'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
|
|
case "${1:-}" in
|
|
list)
|
|
if [[ -n ${DDC_LIST_JSON:-} ]]; then
|
|
printf '%s\n' "$DDC_LIST_JSON"
|
|
else
|
|
printf '%s\n' '{"displays":[],"error":"No displays"}'
|
|
fi
|
|
;;
|
|
get)
|
|
[[ ${DDC_FAIL_GET_BUS:-} != "${2:-}" ]] || exit 1
|
|
if [[ -s $OSD_DDC_STATE ]]; then
|
|
cat "$OSD_DDC_STATE"
|
|
else
|
|
printf '%s\n' "${DDC_GET_VALUE:-40}"
|
|
fi
|
|
;;
|
|
set)
|
|
if [[ -n ${DDC_SET_DELAY:-} ]]; then
|
|
if ! mkdir "$OSD_DDC_PROBE" 2>/dev/null; then
|
|
printf 'ddc-overlap\n' >>"$OSD_TEST_LOG"
|
|
fi
|
|
sleep "$DDC_SET_DELAY"
|
|
rmdir "$OSD_DDC_PROBE" 2>/dev/null || true
|
|
fi
|
|
printf '%s\n' "${3:-0}" >"$OSD_DDC_STATE"
|
|
;;
|
|
*) exit 2 ;;
|
|
esac
|
|
SH
|
|
|
|
cat >"$scratch/bin/hyprctl" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='hyprctl'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
printf '[{"name":"%s","focused":true}]\n' "${FOCUSED_MONITOR:-DP-2}"
|
|
SH
|
|
|
|
cat >"$scratch/bin/notify-send" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='notify-send'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
SH
|
|
|
|
cat >"$scratch/bin/playerctl" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='playerctl'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
if [[ $1 == "metadata" ]]; then
|
|
printf '%s\n' "${PLAYER_OUTPUT:-Horizon — Tycho}"
|
|
elif [[ $1 == "status" ]]; then
|
|
printf '%s\n' "${PLAYER_STATUS:-Playing}"
|
|
fi
|
|
SH
|
|
|
|
cat >"$scratch/bin/pw-play" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='pw-play'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
SH
|
|
|
|
cat >"$scratch/bin/qs" <<'SH'
|
|
#!/bin/bash
|
|
# One atomic append per call: the blip's pw-play runs in the background
|
|
# and a multi-write log line would interleave with the next stub's.
|
|
line='qs'; for a in "$@"; do line+=" <$a>"; done
|
|
printf '%s\n' "$line" >>"$OSD_TEST_LOG"
|
|
[[ ${OSD_TEST_FAIL_QS:-false} != true ]]
|
|
SH
|
|
|
|
chmod +x "$scratch/bin/"*
|
|
|
|
# The blip and the over-amplification limit are both read out of Panama's own
|
|
# settings.json, so every run below gets its own config root. Without this the
|
|
# helper would read Gabriel's real preferences and the assertions would pass or
|
|
# fail depending on which switches he happens to have on.
|
|
blip_sound="$scratch/blip.oga"
|
|
: >"$blip_sound"
|
|
mkdir -p "$scratch/config-default"
|
|
|
|
# Write a settings.json for one run. No arguments means no file at all, which
|
|
# is what a fresh install looks like: the helper must fall back to the schema
|
|
# defaults rather than treating an absent file as an error.
|
|
settings_root() {
|
|
# Three separate statements on purpose: bash expands every word of one
|
|
# `local` command before performing any of its assignments, so $name in
|
|
# the third RHS would be unbound (set -u) if these shared a line.
|
|
local name="$1"
|
|
local body="${2:-}"
|
|
local root="$scratch/config-$name"
|
|
rm -rf "$root"
|
|
mkdir -p "$root/panama"
|
|
[[ -n "$body" ]] && printf '%s\n' "$body" >"$root/panama/settings.json"
|
|
printf '%s' "$root"
|
|
}
|
|
|
|
run_helper() {
|
|
local runtime="${OSD_RUNTIME_DIR:-$scratch/runtime-default}"
|
|
mkdir -p "$runtime"
|
|
PATH="$scratch/bin:$PATH" OSD_TEST_LOG="$log" \
|
|
XDG_CONFIG_HOME="${OSD_CONFIG_HOME:-$scratch/config-default}" \
|
|
PANAMA_OSD_BLIP_SOUND="${OSD_BLIP_SOUND:-$blip_sound}" \
|
|
OSD_TEST_FAIL_QS="${OSD_TEST_FAIL_QS:-false}" \
|
|
PANAMA_OSD_STRICT="${PANAMA_OSD_STRICT:-false}" \
|
|
PANAMA_OSD_BRIGHTNESS_HELPER="$scratch/bin/panama-brightness" \
|
|
PANAMA_OSD_RUNTIME_DIR="$runtime" \
|
|
OSD_DDC_STATE="$runtime/ddc-state" \
|
|
OSD_DDC_PROBE="$runtime/ddc-probe" \
|
|
BACKLIGHT_AVAILABLE="${BACKLIGHT_AVAILABLE:-true}" \
|
|
DDC_LIST_JSON="${DDC_LIST_JSON:-}" \
|
|
DDC_GET_VALUE="${DDC_GET_VALUE:-40}" \
|
|
DDC_FAIL_GET_BUS="${DDC_FAIL_GET_BUS:-}" \
|
|
DDC_SET_DELAY="${DDC_SET_DELAY:-}" \
|
|
FOCUSED_MONITOR="${FOCUSED_MONITOR:-DP-2}" \
|
|
"$helper" "$@"
|
|
}
|
|
|
|
assert_line() {
|
|
local expected="$1"
|
|
grep -Fqx -- "$expected" "$log" || {
|
|
printf 'osd helper contract: missing call\n%s\nactual:\n' "$expected" >&2
|
|
cat "$log" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
# The blip is backgrounded and never waited on -- deliberately, so it cannot sit
|
|
# between the key press and the OSD. Which means the line may land after the
|
|
# helper has already exited, and asserting it needs a moment rather than an
|
|
# instant. Waiting here also keeps a slow blip from bleeding into the next
|
|
# case's freshly truncated log.
|
|
await_line() {
|
|
local expected="$1"
|
|
for _ in $(seq 1 60); do
|
|
grep -Fqx -- "$expected" "$log" && return 0
|
|
sleep 0.05
|
|
done
|
|
printf 'osd helper contract: missing call\n%s\nactual:\n' "$expected" >&2
|
|
cat "$log" >&2
|
|
exit 1
|
|
}
|
|
|
|
refute_line() {
|
|
local unexpected="$1" why="$2"
|
|
if grep -Fq -- "$unexpected" "$log"; then
|
|
printf 'osd helper contract: %s\nunexpected: %s\nactual:\n' "$why" "$unexpected" >&2
|
|
cat "$log" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
: >"$log"
|
|
run_helper volume up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
assert_line 'wpctl <get-volume> <@DEFAULT_AUDIO_SINK@>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
|
|
|
|
: >"$log"
|
|
WPCTL_OUTPUT='Volume: 0.58 [MUTED]' run_helper volume toggle
|
|
assert_line 'wpctl <set-mute> <@DEFAULT_AUDIO_SINK@> <toggle>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume-muted> <58> <100> <Muted>'
|
|
|
|
: >"$log"
|
|
WPCTL_OUTPUT='Volume: 0.72 [MUTED]' run_helper microphone toggle
|
|
assert_line 'wpctl <set-mute> <@DEFAULT_AUDIO_SOURCE@> <toggle>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <microphone-muted> <72> <100> <Muted>'
|
|
|
|
# ── Over-amplification: the ceiling is a setting, not a constant ─────────────
|
|
# `-l` caps the *result*, and without it wpctl caps nothing -- 100% is not a
|
|
# ceiling it enforces on its own. The limit is on the down step for that reason:
|
|
# with over-amplification off, stepping down from a volume already above 100%
|
|
# has to land under the ceiling rather than merely one step lower.
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root overamp '{"overAmplification": true}')" \
|
|
run_helper volume up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1.5> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root overamp '{"overAmplification": true}')" \
|
|
run_helper volume down 6
|
|
assert_line 'wpctl <set-volume> <-l> <1.5> <@DEFAULT_AUDIO_SINK@> <6%->'
|
|
|
|
# The microphone is not part of the bargain: no amount of gain past 100% makes
|
|
# a capture device better, and the Sound page's input slider stays 0-100 to
|
|
# match.
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root overamp '{"overAmplification": true}')" \
|
|
run_helper microphone up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SOURCE@> <6%+>'
|
|
refute_line '<-l> <1.5> <@DEFAULT_AUDIO_SOURCE@>' \
|
|
'over-amplification leaked onto the microphone'
|
|
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root plain '{"overAmplification": false}')" \
|
|
run_helper volume up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
|
|
# A settings file that predates the key, and a settings file that is not JSON
|
|
# at all, both mean "off" rather than "no volume keys work today".
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root nokey '{"barBackdrop": true}')" run_helper volume up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root broken '{not json')" run_helper volume up 6
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
|
|
|
|
# ── The volume blip ─────────────────────────────────────────────────────────
|
|
# Default on, per the schema, so a settings.json that has never been written
|
|
# still clicks. Fire-and-forget: the blip must never gate the OSD.
|
|
#
|
|
# Each case below awaits its own click before the next one starts, which is also
|
|
# what keeps them independent: the helper takes a non-blocking lock in its
|
|
# runtime directory so a held-down volume key clicks once rather than stacking
|
|
# one pw-play per repeat, and a click still sounding would suppress the next.
|
|
: >"$log"
|
|
run_helper volume up 6
|
|
await_line "pw-play <$blip_sound>"
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
|
|
|
|
: >"$log"
|
|
run_helper volume down 6
|
|
await_line "pw-play <$blip_sound>"
|
|
|
|
: >"$log"
|
|
WPCTL_OUTPUT='Volume: 0.58' run_helper volume toggle
|
|
await_line "pw-play <$blip_sound>"
|
|
|
|
: >"$log"
|
|
OSD_CONFIG_HOME="$(settings_root silent '{"volumeChangeBlip": false}')" \
|
|
run_helper volume up 6
|
|
refute_line 'pw-play' 'the blip played with volumeChangeBlip off'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
|
|
|
|
# Brightness and the microphone are not volume changes.
|
|
: >"$log"
|
|
run_helper brightness up 5
|
|
refute_line 'pw-play' 'a brightness step played the volume blip'
|
|
|
|
: >"$log"
|
|
WPCTL_OUTPUT='Volume: 0.72' run_helper microphone up 6
|
|
refute_line 'pw-play' 'a microphone step played the volume blip'
|
|
|
|
# A distribution without the freedesktop sound theme has no file to play. That
|
|
# is a silent desktop, not a broken volume key.
|
|
: >"$log"
|
|
OSD_BLIP_SOUND="$scratch/nothing-here.oga" run_helper volume up 6
|
|
refute_line 'pw-play' 'the blip ran against a file that does not exist'
|
|
assert_line 'wpctl <set-volume> <-l> <1> <@DEFAULT_AUDIO_SINK@> <6%+>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <volume> <58> <100> <58%>'
|
|
|
|
: >"$log"
|
|
run_helper brightness up 5
|
|
assert_line 'brightnessctl <-m> <-c> <backlight>'
|
|
assert_line 'brightnessctl <-e4> <-n2> <-c> <backlight> <set> <5%+>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <50> <100> <50%>'
|
|
if grep -Fq 'panama-brightness' "$log"; then
|
|
printf 'osd helper contract: DDC fallback ran despite a native backlight\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
run_helper brightness up 5
|
|
assert_line 'hyprctl <-j> <monitors>'
|
|
assert_line 'panama-brightness <list>'
|
|
assert_line 'panama-brightness <get> <5>'
|
|
assert_line 'panama-brightness <set> <5> <45>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <45> <100> <45%>'
|
|
|
|
# A cached bus avoids the expensive display scan on subsequent key presses.
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":45}],"error":""}' \
|
|
run_helper brightness down 5
|
|
assert_line 'panama-brightness <get> <5>'
|
|
assert_line 'panama-brightness <set> <5> <40>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <40> <100> <40%>'
|
|
if grep -Fq 'panama-brightness <list>' "$log"; then
|
|
printf 'osd helper contract: cached DDC bus triggered another display scan\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A disconnected cached monitor is discarded and rediscovered once.
|
|
mkdir -p "$scratch/runtime-ddc-stale"
|
|
printf '9\tDP-9\n' >"$scratch/runtime-ddc-stale/brightness-bus"
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc-stale" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_FAIL_GET_BUS=9 \
|
|
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
run_helper brightness up 5
|
|
assert_line 'panama-brightness <get> <9>'
|
|
assert_line 'panama-brightness <list>'
|
|
assert_line 'panama-brightness <get> <5>'
|
|
assert_line 'panama-brightness <set> <5> <45>'
|
|
|
|
# If the focused output is not DDC-capable, use the first discovered display.
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc-first" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
FOCUSED_MONITOR='eDP-1' \
|
|
DDC_GET_VALUE=35 \
|
|
DDC_LIST_JSON='{"displays":[{"bus":3,"connector":"HDMI-A-1","value":35},{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
run_helper brightness down 10
|
|
assert_line 'panama-brightness <get> <3>'
|
|
assert_line 'panama-brightness <set> <3> <25>'
|
|
assert_line 'qs <ipc> <call> <osd> <progress> <brightness> <25> <100> <25%>'
|
|
|
|
# Permission and discovery errors must be visible, never masquerade as 0%.
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc-error" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_LIST_JSON='{"displays":[],"error":"Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm"}' \
|
|
run_helper brightness up 5
|
|
assert_line 'qs <ipc> <call> <osd> <message> <dialog-warning-symbolic> <Brightness needs permission>'
|
|
assert_line 'notify-send <--app-name=Panama> <--icon=display-brightness-symbolic> <Brightness unavailable> <Run sudo udevadm control --reload-rules && sudo udevadm trigger --subsystem-match=i2c-dev --subsystem-match=drm>'
|
|
if grep -Fq 'osd> <progress> <brightness>' "$log"; then
|
|
printf 'osd helper contract: unavailable brightness rendered a false percentage\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Separate key-repeat processes must not overlap their DDC transactions.
|
|
: >"$log"
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_SET_DELAY=0.15 \
|
|
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
run_helper brightness up 5 &
|
|
first_pid=$!
|
|
OSD_RUNTIME_DIR="$scratch/runtime-ddc-lock" \
|
|
BACKLIGHT_AVAILABLE=false \
|
|
DDC_SET_DELAY=0.15 \
|
|
DDC_LIST_JSON='{"displays":[{"bus":5,"connector":"DP-2","value":40}],"error":""}' \
|
|
run_helper brightness up 5 &
|
|
second_pid=$!
|
|
wait "$first_pid"
|
|
wait "$second_pid"
|
|
if grep -Fqx 'ddc-overlap' "$log"; then
|
|
printf 'osd helper contract: concurrent DDC transactions overlapped\n' >&2
|
|
exit 1
|
|
fi
|
|
if [[ $(<"$scratch/runtime-ddc-lock/ddc-state") != 50 ]]; then
|
|
printf 'osd helper contract: serialized key repeats did not both apply\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
: >"$log"
|
|
run_helper media next
|
|
assert_line 'playerctl <next>'
|
|
assert_line 'qs <ipc> <call> <osd> <message> <media-next> <Horizon — Tycho>'
|
|
|
|
: >"$log"
|
|
run_helper message notifications-disabled-symbolic 'Do Not Disturb On'
|
|
assert_line 'qs <ipc> <call> <osd> <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
|
|
|
: >"$log"
|
|
if OSD_TEST_FAIL_QS=true PANAMA_OSD_STRICT=1 run_helper message dialog-error-symbolic 'Strict failure'; then
|
|
printf 'osd helper contract: strict delivery swallowed an IPC failure\n' >&2
|
|
exit 1
|
|
fi
|
|
|
|
: >"$log"
|
|
OSD_TEST_FAIL_QS=true run_helper message dialog-information-symbolic 'Best effort'
|
|
|
|
printf 'osd helper contract: PASS\n'
|