Skip to content

keybind-cheatsheet 0.2.5: fix the panel never loading (and dying on the first click) on mid-size configs - #534

Open
UmedjonBA wants to merge 1 commit into
noctalia-dev:mainfrom
UmedjonBA:keybind-cheatsheet-cpu-budget
Open

keybind-cheatsheet 0.2.5: fix the panel never loading (and dying on the first click) on mid-size configs#534
UmedjonBA wants to merge 1 commit into
noctalia-dev:mainfrom
UmedjonBA:keybind-cheatsheet-cpu-budget

Conversation

@UmedjonBA

Copy link
Copy Markdown
Contributor

Plugin

  • Id: kenn/keybind-cheatsheet
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

What it does

Fixes the plugin being unusable on a mid-size niri config. On my ~800-line config.kdl (130 bindings) the panel never left "Reading keybindings…", and once a cached list did appear, the first click on Refresh / edit mode / the palette produced an error and the panel stopped responding entirely.

Everything traces back to the Luau CPU budget (~25 ms per callback), and three findings that shaped the fix — happy to be corrected if any of them is wrong on your setup:

  1. The parse alone is past the budget. Tokenizing the 33 KB config measured 31.6 ms, before any binding was built.
  2. A coroutine cannot chunk it. The budget accumulates over the coroutine's whole lifetime, not per resume: with 3-operation slices the parse still died, at slice 2194. Only separate callbacks/ticks get a fresh budget.
  3. The panel had the same problem. A full 130-row redraw cost ~22 ms, so an interactive callback (its own work + the redraw) went over — and a callback killed mid-render left the panel's in-progress flags set, which is why everything stayed dead afterwards.

Service

  • Niri configs are read by a small sh/sed/awk pipeline that emits one line per binding; the Lua side only converts those rows, a few per update() tick (an async callback's budget is smaller still). Include directives, globs, multi-line binds, hotkey-overlay-title and // #"Category" headers are all handled; the output matches tests/fixtures/niri and tests/expected.json exactly.
  • parseNiriContent and the other in-process parsers are left in place (still exported, still covered by the self-test, which passes on all four fixtures) — the shell path is only used for the live niri refresh.
  • Cheaper tokenizer paths (a whole quoted string, a whitespace run, a word — one find/match each instead of a per-character loop) for the parsers that remain in-process.

Panel

  • Per-binding caches for the search haystack, the resolved category and the built row node (ui nodes are immutable, so an unchanged row can be reused): a full 130-row redraw drops from ~22 ms to ~6–7 ms.
  • Progressive rendering: at most 24 rows per pass, the rest fills in over a chain of async callbacks; every render outside that chain restarts from a small chunk, so a click or a keystroke never pays for the whole tree.
  • render() only schedules — the draw itself runs in its own callback, so a handler's budget is not spent on it.
  • onOpen no longer does preferences JSON + snapshot + draw in one go.
  • No sticky "in progress" flags left: the fill-in mode is a parameter, and the "scheduled" flag is cleared before the draw, so a killed callback can no longer freeze the panel permanently.

Result on the same config: panel opens immediately, fills in within ~0.3 s, search and buttons stay responsive, and the log shows zero budget errors across repeated opens, refreshes and typing (previously one on nearly every action).

External dependencies

awk, sed, sh (added to dependencies, documented in Requirements) — base tools on any system Noctalia runs on, used only for the niri config read. hyprctl unchanged. No new network access, no files written beyond the existing cache/preferences.

Testing

  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Noctalia version tested against: v5.0.0-beta.10 (noctalia-git 5.0.0.r5344.g74e6c2790)
  • Plugin API level: 9

Real config (~800 lines, 130 bindings, includes): panel open/close repeatedly, refresh via IPC and the header button, search typing, edit mode, colors view — no budget errors. The plugin's own self-test (noctalia msg plugin kenn/keybind-cheatsheet:data all self-test) passes: hypr_conf 5/5, hypr_lua 4/4, mango 9/9, niri 5/5. The awk parser was diffed against tests/fixtures/niri and returns the same five bindings with the same descriptions and categories. Render costs were measured with temporary instrumentation (removed before this PR), which is where the numbers above come from.

Hyprland/Mango paths are untouched apart from the tokenizer speedups, so they would benefit from a second pair of eyes on hardware I don't have.

Screenshots / Videos

The change is not visual — same panel, same layout. For reference, the fully populated panel on the config that used to hang (the descriptions are user-supplied via the plugin's own edit mode, hence Russian):

Panel filled in

Checklist

Ready-for-review requirement: Every box in this section must be checked. If any statement is not true, keep the
pull request as Draft. An explanation does not replace a required check.

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

Plugins run as trusted, unsandboxed Luau in the user's session. Confirm:
Ready-for-review requirement: Every attestation below must be checked.

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

@kenn — this is your plugin, so it needs your call. The thumbnail and translations are untouched; if you would rather keep the whole parse in Luau, or split this into "service fix" and "panel fix" PRs, I am glad to rework it.

…CPU budget)

On a ~800-line niri config (130 bindings) the plugin never leaves
"Reading keybindings...": the Luau runtime kills every callback that
exceeds its CPU budget, and both the config parse and the panel render
run past it. Interacting with an already-drawn panel (search, refresh,
edit mode) failed the same way, which also left internal state stuck.

Service:
- Parse niri configs with a small sh/sed/awk pipeline instead of the
  in-process tokenizer: tokenizing a 33 KB config alone measured 31.6 ms.
  Chunking it with a coroutine does not help - the budget accumulates over
  the coroutine's whole lifetime (verified: 3-op slices still died, at
  slice 2194). The Lua parsers are untouched and still cover the self-test
  fixtures; the awk output matches tests/fixtures/niri exactly.
- Convert the emitted rows into bindings a few per update() tick, since an
  async callback's budget is smaller still.
- Faster tokenizer paths (whole strings, whitespace runs and words in one
  match each) for the parsers that remain in-process.

Panel:
- Cache each binding's search haystack, resolved category and built row
  node; a full 130-row redraw drops from ~22 ms to ~6-7 ms.
- Render progressively: at most 24 rows per pass, the rest fills in over a
  chain of async callbacks, and every render outside that chain restarts
  from a small chunk, so a click never pays for the whole tree.
- render() only schedules; the draw happens in its own callback.
- Split onOpen (preferences JSON + snapshot + draw) into async steps.
- Drop the sticky in-progress flags: a callback killed mid-render used to
  leave them set, and the panel stopped redrawing entirely until restart.

Version 0.2.5; declares the awk/sed/sh the niri path now uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

CC @cheerfulScumbag

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.

1 participant