132 lines
6.0 KiB
Bash
Executable File
132 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Every enum backed by a Hyprland option must offer values that option accepts.
|
|
#
|
|
# This exists because of a bug that shipped: followMouse offered 0/1/2 labeled
|
|
# "Never" / "Click to focus" / "Sloppy focus", while Hyprland's actual mapping
|
|
# is disabled=0, follow=1, detached=2, separate=3. The desktop was labeled
|
|
# "Click to focus" and was in fact following the pointer, the way to GET click
|
|
# to focus was to choose "Never", and value 3 did not exist in the UI at all.
|
|
#
|
|
# Nothing detects that. The compositor accepts 1, reads back 1, and verification
|
|
# passes -- the value is valid, it just means something else entirely. The only
|
|
# authority on what each number MEANS is the compositor, which publishes it:
|
|
#
|
|
# hyprctl descriptions -> { "name": "input:follow_mouse",
|
|
# "map": [{"separate":3},{"detached":2},...] }
|
|
#
|
|
# So this checks the schema's enum values against that map, and against the
|
|
# min/max range for mapped options that have no named map.
|
|
#
|
|
# String-valued options are checked too, less strictly and for a reason. They
|
|
# carry no `map` at all -- Hyprland states their accepted values inside the
|
|
# description, as `input:scroll_method` does with "[2fg/edge/on_button_down/
|
|
# no_scroll]". That list is prose, so it is trustworthy in one direction only:
|
|
# a value the compositor does not name is a value it will reject or silently
|
|
# ignore, which this fails on; a value the compositor names but Settings does
|
|
# not offer may simply be one Panama has decided against (accel_profile's
|
|
# `custom` needs a scroll_points curve to mean anything, and is a stated
|
|
# non-goal), so that direction is not an error here. The empty string is always
|
|
# allowed: it is how a schema entry says "leave it at the compositor's default",
|
|
# which is what `[[EMPTY]]` reads back as.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
|
|
|
fail() {
|
|
printf 'enum hypr map contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
command -v hyprctl >/dev/null 2>&1 || { printf 'enum hypr map contract: SKIP (no compositor)\n'; exit 0; }
|
|
descriptions="$(hyprctl descriptions 2>/dev/null)" || fail 'could not read hyprctl descriptions'
|
|
jq -e 'type == "array" and length > 0' >/dev/null <<<"$descriptions" \
|
|
|| fail 'hyprctl descriptions did not return a list'
|
|
|
|
# Pull every enum entry that carries a hypr option, as:
|
|
# key<TAB>option<TAB>kind<TAB>values (kind is "int" or "str")
|
|
entries="$(python3 - "$schema" <<'PY'
|
|
import re, sys
|
|
|
|
text = open(sys.argv[1]).read()
|
|
# Each schema entry is a brace-delimited block starting with `key:`.
|
|
for block in re.findall(r'\{\s*\n?\s*key:\s*"([^"]+)"(.*?)\n \}', text, re.S):
|
|
name, body = block
|
|
if 'type: "enum"' not in body:
|
|
continue
|
|
option = re.search(r'option:\s*"([^"]+)"', body)
|
|
if not option:
|
|
continue
|
|
numbers = re.findall(r'value:\s*(-?\d+)\s*[,}]', body)
|
|
strings = re.findall(r'value:\s*"([^"]*)"\s*[,}]', body)
|
|
if numbers:
|
|
print(f"{name}\t{option.group(1)}\tint\t{','.join(numbers)}")
|
|
elif strings:
|
|
print(f"{name}\t{option.group(1)}\tstr\t{','.join(strings)}")
|
|
PY
|
|
)"
|
|
|
|
[[ -n "$entries" ]] || fail 'found no compositor-backed enums in the schema -- this contract is not reading it correctly'
|
|
|
|
checked=0
|
|
while IFS=$'\t' read -r key option kind values; do
|
|
[[ -n "$key" ]] || continue
|
|
|
|
entry="$(jq -c --arg name "$option" '.[] | select(.name == $name)' <<<"$descriptions")"
|
|
[[ -n "$entry" ]] || fail "$key maps to \"$option\", which the compositor does not publish"
|
|
|
|
# ── String-valued options: no map, an accepted-value list in the prose ────
|
|
if [[ "$kind" == "str" ]]; then
|
|
published="$(jq -r '.description // ""' <<<"$entry" \
|
|
| grep -oE '\[[a-z0-9_/-]+\]$' | tr -d '[]' | tr '/' '\n')"
|
|
if [[ -z "$published" ]]; then
|
|
printf 'enum hypr map contract: %s maps to "%s", which publishes no value list to check against\n' \
|
|
"$key" "$option" >&2
|
|
checked=$((checked + 1))
|
|
continue
|
|
fi
|
|
IFS=',' read -ra wanted <<<"$values"
|
|
for value in "${wanted[@]}"; do
|
|
# "" is the schema saying "leave the compositor's own default".
|
|
[[ -n "$value" ]] || continue
|
|
grep -qx "$value" <<<"$published" \
|
|
|| fail "$key offers \"$value\" for $option, which the compositor does not accept (it names: $(tr '\n' ' ' <<<"$published"))"
|
|
done
|
|
checked=$((checked + 1))
|
|
continue
|
|
fi
|
|
|
|
map_values="$(jq -r 'if .map then (.map | map(to_entries[].value) | join(",")) else "" end' <<<"$entry")"
|
|
|
|
IFS=',' read -ra wanted <<<"$values"
|
|
for value in "${wanted[@]}"; do
|
|
if [[ -n "$map_values" ]]; then
|
|
grep -qx "$value" <<<"$(tr ',' '\n' <<<"$map_values")" \
|
|
|| fail "$key offers $value for $option, which the compositor's map does not contain (it publishes: $map_values). A value outside the map is accepted and read back unchanged, so nothing else notices -- it simply means something other than the label says."
|
|
else
|
|
min="$(jq -r '.min // empty' <<<"$entry")"
|
|
max="$(jq -r '.max // empty' <<<"$entry")"
|
|
if [[ -n "$min" && -n "$max" ]]; then
|
|
(( value >= min && value <= max )) \
|
|
|| fail "$key offers $value for $option, outside the compositor's range $min..$max"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# Every value the compositor names should be offered. A missing one is a
|
|
# capability the user simply cannot reach -- value 3 was missing here.
|
|
if [[ -n "$map_values" ]]; then
|
|
while read -r published; do
|
|
[[ -n "$published" ]] || continue
|
|
grep -qx "$published" <<<"$(tr ',' '\n' <<<"$values")" \
|
|
|| fail "$option publishes value $published but $key does not offer it, so that behavior is unreachable from Settings"
|
|
done <<<"$(tr ',' '\n' <<<"$map_values")"
|
|
fi
|
|
|
|
checked=$((checked + 1))
|
|
done <<<"$entries"
|
|
|
|
printf 'enum hypr map contract: PASS (%d mapped enums)\n' "$checked"
|