A Rust-based Linux input device event mapper for Kindle e-readers. Maps button presses from input devices to shell scripts.
- Map buttons to shell scripts
- Page turns and reader controls for both the native Kindle reader and KOReader
- Long press support with separate actions
- Auto-repeat when buttons are held
- Debouncing to prevent double-triggers
- Auto-reconnect on device disconnect
- Optional exclusive device grab
- Non-US keyboard layout via XKB override, with optional Alt+Shift toggle
| Bindings | Device | Debug | Action picker |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
A touchscreen UI for editing mappings without SSH lives in illusion/MapperManager/. The daemon stays a plain runtime — the WAF app spawns a --waf-helper HTTP server (localhost:8322) only while the app is open, edits config.ini, and restarts the daemon via initctl restart kindle-button-mapper.
Deploy and register:
just deploy # ship the binary + config + init script
just deploy-waf # ship the illusion/ app, restart helper, launch WAF
ssh kindle "sh /mnt/us/kindle-button-mapper/illusion/install-waf-app.sh" # first time onlyThe app has three tabs:
- Bindings — list of current button / D-pad / trigger mappings per device. Tap + Add to capture a button and pick an action. The action picker opens on Kindle (native reader) and also offers KOReader, a keyboard key, or a custom shell command.
- Device — list of configured devices, each matched by its Bluetooth MAC or name. Add, edit, or remove a device, or tap one seen on
/dev/inputto prefill its name and MAC. - Debug — live button capture for discovering codes, and a raw
config.inieditor.
cargo build --releasecargo build --release --target armv7-unknown-linux-gnueabihfkindle-button-mapper /path/to/config.iniEnable debug logging with RUST_LOG=debug.
INI format configuration file. Each device is its own [device.ID] block with
[device.ID.{kind}] subsections for the mappings:
[settings]
debounce_ms = 200
long_press_ms = 500
repeat_ms = 100
log_buttons = true
keep_awake = true
on_connect = /path/to/script.sh
on_disconnect = /path/to/script.sh
[device.gamepad]
name = Device Name
uniq = AA:BB:CC:DD:EE:FF # Bluetooth MAC; matched first when set
grab = true
# type = mouse # mapped buttons act instead of clicking
# keyboard_layout = fr # XKB layout override (comma list for an Alt+Shift toggle, e.g. us,ru)
[device.gamepad.buttons]
# button_code = /path/to/script.sh
[device.gamepad.longpress]
# button_code = /path/to/script.sh
[device.gamepad.dpad]
# up/down/left/right = /path/to/script.sh
[device.gamepad.dpad_longpress]
# up/down/left/right = /path/to/script.sh
[device.gamepad.triggers]
# lt/rt = /path/to/script.sh
[device.gamepad.triggers_longpress]
# lt/rt = /path/to/script.shDevices are matched by identity, never by /dev/input/eventX path (that index is
unstable across reconnects): the mapper uses the Bluetooth MAC (uniq) when set,
otherwise the device name. Set at least one.
Set keyboard_layout to an XKB layout code (e.g. fr, de, ro, fr(oss)) to type correctly on a non-US Bluetooth keyboard. The reader re-pins the us core keymap on every focus and /usr/share/X11/xkb is read-only, so the mapper bind-mounts a generated us symbols file over the system one; every re-pin then resolves to your layout, reverted when the daemon stops. Give a comma list for an Alt+Shift toggle (us,ru); it's a system-wide override taken from the first device that sets one. Leave it unset to keep the system default.
Set type = mouse to map a mouse's buttons. Mapped buttons run their action
instead of clicking; the pointer, wheel and unmapped buttons keep working as a
normal mouse.
A button fires as soon as it goes down. Give it a longpress mapping and it
fires on release instead, since a short press is only a short press once you
let go.
Use log_buttons = true to discover button codes for your device.
Three helper scripts ship with the mapper, so a binding can drive either reader:
scripts/auto.sh (whatever is on screen) |
scripts/kindle.sh (native reader) |
scripts/koreader.sh (KOReader) |
|
|---|---|---|---|
| How it talks to the reader | picks one of the two below | virtual keyboard + lipc |
HTTP Inspector on localhost:8080 |
| Setup needed | none | none | HTTP Inspector auto-start |
| Actions | next_page, prev_page, menu, brightness <n>, brightness_toggle |
next_page, prev_page, next_page_tap, prev_page_tap, home, back, toolbar, brightness <n>, brightness_toggle |
next_page, prev_page, menu, night_mode, rotate, font_up/font_down, toggle_status_bar, brightness <n>, brightness_toggle |
auto.sh is what the Auto tab in MapperManager writes, and it is the one to use
if you read in both: it sends the event to KOReader's HTTP Inspector and falls
back to the native reader when nothing is listening there. One binding per
button covers both readers.
The daemon recognises those three scripts and runs the page-turn and reader
events itself — a socket write, or a write to the injector it already owns —
instead of forking a shell and a curl per press. A binding it does not
recognise, including anything of your own and the lipc actions, still runs as
a shell command. The scripts stay the interface, so a binding works the same
from a terminal.
Page turns are injected rather than driven by the UI, so there are no
coordinates to get wrong.
The daemon reads requests off /var/run/kindle-button-mapper-key.fifo, which is
how scripts/key.sh works without any external tool, and it picks the device.
On a model with physical page buttons the framework only turns pages for that
node, so the daemon writes KEY_PAGEDOWN/KEY_PAGEUP straight into it and the
reader sees a normal button press. Everywhere else the window manager swallows
page keys, since it only forwards them on models devcap-get-feature -a button.keypad says have the buttons, so the daemon sends PageDown/PageUp over
the X socket straight to the window winmgr reports in front, which is the
reader you are looking at. If that window cannot be found it falls back to
KEY_DOWN as next page and KEY_UP as previous page on the virtual keyboard,
which is what the reader takes on some models.
Some firmware ignores injected keys altogether, so next_page_tap and
prev_page_tap go in as a touch on the screen instead, near the right and left
edges. That one runs entirely in scripts/tap.sh, which finds the panel in
/proc/bus/input/devices, takes the screen size from the framebuffer and
writes the events itself, no daemon involved. Only reach for these if the plain
page turn does nothing, since a tap follows whatever tap zones you have set in
the reader.
The panel never turns with the screen, so a tap for the right edge of a
landscape page has to be rotated back into panel coordinates. tap.sh asks the
framework which way up it is and does that itself. KOReader rotates in software
and tells nobody, so there it reads as upright and the tap lands where it did in
portrait — use its HTTP Inspector, which turns the page whatever the rotation.
Set TAP_ROTATION to 0, 90, 180 or 270 to force the angle.
Everything else goes over lipc.
keep_awake = true (default) resets the screensaver timer on input so the device stays awake while a controller is connected, without blocking the power button.
Grab the latest kindle-button-mapper-armv7.tar.gz from the releases page, copy it to the Kindle, extract, and run the installer:
scp kindle-button-mapper-armv7.tar.gz kindle:/mnt/us/
ssh kindle "mkdir -p /mnt/us/kbm && tar -xzf /mnt/us/kindle-button-mapper-armv7.tar.gz -C /mnt/us/kbm && sh /mnt/us/kbm/install.sh"The tarball ships the pre-built ARMv7 binary, so no host toolchain or cross-compile is needed. install.sh detects the extracted-tarball layout automatically.
# 1. Cross-compile the ARM binary on your host
rustup target add armv7-unknown-linux-musleabihf
cargo build --release --target armv7-unknown-linux-musleabihf
# 2. Copy the repo to the Kindle and run the installer
rsync -av --exclude target/ . kindle:/mnt/us/kindle-button-mapper-src/
ssh kindle "sh /mnt/us/kindle-button-mapper-src/install.sh"Uninstall: ssh kindle "sh /mnt/us/kindle-button-mapper/uninstall.sh" (the script is copied to the install dir, so you can run it even after the source tree is gone).
- Jailbroken Kindle (Kindle 5+ / FW 5.x).
- Linux kernel with evdev (
/dev/input/eventX) — present on all stock Kindles. - An input device the Kindle can see — e.g. a Bluetooth gamepad/remote bridged via kindle-hid-passthrough, or any USB OTG HID device.
- Nothing extra for the native Kindle reader —
scripts/kindle.shandscripts/auto.shonly need the daemon running. - KOReader HTTP Inspector (for KOReader integration): enable auto-start once in KOReader → Tools → More Tools → HTTP Inspector → Auto-start HTTP server. The default mappings in
scripts/koreader.shsend commands tolocalhost:8080. MapperManager warns you in the KOReader action tab when this auto-start is off.
Tested on:
- Device: Kindle MT8110 Bellatrix (Paperwhite 12)
- SoC: MediaTek MT8512 (ARMv7-A Cortex-A53)
- Kernel: Linux 4.9.77-lab126
The release binary is a static ARMv7 musl build (~1.1 MB, no glibc dependency) and should work on any ARMv7 Kindle running a jailbroken FW that allows running native binaries from /mnt/us. No per-FW binary is required.
GPL-3.0-or-later — see LICENSE.



