Virtual Macropad

August 10, 2026 · View on GitHub

status: work in progress platform: linux licence: MIT

A software macropad for laptop users. Press a leader key, a grid appears, press one more key, the macro fires and the grid disappears.

Work in progress. The idea is the point of this repository right now; the code is an initial implementation of it. It runs, and it was developed against KDE Plasma 6.6 on Wayland — it has not been tested anywhere else. Expect rough edges, and read Design notes and open questions before building on it. Feedback on the concept is at least as welcome as bug reports.

The macropad overlay

The idea

When I'm working at a computer — which is most of the time — it's a desktop, and that desktop has a macropad on it. Usually a Stream Deck as well. So I never run into keyboard shortcut problems, or rather, I never have to solve them with the keyboard: I configure the shortcut on the macropad and it gets its own physical button.

At the laptop, everything falls apart. All the little macros I rely on dozens of times a day suddenly become cumbersome. There are only so many keys, and only so many key combinations. The good ones are already taken by applications, the ones that are left are three-finger contortions, and every new binding is an argument with something else that wanted the same key.

A hardware macropad solves this by adding keys. This solves it by adding a layer instead.

One leader key, pressed once, opens an on-screen pad. Everything behind it is a plain, unmodified keystroke — 1, 2, 3 — because while the pad has focus it is just an ordinary window reading ordinary key events. No global grabs, no collisions, nothing to negotiate with your applications over.

So you spend exactly one scarce global shortcut, and you get a whole pad back.

F9  →  [ pad opens ]  →  1  →  types "claude" + Enter, pad closes

Why F9 is the default

I picked it by looking at the keyboard and asking two questions about each key:

  1. Is it commonly an application-level shortcut? F1 is help, F5 is refresh, F11 is fullscreen, F12 is dev tools. F9 mostly isn't anything.
  2. Does it have a manufacturer mapping people actually use? On a lot of laptops F9 ships as a messaging or notifications toggle out of the box — a function I have never once seen anybody deliberately press.

That combination makes it about the cheapest key on the board to spend. It is only a default, though; any key works, and picking one is a real decision with real tradeoffs — see Choosing a leader key.

What this is not

It is not a hardware macropad, and it is not trying to be. There is no substitute for a physical button you can hit without looking. This is for the machine where you don't have one, and where the alternative is Ctrl+Alt+Shift+F7.

Status

RunsYes, on KDE Plasma 6.6 / Wayland
Tested elsewhereNo
Config, backend detection, renderingVerified
Full press-to-typed-text pathLightly tested — this is the newest part
API and config formatExpect them to change

What a macro can do

ActionDoes
typeTypes text into whatever window you were in, optionally pressing Enter
commandRuns a shell command
keysSends a key combination, e.g. ctrl+shift+t

Install

Requires Python 3.10+, and an input backend (see below).

git clone https://github.com/danielrosehill/Virtual-Macropad.git
cd Virtual-Macropad
./install.sh

install.sh installs the package, drops in a desktop entry, seeds a config, and on KDE Plasma binds the leader key for you. Use a different key with:

LEADER_KEY=F10 ./install.sh

On other desktops it stops short of binding and tells you what to point your compositor's shortcut settings at (virtual-macropad show).

Check everything at any time:

virtual-macropad check

That validates your config, lists which input backends are usable, says which one it picked, and warns you about the layout caveat below if it applies to you.

Configuration

~/.config/virtual-macropad/config.yaml, seeded from config/config.example.yaml on first run.

columns: 3
injector: auto
focus_delay_ms: 120

macros:
  - key: "1"
    label: "Claude Code"
    action: type
    text: "claude"
    enter: true

  - key: "4"
    label: "Terminal"
    action: command
    command: "konsole"

  - key: "7"
    label: "New tab"
    action: keys
    keys: "ctrl+shift+t"

key is a single character and must be unique. Slots appear in file order.

Input backends

Typing into another window is the awkward part, and there is no one way to do it that works everywhere. The pad auto-detects, preferring the one without caveats:

BackendWorks onNotes
wtypeWayland compositors implementing zwp_virtual_keyboard_manager_v1Sends real text. No layout caveat. Preferred.
ydotoolAnywhere, via /dev/uinputNeeds ydotoold running. Emits raw keycodes — see below.
xdotoolX11 onlyReaches XWayland clients at best under Wayland.

Not every compositor implements the virtual keyboard protocol — KWin did not, in the session this was developed against — so ydotool is the realistic backend on a lot of Wayland systems. Check with wayland-info | grep virtual_keyboard.

The layout caveat

ydotool emits keycodes, not characters, and the compositor maps those through whichever keyboard layout is active. If you have a non-Latin layout configured — Hebrew, Russian, Greek — then a type macro fired while it is selected produces the wrong characters entirely.

If that is you, set layout_pin to the index of your Latin layout, counting from zero in the order your layouts are listed:

layout_pin: 0

The pad then forces that layout for the duration of the injection and restores the previous one afterwards. Currently implemented for KWin only; elsewhere it is a harmless no-op. On KDE your layout order is the LayoutList line in ~/.config/kxkbrc.

Choosing a leader key

The leader key is consumed globally — while the binding exists, that key does nothing else, anywhere. So pick one you do not otherwise use. Good candidates on most laptops:

  • F9, and the neighbouring function keys nothing has claimed
  • Scroll Lock, Pause/Break — inert on modern desktops
  • Menu / context key, right of the space bar
  • The numeric keypad's *, / or -, if you have a keypad and do not do arithmetic on it

Two traps, both learned the hard way:

  • Numpad keys are unreliable as compositor-level shortcuts. They are genuinely distinct keys at the hardware level — numpad + is KEY_KPPLUS, the main-row + is Shift + KEY_EQUAL — but not every compositor matches them correctly. KWin 6.6 accepted the binding, persisted it, and then silently never fired it. Test before you commit to one.
  • Terminals cannot tell numpad keys apart at all. A numeric keypad only sends distinguishing escape sequences in keypad application mode; otherwise numpad + sends the same bare + byte as the main-row key. That rules out doing any of this with a shell-level binding.

Both are written up, with the evidence, in docs/input-notes.md.

Documentation

  • Design notes and open questions — the known pitfalls, the tradeoffs baked into the current design, and the things still worth arguing about. Start here if you want to help shape it.
  • Input and shortcut notes — the platform behaviour this depends on: keycodes, Wayland injection backends, layout sensitivity, and how global shortcuts actually register on Plasma 6.

Licence

MIT. See LICENSE.