#!/usr/bin/env bash # Bluetooth discovery holds the radio, so who turns it on and off is a contract # rather than a detail. # # The pins below started life against modules/quicksettings/BluetoothList.qml, # which computed the desired state and wrote BlueZ itself. That was correct # until a second surface -- the settings page, through Connectivity.qml -- began # doing the same thing from its own visibility flag, at which point the last # writer decided for both: closing the settings page stopped discovery under an # open quick-settings panel, which then sat on "Searching…" over a radio that # had stopped. # # The fix moved the BlueZ write into Connectivity.qml behind named holds, so the # same five guarantees are checked there, plus a sixth that keeps them there: # # 1. the desired state is calculated from state, not toggled; # 2. a redundant start is not issued; # 3. discovery this desktop did not start is never stopped; # 4. merely constructing an inactive picker does not write BlueZ state; # 5. an adapter appearing does not start discovery nobody asked for; # 6. a surface releases its hold when it is destroyed. # # Read-only. It changes no Bluetooth state. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" bluetooth="$repo_dir/config/dot/quickshell/modules/quicksettings/BluetoothList.qml" service="$repo_dir/config/dot/quickshell/services/Connectivity.qml" for path in "$bluetooth" "$service"; do [[ -r "$path" ]] || { printf 'bluetooth discovery contract: missing %s\n' "$path" >&2 exit 1 } done rg -q 'const shouldDiscover = root\.discoveryWanted' "$service" || { printf 'bluetooth discovery contract: desired state is not calculated idempotently\n' >&2 exit 1 } rg -q 'shouldDiscover && !root\.adapter\.discovering' "$service" || { printf 'bluetooth discovery contract: redundant BlueZ starts are not guarded\n' >&2 exit 1 } rg -q '!shouldDiscover && root\.discoveryOwned && root\.adapter\.discovering' "$service" || { printf 'bluetooth discovery contract: the service may stop discovery it does not own\n' >&2 exit 1 } # The holds themselves. Without both halves the refcount is decorative and the # two surfaces are back to overwriting each other. rg -q 'function acquireDiscovery' "$service" || { printf 'bluetooth discovery contract: nothing can take a hold on discovery\n' >&2 exit 1 } rg -q 'function releaseDiscovery' "$service" || { printf 'bluetooth discovery contract: a hold on discovery cannot be released\n' >&2 exit 1 } rg -q 'discoveryWanted: root\.discoveryHolders\.length > 0' "$service" || { printf 'bluetooth discovery contract: discovery does not follow the outstanding holds\n' >&2 exit 1 } # 6. One writer. A picker that still wrote adapter.discovering would reopen the # exact fight the holds exist to settle, and it would do it silently. ! rg -q 'discovering *=' "$bluetooth" || { printf 'bluetooth discovery contract: the picker writes BlueZ discovery directly, behind the holds\n' >&2 exit 1 } ! rg -q 'Component\.onCompleted: root\.(syncDiscovery|acquireDiscovery)' "$bluetooth" || { printf 'bluetooth discovery contract: inactive construction still takes a discovery hold\n' >&2 exit 1 } rg -Uq 'onActiveChanged:[^{\n]*\{[^}]*Connectivity\.acquireDiscovery' "$bluetooth" || { printf 'bluetooth discovery contract: discovery is not gated by an open panel\n' >&2 exit 1 } rg -q 'Component\.onDestruction: Connectivity\.releaseDiscovery' "$bluetooth" || { printf 'bluetooth discovery contract: the hold is not released on destruction\n' >&2 exit 1 } printf 'bluetooth discovery contract: PASS\n'