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.GlobalShortcutsportal. 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:
- Pages —
F9thenTabto cycle. Cheap, but adds a keystroke. - Nested groups —
F9 g sfor "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.