Parse plain hyprctl binds output to survive Hyprland 0.56's broken JSON

Hyprland 0.56.0 emits invalid JSON from `hyprctl -j binds`: the new
allow_input_capture field was added to the format template at slot 16
but its argument at position 8, shifting every value from
has_description onward and landing unquoted strings in numeric slots.
The jq parse failure emptied the keybindings menu down to the two
static web-app entries — and the empty result got cached under a
stable key, permanently.

Parse the plain-format output instead, which is unaffected (and also
immune to the older quotes-in-args JSON breakage), refuse to cache a
record set with zero dynamic binds, and bump the cache version so
poisoned caches invalidate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-07-20 20:42:37 -07:00
co-authored by Claude Fable 5
parent 79e9f38c0c
commit 994e4dfa7d
+31 -10
View File
@@ -3,9 +3,8 @@
# omarchy:summary=Display Hyprland keybindings defined in your configuration using an interactive search menu.
# Hyprland's Lua config provider currently reports Lua binds as dispatcher
# __lua and code:... binds from hl.bind() as key="" and keycode=0 in
# `hyprctl -j binds`. Keep a lightweight source-derived cache so the menu can
# still show and dispatch those bindings.
# __lua in `hyprctl binds`. Keep a lightweight source-derived cache so the
# menu can still show and dispatch those bindings.
declare -A LUA_BIND_KEY_MAP
declare -A LUA_BIND_DISPATCHER_MAP
declare -A LUA_BIND_ARG_MAP
@@ -65,7 +64,7 @@ parse_keycodes() {
'
}
# Supplement `hyprctl -j binds` for Lua-only binds that Hyprland currently
# Supplement `hyprctl binds` for Lua-only binds that Hyprland currently
# reports as dispatcher __lua and without their original key for code: binds.
build_lua_bind_cache() {
local modmask description key dispatcher arg cache_key
@@ -271,9 +270,24 @@ modmask_to_text() {
dynamic_bindings() {
local modmask key keycode description dispatcher arg modifiers cache_key
hyprctl -j binds |
jq -r '.[] | [.modmask, (.key // ""), (.keycode // 0), (.description // ""), (.dispatcher // ""), (.arg // "") | tostring] | join("\u001f")' |
# Parse the plain `hyprctl binds` output rather than `hyprctl -j binds`:
# Hyprland 0.56.0 emits invalid JSON for binds (misaligned fields in
# bindsRequest), and older versions broke on quotes in bind args.
hyprctl binds | awk '
function emit() {
if (!seen) return
seen = 0
printf "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s\n", f["modmask"], f["key"], f["keycode"], f["description"], f["dispatcher"], f["arg"]
}
/^bind/ { emit(); seen = 1; delete f; next }
seen && match($0, /^\t[a-z]+: /) { f[substr($0, 2, RLENGTH - 3)] = substr($0, RLENGTH + 1) }
END { emit() }
' |
while IFS=$'\x1f' read -r modmask key keycode description dispatcher arg; do
# Lua binds report their full display key ("SUPER + code:20"); the
# modifiers are already carried separately in modmask.
key="${key##* + }"
if [[ -z $key && $keycode != "0" ]]; then
key="code:$keycode"
fi
@@ -421,23 +435,30 @@ prioritize_entries() {
}
output_binding_records_uncached() {
local dynamic
build_lua_bind_cache
dynamic=$(dynamic_bindings)
{
dynamic_bindings
[[ -n $dynamic ]] && printf '%s\n' "$dynamic"
static_bindings
} |
sort -u |
parse_keycodes |
parse_binding_records |
prioritize_entries
# Fail when Hyprland reported no binds so the fallout of a broken hyprctl
# is never cached.
[[ -n $dynamic ]]
}
keybindings_cache_key() {
{
printf 'v5\n'
hyprctl -j devices 2>/dev/null | jq -r '.keyboards[].active_keymap' 2>/dev/null
hyprctl -j binds 2>/dev/null
printf 'v6\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }'
}