#!/usr/bin/env bash # Every compositor-owned preference must be read at config time, not only # applied live. # # A schema entry with `hypr:` metadata gets two delivery paths by design # (stated in services/SystemSettings.qml): hyprctl eval reaches the running # compositor, and a prefs.get() in the Lua is what survives `hyprctl reload` # and the moment before the shell starts. Twenty-one keys once shipped with # only the live half -- the entire Mouse & Touchpad page quietly reverted on # every reload, and the schema's touchpad natural-scroll default disagreed # with Hyprland's, so the page misreported the hardware until the shell's # startup replay ran. # # Two properties, then: every hypr-backed key has a prefs.get() somewhere in # config/dot/hypr/, and the Lua fallback equals the schema default -- a # different fallback is the same lie on a different day. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" python3 - "$repo_dir" <<'PY' import re, sys, pathlib repo = pathlib.Path(sys.argv[1]) schema = (repo / "config/dot/quickshell/config/PreferenceSchema.qml").read_text() # Pair each key with its entry body (up to the next key) and keep the ones # carrying hypr: metadata, along with their declared default. keys = [(m.group(1), m.start()) for m in re.finditer(r'key: "([A-Za-z]+)"', schema)] failures = [] backed = {} for i, (key, pos) in enumerate(keys): end = keys[i + 1][1] if i + 1 < len(keys) else len(schema) body = schema[pos:end] if "hypr: {" not in body: continue m = re.search(r'def: (\([^)]*\)|"[^"]*"|[-\d.]+|true|false)', body) if not m: failures.append(f"{key}: hypr-backed but its default could not be read") continue backed[key] = m.group(1).strip('"') lua = "" for path in (repo / "config/dot/hypr").glob("*.lua"): lua += path.read_text() def normalize(value): # A bool schema key can back an int compositor option (autoHdr -> render: # cm_auto_hdr), read through prefs.getInt; true/1 and false/0 are the # same declared default on the two sides. v = {"true": "1", "false": "0"}.get(str(value), str(value)) try: return repr(float(v)) except ValueError: return v for key, default in sorted(backed.items()): reads = re.findall( r'prefs\.get(?:Int)?\(\s*"' + key + r'"\s*,\s*("[^"]*"|[-\d.]+|true|false)\s*\)', lua) if not reads: failures.append(f"{key}: no prefs.get() in config/dot/hypr -- reverts on hyprctl reload") continue for fallback in reads: if normalize(fallback.strip('"')) != normalize(default): failures.append( f"{key}: Lua fallback {fallback} disagrees with schema default {default!r}") if failures: print(f"hypr prefs contract: {len(failures)} finding(s)") for failure in failures: print(f" - {failure}") sys.exit(1) print(f"hypr prefs contract: ok ({len(backed)} compositor-owned keys read at config time)") PY