Getting Started
Install via Package Manager
Arch Linux (AUR)
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:
sudo padctl uninstall --prefix /usr --no-immutable
yay -S padctl-bin # or: yay -S padctl-git
Debian / Ubuntu
curl -fLO https://github.com/BANANASJIM/padctl/releases/latest/download/padctl_amd64.deb
sudo dpkg -i padctl_amd64.deb
For arm64:
curl -fLO https://github.com/BANANASJIM/padctl/releases/latest/download/padctl_arm64.deb
sudo dpkg -i padctl_arm64.deb
Package installs place the binary, udev rules, device configs, and system-wide
user unit on disk. They do not run the live padctl install phase, so enable
the service from your normal login session and reload udev rules:
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 under
/run/user/<uid>/ or /run/padctl/). Unplug and replug the controller after
starting the service. When the service is stopped or disabled, the rules leave
the kernel driver alone and restore it on the next unplug.
Prerequisites
- Zig 0.15+ (build from source)
- Linux kernel ≥ 5.10 (uinput + hidraw support)
- libusb-1.0 (system package, optional — pass
-Dlibusb=falseto build without) - A HID gamepad accessible via
/dev/hidraw*
Build from Source
git clone https://github.com/BANANASJIM/padctl
cd padctl
zig build -Doptimize=ReleaseSafe
Optional build flags:
-Dlibusb=false— disable libusb linkage (uses hidraw-only path)-Dwasm=false— disable WASM plugin runtime
GCC 15 build failure (issue #147): Arch Linux and similar distros with glibc 2.43+ may hit
error: relocation R_X86_64_PC64 in .sframe section is unsupported— glibc 2.43 adds.sframesections tocrt1.ostartup objects, which Zig 0.15.x's linker does not yet handle. This is an upstream Zig limitation, not a padctl bug. Build inside the canonical Docker image (./scripts/padctl-docker build, Debian bookworm + glibc 2.36) — see Build with Docker below — or install Zig 0.15.2 from the official tarball on a system with glibc ≤ 2.41 (Debian 12, Ubuntu 22.04/24.04 all work; Arch with glibc 2.43+ does NOT). Upstream fix: ziglang/zig#31272.
Build with Docker
If you do not have Zig installed — or hit the glibc 2.43+ linker error above — build inside the canonical Docker image. It pins the Zig version from .zigversion against Debian bookworm (glibc 2.36) and matches the CI build environment, so it needs only Docker:
./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 run builds the image (padctl-build:<zig-version>) and later runs reuse it. The repository is bind-mounted at /src, so zig-out/ appears in your working tree as with a native build.
Install
sudo ./zig-out/bin/padctl install
This copies the binary, systemd service, device configs, and udev rules into /usr. It also runs systemctl daemon-reload and udevadm trigger automatically, and removes any legacy udev rules left by previous installs.
Custom prefix (e.g. for packaging):
sudo ./zig-out/bin/padctl install --prefix /usr --destdir "$DESTDIR"
Lifecycle scope override
PADCTL_INSTALL_PHASE=package forces the installer to act as if invoked by a
package post-install script (no service start/enable, no XDG path creation,
no UDEV reload). Useful for dpkg --configure, rpm --install, and AUR
PKGBUILD package() functions. Scope is resolved in this priority order:
--destdir flag > PADCTL_INSTALL_PHASE env > DESTDIR env > --scope
flag > euid > --prefix.
Additional Services
padctl install also sets up the following on all systems:
padctl-reconnect— A hotplug script triggered by udev when a controller is plugged in. It re-applies the active mapping through any running padctl daemon socket. It does not start or restart the daemon;padctl installenables/startspadctl.serviceunless you pass--no-enableor--no-start. After suspend/resume the kernel re-emits udev events for re-enumerated devices, so the same hook handles post-wake reconnect — no separate resume unit is needed.- Driver conflict rules — Auto-generated udev rules that unbind conflicting kernel drivers (e.g.,
xpad) from devices that padctl manages. Configured per-device viablock_kernel_driversin device TOML configs. The udev rule only detaches a kernel driver while a padctl daemon is running (its control socket exists), so a plain package install never strips a controller before the service starts. Whenpadctl installruns as root, it also walks/sys/bus/usb/drivers/<driver>/unbindfor matching VID:PID pairs immediately, so already-bound devices are evicted without waiting for replug (issue #162).
Install a Mapping
To install a mapping config to /etc/padctl/mappings/ during install:
sudo ./zig-out/bin/padctl install --mapping vader5
The --mapping flag is repeatable. Use --force-mapping to overwrite existing mapping files.
When --mapping is given, the installer also writes a device-to-mapping binding in /etc/padctl/config.toml so the daemon auto-applies the mapping on every boot. Use --force-binding to overwrite an existing binding for the same device.
Bazzite / immutable distros: See the Bazzite / Immutable Distros guide for special installation steps.
Install problems? See Troubleshooting for daemon/socket checks, udev permission issues, missing device configs, and known build failures.
Verify
padctl status
padctl scan
padctl list-mappings
padctl status verifies the user service socket is reachable. padctl scan
lists connected HID devices and shows whether a matching device config was found
for each. padctl list-mappings verifies the mapping search paths are readable.
If padctl status fails, run padctl doctor — it checks the daemon, the
systemd service state, and every supported device, and prints next-step hints.
Run as Service
If you built from source, run the installer first — zig build alone does not install the service file:
zig build
sudo ./zig-out/bin/padctl install # installs binary, service, device configs, and udev rules
padctl install automatically runs daemon-reload, enables, and starts padctl.service via sudo -u $SUDO_USER systemctl --user. Driver-block udev rules activate on their own once the daemon is running. The systemctl --user enable --now padctl.service line is only needed if you used --no-enable or --no-start.
To auto-start at boot without an active login session (headless setups, Steam Deck game mode):
sudo loginctl enable-linger $USER
The service runs padctl in daemon mode, scanning all config directories (user, system, and builtin) with automatic hotplug support. udev rules grant access via uaccess — no sudo needed for the logged-in user.
Check the daemon is running:
$ padctl status
STATUS device=Flydigi Vader 5 Pro state=active mapping=fps
Each managed device prints one space-separated triple: device=<name>,
state=<active|suspended>, mapping=<active mapping name|(none)>. Multiple
devices appear on the same line. Exit code is 0 when the daemon answered
and 1 when the response begins with ERR or the socket is unreachable.
Run Manually
Bare invocation — padctl auto-discovers configs via XDG paths:
padctl
Or target specific configs:
# Single config
padctl --config /usr/share/padctl/devices/sony/dualsense.toml
# All configs in a directory
padctl --config-dir /usr/share/padctl/devices/
Validate a Config
padctl --validate devices/sony/dualsense.toml # device config
padctl --validate ~/.config/padctl/mappings/fps.toml # mapping config
--validate auto-detects which schema to apply by scanning for a [device]
section header — files containing [device], [device.*], or [[device.*]]
are validated as device configs; everything else (including bare name = ...
mapping files) is validated against the mapping schema.
Exit 0 = valid. Exit 1 = validation errors printed to stderr. Exit 2 = file not found or parse failure.
The flag is repeatable: padctl --validate a.toml --validate b.toml validates both files and exits with the worst code seen.
Adding a new device
padctl 的目标是「新设备 = 一个 .toml,零代码」。当 padctl scan 显示某个设备
unmatched(没有匹配的 device config)时,用 padctl-capture 录制它的 HID 报文并
生成一个起始 TOML 骨架:
padctl-capture --vid 0xVVVV --pid 0xPPPP --output mypad.toml
padctl-capture 会提示后续步骤。完整流程:
- Capture — 运行上面的命令录制报文,得到
mypad.toml骨架。 - Refine — 检查并调整
[report]字段的 offset / type,使按键和摇杆映射正确。 - Install — 放到用户配置目录:
mkdir -p ~/.config/padctl/devices && cp mypad.toml ~/.config/padctl/devices/ - Validate —
padctl --validate ~/.config/padctl/devices/mypad.toml - Test decode —
padctl config test实时预览解码出的具名按键/摇杆事件,确认字段正确。
Generate Device Docs
padctl --doc-gen --config devices/sony/dualsense.toml
User Config
padctl reads a config file to set per-device default mappings. The loader checks these paths in order (first found wins):
~/.config/padctl/config.toml— user overrides (highest priority)/etc/padctl/config.toml— system-wide defaults (written bypadctl install --mapping)
version = 1
[[device]]
name = "Flydigi Vader 5 Pro"
default_mapping = "fps"
On daemon start, padctl matches the connected device name (case-insensitive) and loads the named mapping profile automatically. The system path is the fallback for environments where HOME is not set (e.g. systemd services).
padctl switch <name> automatically updates the user config, so the choice is remembered for bare padctl switch (re-apply without a name). Bare padctl switch (no argument) reads default_mapping from the connected device's entry in config.toml; if no entry exists, it prints error: no default_mapping in config.toml for device "<name>" and exits. To make the choice survive reboots, use padctl switch <name> --persist which copies the mapping and config to /etc/padctl/ via sudo.
CLI Reference
padctl switch [name] [--device <id>] # switch mapping; omit name to fall back to default_mapping from config.toml
padctl switch <name> --persist # switch + copy to /etc/padctl/ for reboot persistence (sudo)
padctl status [--socket <path>] # show daemon status
padctl devices [--socket <path>] # list connected devices
padctl list-mappings [--config-dir <dir>] # list available mapping profiles
padctl reload [--pid <pid>] # send SIGHUP to reload configs
padctl config list # show XDG config search paths
padctl config init [--device <name>] # interactive mapping creator; templates: default, fps, racing, fighting
padctl config edit [name] # open mapping in $VISUAL/$EDITOR
padctl config test [--config] [--mapping] # live input preview; prints decoded named events by default, --raw restores hex dump
padctl dump enable|disable # toggle diagnostic logging (persists)
padctl dump status # show dump state, log path, size, time span
padctl dump export --period Nm|Nh|Nd [-o file] # export filtered log window
padctl dump clear # delete all log files
See the Diagnostic Logging guide for the full padctl dump workflow, log paths, and the [diagnostics] config section.
udev Permissions
padctl needs access to /dev/hidraw*, /dev/uinput, and /dev/uhid. The first two are standard for HID gamepad daemons; /dev/uhid is required for the SDL3-visible IMU pairing path (per ADR-015). padctl install writes the necessary udev rules (60-padctl.rules) automatically.
The padctl install command generates and installs udev rules automatically from device configs.
If you need to regenerate rules after adding custom device configs:
sudo padctl install
The udev rules use TAG+="uaccess" to grant the active graphical user access
without requiring root. For SSH, headless, or test sessions without desktop ACLs,
the rules also grant GROUP="input", MODE="0660"; add your user to the input
group and log in again if those sessions need direct device access.