#!/usr/bin/env bash # The safety idioms are only worth having if a refactor cannot quietly drop # them. This pins the Tier-1 layer: # # 1. SettingsButton carries its own keyboard contract (Tab stop, Return / # Enter / Space, Accessible role, focus ring) -- the reason no consumer # hand-rolls those any more. # 2. ConfirmAction exists, is registered, and every instantiation names an # actionId; the one-armed-at-a-time token it arbitrates through lives on # ShellState. An anonymous ConfirmAction shares "" with every other # anonymous one, which arms them all at once. # 3. ErrorRow instantiations bind a message, and NotMeasuredRow # instantiations say `because:` -- a bare "Not measured" invites the # reader to assume zero, and zero is a measurement. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" qs="$repo_dir/config/dot/quickshell" fail() { printf 'settings idiom contract: %s\n' "$1" >&2; exit 1; } [[ -r "$qs/modules/settings/SettingsButton.qml" ]] || fail "missing SettingsButton.qml" python3 - "$qs" <<'PY' import re, sys, pathlib qs = pathlib.Path(sys.argv[1]) settings = qs / 'modules' / 'settings' problems = [] # 1. The button's own keyboard contract. button = (settings / 'SettingsButton.qml').read_text() for needle, why in [ ('activeFocusOnTab', 'no Tab stop'), ('Keys.onReturnPressed', 'Return does nothing'), ('Keys.onSpacePressed', 'Space does nothing'), ('Accessible.role', 'invisible to screen readers'), ('Theme.accentSecondary', 'no focus ring'), ]: if needle not in button: problems.append(f'SettingsButton.qml: {needle} gone -- {why}') # 2. ConfirmAction registered, arbitrated through ShellState, always named. qmldir = (settings / 'qmldir').read_text() for component in ('ConfirmAction', 'ErrorRow', 'NotMeasuredRow'): if not re.search(rf'^{component} ', qmldir, re.M): problems.append(f'qmldir: {component} unregistered -- pages referencing it fail to load') confirm = (settings / 'ConfirmAction.qml').read_text() if 'ShellState.armedConfirm' not in confirm: problems.append('ConfirmAction.qml: not arbitrated through ShellState.armedConfirm -- two rows can be armed at once') shellstate = (qs / 'services' / 'ShellState.qml').read_text() if 'armedConfirm' not in shellstate: problems.append('ShellState.qml: armedConfirm token gone -- ConfirmAction has nothing to arbitrate through') # 3. Instantiation-shape checks. A component use spans the braces that follow # it; requiring the property inside that span is a cheap parse that has caught # every real miss so far. def block_after(text, start): depth, i = 0, text.index('{', start) for j in range(i, len(text)): if text[j] == '{': depth += 1 elif text[j] == '}': depth -= 1 if depth == 0: return text[i:j] return text[i:] REQUIRED = {'ConfirmAction': 'actionId', 'ErrorRow': 'message', 'NotMeasuredRow': 'because'} for page in sorted(qs.rglob('*.qml')): if page.name in ('ConfirmAction.qml', 'ErrorRow.qml', 'NotMeasuredRow.qml'): continue text = page.read_text() for component, prop in REQUIRED.items(): for m in re.finditer(rf'\b{component}\s*\{{', text): if not re.search(rf'\b{prop}\s*:', block_after(text, m.start())): problems.append(f'{page.name}: {component} without {prop}:') if problems: print(f"settings idiom contract: {len(problems)} problem(s)") for p in problems: print(" -", p) sys.exit(1) uses = {c: 0 for c in REQUIRED} for page in qs.rglob('*.qml'): if page.name in ('ConfirmAction.qml', 'ErrorRow.qml', 'NotMeasuredRow.qml'): continue text = page.read_text() for c in uses: uses[c] += len(re.findall(rf'\b{c}\s*\{{', text)) print("settings idiom contract: ok (" + ", ".join(f"{n} {c}" for c, n in uses.items()) + ", every one carries its required property)") PY