Prevent shortcut reset collisions

This commit is contained in:
Gabriel Brown
2026-08-18 01:47:51 -04:00
parent 787d2b121a
commit b3b8e0d66d
3 changed files with 61 additions and 6 deletions
+10 -2
View File
@@ -13,9 +13,16 @@ ShellRoot {
return Keybinds.rebind(current, next);
}
function resetBind(current: string): void { Keybinds.resetBind(current); }
function resetBind(current: string): bool { return Keybinds.resetBind(current); }
function resetAll(): void { Keybinds.resetAll(); }
function seedResetCollision(shipped: string, current: string, occupantShipped: string): void {
const overrides = {};
overrides[shipped] = current;
overrides[occupantShipped] = shipped;
DesktopPreferences.set("keybindOverrides", overrides);
}
function chordFor(description: string): string {
const found = Keybinds.binds.find(bind => bind.description === description);
return found ? found.luaChord : "";
@@ -24,7 +31,8 @@ ShellRoot {
function overrideState(): string {
return JSON.stringify({
overrides: Keybinds.overrides,
count: Object.keys(Keybinds.overrides).length
count: Object.keys(Keybinds.overrides).length,
lastError: Keybinds.lastError
});
}
+25 -3
View File
@@ -108,6 +108,17 @@ Singleton {
return "";
}
// A moved shortcut vacates its shipped chord, so another override may use
// it legitimately. Refuse to reset the first shortcut until that occupant
// moves away; otherwise Hyprland would receive two binds on one chord.
function overrideOccupantFor(chord: string, exceptShipped: string): string {
for (const shipped in root.overrides) {
if (shipped !== exceptShipped && root.overrides[shipped] === chord)
return shipped;
}
return "";
}
function rebind(currentChord: string, newChord: string): bool {
if (newChord === "" || newChord === currentChord)
return false;
@@ -133,14 +144,25 @@ Singleton {
return true;
}
function resetBind(currentChord: string): void {
function resetBind(currentChord: string): bool {
const shipped = root.shippedChordFor(currentChord);
if (shipped === currentChord)
return;
return true;
const occupant = root.overrideOccupantFor(shipped, shipped);
if (occupant !== "") {
root.lastError = `${shipped} is used by another rebound shortcut. Reset that shortcut first.`;
return false;
}
const next = Object.assign({}, root.overrides);
delete next[shipped];
DesktopPreferences.set("keybindOverrides", next);
if (!DesktopPreferences.set("keybindOverrides", next)) {
root.lastError = "That shortcut could not be reset.";
return false;
}
root.applyReload();
return true;
}
function resetAll(): void {
+26 -1
View File
@@ -17,13 +17,25 @@ set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
harness="$repo_dir/config/dot/quickshell/keybinds-harness.qml"
config_home="$(mktemp -d /tmp/panama-rebind-config.XXXXXX)"
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
@@ -87,6 +99,19 @@ 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'