https://github.com/bananasjim/padctl
HID gamepad daemon with declarative TOML device config
https://github.com/bananasjim/padctl
gamepad hid hidraw linux toml uinput zig
Last synced: about 5 hours ago
JSON representation
HID gamepad daemon with declarative TOML device config
- Host: GitHub
- URL: https://github.com/bananasjim/padctl
- Owner: BANANASJIM
- License: other
- Created: 2026-03-20T18:34:51.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-07-07T23:55:03.000Z (about 16 hours ago)
- Last Synced: 2026-07-08T01:14:20.588Z (about 15 hours ago)
- Topics: gamepad, hid, hidraw, linux, toml, uinput, zig
- Language: Zig
- Size: 3.14 MB
- Stars: 70
- Watchers: 1
- Forks: 8
- Open Issues: 23
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# padctl
**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](https://github.com/BANANASJIM/padctl/issues)!


## What is padctl
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.
## Features
- **Declarative device configs** — add new devices with a `.toml` file, 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** — `SIGHUP` re-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 ` changes profiles without restart
- **Persistent mapping** — `padctl install --mapping ` writes a device binding to `/etc/padctl/config.toml` that auto-applies on every boot
- **User config** — `~/.config/padctl/config.toml` for per-device default mappings (system fallback: `/etc/padctl/config.toml`)
- **Opt-in diagnostic logging** — `padctl dump enable` turns 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`
## Architecture
```text
+----------------------------+
| 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 |
+----------------+ +------------------+
```
## Supported Devices
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 →](https://bananasjim.github.io/padctl/devices/)
## Installation
### Arch Linux (AUR)
```sh
yay -S padctl-bin # prebuilt binary
yay -S padctl-git # build from source
```
If you previously installed from source with `sudo padctl install`, remove that
manual install before switching to AUR so pacman can own the files:
```sh
sudo padctl uninstall --prefix /usr --no-immutable
yay -S padctl-bin # or: yay -S padctl-git
```
### Debian / Ubuntu
```sh
curl -fLO https://github.com/BANANASJIM/padctl/releases/latest/download/padctl_amd64.deb
sudo dpkg -i padctl_amd64.deb
```
For arm64:
```sh
curl -fLO https://github.com/BANANASJIM/padctl/releases/latest/download/padctl_arm64.deb
sudo dpkg -i padctl_arm64.deb
```
Package installs place files on disk; they do not run the live `padctl install`
phase. Enable the user service from your normal login session:
```sh
systemctl --user daemon-reload
systemctl --user enable --now padctl.service
sudo udevadm control --reload-rules
```
If your controller config uses `block_kernel_drivers` (currently Flydigi Vader
5), the driver-block udev rules act on their own: they unbind the kernel driver
only while a padctl daemon is running (its control socket exists), so just
unplug/replug the controller after starting the service.
### From Source
See [Quick Start](#quick-start) below. For other distros, see [CONTRIBUTING.md](CONTRIBUTING.md#packaging).
## Quick Start
```sh
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 # switch mapping profile without restart
```
padctl 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):
```sh
sudo loginctl enable-linger $USER
```
> **Bazzite / 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](https://bananasjim.github.io/padctl/getting-started.html) for detailed setup.
## CLI Reference
| Command | Description |
|---------|-------------|
| `padctl status` | Show daemon state and active devices |
| `padctl doctor` | Diagnose daemon, service, and device problems with next-step hints |
| `padctl devices` | List detected HID/USB devices |
| `padctl list-mappings` | Show available mapping profiles |
| `padctl switch ` | Switch to a named mapping profile |
| `padctl config init [--device ]` | Interactively create a new mapping file in `~/.config/padctl/mappings/` from a template (`default`, `fps`, `racing`, `fighting`) |
| `padctl config edit ` | Open mapping in `$VISUAL` or `$EDITOR` |
| `padctl config test ` | 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 m\|h\|d [-o file]` | Export recent log window for bug reports |
| `padctl dump clear` | Delete all log files |
## Build
**Requirements:** Zig 0.15+, libusb-1.0
```sh
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 |
### Known build issues
**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:
1. **Use the canonical Docker image (recommended)** — `./scripts/padctl-docker build` builds 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](#build-with-docker) below.
2. **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).
3. Track upstream fix progress at [ziglang/zig#31272](https://codeberg.org/ziglang/zig/issues/31272).
## Build with Docker
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.
```sh
./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 debugging
```
The first invocation builds the image (`padctl-build:`); 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.
## Bazzite / Immutable Distros
On immutable distributions (Bazzite, Fedora Atomic, etc.) where `/usr` is read-only, use the bootstrap script for a complete one-command setup:
```sh
curl -fsSL https://raw.githubusercontent.com/BANANASJIM/padctl/main/scripts/bazzite-setup.sh \
| bash -s -- --mapping vader5
```
Replace `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](docs/src/immutable-install.md) 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-linger` is required for auto-start without an active session but its interaction with game-mode auto-login is unconfirmed.
## Documentation
Full documentation: [bananasjim.github.io/padctl](https://bananasjim.github.io/padctl/)
- [Getting started](https://bananasjim.github.io/padctl/getting-started.html)
- [Device config reference](https://bananasjim.github.io/padctl/device-config.html)
- [Mapping config reference](https://bananasjim.github.io/padctl/mapping-config.html)
- [Supported devices](https://bananasjim.github.io/padctl/devices/)
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on adding device configs or contributing code.
## License
LGPL-2.1-or-later — see [LICENSE](LICENSE).