#!/usr/bin/env bash # Rebinding a keyboard shortcut. # # This is the highest-consequence write in the settings app: a mistake here # costs the user their keymap, and the keymap is how they reach everything else. # The properties that matter: # # * an override moves exactly the bind it names and nothing else -- keying by # description moved every bind sharing one, which silently cost the # XF86Calculator hardware key when SUPER+C was rebound; # * only the chord is ever stored, never the action; # * a chord already in use is refused rather than shadowing the existing bind; # * resetting returns the exact shipped keymap. set -euo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" harness="$repo_dir/config/dot/quickshell/keybinds-harness.qml" service="$repo_dir/config/dot/quickshell/services/Keybinds.qml" fail() { printf 'keybind rebind contract: %s\n' "$1" >&2 exit 1 } rg -Fq 'function overrideOccupantFor(chord: string, exceptShipped: string): string' "$service" \ || fail 'resetBind has no override collision guard' rg -Fq 'root.overrideOccupantFor(shipped, shipped)' "$service" \ || fail 'resetBind does not check whether another override occupies its shipped chord' if [[ "${PANAMA_KEYBINDS_STATIC_ONLY:-0}" == "1" ]]; then printf 'keybind rebind contract: PASS (static)\n' exit 0 fi config_home="$(mktemp -d /tmp/panama-rebind-config.XXXXXX)" # The compositor is the live one -- that is the point -- but preferences are # isolated so this cannot leave an override in the user's real settings. # hyprctl reload re-reads the real settings file, so the compositor is only # exercised through the shipped configuration here; the override logic itself is # what is under test. qs_for_harness() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@" } cleanup() { qs_for_harness ipc call keybinds-test resetAll >/dev/null 2>&1 || true qs_for_harness kill >/dev/null 2>&1 || true rm -rf "$config_home" } trap cleanup EXIT XDG_CONFIG_HOME="$config_home" qs -p "$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' for _ in $(seq 1 40); do [[ "$(qs_for_harness ipc call keybinds-test status | jq -r .loaded)" == "true" ]] && break sleep 0.1 done # ── Nothing is overridden to begin with ────────────────────────────────────── [[ "$(qs_for_harness ipc call keybinds-test overrideState | jq -r .count)" == "0" ]] \ || fail 'the isolated store started with overrides' # ── A chord already in use is refused ──────────────────────────────────────── terminal="$(qs_for_harness ipc call keybinds-test chordFor Terminal)" [[ -n "$terminal" ]] || fail 'could not find the Terminal bind' files="$(qs_for_harness ipc call keybinds-test chordFor Files)" [[ -n "$files" ]] || fail 'could not find the Files bind' [[ "$(qs_for_harness ipc call keybinds-test rebind "$terminal" "$files")" == "false" ]] \ || fail 'rebinding onto a chord already in use was accepted' [[ "$(qs_for_harness ipc call keybinds-test overrideState | jq -r .count)" == "0" ]] \ || fail 'a refused rebind still stored an override' # ── A rebind stores only the chord, keyed by the shipped chord ─────────────── [[ "$(qs_for_harness ipc call keybinds-test rebind "$terminal" "SUPER + SHIFT + F9")" == "true" ]] \ || fail 'a valid rebind was refused' state="$(qs_for_harness ipc call keybinds-test overrideState)" jq -e --arg k "$terminal" '.overrides[$k] == "SUPER + SHIFT + F9"' <<<"$state" >/dev/null \ || fail "the override was not keyed by the shipped chord: $state" jq -e '.count == 1' <<<"$state" >/dev/null || fail "exactly one override expected: $state" # Only a chord is stored. Nothing resembling an action or command may appear, # because that is what keeps a user-editable file from being executable. jq -e '[.overrides[]] | all(type == "string" and (length < 64))' <<<"$state" >/dev/null \ || fail 'an override value is not a plain chord' # ── Reset clears it ────────────────────────────────────────────────────────── qs_for_harness ipc call keybinds-test resetAll >/dev/null sleep 0.5 [[ "$(qs_for_harness ipc call keybinds-test overrideState | jq -r .count)" == "0" ]] \ || fail 'resetAll left overrides behind' # Terminal moved away from its shipped chord, then Files moved into it. # Resetting Terminal must refuse instead of producing two binds on one chord. qs_for_harness ipc call keybinds-test seedResetCollision \ "$terminal" "SUPER + SHIFT + F9" "$files" >/dev/null sleep 0.2 [[ "$(qs_for_harness ipc call keybinds-test resetBind "SUPER + SHIFT + F9")" == "false" ]] \ || fail 'resetBind reclaimed a shipped chord occupied by another override' collision_state="$(qs_for_harness ipc call keybinds-test overrideState)" jq -e --arg terminal "$terminal" --arg files "$files" \ '.count == 2 and .overrides[$terminal] == "SUPER + SHIFT + F9" and .overrides[$files] == $terminal and (.lastError | length > 0)' \ <<<"$collision_state" >/dev/null \ || fail "a refused reset changed overrides or gave no explanation: $collision_state" trap - EXIT cleanup printf 'keybind rebind contract: PASS\n'