Conversation
chadmandoo
left a comment
There was a problem hiding this comment.
Tested head 964bfe1 on quattro b679363: the suites on this box, and a QEMU guest running Omarchy 4.0.3 with the tree dev-linked so Hyprland loads the collector.
Two things to change in the migration:
hyprctl reloadruns bare, and omarchy-migrate runs each file withbash -euo pipefail. A HYPRLAND_INSTANCE_SIGNATURE that names a compositor that is gone (a tmux session or a shell that outlived a restart) makes hyprctl exit 4, the migration stays pending and the queue would stop behind it. 1784767406.sh and 1787618700.sh guard the same call with>/dev/null 2>&1 || true; the update hook already forces a reload before migrations run, so the guard costs nothing.- The collector is installed only by bootstrap.lua, and the migration assumes every entrypoint loads it. 1781043107.sh exists because some hand-edited hyprland.lua files never
dofilebootstrap and set package.path themselves; on those,omarchy_keybindingsnever exists and SUPER + K fails on every open, with nothing on screen. Either insert thedofileline whengrep -F '/default/hypr/bootstrap.lua'misses, or say which line is missing.
What holds: #7025 reproduces on quattro, where the one-line ipairs reproducer in a fake home makes the old --print hang until killed (10 s, exit 124). In the guest on this branch --print lists 224 bindings in 40 ms, and still does after the reproducer is appended to the real hyprland.lua and reloaded, with no lua process left behind. Your test/acceptance.d/keybindings-test.sh passes in the guest, all 11 assertions; ./test/all gains 7 assertions and loses none, with locate-test.sh failing the same way on both trees.
Three smaller things inside the change: the "Reload Hyprland" message goes to stderr while SUPER + K launches the menu without a terminal, and a rejected invoke (stale generation, disabled binding) is discarded the same way, so both look like nothing happened; omarchy-notification-send would show them. keybindings-menu-test.sh writes the while true config and stubs lua with exit 99 alongside it, so that assertion does not exercise a loop. docs/keybindings-menu.md says QMP keyboard checks verify global shortcuts; nothing in the tree does that.
Testing performed by Claude Fable 5.1 via Claude Code; reviewed by me before posting.
|
Addressed in All 25 VM acceptance checks pass, including custom-entrypoint recovery and stale/disabled selections. CLI and 238/239 shell tests pass; the remaining Wi-Fi test fails identically on the unchanged base because the isolated VM lacks a default route. Ready for another look—thanks for the thorough review. |
chadmandoo
left a comment
There was a problem hiding this comment.
Retested head 2222bab on quattro b679363: the suites on this box, and the same QEMU guest running Omarchy 4.0.3 with the tree dev-linked.
Both changes hold. The migration under bash -euo pipefail, with a real hyprctl and a signature that names no compositor, exits 0 (964bfe1 exited 4); with a live signature it reloads once per run, without one it never calls hyprctl. A hyprland.lua that sets package.path itself and never loads bootstrap gets the dofile line printed on every run and stays byte for byte the same. Your keybindings-test.sh passes in the guest, all 25 assertions: the toast after a disabled binding and after a reload, the offline, expired and live migration runs against the custom entrypoint, the bootstrap toast, the click that opens the full text in a terminal, and the collector coming back once the documented line is added. On this box, whose compositor has no collector, --print writes the recovery text to stderr and sends nothing, and the interactive path sends one notification with the two-line message as a single argument and the terminal command on --exec. ./test/all gains 10 assertions on this box and in the guest and loses none; the two tightened shell tests fail on 964bfe1 at exactly their new assertions and pass on 2222bab. With the #7025 reproducer in the guest's real hyprland.lua, --print still lists 224 bindings in 43 ms. docs/keybindings-menu.md now calls the QMP checks manual.
One improvement inside the change. The recovery line at migrations/1789328800.sh:8 and bin/omarchy-menu-keybindings:92 is dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua"). The shipped config/hypr/hyprland.lua:4 and default/hypr/bootstrap.lua:37 write (os.getenv("OMARCHY_PATH") or "/usr/share/omarchy"), paths.lua falls back the same way, and migration 1781063758.sh inserted the guarded form. uwsm exports the variable before Hyprland starts, so the line works in a normal session. Where it is missing, the line raises at load and nothing after it runs, so the user has no bindings at all. Suggested: dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/bootstrap.lua"), the line hyprland.lua ships. keybindings-menu-test.sh:285, keybindings-registry-test.sh:46 and acceptance.d/keybindings-test.sh:170 grep for the exact string and :182 applies it, so the change is those three tests plus the two sources.
One thing inside your test. keybindings-test.sh:174-178 needs the bootstrap toast still on screen when omarchy-shell notifications invokeLast runs, and a normal-urgency popup expires after 8 s (Service.qml normalPopupDuration). screen_contains captures at 2x and runs tesseract, so one missed pass on a slow guest lands after the popup is gone; invokeLast then prints "none" with exit 0, the output is discarded, and the file fails at "notification opens complete setup instructions". It passed here, the whole file in 37 s. Checking that invokeLast printed ok would fail with the right message, and a longer -t on that one notification would give a user more than 8 s to click as well.
Not run: omarchy-migrate end to end in the guest. The dev-linked tree carries fifteen migrations newer than the installed 4.0.3, so the queue stops before this one; the direct runs above stand in for it.
Testing performed by Claude Fable 5.1 via Claude Code; reviewed by me before posting.
|
Addressed both follow-ups in
Validated the updated checkout in the isolated Omarchy 4.0.3 / Hyprland 0.56.2 VM: all 25 keybindings acceptance checks, both focused shell tests, and the CLI suite passed. A separate delayed-invocation check confirmed that the notification remains visible and opens the complete instructions after waiting 12 seconds; screenshots and the timing capture were inspected. The full shell suite was not rerun for this follow-up. Ready for another look—thanks for catching these. |
chadmandoo
left a comment
There was a problem hiding this comment.
Approving. Head 901a57d on quattro 86a2e58. The #7025 hang reproduces on quattro and is gone on this head. The live listing, the migration and the recovery path ran in a QEMU guest running Omarchy 4.0.3 with the tree dev-linked, through all 25 of the PR's acceptance assertions. Both rounds of review notes are closed with nothing left to change. Details in my comments above.
Testing performed by Claude Fable 5.1 via Claude Code; reviewed by me before posting.
|
I want to make the stakes and the reason for this approach clearer. Opening the keybindings menu can leave the whole machine struggling. When the scan hangs, the menu never appears, but its Lua process keeps running. Pressing Super+K again can leave another process behind. On the machine that prompted this PR, 14 stuck processes saturated all 12 logical CPUs; one had been running for about 43 hours. In #7025, another user reported 63 processes consuming about 56 GB of RAM, followed by the desktop session being killed. The confusing part is that the user's configuration works normally in Hyprland. The old menu runs that same configuration again in a separate Lua process, where Hyprland is replaced by fake objects. Some pretend to be commands and build up text describing what to run later. Others stand in for almost everything else: asking for a monitor, one of its properties, or the next item in a list can all return the same placeholder object. That means the same configuration has two different meanings. In Hyprland, “loop over the monitors” walks a real list and finishes. In the menu's imitation, every request for the next item gets another placeholder, so the loop never ends. Code that expects a number can receive that same object and fail instead. Those errors are normally hidden, so the scan can silently skip the bindings that come afterward. To understand or change the old menu, you have to follow several steps: what the fake objects return, how fake commands become text, how those results are matched back to Hyprland's binding list, and which result the cache serves. The menu is maintaining a second interpretation of the configuration, and every behavior the imitation misses can create another mismatch. Plain Lua functions are another gap: the scanner cannot reconstruct their actions, so they can appear in the menu without working when selected. The replacement is a small lookup table inside the running Hyprland process: binding ID → real binding and original action.
The menu also sends back an identifier for the configuration load it read. If Hyprland has reloaded, an old selection is rejected, so yesterday's The menu no longer needs to rerun the configuration, imitate Hyprland, reconstruct commands, or maintain a disk cache. It needs to know which binding the user selected; Hyprland already has the action. I found at least eight issue reports and eight other PRs about the same scanner loop:
There is related work on repeated cache rebuilds (#8140 / #8229) and function bindings that do nothing when selected (#11851). Removing the cache and retaining the original actions addresses those problems as well. How this differs from the other fixesThe eight other PRs addressing the hang keep the separate Lua scanner. They change the fake objects so list iteration stops; some also teach those objects to handle comparisons and arithmetic. #7564 and #11027 add a timeout, and #11027 also reports interrupted scans and prevents their incomplete results from being cached. Those changes address real failures in the existing approach. This PR removes the need for that imitation. Hyprland loads the configuration, and we keep a lookup table from binding IDs to the real bindings and their original actions. The menu reads the list and sends back the selected ID. It no longer needs to execute user configuration in a second environment or reconstruct what a shortcut should do. That also covers problems beyond the loop: the cache rebuild issue in #8140 / #8229 disappears because there is no disk cache, and function actions covered by #11851 work because we retain the original functions. The tradeoff is a broader change: it adds a collector inside Hyprland that must load before bindings are registered. The smaller fixes can stay within the existing scanner. The latest review reproduced the original hang, confirmed it was gone with this change, and passed all 25 acceptance checks in an Omarchy VM. Next steps for mergingFor the maintainers: what is the process for getting this merged, and what else do you need from me? The PR now has an approving review, with all 25 acceptance checks passing in the reviewer’s VM. Given the reports of runaway processes exhausting memory and killing the Omarchy desktop session, I’d like to help get a fix to users soon. Is there another review, test, or change needed before this can merge? |
Opening the keybindings menu with Super+K can leave a Lua process running forever. The menu never appears, but its background process keeps using an entire CPU core. Pressing Super+K again can leave another process running.
On the machine that prompted this PR, 14 stuck processes saturated all 12 logical CPUs. Thirteen came from repeated menu launches; another came from
omarchy-menu-keybindings --printand had been running for about 43 hours. Another user reported 63 processes consuming about 56 GB of RAM before their desktop session was killed.This PR makes the menu read shortcuts from the running Hyprland process. Hyprland already knows what each shortcut does. The menu shows the shortcuts, then tells Hyprland which one the user selected. Hyprland runs that shortcut's original action.
Fixes #7025. AI helped diagnose and implement this change.
Why the menu gets stuck
The current menu runs the user's configuration again in a separate Lua process. It uses fake Hyprland objects to discover shortcuts and reconstruct the commands to run when someone selects them.
Those objects behave differently from Hyprland. This loop is enough to make the keybindings menu freeze:
The loop goes through the workspaces one at a time. In Hyprland, it reaches the end of the list and stops.
The menu runs the same code using a fake workspace list. Whenever the loop asks for the next workspace, that list returns another placeholder—even when it should say there are no more. Specifically, the catch-all placeholder returns itself for every numeric index, so
ipairsnever reachesnil. The loop never ends, so the menu never opens and its background process keeps using CPU.On the affected machine, the loop was in a user module that called
hl.get_workspaces()to calculate terminal margins. Hyprland loaded that configuration normally, with no configuration errors. A normal loop over monitors can trigger the same problem.The old scanner exists because Hyprland's bindings query did not expose all the original key spellings and actions the menu needed. Returning empty tables for individual list getters would stop this loop, but the scanner would still run user configuration against an incomplete imitation of Hyprland. Maintaining it requires following what the fake objects return, how they reconstruct commands, and which results the disk cache serves.
How the replacement works
A small collector loads inside Hyprland before the default and user bindings are registered. It keeps a lookup table: binding ID → real binding and original action.
hl.bindcall and returns the original native handle—the binding object supplied by Hyprland. It keeps the original action, including Lua functions and the variables they captured when created.hyprctl repl, with a five-second client timeout. The binding objects supply the original key spellings, descriptions, submaps, and whether each binding is enabled. New bindings and enable/disable changes appear without a disk cache.hl.dispatchwith its original action.The menu also sends an identifier for the configuration load it read. Suppose someone opens the menu, then reloads the configuration while the menu is still open. The shortcuts may have changed, so the menu's list could be out of date. If the identifier no longer matches, the selection is rejected. Restarting Hyprland also invalidates old selections.
Removed and disabled bindings cannot run. When the collector scans an expired binding, it releases the action it had kept for it.
The menu preserves shortcut ordering, keycode display, browser-extension shortcuts, and groups of alternative key combinations for the same action. Those default aliases now share the same action object. Separate actions with identical labels remain independently selectable.
This removes the second Lua interpreter, fake Hyprland API, reconstructed callback expressions, and disk cache. It does not need access to Lua's private registry. Keeping the original functions also addresses bindings that appear in the menu but do nothing when selected.
Upgrade and error handling
The collector must load before bindings are registered. Omarchy's shipped configuration already loads the required bootstrap file in that order.
The upgrade migration attempts to reload Hyprland. A stale connection identifier for a compositor that has already exited does not block the remaining migrations.
For custom configurations that omit the bootstrap file, the migration and menu provide the exact
dofileline to add and explain where to put it, before bindings and imports. They leave the user's Lua configuration unchanged. The suggested line matches the shipped configuration, including the/usr/share/omarchyfallback whenOMARCHY_PATHis unset.Failures to list or run a shortcut appear as desktop notifications. This includes Lua errors returned by
hyprctleven when the command exits with status zero. The bootstrap notification stays visible for 30 seconds and opens the full recovery instructions in a terminal, so the notification's three-line limit does not cut them off. In print mode, errors go only to stderr.Hyprland compatibility
Hyprland 0.56.2 can crash if
is_enabled()is called on an expired native keybinding handle. This was reproduced without the collector: create a binding, remove it, then callis_enabled()on its handle.The collector first checks the handle's safe
tostringresult forHL.Keybind(expired)before accessing properties or methods. This check is isolated and covered by a focused test and VM tests that remove and unbind real bindings.Testing
The latest follow-up,
901a57d0, passed all 25 keybindings acceptance checks, both focused shell tests, and the CLI suite in an isolated Omarchy 4.0.3 / Hyprland 0.56.2 VM. The approving reviewer also confirmed that the original hang was fixed and all 25 acceptance checks passed in their VM.The most recent full shell run passed 238 of 239 test files. The remaining
network-qr-test.shfailure also occurs on the unchanged PR base: the isolated VM has no default route, so its unstubbedip route get 1.1.1.1exits 2. No unrelated network code was changed. The full shell suite was not rerun after the final follow-up.Detailed test coverage, earlier results, and timing measurements
Initial testing
Testing used a disposable QEMU VM running Omarchy 4.0.3-1 and Hyprland 0.56.2-2, with two CPUs and 2 GiB RAM. The working checkout was installed with
omarchy-dev-link../test/allpassed the CLI tests and all 239 shell test files. The repository's package and ISO test dependencies were provided. Inherited display, locale, andNO_COLORoverrides were removed, and Python bytecode generation was disabled for the run.test/acceptance.d/keybindings-test.shpassed. It covered all three live list getters; twelve simultaneous listings without repeating configuration side effects; native dispatchers; functions with captured variables; submaps; disabled, unbound, and removed bindings; re-enabling bindings; rejecting outdated selections; and graphical menu search and selection.September 14 review follow-up
The updated checkout was tested in a QEMU VM running Omarchy 4.0.3 and Hyprland 0.56.2.
Final follow-up:
901a57d0OMARCHY_PATHunset and with a custom install path.notifications invokeLastreturnsok, so an expired notification produces a direct diagnostic.Timing
Across twenty samples in the initial VM:
--print: median 36.84 ms, maximum 41.89 ms.These are VM measurements, not a direct speedup comparison against the affected host.
Architecture and repeatable test commands are documented in
docs/keybindings-menu.md.Related reports and PRs
Eight reports describe the same scanner loop: #7025, #8819, #9052, #9395, #10214, #10737, #10972, #11094.
Eight other PRs propose fixes: #7564, #8876, #10212, #10388, #10562, #11027, #11095, #11867.
Related work covers repeated cache rebuilds (#8140 / #8229) and Lua function bindings that appear in the menu but do nothing when selected (#11851). This PR addresses those problems by removing the cache and keeping the original functions.