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 production configs for 12 supported device families across 8
vendors. devices/ also contains a Flydigi DInput/2.4G compatibility variant
plus example/test fixtures, so the raw TOML file count is higher:
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/latest/download/padctl_amd64.deb
sudo dpkg -i padctl_amd64.debFor arm64:
curl -fLO https://github.com/BANANASJIM/padctl/releases/latest/download/padctl_arm64.deb
sudo dpkg -i padctl_arm64.debAfter installing via a package manager, enable the user service from your normal login session:
systemctl --user daemon-reload
systemctl --user enable --now padctl.serviceSee Quick Start below. For other distros, see CONTRIBUTING.md.
zig build # build from source
sudo zig-out/bin/padctl install # install binary, udev rules, service; starts user service when run via sudo
padctl status # check daemon and detected devices
padctl config init # create a mapping in ~/.config/padctl/mappings/ interactively
padctl list-mappings # show generated and installed mapping profiles
padctl switch <name> # switch mapping profile without restartpadctl runs as a systemd user service. The default root install writes the
system-wide user unit under /usr/lib/systemd/user/ and enables/starts it for
the invoking user via systemctl --user; immutable and custom-prefix installs
may use /etc/systemd/user/. The binary and udev rules still require root to
install, but the daemon runs as your own user. Run
systemctl --user enable --now padctl.service manually only if install was run
with --no-enable, --no-start, or without a discoverable SUDO_USER.
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 [--preset <name>] |
Interactively create a new mapping file in ~/.config/padctl/mappings/. Valid --preset values: xbox-360, xbox-elite2, dualsense, switch-pro. |
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 canonical Docker image (recommended) —
./scripts/padctl-docker buildbuilds inside the Debian bookworm image (glibc 2.36) with the Zig version pinned by.zigversion, which is the supported CI build environment. See Build with Docker below. - 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.
If you cannot install Zig locally — or hit the glibc 2.43+ linker error above — build padctl inside the canonical Docker image instead. It pins the exact Zig version from .zigversion against Debian bookworm (glibc 2.36), so it matches the CI build environment.
./scripts/padctl-docker build # zig build inside the image
./scripts/padctl-docker test # zig build test inside the image
./scripts/padctl-docker shell # interactive shell for debuggingThe first invocation builds the image (padctl-build:<zig-version>); later runs reuse it. The repository is bind-mounted at /src, so build output lands in your working tree as usual. Requires only Docker — no local Zig toolchain.
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.