Stage 3 and 4 of docs/superpowers/plans/2026-08-17-panama-cohesion.md. Add SettingsPage plus ToggleRow, SliderRow, ChoiceRow, ActionRow, and TextRow. A row names a schema key and needs nothing else: label, detail, range, and unit come from PreferenceSchema, and writes go through SystemSettings.commitPreference, which routes compositor-backed keys through apply-and-verify and local keys straight to the store. The page scaffold that was copy-pasted eleven times is now one component. Rebuild Appearance around a live preview of the real desktop, scaled by the ratio between the preview and the actual monitor so a 10px gap on a 4500px display looks as small as it is. Rebuild Desktop & Dock and Input & Shortcuts on the shared rows, replacing the read-only text that stood in for controls that were merely expensive to add. Generate the shortcut list from hyprctl binds. The page held a hand-typed nineteen entries against a real keymap of a hundred and thirteen; it could not show the rest and went stale whenever a bind changed. Every bind now carries its own description -- backfilled for the twenty-nine that lacked one -- and keybinds-contract.sh fails if any bind lacks one, since undescribed binds are dropped from the page. Make Restore defaults span every store Panama owns. Resetting only the schema store left the Home accessory arrangement customised while claiming to restore defaults, which is worse than no reset because it is silent. Done through HomePreferences' existing public aliases rather than a new API. Four defects found while building: cursor:inactive_timeout is answered by getoption as float, not int. A wrong readAs does not fail loudly; it makes every write to that key look rejected, and the user saw an error for a change that worked. schema-hypr-shape-contract.sh now checks all 23 mapped options against the running compositor. The Settings window is tiled, so implicitWidth is only a hint and rows must survive roughly 400px. SliderRow stacks its control under the label below 520px. Binding an anchor to undefined to switch layouts does not reliably release it. Both row layouts are positioned explicitly. Concurrent compositor writes are queued and merged rather than refused. The startup replay of every compositor-backed preference routinely overlaps a UI change, and refusing left the store and the compositor disagreeing. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
81 lines
3.2 KiB
Bash
Executable File
81 lines
3.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The Shortcuts page is generated from the compositor, so it cannot drift from
|
|
# the real keymap. Two things have to hold for that to be true:
|
|
#
|
|
# * every bind Hyprland reports is presented -- the page count matches
|
|
# `hyprctl binds -j` exactly, so adding a bind cannot silently go missing;
|
|
# * every bind carries a description. Hyprland reports Lua-defined binds with
|
|
# dispatcher "__lua" and a bytecode offset as the argument, so a bind
|
|
# without a description has nothing a person could read beside its chord.
|
|
# The service drops those rather than showing a mystery row, which means an
|
|
# undescribed bind disappears from the page -- this contract is what stops
|
|
# that from being a silent loss.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
harness="$repo_dir/config/dot/quickshell/keybinds-harness.qml"
|
|
|
|
fail() {
|
|
printf 'keybinds contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
qs_for_harness() {
|
|
qs -p "$harness" "$@"
|
|
}
|
|
|
|
cleanup() {
|
|
qs_for_harness kill >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# ── Every bind in the running compositor must be describable ─────────────────
|
|
total="$(hyprctl -j binds | jq 'length')"
|
|
undescribed="$(hyprctl -j binds | jq '[.[] | select((.description // "") == "")] | length')"
|
|
|
|
[[ "$total" -gt 0 ]] || fail 'the compositor reports no binds at all'
|
|
|
|
if [[ "$undescribed" -ne 0 ]]; then
|
|
printf 'keybinds contract: %s bind(s) have no description and would be dropped from the page:\n' "$undescribed" >&2
|
|
hyprctl -j binds | jq -r '.[] | select((.description // "") == "") | " modmask=\(.modmask) key=\(.key)"' >&2
|
|
fail 'add a description to each in config/dot/hypr/keybinds.lua'
|
|
fi
|
|
|
|
# ── The page must present all of them ────────────────────────────────────────
|
|
qs_for_harness --daemonize >/dev/null
|
|
for _ in $(seq 1 40); do
|
|
qs_for_harness ipc show 2>/dev/null | rg -q '^target keybinds-test$' && break
|
|
sleep 0.1
|
|
done
|
|
qs_for_harness ipc show 2>/dev/null | rg -q '^target keybinds-test$' || fail 'test IPC target did not start'
|
|
|
|
state='{}'
|
|
for _ in $(seq 1 40); do
|
|
state="$(qs_for_harness ipc call keybinds-test status | jq -c .)"
|
|
jq -e '.loaded == true' <<<"$state" >/dev/null 2>&1 && break
|
|
sleep 0.1
|
|
done
|
|
|
|
jq -e '.loaded == true' <<<"$state" >/dev/null || fail "the keymap never loaded: $state"
|
|
|
|
presented="$(jq -r .count <<<"$state")"
|
|
[[ "$presented" == "$total" ]] \
|
|
|| fail "the page presents $presented of $total binds — the two must match exactly"
|
|
|
|
grouped_total="$(jq -r .groupedCount <<<"$state")"
|
|
[[ "$grouped_total" == "$total" ]] \
|
|
|| fail "grouping lost binds: $grouped_total grouped from $total"
|
|
|
|
# ── Chords must be rendered for people, not dumped raw ───────────────────────
|
|
jq -e '.sample | test("^Super \\+ ")' <<<"$state" >/dev/null \
|
|
|| fail "Super+T did not render as a readable chord: $(jq -r .sample <<<"$state")"
|
|
|
|
[[ "$(jq -r .emptyDescriptions <<<"$state")" == "0" ]] \
|
|
|| fail 'a presented bind has an empty description'
|
|
|
|
trap - EXIT
|
|
cleanup
|
|
printf 'keybinds contract: PASS (%s binds, all described)\n' "$total"
|