Universal Linux gamepad compatibility layer
This project is very much a work in progress. Feedback, bug reports, and feature requests are welcome — please open an issue!
padctl is a userspace daemon that maps vendor-specific USB/HID gamepad reports to standard Linux input events via uinput. Device support is driven entirely by declarative TOML configs — no kernel patches, no custom drivers.
- Declarative device configs — add new devices with a
.tomlfile, no recompilation - Layer system — hold/toggle/tap-hold layers with independent remaps, gyro, and stick modes
- Gyro mouse — gyro-to-mouse with sensitivity, deadzone, smoothing, and curve controls
- Stick mouse/scroll — left or right stick as mouse or scroll wheel
- Macros — named key sequences bound to any button
- Exclusive device grab — grabs the hidraw/evdev node so the original device is hidden from other processes while padctl is running
- Multi-device + hotplug — automatic device detection and per-device threads via netlink
- Hot-reload —
SIGHUPre-reads configs without restart, diffed per physical device - Force feedback — FF_RUMBLE passthrough from uinput to physical device with userspace auto-stop timer (compensates for uinput not using the kernel's ff-memless driver)
- Runtime mapping switch —
padctl switch <name>changes profiles without restart - Persistent mapping —
padctl install --mapping <name>writes a device binding to/etc/padctl/config.tomlthat auto-applies on every boot - User config —
~/.config/padctl/config.tomlfor per-device default mappings (system fallback:/etc/padctl/config.toml) - Opt-in diagnostic logging —
padctl dump enableturns on a general-purpose, togglable file logger so users can produce a structured log for any class of bug report (force-feedback, input, mapping, hotplug, …). Today it is wired deepest into the rumble/HID path; more subsystems will be instrumented over time. Rotated, bounded on disk, and zero overhead when disabled (default) - CLI tools —
padctl status,padctl devices,padctl list-mappings,padctl config init/edit/test,padctl dump enable/disable/status/export/clear
+----------------------------+
| Physical Device (USB / BT) |
+----------------------------+
|
+-------+-------+
| |
v v
+----------------+ +-------------------+
| HID / hidraw | | Vendor / libusb |
| io/hidraw.zig | | io/usbraw.zig |
+----------------+ +-------------------+
\ /
\ /
v v
+--------------------+
| DeviceIO (unified) |
+--------------------+
|
v
+--------------------+
| main loop (ppoll) |
+--------------------+
|
+---------+---------+
| |
v v
+------------------+ +------------------+
| config/device.zig| | io/hotplug.zig |
| devices/*.toml | | udev monitor |
+------------------+ +------------------+
|
v
+-----------------------------------------+
| [input rules] -> interpreter -> state |
| [output] -> OutputConfig |
+-----------------------------------------+
|
v
+----------------------+
| mapper (layer/remap) |
+----------------------+
| |
v v
+----------------+ +------------------+
| gamepad output | | generic output |
| uinput + aux | | generic + touch |
+----------------+ +------------------+
Ships with configs for 12 devices across 8 vendors:
Sony (3) · Nintendo (1) · Microsoft (1) · Valve (1) · 8BitDo (1) · Flydigi (2) · HORI (1) · Lenovo (2)
Full device list with feature matrix →
yay -S padctl-bin # prebuilt binary
yay -S padctl-git # build from sourcecurl -fLO https://github.com/BANANASJIM/padctl/releases/download/v0.1.2/padctl_0.1.2_amd64.deb
sudo dpkg -i padctl_0.1.2_amd64.debFor arm64, replace amd64 with arm64.
See Quick Start below. For other distros, see CONTRIBUTING.md.
zig build # build from source
sudo zig-out/bin/padctl install # install binary, udev rules; writes user service unit
systemctl --user enable --now padctl.service # start the user service
padctl config init # create a mapping in ~/.config/padctl/mappings/ interactively
padctl status # check daemon and detected devices
padctl switch <name> # switch mapping profile without restartpadctl runs as a systemd user service (~/.config/systemd/user/padctl.service). The binary and udev rules still require root to install, but the service runs as your own user — no User= directive or ProtectHome needed.
To auto-start at boot without an active login session (headless setups, Steam Deck game mode):
sudo loginctl enable-linger $USERBazzite / Steam Deck: linger behavior depends on the desktop session configuration. Auto-start at boot without login is not verified on these platforms.
See the getting started guide for detailed setup.
| Command | Description |
|---|---|
padctl status |
Show daemon state and active devices |
padctl devices |
List detected HID/USB devices |
padctl list-mappings |
Show available mapping profiles |
padctl switch <name> |
Switch to a named mapping profile |
padctl config init |
Interactively create a new mapping file in ~/.config/padctl/mappings/ |
padctl config edit <mapping> |
Open mapping in $VISUAL or $EDITOR |
padctl config test <mapping> |
Live input preview against the mapping (no apply) |
padctl scan |
Re-scan for connected devices |
padctl dump enable|disable |
Toggle opt-in diagnostic logging (persists across reboots) |
padctl dump status |
Show logging state, log path, size, and time span |
padctl dump export --period <N>m|<N>h|<N>d [-o file] |
Export recent log window for bug reports |
padctl dump clear |
Delete all log files |
Requirements: Zig 0.15+, libusb-1.0
zig build # build all binaries
zig build test # run unit tests
zig build check-all # all checks (test + safe + fmt)| Flag | Default | Effect |
|---|---|---|
-Dlibusb=false |
true |
Disable libusb linkage (hidraw-only) |
-Dwasm=false |
true |
Disable WASM plugin runtime |
GCC 15 — R_X86_64_PC64 in .sframe linker error (issue #147)
glibc 2.43+ (shipped on Arch, Artix, and similar bleeding-edge distros) adds .sframe sections to crt1.o and related startup objects. Zig 0.15.x's linker does not yet handle the R_X86_64_PC64 relocation type used there, producing:
error: relocation R_X86_64_PC64 in .sframe section is unsupported
This is an upstream Zig limitation, not a padctl bug. Workarounds:
- Use the Debian bookworm Docker container (recommended) —
Dockerfile.wave5in the repo root builds with Zig 0.15.2 from the official tarball against Debian's GCC 12, which is the supported CI build environment. - Install Zig 0.15.2 from the official tarball (
https://ziglang.org/download/) on a system with glibc ≤ 2.41 (Debian 12 = glibc 2.36, Ubuntu 22.04 = glibc 2.35, Ubuntu 24.04 = glibc 2.39 all work; Arch with glibc 2.43+ does NOT). - Track upstream fix progress at ziglang/zig#31272.
On immutable distributions (Bazzite, Fedora Atomic, etc.) where /usr is read-only, use the bootstrap script for a complete one-command setup:
curl -fsSL https://raw.githubusercontent.com/BANANASJIM/padctl/main/scripts/bazzite-setup.sh \
| bash -s -- --mapping vader5Replace vader5 with the mapping for your controller, or omit --mapping to install without a mapping. When run locally (bash scripts/bazzite-setup.sh), the script prompts for mapping selection interactively.
See the Bazzite / Immutable Distros guide for full details on what the install does, the --immutable flag, security notes, and mapping management.
Tested on: Bazzite (Fedora Atomic / ostree). Other immutable distros may work but are untested.
V2 note: Bazzite and Steam Deck default linger state has not been verified with the user-service install.
loginctl enable-lingeris required for auto-start without an active session but its interaction with game-mode auto-login is unconfirmed.
Full documentation: bananasjim.github.io/padctl
See CONTRIBUTING.md for guidelines on adding device configs or contributing code.
LGPL-2.1-or-later — see LICENSE.