An open API service indexing awesome lists of open source software.

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

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)!

![CI](https://github.com/BANANASJIM/padctl/actions/workflows/ci.yml/badge.svg)
![License](https://img.shields.io/badge/license-LGPL--2.1--or--later-blue)

## 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).