Skip to content

Fix keybindings scanner hanging on Hyprland list getters - #11027

Open
Ta-noshii wants to merge 1 commit into
omacom:quattrofrom
Ta-noshii:fix/keybindings-scan-list-getters
Open

Fix keybindings scanner hanging on Hyprland list getters#11027
Ta-noshii wants to merge 1 commit into
omacom:quattrofrom
Ta-noshii:fix/keybindings-scan-list-getters

Conversation

@Ta-noshii

Copy link
Copy Markdown

Problem

omarchy-menu-keybindings discovers Lua-only binds by running ~/.config/hypr/hyprland.lua in a standalone lua under a stub hl. Every lookup the stub does not model answers a truthy sentinel that also answers every key with itself, so:

for _, window in ipairs(hl.get_windows()) do end  -- never returns

ipairs stops at the first nil, and the sentinel never produces one. The same holds for hl.get_monitors(), hl.get_workspaces(), and any other getter the stub does not name. pcall cannot help because nothing raises. The scan has no timeout, so every SUPER + K starts another scan and nothing reaps the previous one. On the machine this was found on, 11 lua processes had each held a full core for about 82 minutes; the Hyprland config loaded a module that reconciles window state with ipairs(hl.get_windows()) at startup, which is fine against the real API.

Isolated reproduction, no user config involved:

local noop
noop = setmetatable({}, {
  __index = function() return noop end,
  __call = function() return noop end,
})
local hl = setmetatable({}, { __index = function() return noop end })

local count = 0
for _ in ipairs(hl.get_windows()) do
  count = count + 1
  if count == 10000 then break end
end
assert(count == 10000)
assert(hl.get_windows()[10001] == noop)

Fix

  • hl.get_monitors, hl.get_windows and hl.get_workspaces are stubbed with functions returning fresh empty tables, next to the existing get_config stub, so #, ipairs and pairs see an empty compositor.
  • The sentinel's __index answers nil for numeric keys, so ipairs over a getter the stub does not name (hl.get_loaded_plugins() came up in the issue) ends too. String keys still chain, and nothing in the scanner indexes the sentinel numerically.
  • The scan runs under timeout -k 2 10 (overridable with OMARCHY_KEYBINDINGS_SCAN_TIMEOUT), so a config the stub still cannot satisfy cannot outlive the menu. lua installs no SIGTERM handler, so the child goes away and nothing is left behind.
  • A cut-off scan is reported on stderr, runs only once per menu (the cache-miss path used to call the scanner twice: once to fill the cache and once more as the fallback when filling fails), and is never cached: output_binding_records_uncached now fails when the scan did not finish, so the menu still opens with what hyprctl binds reported but the next press scans again instead of serving an incomplete menu until the next binding change.

Existing fallback behaviour is unchanged: a scan that finishes is cached exactly as before, and a broken hyprctl still refuses the cache.

Validation

New test/shell.d/keybindings-menu-lua-scan-test.sh runs the real script against a stubbed hyprctl and isolated fixture configs in a throwaway HOME, in its own session so a leftover lua is detectable:

  • ipairs over all three getters, over an unnamed getter, a #-bounded numeric loop and a while cursor do cursor = cursor.next end walk all terminate, and two hl.bind calls declared after them are still discovered (the chords merge onto one row, and a code: key is recovered).
  • A finished scan is cached.
  • A while hl.get_active_monitor() do end fixture is cut off within the bound, says so on stderr, leaves no lua in the session, still renders Hyprland's binds, and leaves neither a records file nor a keybindings.XXXXXX temp file in the cache.

Results on this branch:

  • bash test/shell.d/keybindings-menu-lua-scan-test.sh: 6 ok in ~2 s.
  • The same test against the unpatched script hangs until killed and leaves a lua at 99% CPU behind, which is the reported failure.
  • bash test/shell.d/keybindings-menu-test.sh: 15 ok.
  • bash test/shell.d/bin-style-test.sh, ./test/cli, bash -n on both files, git diff --check: pass.
  • ./test/shell: 233 of 237 files pass. The four failures (config, snapper, unowned-system-paths, locate) fail identically on unmodified quattro in this checkout: three need a sibling omarchy-pkgs checkout and locate-test.sh trips on a non-UTF-8 local file.

Relation to #7564 and #8876

Both open PRs make the sentinel answer nil for numeric keys, and #7564 adds timeout 10. This change keeps that mechanism and closes what they leave open:

  • A cut-off scan is never cached. With timeout alone, a scan that is killed still produces a records file: the success test is only whether hyprctl binds returned anything, so the incomplete result is promoted to the cache and every later press serves it, silently missing every bind declared after the cut-off, until the next binding change. Here the scanner's status feeds the cache decision.
  • A cut-off scan is reported. pcall cannot report an externally killed process, so with timeout alone nothing is printed even under DEBUG=1. Here the timeout is named on stderr, which reaches the journal when the menu is launched from a bind.
  • The scan runs once per menu. The cache-miss path called the scanner twice: once to fill the cache, then again as the fallback when filling fails. With a timeout that fallback would cost a second full wait. Here the second call returns the first scan's result.
  • The three documented list getters are named with real empty tables, next to the existing get_config stub.
  • The regression test drives the real script with a stubbed hyprctl, in its own session, and asserts the cut-off leaves no lua behind and nothing in the cache.

Fixes #7025

🤖 Generated with Claude Code

The keybindings menu discovers Lua-only binds by running the user's
hyprland.lua under a stub hl whose every lookup answered a truthy
sentinel, so ipairs over hl.get_monitors(), hl.get_windows() or
hl.get_workspaces() never reached nil. A valid config looping over any
of them spun a lua at 100% CPU forever, and every SUPER + K added one.

Stub the three list getters with real empty tables and make the
sentinel answer nil for numeric keys, so iteration over a getter the
stub does not name ends too. Time-box the scan, say so on stderr when it
is cut off, run it once per menu, and refuse to cache a cut-off result
so an incomplete menu is not served until the next binding change.

Fixes omacom#7025

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

omarchy-menu-keybindings hangs forever when a user Lua config iterates hl.get_monitors()

1 participant