#!/usr/bin/env bash set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" component="$repo_dir/config/dot/quickshell/modules/settings/DisplayArrangement.qml" page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.qml" harness="$repo_dir/config/dot/quickshell/display-arrangement-harness.qml" identify="$repo_dir/config/dot/quickshell/modules/settings/DisplayIdentify.qml" service="$repo_dir/config/dot/quickshell/services/Displays.qml" shell="$repo_dir/config/dot/quickshell/shell.qml" fail() { printf 'display arrangement contract: %s\n' "$1" >&2 exit 1 } [[ -f "$component" ]] || fail 'DisplayArrangement.qml is missing' for contract in \ 'required property var displayService' \ 'property var draftLayout:' \ 'function setDraftPosition(' \ 'function nudge(' \ 'function applyDraft(' \ 'function makePrimary(' \ 'Accessible.name: "Move "' \ 'DragHandler {' \ 'Keys.onPressed:'; do rg -Fq "$contract" "$component" \ || fail "arrangement interaction is missing: $contract" done rg -Fq 'DisplayArrangement {' "$page" \ || fail 'Displays page does not expose the arrangement canvas' # Flipped by the displays redesign, deliberately and in this direction. # # The canvas used to be hidden below two displays. That left a laptop opening # its Displays page on a picker offering a list of one, and made the page's # most legible surface the one thing a single-display machine never saw. It is # the hero of the page now: a solo display is rendered in it, and only dragging # goes away, because there is nothing to arrange that display against. # The bindings of one QML element, found by matching its braces. A fixed-size # window around the element would either miss a binding or catch a neighbour's, # and both of those are the wrong answer here: the page's selector chips and its # Workspaces card are legitimately gated on having two displays. element_binding() { # file, element, pattern, present|absent python3 - "$@" <<'PY' import re import sys path, opener, pattern, expectation = sys.argv[1:5] text = open(path, encoding="utf-8").read() start = text.find(opener) if start < 0: raise SystemExit(1) depth = 0 end = len(text) for index in range(start + len(opener) - 1, len(text)): if text[index] == "{": depth += 1 elif text[index] == "}": depth -= 1 if depth == 0: end = index break found = re.search(pattern, text[start:end]) is not None raise SystemExit(0 if found == (expectation == "present") else 1) PY } python3 - "$page" <<'PY' \ || fail 'the arrangement canvas is gated on a monitor count again -- on the canvas itself or on the card holding it -- so a single display sees no canvas' import re import sys text = open(sys.argv[1], encoding="utf-8").read() start = text.find("DisplayArrangement {") if start < 0: raise SystemExit(1) def own_bindings(open_brace, stop): """The block's own bindings, with nested elements left out: a sibling card's gate is its business, and this is only about the canvas.""" kept = [] depth = 0 for char in text[open_brace + 1:stop]: if char == "{": depth += 1 elif char == "}": depth -= 1 if depth < 0: break elif depth == 0: kept.append(char) return "".join(kept) # The canvas's own bindings, and those of whatever element holds it: a gate on # either one is a canvas a single display never sees. This lived on the card # rather than on the canvas before the redesign, which is exactly why the # element alone is not enough to look at. stack = [] for index, char in enumerate(text[:start]): if char == "{": stack.append(index) elif char == "}" and stack: stack.pop() if not stack: raise SystemExit(1) regions = [own_bindings(start + len("DisplayArrangement {") - 1, len(text)), own_bindings(stack[-1], start)] gate = re.compile(r"visible:.*monitors\.length") raise SystemExit(1 if any(gate.search(region) for region in regions) else 0) PY for contract in \ 'One display connected' \ 'draggable'; do rg -Fq "$contract" "$component" \ || fail "a solo display is not explained or not protected from dragging: $contract" done element_binding "$component" 'DragHandler {' 'enabled:' present \ || fail 'the drag handler is unconditional, so a solo display can be dragged around a canvas with nothing to arrange against' # Mirroring has no position of its own: the compositor stacks a mirrored output # on its target. The canvas says which display it is mirroring rather than # drawing it wherever its stale coordinates happen to point. rg -Fq 'Mirrors ' "$component" \ || fail 'a mirrored display is drawn with no badge saying what it mirrors' rg -Fq 'mirrorOf' "$component" \ || fail 'the canvas does not read the mirror flag off its rects' [[ -f "$identify" ]] || fail 'DisplayIdentify.qml is missing' for contract in \ 'model: Quickshell.screens' \ 'WlrLayershell.keyboardFocus: WlrKeyboardFocus.None' \ 'mask: Region {}' \ 'visible: Displays.identifying' \ 'text: String(win.number)' \ 'text: win.connector'; do rg -Fq "$contract" "$identify" \ || fail "display identification overlay is incomplete: $contract" done rg -Fq 'DisplayIdentify {}' "$shell" \ || fail 'display identification overlays are not shell-owned' rg -Fq 'function identify()' "$service" \ && rg -Fq 'interval: 3000' "$service" \ || fail 'display identification does not use one three-second service timer' state_home="$(mktemp -d /tmp/panama-display-arrangement-state.XXXXXX)" harness_pid="" cleanup() { [[ "$harness_pid" =~ ^[0-9]+$ ]] && kill "$harness_pid" 2>/dev/null || true rm -rf "$state_home" } trap cleanup EXIT XDG_STATE_HOME="$state_home" qs -p "$harness" --daemonize >/dev/null for _ in $(seq 1 60); do harness_pid="$(qs list --all 2>/dev/null | awk -v expected="$harness" ' /^Instance / {pid=""} /^[[:space:]]*Process ID:/ {pid=$3} /^[[:space:]]*Config path:/ {path=$0; sub(/^[[:space:]]*Config path: /,"",path); if(path==expected) print pid}' | head -1)" [[ "$harness_pid" =~ ^[0-9]+$ ]] \ && XDG_STATE_HOME="$state_home" qs -p "$harness" ipc --pid "$harness_pid" show 2>/dev/null \ | rg -q '^target display-arrangement-test$' && break sleep 0.1 done [[ "$harness_pid" =~ ^[0-9]+$ ]] || fail 'arrangement harness did not start' ipc() { XDG_STATE_HOME="$state_home" qs -p "$harness" ipc --pid "$harness_pid" \ call display-arrangement-test "$@" } wide="$(ipc status 800)" narrow="$(ipc status 500)" for snapshot in "$wide" "$narrow"; do jq -e '(.rects | length) == 2 and .scale > 0 and (.rects[0].width / .rects[0].height - 1.5 | fabs) < 0.0001 and (.rects[1].width / .rects[1].height - (2560 / 1440) | fabs) < 0.0001' \ <<<"$snapshot" >/dev/null \ || fail "canvas lost monitor geometry at a supported width: $snapshot" done drag="$(ipc dragFixture)" jq -e '.[0].x == 0 and .[1].x == 3000' <<<"$drag" >/dev/null \ || fail "drag release did not snap and apply the complete layout: $drag" keyboard="$(ipc keyboardFixture)" jq -e '.afterArrow == 2990 and .afterShiftArrow == 2890' <<<"$keyboard" >/dev/null \ || fail "keyboard movement did not use 10/100 logical-pixel steps: $keyboard" solo="$(ipc soloFixture)" jq -e '(.rects | length) == 1 and .scale > 0 and .draggable == false' <<<"$solo" >/dev/null \ || fail "a single display did not render solo with dragging disabled: $solo" mirror="$(ipc mirrorFixture)" jq -e '(.rects | length) == 2 and (.rects[1].mirrorOf == "DP-2") and (.rects[1].x == .rects[0].x) and (.rects[1].y == .rects[0].y)' \ <<<"$mirror" >/dev/null \ || fail "a mirrored display was not stacked on its target and flagged: $mirror" primary="$(ipc primaryFixture)" jq -e '. == [ {"name":"DP-2","x":-3000,"y":0,"primary":false}, {"name":"HDMI-A-1","x":0,"y":0,"primary":true} ]' <<<"$primary" >/dev/null \ || fail "Make primary did not normalize the selected output to 0,0: $primary" ipc identify >/dev/null [[ "$(ipc identifying)" == "true" ]] \ || fail 'identify did not reveal the overlays' sleep 3.2 [[ "$(ipc identifying)" == "false" ]] \ || fail 'identify overlays did not disappear after one three-second timer' printf 'display arrangement contract: PASS\n'