Rebinded

Cross-platform key remapping daemon with per-app context

Daemon Active Updated 4d ago

Rebinded is a key-remapping daemon for Windows and Linux, written in Rust. It claims the keys nobody uses — the F13 through F24 block some keyboards expose, scroll-wheel tilt, spare mouse buttons — and turns them into media controls, browser navigation, or whatever else, choosing each binding from whatever window is focused. One TOML file drives both platforms.

The mapping table is the easy part. The work is intercepting input cleanly enough that the daemon stays invisible whenever it does nothing, and deciding what an abstract action like next track should actually do on a desktop running several things that could be playing audio.

The interception boundary

The daemon has to sit between the hardware and every other application, see every event, and forward the ones it does not care about without anyone noticing. Each platform reaches that point differently.

On Linux it grabs keyboards exclusively through evdev, routing their events to rebinded alone, and re-injects anything meant to pass through with a virtual uinput device. The mouse is the awkward case: grabbing it wholesale would freeze pointer motion for every application at once. So rebinded never grabs the mouse. It takes XInput2 passive grabs on just the two scroll-wheel buttons, at the X11 level, leaving cursor movement and clicks flowing through the physical device.

How a grabbed scroll tick passes through

The scroll grab is synchronous: a tick is frozen the moment it arrives and the pointer stays blocked until the daemon decides what to do with it. If the binding resolves to passthrough, rebinded tells X11 to replay the event as though the grab never happened, so the application underneath sees an ordinary scroll. If the binding consumes it, the frozen event is dropped. Nothing downstream can tell the difference.

On Windows the same job is a low-level keyboard hook (WH_KEYBOARD_LL) installed on a dedicated message-pump thread, with SendInput for synthetic keys and the foreground window queried directly from the Win32 API.

Which player should this control?

A media key means nothing until you decide which player it drives. On Linux, media control travels over D-Bus through MPRIS, and a normal desktop exposes several MPRIS players at once — a browser tab, a music app, a video player. Play/pause should reach the one you actually mean.

rebinded scores every player and takes the highest by a strict order of preference:

  • a player that is currently playing beats everything else;

  • then one whose window is the window you have focused right now;

  • then one in the same process family, so vivaldi-bin still matches vivaldi;

  • then the most recently focused player, provided it was focused within the last ten minutes;

  • then, as a fallback, whatever played most recently.

A background task polls the focused window twice a second to keep that focus history fresh. The matching is deliberately fuzzy — it compares the D-Bus service name, the player's self-reported identity, and the window's binary and class in both directions, stripping suffixes like -bin and -browser — because none of those names reliably agree across applications.

Debouncing a tilt button

The scroll wheel tilts left and right, and the tilts register as buttons — but they are far too easy to nudge by accident mid-scroll. Bind them straight to previous/next track and every stray tilt skips a song.

The gated-hold strategy is a small state machine that fixes this. A tilt has to be held past an initial gate — 110 ms in the sample config — before it fires at all, which discards the accidental taps. Once it has fired, a repeat window keeps subsequent presses instant for a couple of seconds, so deliberately skipping through several tracks stays responsive. Keys that share a strategy share the gate, so opening it once opens it for the whole group.

The same machinery carries a second behaviour. While a tilt is held, an incoming scroll tick is diverted: instead of skipping tracks, scrolling adjusts the volume. Holding the button turns into a transient volume mode, and the key's pending action is cancelled or released cleanly depending on whether it had already fired.

Configuration

Both platforms read a single TOML file. A binding is either one action or a list of condition-and-action rules evaluated top to bottom, first match wins. Conditions test the focused window's title, class, or binary against glob patterns, can be negated with a not_ prefix, and AND together within a rule. A binding that matches nothing falls through to passthrough.

toml
[bindings.f17]
action = [
  { condition = { window = { title = "*Firefox*" } }, action = "browser_back" },
  { condition = { window = { binary = "vivaldi*" } }, action = "browser_back" },
  # no match falls through to passthrough
]

Running it

It is meant to run as a background service; the repository ships a systemd unit for Linux. There it needs read access to /dev/input and write access to /dev/uinput — in practice, membership in the input group and a loaded uinput module. rebinded checks both at startup and prints the exact commands to fix them when they are missing, rather than failing with a bare permission error.

Tradeoffs

The Linux support is X11-only. Both the window queries and the scroll interception lean on X11 and XInput2, so under Wayland the daemon does nothing useful yet. And because it holds exclusive keyboard grabs, a crash could in principle leave a device captured — so it installs a panic hook that ungrabs everything on the way down.

§07

Gallery

Related work

On this page