Design notes and open questions

August 10, 2026 · View on GitHub

The concept is simple; most of the difficulty is in the details underneath it. This file records the tradeoffs already baked into the current implementation, the known pitfalls, and the questions still worth arguing about. It is deliberately written as a working document rather than a spec — the project is early, and several of these decisions should probably be revisited.

If you are here to help shape the thing, this is the file to disagree with.


Known pitfalls

These are real, present in the current implementation, and worth understanding before relying on it.

1. Startup latency

Each press of the leader key starts a fresh Python + Qt process. That is a few hundred milliseconds before anything appears on screen.

For a tool you hit dozens of times a day, that is the difference between something that feels like a macropad and something that feels like launching an application. A hardware macropad is instant; that is most of why it is pleasant.

The fix is a resident daemon that builds the window once at login and then just shows and hides it. This is the single most important change for the thing to feel right, and it is probably v0.2.

The complication is how the leader key reaches an already-running process. Options, roughly in order of preference:

  • D-Bus activation, with the desktop entry pointing at a service that raises the existing instance.
  • A tiny client binary that the shortcut launches, which pokes the daemon over a socket. Fast to start, but still a process spawn.
  • The daemon grabbing the leader key itself, via the org.freedesktop.portal.GlobalShortcuts portal. Cleanest conceptually and the most portable across compositors, but portal support is uneven.

2. Focus handoff is timing based, not event based

The pad must not fire a macro while it still holds keyboard focus, or the synthetic keystrokes land in the pad rather than in the window you were actually working in. So the sequence is: highlight the slot, hide the window, pump the event loop, wait focus_delay_ms, inject.

That wait is a guess. There is no portable way to be told "the window that had focus before now has it again". 120 ms was reliable in testing, but it is a race, and on a loaded machine it will lose occasionally. The symptom is missing leading characters — laude instead of claude.

Open question: is there a compositor-agnostic signal for focus return? On KWin there is enough D-Bus surface to poll the active window, but polling to fix a race is not obviously better than a slightly generous delay.

3. The leader key is consumed globally

While the binding exists, that key does nothing else anywhere on the system. That is inherent to a global shortcut, not a bug, but it is a genuine cost and it is the reason choosing the key matters.

It also means the tool is quietly hostile to anyone who does use F9 for something. The default should probably prompt on first run rather than assume.

4. Numpad keys are a trap for the leader key

Distinct at the hardware level, unreliable as compositor shortcuts. KWin 6.6 accepted a numpad + binding, persisted it to its own config, and then never fired it — while the key demonstrably reached the kernel. Evidence in input-notes.md.

If a numpad key is what you want, the trigger has to move below the compositor: keyd, or a udev hwdb rewrite feeding a userspace reader. That is a much bigger dependency than a global shortcut and is not something this project should require.

5. Injection is layout sensitive on the common backend

ydotool emits raw keycodes, which the compositor maps through the active keyboard layout. With a non-Latin layout selected, a type macro produces the wrong characters. layout_pin works around this by forcing a Latin layout for the duration of the injection, but:

  • it is implemented for KWin only;
  • it mutates global state, briefly, to do a local job;
  • if the process dies mid-injection, the layout stays switched.

wtype has none of this problem because it sends text rather than keycodes. The unfortunate part is that the compositor most likely to need the workaround (KWin) is also the one that does not implement the protocol that would avoid it.

6. Backend fragmentation is doing real damage

Three backends, each broken in a different direction: wtype needs a protocol many compositors do not implement, ydotool needs a daemon and is layout sensitive, xdotool is X11 only. There is no combination that is simply correct.

This is not fixable inside this project. It is worth stating plainly so that nobody assumes the complexity in inject.py is accidental.

7. ydotool requires uinput access

The realistic backend on Wayland needs ydotoold running and the user able to reach its socket. That is a setup step this project does not currently handle, and a barrier for anyone who has not already got it working.


Open design questions

Should the pad be visible at all?

The pad is a discovery aid. Once the bindings are in muscle memory, drawing a window is pure latency — you already know that F9 1 is Claude.

Options worth considering:

  • Show on delay. Fire immediately if the second key arrives within ~200 ms; only draw the pad if the user hesitates. This is how well-designed leader-key systems in editors behave, and it gives you discovery without paying for it once you are fluent.
  • A headless mode for people who never want the window.
  • Show always, as now — simplest, and honest about what it is.

The delayed variant is probably right, but it interacts badly with the startup latency problem: you cannot "fire before drawing" if drawing is what takes the time. Another argument for the daemon.

How many macros, and how are they organised?

Currently: one flat grid, one character per slot, as many as fit.

That does not scale to a Stream Deck's worth of macros. The obvious extensions, each with a cost:

  • PagesF9 then Tab to cycle. Cheap, but adds a keystroke.
  • Nested groupsF9 g s for "git status". Powerful, and the model people already know from editors, but it stops being a pad and starts being a keybinding tree.
  • Per-application sets — the pad shows different macros depending on what was focused when you pressed the leader key. This is the one that would make it genuinely better than a hardware macropad, which cannot know what you are looking at. It is also the most work, and needs a reliable way to identify the previously focused window, which Wayland makes deliberately awkward.

Should it mirror an actual Stream Deck?

Icons, colours, per-slot images. Tempting, and it would look good. But the value of a Stream Deck is the physical button, not the picture on it, and a grid of icons on screen you have to read is slower than a grid of digits you can predict. Probably resist.

What happens on the second press of the leader key?

Currently nothing — the pad has focus, and the leader key is globally grabbed, so it is swallowed. Toggling closed would be the obvious behaviour and is not yet implemented.

Timeout

The pad currently waits indefinitely. If you open it by accident and walk away, it sits there on top of everything. An auto-dismiss after a few seconds is probably right; Esc already works for the deliberate case.


Non-goals

  • Replacing a hardware macropad. If you can put a physical device on the desk, do that instead. This is for the machine where you cannot.
  • Recording macros by demonstration. Config is a text file on purpose: reviewable, diffable, syncable between machines.
  • Cross-platform. Linux only. The input-injection problem is platform-shaped and solving it three times would consume the project.