colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
156 lines
6.3 KiB
Bash
Executable File
156 lines
6.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# A setting has one schema-routed owner. A second page may mirror it only when
|
|
# this contract names the exact owner and mirror set. The same ownership rule
|
|
# says who writes each half of the window border: ColorScheme.qml owns the
|
|
# neutral inactive role AND the accent-derived focused role, restating both
|
|
# together on every scheme or accent change.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
pages_dir="$repo_dir/config/dot/quickshell/modules/settings"
|
|
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
|
search="$repo_dir/config/dot/quickshell/services/SettingsSearch.qml"
|
|
scheme="$repo_dir/config/dot/quickshell/services/ColorScheme.qml"
|
|
looks="$repo_dir/config/dot/hypr/looks.lua"
|
|
readme="$pages_dir/README.md"
|
|
|
|
fail() {
|
|
printf 'settings ownership contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
python3 - "$pages_dir" "$schema" "$search" <<'PY' \
|
|
|| fail 'page ownership or intentional mirrors drifted'
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
pages_dir = Path(sys.argv[1])
|
|
schema_text = Path(sys.argv[2]).read_text(encoding="utf-8")
|
|
search_text = Path(sys.argv[3]).read_text(encoding="utf-8")
|
|
|
|
expected = {
|
|
"animationsEnabled": {"owner": "appearance", "mirrors": {"accessibility"}},
|
|
"cursorInactiveTimeout": {"owner": "mouse", "mirrors": {"accessibility"}},
|
|
"cursorSize": {"owner": "accessibility", "mirrors": {"mouse"}},
|
|
"inactiveOpacity": {"owner": "appearance", "mirrors": {"accessibility"}},
|
|
"lockMinutes": {"owner": "power", "mirrors": {"privacy"}},
|
|
"lockOnSleep": {"owner": "power", "mirrors": {"privacy"}},
|
|
}
|
|
|
|
|
|
def strip_comments(text: str) -> str:
|
|
return re.sub(r"//.*", "", text)
|
|
|
|
|
|
def page_name(path: Path) -> str:
|
|
stem = path.stem.removesuffix("Page")
|
|
return re.sub(r"(?<!^)(?=[A-Z])", "-", stem).lower()
|
|
|
|
|
|
rows: dict[str, list[str]] = defaultdict(list)
|
|
row_pattern = re.compile(
|
|
r"(?:ToggleRow|SliderRow|ChoiceRow|TextEntryRow|TimeOfDayRow)\s*\{(?P<body>.*?)\}",
|
|
re.S,
|
|
)
|
|
for page_path in pages_dir.glob("*Page.qml"):
|
|
text = strip_comments(page_path.read_text(encoding="utf-8"))
|
|
for match in row_pattern.finditer(text):
|
|
setting = re.search(r'setting\s*:\s*"([^"]+)"', match.group("body"))
|
|
if setting:
|
|
rows[setting.group(1)].append(page_name(page_path))
|
|
|
|
duplicates = {key: set(pages) for key, pages in rows.items() if len(pages) > 1}
|
|
if set(duplicates) != set(expected):
|
|
raise SystemExit(
|
|
f"duplicate keys are {sorted(duplicates)}, expected {sorted(expected)}"
|
|
)
|
|
|
|
group_pages = dict(re.findall(r'"([^"]+)"\s*:\s*"([^"]+)"', search_text))
|
|
for key, policy in expected.items():
|
|
wanted_pages = {policy["owner"], *policy["mirrors"]}
|
|
if duplicates[key] != wanted_pages:
|
|
raise SystemExit(f"{key} appears on {sorted(duplicates[key])}, expected {sorted(wanted_pages)}")
|
|
|
|
block = re.search(
|
|
r'\{\s*\n\s*key:\s*"' + re.escape(key) + r'"(?P<body>.*?)\n\s*\}',
|
|
schema_text,
|
|
re.S,
|
|
)
|
|
if not block:
|
|
raise SystemExit(f"schema entry missing for {key}")
|
|
group = re.search(r'group:\s*"([^"]+)"', block.group("body"))
|
|
if not group:
|
|
raise SystemExit(f"schema group missing for {key}")
|
|
routed = group_pages.get(group.group(1))
|
|
if routed != policy["owner"]:
|
|
raise SystemExit(
|
|
f"{key} routes to {routed!r}, expected primary owner {policy['owner']!r}"
|
|
)
|
|
PY
|
|
|
|
for needle in \
|
|
'## Setting ownership' \
|
|
'one primary page' \
|
|
'Intentional mirrors' \
|
|
'`animationsEnabled`' \
|
|
'`cursorInactiveTimeout`' \
|
|
'`cursorSize`' \
|
|
'`inactiveOpacity`' \
|
|
'`lockMinutes`' \
|
|
'`lockOnSleep`' \
|
|
'scheme-relative role' \
|
|
'mode, scale, rotation, arrangement, and primary role'; do
|
|
rg -Fq "$needle" "$readme" || fail "README is missing $needle"
|
|
done
|
|
|
|
python3 - "$scheme" "$looks" <<'PY' \
|
|
|| fail 'scheme-relative border ownership drifted'
|
|
import re
|
|
import sys
|
|
|
|
scheme = open(sys.argv[1], encoding="utf-8").read()
|
|
looks = open(sys.argv[2], encoding="utf-8").read()
|
|
|
|
dark = re.search(r'property string inactiveBorderDark:\s*"([^"]+)"', scheme)
|
|
light = re.search(r'property string inactiveBorderLight:\s*"([^"]+)"', scheme)
|
|
effective = re.search(r'property string inactiveBorder:\s*root\.dark\s*\?\s*root\.inactiveBorderDark\s*:\s*root\.inactiveBorderLight', scheme)
|
|
if not dark or not light or not effective:
|
|
raise SystemExit("ColorScheme does not expose the two inactive-border roles")
|
|
if dark.group(1) not in looks or light.group(1) not in looks:
|
|
raise SystemExit("Hyprland startup values disagree with the live scheme roles")
|
|
|
|
without_comments = re.sub(r"//.*", "", scheme)
|
|
|
|
# The focused border is the accent role, and ColorScheme.qml owns it too:
|
|
# each named accent carries a separate pair per scheme, so a scheme change
|
|
# must restate the focused border, not just the neutral one, or a chosen
|
|
# accent goes stale the moment light/dark flips.
|
|
start = re.search(r'property string accentBorderStart:\s*root\.hyprColor\(Theme\.accent\)', scheme)
|
|
end = re.search(r'property string accentBorderEnd:\s*root\.hyprColor\(Theme\.accentSecondary\)', scheme)
|
|
if not start or not end:
|
|
raise SystemExit("ColorScheme does not derive the focused border from the chosen accent")
|
|
|
|
# Written as a Lua TABLE, not a string: the string form of a Hyprland gradient
|
|
# carries only one stop, so writing it that way is accepted and silently
|
|
# keeps whatever the previous accent left behind. Built through the same
|
|
# serializeValue() SystemSettings.qml uses for every other gradient, rather
|
|
# than a second hand-rolled (and unescaped) copy of that table syntax here.
|
|
if 'SystemSettings.serializeValue({' not in without_comments:
|
|
raise SystemExit("ColorScheme does not build the focused border through the shared gradient serializer")
|
|
if 'colors: [root.accentBorderStart, root.accentBorderEnd]' not in without_comments:
|
|
raise SystemExit("ColorScheme does not derive the focused-border gradient from the chosen accent")
|
|
if 'active_border = ${activeBorder}' not in without_comments:
|
|
raise SystemExit("ColorScheme does not write the focused border as the serialized gradient table")
|
|
|
|
if 'inactive_border = "${root.inactiveBorder}"' not in scheme:
|
|
raise SystemExit("ColorScheme does not apply its effective inactive role")
|
|
PY
|
|
|
|
printf 'settings ownership contract: PASS\n'
|