Phase 6, the last of the fresh-install spec. 159 scripts lose their .sh: 110 contracts, 47 Vicinae commands, 2 compositor contracts. A shebang and the executable bit already select the interpreter. The extension only ever added something that had to stay in sync, and the rename proved the point twice over in the space of an hour. The spec's stated risk was Vicinae's script discovery. One script was renamed and reloaded on its own before the other 46 followed; it came back as scripts:panama.capture and all 47 resolve. What the probe turned up instead is that the extension was never only a filename: Vicinae's command IDs embed it, so every ID changed. Nothing in this repository refers to them, so nothing breaks. The only trace is Vicinae's metadata.json, whose visited map had two Panama entries that are now orphaned -- two commands lost their usage ranking and will earn it back. Worth knowing before anyone renames these again on a machine that has a keybind pointing at one. Rewriting the references by exact filename missed two things it structurally could not see: a name built from a variable, settings-$page.sh, and a glob, -name '*.sh'. Both were in the contract that counts the generated commands, which promptly reported 47 expected and 0 found. The mechanical part of a rename is the part that looks finished. The three subcommands. panama doctor fronts a health check that already existed and already ran at the end of every install but could not be reached from a terminal. panama upgrade re-runs the installer from anywhere. panama test runs the suite, which had no entry point at all -- 121 files that were the main safety net in this repository and were invisible in it. Writing that runner found three tests nothing was running. calendar_agenda_bridge_test, home_assistant_bridge_test and kdeconnect_bridge_test are unittest suites without the executable bit, so no contract invoked them and the first draft of the runner skipped them silently. All three pass, and have passed unobserved for weeks. The runner collects *_test.py as well now, because a runner with a blind spot is worse than no runner for the same reason a dependency checker with one is: it reports PASS. Six worktrees pruned. Each was re-checked rather than trusted to the spec's list, and two needed it: panama-commands is not on feat/panama-commands but on feat/gnome-tweaks-parity, and fix/panama-displays-review reads [ahead 3] -- ahead of its remote, not of main, with every commit patch-equivalent to landed work. roadmap-completion stays; it has five commits that are genuinely unlanded. The branches are left alone: pruning a worktree costs nothing, deleting a branch is a decision. 121 contracts pass. Claude-Session: https://claude.ai/code/session_01NvgBuSWB5sE43yWmg21ozj
164 lines
7.0 KiB
Bash
Executable File
164 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# panama-brightness enumerates monitors from sysfs and speaks DDC/CI to them.
|
|
#
|
|
# The parts worth pinning down are the ones that decide whether a slider appears
|
|
# at all, and whether it appears attached to the right screen:
|
|
#
|
|
# * only connectors with something plugged in are probed, because probing an
|
|
# empty bus costs a timeout each and there are fourteen of them here;
|
|
# * a panel that cannot report brightness is omitted rather than shown as a
|
|
# control that does nothing;
|
|
# * the connector name matches what Hyprland calls the output, since the UI
|
|
# joins on it to get the monitor's description;
|
|
# * no I2C access produces the udev command that fixes it, not "no displays".
|
|
#
|
|
# Runs entirely against fixtures. Real monitors are never touched: both the
|
|
# sysfs root and the device root are redirected, and ddcutil is replaced on PATH.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-brightness"
|
|
|
|
fail() {
|
|
printf 'brightness helper contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
fixture="$(mktemp -d /tmp/panama-brightness.XXXXXX)"
|
|
trap 'rm -rf "$fixture"' EXIT
|
|
|
|
mkdir -p "$fixture/drm" "$fixture/dev" "$fixture/bin" "$fixture/i2c"
|
|
|
|
# Two connectors with a monitor, two without. DP-2 answers DDC; DP-3 is
|
|
# connected but does not implement brightness. HDMI-A-1 and DP-1 are empty and
|
|
# must never be probed at all.
|
|
# A connector has an EDID bus (the `ddc` symlink) and, on DisplayPort, an AUX
|
|
# bus that appears as a child directory. DDC/CI rides the AUX channel where one
|
|
# exists, and the `ddc` line answers nothing on a DP connector even though it
|
|
# still resolves -- so the aux argument here is what a real DisplayPort monitor
|
|
# looks like, and omitting it is what HDMI and DVI look like.
|
|
make_connector() {
|
|
local name="$1" ddc_bus="$2" status="$3" aux_bus="${4:-}"
|
|
local device="$fixture/devices/$name"
|
|
|
|
# /sys/class/drm/<connector> is a SYMLINK to the real device directory, and
|
|
# this fixture mirrors that rather than using a plain directory. It matters:
|
|
# `find` does not follow the path it is given, so code that searches the
|
|
# unresolved path finds nothing while appearing to work anywhere the entry
|
|
# happens to be a real directory.
|
|
mkdir -p "$device"
|
|
printf '%s\n' "$status" >"$device/status"
|
|
mkdir -p "$fixture/i2c/i2c-$ddc_bus"
|
|
ln -sfn "$fixture/i2c/i2c-$ddc_bus" "$device/ddc"
|
|
[[ -n "$aux_bus" ]] && mkdir -p "$device/i2c-$aux_bus"
|
|
|
|
mkdir -p "$fixture/drm"
|
|
ln -sfn "$device" "$fixture/drm/$name"
|
|
return 0
|
|
}
|
|
# DP-2 is the DisplayPort case: its EDID line is bus 5, which answers nothing,
|
|
# and its AUX child is bus 9, which does. Choosing bus 5 here finds no monitor
|
|
# at all, which is exactly the bug this pins down.
|
|
make_connector card1-DP-1 4 disconnected
|
|
make_connector card1-DP-2 5 connected 9
|
|
make_connector card1-DP-3 6 connected
|
|
make_connector card1-HDMI-A-1 7 disconnected
|
|
|
|
# has_accessible_bus only needs one readable/writable node to exist.
|
|
touch "$fixture/dev/i2c-5"
|
|
|
|
# Stub ddcutil. Records every bus it is asked about so the test can prove the
|
|
# disconnected ones were skipped. Bus 6 refuses, standing in for a panel without
|
|
# VCP 0x10.
|
|
#
|
|
# Bus 5 reports its brightness out of 200 rather than 100. Most panels do use
|
|
# 100, which is exactly the problem: with a maximum of 100 the scaling
|
|
# arithmetic is the identity, so a helper that ignored the reported maximum
|
|
# entirely would pass every assertion. 200 makes reads and writes that skip the
|
|
# conversion visibly wrong.
|
|
cat >"$fixture/bin/ddcutil" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
bus=""
|
|
args=("$@")
|
|
for ((i = 0; i < ${#args[@]}; i++)); do
|
|
[[ "${args[$i]}" == "--bus" ]] && bus="${args[$((i + 1))]}"
|
|
done
|
|
printf '%s\n' "$bus" >>"$DDCUTIL_PROBE_LOG"
|
|
|
|
for arg in "$@"; do
|
|
if [[ "$arg" == "setvcp" ]]; then
|
|
printf 'set %s %s\n' "$bus" "${args[-1]}" >>"$DDCUTIL_SET_LOG"
|
|
exit 0
|
|
fi
|
|
done
|
|
|
|
case "$bus" in
|
|
9) printf 'VCP 10 C 120 200\n'; exit 0 ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$fixture/bin/ddcutil"
|
|
|
|
export DDCUTIL_PROBE_LOG="$fixture/probes.log"
|
|
export DDCUTIL_SET_LOG="$fixture/sets.log"
|
|
: >"$DDCUTIL_PROBE_LOG"
|
|
: >"$DDCUTIL_SET_LOG"
|
|
|
|
run_helper() {
|
|
PATH="$fixture/bin:$PATH" \
|
|
PANAMA_BRIGHTNESS_DRM_ROOT="$fixture/drm" \
|
|
PANAMA_BRIGHTNESS_DEV_ROOT="$fixture/dev" \
|
|
"$helper" "$@"
|
|
}
|
|
|
|
# ── Enumeration ──────────────────────────────────────────────────────────────
|
|
listing="$(run_helper list)"
|
|
jq -e . >/dev/null 2>&1 <<<"$listing" || fail "list did not emit JSON: $listing"
|
|
|
|
[[ "$(jq -r '.displays | length' <<<"$listing")" == "1" ]] \
|
|
|| fail "expected exactly one controllable display, got: $listing"
|
|
|
|
[[ "$(jq -r '.displays[0].connector' <<<"$listing")" == "DP-2" ]] \
|
|
|| fail "the connector name must match Hyprland's output name: $listing"
|
|
|
|
# The AUX bus, not the EDID bus its `ddc` symlink points at.
|
|
[[ "$(jq -r '.displays[0].bus' <<<"$listing")" == "9" ]] \
|
|
|| fail "the display was mapped to its EDID bus instead of its DisplayPort AUX bus, where nothing answers: $listing"
|
|
|
|
# 120 of a maximum of 200 is 60%.
|
|
[[ "$(jq -r '.displays[0].value' <<<"$listing")" == "60" ]] \
|
|
|| fail "brightness was not read as a percent of the reported maximum: $listing"
|
|
|
|
[[ "$(jq -r '.error' <<<"$listing")" == "" ]] \
|
|
|| fail "a successful listing must not carry an error: $listing"
|
|
|
|
# A connected panel that cannot report brightness is dropped, not listed.
|
|
jq -e '.displays | map(.connector) | index("DP-3") == null' >/dev/null <<<"$listing" \
|
|
|| fail 'a display without VCP 0x10 was listed as controllable'
|
|
|
|
# ── Disconnected connectors are never probed ─────────────────────────────────
|
|
if grep -qxE '4|7' "$DDCUTIL_PROBE_LOG"; then
|
|
fail "a disconnected connector was probed -- each empty bus costs a timeout: $(tr '\n' ' ' <"$DDCUTIL_PROBE_LOG")"
|
|
fi
|
|
|
|
# ── Writes scale to the reported maximum ─────────────────────────────────────
|
|
run_helper set 9 40
|
|
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 9 80" ]] \
|
|
|| fail "set did not scale to the display's maximum: $(cat "$DDCUTIL_SET_LOG")"
|
|
|
|
run_helper set 9 500
|
|
[[ "$(tail -1 "$DDCUTIL_SET_LOG")" == "set 9 200" ]] \
|
|
|| fail "an out-of-range percent was not clamped: $(cat "$DDCUTIL_SET_LOG")"
|
|
|
|
# ── No I2C access explains itself ────────────────────────────────────────────
|
|
rm -f "$fixture/dev"/i2c-*
|
|
denied="$(run_helper list)"
|
|
[[ "$(jq -r '.displays | length' <<<"$denied")" == "0" ]] \
|
|
|| fail "displays were reported without I2C access: $denied"
|
|
grep -q 'udevadm' <<<"$(jq -r '.error' <<<"$denied")" \
|
|
|| fail "the no-access error must name the command that fixes it, got: $(jq -r '.error' <<<"$denied")"
|
|
|
|
printf 'brightness helper contract: PASS\n'
|