Skip to content

Fix Super+K hang when config iterates hl.get_workspaces() - #14128

Open
AnPod wants to merge 2 commits into
omacom:quattrofrom
AnPod:fix/issue-13979
Open

AnPod wants to merge 2 commits into
omacom:quattrofrom
AnPod:fix/issue-13979

Conversation

@AnPod

@AnPod AnPod commented Oct 3, 2026 •

Copy link
Copy Markdown

Fixes #13979

What

  • bin/omarchy-menu-keybindings: in the mocked hl API used by the Lua bind scanner, plural runtime queries (hl.get_workspaces(), get_monitors(), get_clients(), get_windows(), …, any get_*s member) now return an empty table instead of the self-returning noop proxy, so ipairs(...) terminates. Singular queries keep returning the truthy proxy and get_config() still returns nil, as on quattro.
  • The scan of the user's config runs under a debug.sethook instruction guard (50M instructions) so any other runaway loop ends instead of spinning. If the scan errors or hits the guard, the bindings collected so far are still used for this menu, but the result is not written to the cache, so the next Super+K rescans.
  • Cache key keeps the upstream shape; only the version is bumped.

An earlier revision of this PR fed live hyprctl -j snapshots into the scan. That added more failure modes than it fixed (Greptile's trailing-] quoting hang, oversized snapshots skipping the scan, cache churn on window title changes), so it has been dropped in favour of the minimal fix above.

Why

Unimplemented members of the mocked hl returned a noop table whose __index/__call return itself, so ipairs(hl.get_workspaces()) never saw nil and looped forever. Each Super+K on a cache miss left a CPU-bound Lua process behind.

Testing

  • bash -n bin/omarchy-menu-keybindings
  • test/shell.d/keybindings-menu-test.sh passes, including new cases: a loop over each plural query terminates under timeout 5 and a bind declared after the loop is recovered (so the stub, not the guard, ends the loop); a bind under if hl.get_active_monitor() then is still recovered; an erroring or infinite config renders rows, publishes no cache, and the next run with the same config rescans; a finite 2M-iteration config is not cut short.

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the remaining issue is a non-blocking change to shortcut ordering.

Summary

The PR prevents plural mocked hl queries from trapping the keybinding scan in an endless iteration, guards other runaway scans, and avoids caching scans stopped by that guard. Subsequent changes add platform-specific display names for keys, include those names in cache invalidation, and isolate the Lua scan from Lua environment startup settings.

Reviews (5) · Last reviewed commit: "Keep singular getters usable and cache o..." · Reviewed by Greptile

Comment thread bin/omarchy-menu-keybindings Outdated
Comment thread bin/omarchy-menu-keybindings Outdated
Comment thread test/shell.d/keybindings-menu-test.sh Outdated
Comment thread bin/omarchy-menu-keybindings Outdated
Comment thread bin/omarchy-menu-keybindings Outdated
Comment thread bin/omarchy-menu-keybindings Outdated
Comment thread bin/omarchy-menu-keybindings Outdated
@jandrusk

jandrusk commented Oct 6, 2026

Copy link
Copy Markdown

Thanks for narrowing this down to the minimal fix. Two things in the stub:

Singular getters that end in s now return a plain empty table. key:match("^get_.*s$") also matches things like get_cursor_pos, get_options, get_status. A config that does local p = hl.get_cursor_pos(); if p.x > 0 then ... end now raises on nil > 0. The scan aborts, and every bind declared after that point is lost. Before, those calls got the noop proxy. I think this is Greptile's "Singular queries skip bindings" finding. Returning an empty table that still answers field lookups with noop covers both cases. In Lua 5.4/5.5, ipairs respects __index, so it stops at [1] == nil:

local function empty_list()
  return setmetatable({}, {
    __index = function(_, k)
      if type(k) == "number" then return nil end
      return noop
    end,
  })
end

-- in hl's metatable
  __index = function(_, key)
    if type(key) == "string" and key:match("^get_.*s$") then
      return empty_list
    end
    return noop
  end,

(The type(key) == "string" guard also avoids an error if anything indexes hl with a non-string key.)

Nit: scan errors now disable the cache for good. Any config that errors under the mock (an unhandled require, a missing API) used to be cached anyway. Now it never is, so for those users every Super+K re-runs the full Lua scan. Could you cache when the scan errors, and only skip caching when it hits the instruction guard?

For reference, the same hang is reported in #12052, #7025, #12124, #14243 and #13979. #12916 and #7564 are other open attempts at containing the scan.

Comment thread bin/omarchy-menu-keybindings
@jandrusk

jandrusk commented Oct 7, 2026

Copy link
Copy Markdown

Thanks, re-checked: singular getters now get a list that still answers field lookups, and ordinary scan errors are cached again. Both look good.

About Greptile's P1 on partial scans being cached: that's the tradeoff from my nit, so it's on me. It's fine as long as editing the config invalidates the cache. If the cache key doesn't already cover config changes (mtime or a hash), please add that so a fixed config gets rescanned. With that, LGTM.

@jandrusk

jandrusk commented Oct 7, 2026

Copy link
Copy Markdown

Thanks, both points are handled: singular getters still answer field lookups, and ordinary scan errors are cached again, with only the instruction guard skipping the cache.

About Greptile's "partial scans become permanent": caching after an ordinary error is the behavior from before this PR (the pcall already swallowed errors and the result was cached), so this doesn't regress anything. The cache key also hashes hyprctl binds, so editing a bind invalidates it. The only case that stays cached is a config that errors under the mock but works in Hyprland, and that's also how it worked before. I'd leave it as is. LGTM.

AnPod added 2 commits October 11, 2026 07:39
The mocked hl API answered every unknown member with a self-returning noop
proxy, so ipairs(hl.get_workspaces()) never terminated and each Super+K on
a cache miss left a CPU-bound Lua process behind (omacom#13979).

Return empty tables for plural hl.get_*s() queries, keep singular queries
truthy and get_config() nil, and run the config scan under a generous 50M
instruction guard so any other runaway loop ends. Bindings collected before
a failed scan are still used, but that run is not cached so the next Super+K
rescans.
Plural hl.get_*s() stubs now return an empty list whose numeric keys are
nil (so ipairs/# terminate) but whose string fields still answer with the
noop proxy, so singular getters that happen to end in s (get_cursor_pos,
get_status, ...) no longer abort the scan. Only the instruction guard
skips the cache; a config that merely errors under the mock is cached
again as before.
@greptile-apps

greptile-apps Bot commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Comments Outside Diff

These findings could not be posted inline.

  • P2 Media keys lose menu priority bin/omarchy-menu-keybindings:81 ▶

    When a platform displays an XF86 brightness key as F1 or F2, this rename removes XF86 before the menu sorts its rows. The existing rule that places media keys near the bottom no longer matches, so those shortcuts move into the middle of the list and make it harder to scan.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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.

Super + K hangs and spawns CPU-bound Lua processes when config iterates hl.get_workspaces()

2 participants