Repository navigation
fix(keybindings): keep the Lua bind scan alive past qconsole.lua - #13276
playGitboy wants to merge 1 commit into
Conversation
`hyprctl binds` reports every Lua-config binding as dispatcher `__lua` with
an opaque argument, so omarchy-menu-keybindings reconstructs the real
dispatcher by re-running the user's config in Lua against a stub
(build_lua_bind_cache). That scan aborts early:
$ DEBUG=1 omarchy menu keybindings --print
[DEBUG] lua bind scan failed: .../qconsole.lua:123: attempt to compare number with table
qconsole.lua:97 evaluates `mon.scale > 0`, and the stub answers
`hl.get_active_monitor()` with a table defining neither __lt nor any
arithmetic metamethod. The whole load sits in a bare `pcall` (line 234) that
reports only when DEBUG=1, so the failure is invisible in normal use.
The crash lands in the worst possible place. qconsole is required after all
the default binds but before `require("hypr.bindings")`, so the scan dies
immediately before the user's own config is reached, and every binding
declared there loses its dispatcher and argument. dynamic_bindings() then
overwrites the __lua dispatcher/index that Hyprland did report with the two
empty strings it found in the necessarily empty source map:
if [[ $dispatcher == "__lua" && -n $description && -n $key ]]; then
dispatcher="${LUA_BIND_DISPATCHER_MAP["$cache_key"]}" # -> ""
arg="${LUA_BIND_ARG_MAP["$cache_key"]}" # -> ""
fi
On my machine that left 22 of 260 cached rows with an empty dispatcher and
argument, and they are exactly the ones coming from
~/.config/hypr/bindings.lua: super+w, super+c, super+v, super+x, print, and
whatever bindings other plugins write into that file. The keybindings menu
still renders them but cannot dispatch them, and any consumer that reads the
cache to resolve a chord -- for example an on-screen keyboard deciding
between a real dispatch and a synthetic key event -- sees an unresolvable
combo and takes the wrong branch.
Make the stub safe to keep reading: comparison, arithmetic and concatenation
now yield inert values instead of raising. Note that __index must return nil
for positive integer keys. Lua 5.4+ ipairs() honours __index, so a stub that
answers t[1], t[2], ... never yields nil and hangs any config that iterates
an API result -- my first attempt deadlocked the scan until it was killed.
Also bump the cache key from v13 to v14 so the already-poisoned
keybindings-*.records files get regenerated rather than reused.
With this, the 16 rows that come from a user's bindings.lua resolve again:
SUPER + W -> Close window permanently exec .../bin/reprieve close
SUPER + Z -> Restore parked window lua hl.dsp.global("...:undo")
PRINT -> Screenshot (save fullscreen) exec omarchy capture screenshot fullscreen save
Six rows still come out empty, unchanged by this patch and unfixable here:
they pass a Lua closure as the dispatcher (super+c, super+v, super+x,
super+ctrl+alt+z, super+ctrl+z, and a Hyper-layer bind), and o.bind forwards a
non-string dispatcher to hl.bind untouched (helpers.lua:101), so there is
nothing to serialise. That is pre-existing and separate from the crash.
de2b84a to
14f7e95
Compare
Automated AI review
Verified: at head Verified: dispatch columns of the cache records written by the real script (
Several open PRs change the same scan codeThe following open PRs also modify
These were compared by reading their diffs; none of them was run. Impact: #7564, #8876 and #12712 edit the same Suggested change: reconcile with #7564 in particular, since it targets the same failure. If this PR and #12362 both land, keep Edge cases of the new stubThese were probed with minimal synthetic configs on Lua 5.5.1, and some also on 5.4.9. The shipped configs do not use these shapes. For code in the user's
Impact: each row is a hypothetical user config. In those configs the scan can record an empty or wrong command, or hang. The new code comment says a config that reads a monitor or workspace "cannot abort the scan", which the third row contradicts. Suggested change:
Optional test improvement: A test in the same style catches the regression. It fails on base with the
Description corrections:
Review informationTest scope: sandboxed runs of the base and head script and extracted Lua program, using copies of the pinned AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check. Opt out: To stop receiving these reviews, reply to this comment saying so. |
|
Another data point, and a note on merge order. What we hit: a local config module walks monitors at load ( Merge order: this PR makes the scan continue past Here's what we verified on
|
|
Two things for this PR: a regression test it can have, and a measurement showing it is a prerequisite for work already in flight. A regression testllstrk's review lists a missing regression test, and @manuaudio suggested on #13790 that a personal-bind repro would make a good check for whichever fix lands. This drops into the existing harness in # The menu recovers a Lua bind's dispatcher by replaying the config under a stub,
# so a bind the replay never reaches has nothing to dispatch. The shipped config
# loads default.hypr.omarchy before hypr.bindings, so a bind the user declares in
# their own bindings.lua is exactly the bind that goes missing when the replay
# gives up partway through the defaults.
cat >>"$home/.config/hypr/bindings.lua" <<'LUA'
o.bind("SUPER + ALT + P", "Personal bind", "true")
LUA
stub_hyprctl <<BINDS
$(lua_bind 72 "SUPER ALT + P" "Personal bind")
BINDS
rm -rf "$tmpdir/cache"
keybindings >/dev/null
grep -qP '→ Personal bind\texec\ttrue$' "$tmpdir"/cache/omarchy/keybindings-*.records ||
fail "a bind declared in the user's own bindings.lua keeps its dispatcher" \
"$(cat "$tmpdir"/cache/omarchy/keybindings-*.records)"
pass "a bind declared in the user's own bindings.lua keeps its dispatcher"It discriminates, which is the only thing that makes it worth adding. At That row is the whole bug in one line: display text intact, both dispatch columns empty. At this PR's head This fix gates the bind-id work#12404 asks for binds to carry a stable id so the keybindings menu stops keying decisions on display text; #12557 adds the id, and Two chords declaring the same
The middle row is the point: threading the id through the parser correctly buys a personal config nothing on its own, because the ids live in the file the scan never reaches. This PR is what turns it on. They also merge cleanly, both directions, One small thing on the cache keyUpstream For disclosure: I opened #13790 on this today before finding your PR. It was a duplicate and my mistake, and it is closed and pointed here. 🤖 Generated with Claude Code |
WokoFlipper
left a comment
There was a problem hiding this comment.
Community review: read the diff and ran it as part of the #12557 + #13276 + id-sort stack on a live translated (RU, 257-bind) config. The hardened noop stub keeps the scan alive past user configs that read monitors/workspaces mid-load (previously aborted before reaching ~/.config/hypr/bindings.lua); all 29 shell tests green, live menu render verified. Small, safe, well-scoped.
|
Reviewed at Both stub the same comparison and arithmetic operators and return nil for integer keys. #7564 is preferred for three reasons, all checked against the source:
Of the other pull requests changing this scan, #12712 fixes only the Codex Medium was asked to choose among the four without being told this review's answer. It picked #7564 for the same reasons. Independence isn't guaranteed, because it can read this session's transcript. Choosing between the two is the maintainer's call, so this is waiting on them. |
Problem
Bindings declared in a user's
~/.config/hypr/bindings.lualose their dispatcher and argument, so the keybindings menu can render them but never dispatch them.Because Hyprland's Lua config reports every bind as
dispatcher=__luawith an opaque argument,omarchy-menu-keybindingsreconstructs the real dispatcher by re-running the config in Lua against a stub (build_lua_bind_cache). That scan dies partway through:qconsole.lua:97evaluatesmon.scale > 0, and the stub answershl.get_active_monitor()with a table that has no__ltand no arithmetic metamethods. The load is wrapped in a barepcall(:234) that only reports whenDEBUG=1, so nothing surfaces in normal use.The crash position is what makes it damaging.
qconsoleis required after all the default binds but beforerequire("hypr.bindings")— so the scan dies immediately before the user's own config is reached.dynamic_bindings()then overwrites the__luadispatcher/index Hyprland did report with the two empty strings it found in the (necessarily empty) source map:Measured on Hyprland 0.56.2: 22 of 260 cached rows had an empty dispatcher and argument, and they were exactly the ones from
bindings.lua—super+w,super+c,super+v,super+x,print, plus everything other plugins write into that file. The 239 Hyprland defaults were unaffected.This also breaks downstream consumers that read
~/.cache/omarchy/keybindings-*.recordsto resolve a chord. The virtual keyboard at simonezpx3/omarchy-vkeyboard does this: it looks a combo up in the records, and falls back towtypewhen the lookup comes back empty. Since Hyprland does not route Waylandvirtual-keyboard-v1events into its own bindings, that fallback is a silent dead end — so the user sees a modifier that appears to do nothing.Fix
Teach the stub to survive being read: comparison, arithmetic and concatenation now return inert values rather than raising.
The non-obvious part, and the one that will bite first —
__indexmust returnnilfor positive integer keys:Also bump the cache key
v13→v14so already-poisonedkeybindings-*.recordsfiles are regenerated instead of reused — otherwise the fix appears to do nothing on an affected machine.Verification
The 16 that this fixes are every binding coming from a user's
bindings.lua. The 6 that remain are a separate, pre-existing limitation andare not addressed by this PR — they pass a Lua closure as the dispatcher,
which cannot be serialised into a text row at all:
o.bindforwards a non-string dispatcher tohl.binduntouched(
helpers.lua:101), so the record ends up with an empty kind and arg bydesign.
super+c/super+v/super+xare still reachable from an on-screenkeyboard because that tool special-cases them itself, and
32 + cis aHyper-layer bind.
and the affected rows now resolve:
super+w,super+returnand the other affected chords then dispatch correctly.Note the scan is inherently a best-effort replay: a config that depends on real runtime state can still fail, and the
pcallwill still swallow it. Worth considering a follow-up that surfaces the failure instead of hiding it, but that is a behaviour change and I have kept this PR to the actual defect.