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=false to 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 .sframe sections to crt1.o startup 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 install enables/starts padctl.service unless you pass --no-enable or --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 via block_kernel_drivers in 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. When padctl install runs as root, it also walks /sys/bus/usb/drivers/<driver>/unbind for 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 会提示后续步骤。完整流程:

  1. Capture — 运行上面的命令录制报文,得到 mypad.toml 骨架。
  2. Refine — 检查并调整 [report] 字段的 offset / type,使按键和摇杆映射正确。
  3. Install — 放到用户配置目录:mkdir -p ~/.config/padctl/devices && cp mypad.toml ~/.config/padctl/devices/
  4. Validatepadctl --validate ~/.config/padctl/devices/mypad.toml
  5. Test decodepadctl 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):

  1. ~/.config/padctl/config.toml — user overrides (highest priority)
  2. /etc/padctl/config.toml — system-wide defaults (written by padctl 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.