Mapping Configuration Guide
Overview
A mapping config controls how padctl translates physical inputs to virtual outputs. It is separate from the device config:
- Device config (
devices/*.toml) — describes the hardware HID protocol. Stable, community-maintained. You usually don't touch this. - Mapping config (
~/.config/padctl/mappings/*.toml) — your personal preferences: remapped buttons, gyro mouse, layers, macros.
Without a mapping config, padctl passes all inputs through unchanged as a standard gamepad.
Quick Start
Create a mapping
Copy the example and edit it:
mkdir -p ~/.config/padctl/mappings/
cp /usr/share/padctl/config/example-mapping.toml ~/.config/padctl/mappings/my-config.toml
$EDITOR ~/.config/padctl/mappings/my-config.toml
Or use the interactive creator:
padctl config init
XDG Search Paths
padctl searches for mapping profiles in this order (first match wins):
~/.config/padctl/mappings/— user overrides/etc/padctl/mappings/— system-wide profiles/usr/share/padctl/mappings/— builtin profiles
Apply a mapping
Switch the active mapping at runtime:
padctl switch fps
Every switch automatically saves your choice to ~/.config/padctl/config.toml, so you can restore it later with a bare switch:
padctl switch # re-applies default_mapping from config.toml for the connected device
Bare padctl switch queries the running daemon for the connected device name, then looks up default_mapping in config.toml (user path first, then /etc/padctl/config.toml). If no entry is found it exits with error: no default_mapping in config.toml for device "<name>".
Persist across reboots (--persist)
By default, padctl switch only saves to your user config (~/.config/padctl/config.toml). The systemd daemon cannot read this at boot because HOME is not set in its service environment. To make the mapping survive reboots:
padctl switch fps --persist
This will:
- Apply the mapping at runtime (same as without
--persist) - Save to your user config (same as without
--persist) - Prompt for confirmation, then ask for your sudo password
- Copy the mapping file to
/etc/padctl/mappings/ - Copy your user config to
/etc/padctl/config.toml
The daemon reads /etc/padctl/ at boot, so the mapping auto-applies on every reboot without manual intervention.
Limitations:
--persistis not yet supported with--device(multi-controller setups). In multi-device sessions, auto-save and barepadctl switchresolve against the first connected device. Usepadctl install --mapping <name>for explicit per-device persistence in multi-controller setups.- A future version may persist by default, but this behavior is uncertain and subject to change.
Config file precedence
The daemon checks these paths in order when resolving default mappings:
~/.config/padctl/config.toml— user overrides (highest priority, only available whenHOMEis set)/etc/padctl/config.toml— system-wide defaults (written bypadctl install --mappingorpadctl switch --persist)
version = 1
[[device]]
name = "Flydigi Vader 5 Pro"
default_mapping = "fps"
If you installed with padctl install --mapping vader5, the system config is already written for you.
Manual run
Or pass a mapping directly when running padctl manually:
padctl --mapping ~/.config/padctl/mappings/my-config.toml
Validate
Mapping configs are validated at daemon startup. Errors are written to the journal:
journalctl -u padctl.service -n 30
Note: padctl --validate is for device configs only.
Configuration Sections
Button Remapping ([remap])
Keys are button names; values are the target action.
[remap]
A = "B" # swap A and B
M1 = "KEY_F13" # back paddle → keyboard key
M2 = "mouse_left" # grip button → mouse left click
M3 = "disabled" # silence an unused button
M4 = "macro:dodge_roll" # run a macro (defined below)
LM = "mouse_side"
RM = "RS"
Available target types:
| Value | Effect |
|---|---|
"A", "B", "LB", … | Remap to another gamepad button |
"KEY_*" | Emit a Linux keyboard key (e.g. "KEY_F13", "KEY_LEFTSHIFT") |
"mouse_left" / "mouse_right" / "mouse_middle" / "mouse_side" / "mouse_extra" | Emit a mouse button |
"mouse_forward" / "mouse_back" | Emit mouse forward/back (button 4/5) |
"disabled" | Suppress the button entirely |
"macro:<name>" | Run a named macro sequence |
Available button names: A, B, X, Y, LB, RB, LT, RT, Start, Select, LS, RS, M1, M2, M3, M4, LM, RM, C, Z
Gyroscope ([gyro])
Translates gyroscope motion to mouse movement. Off by default.
[gyro]
mode = "mouse"
activate = "LS" # hold left stick click to enable gyro
sensitivity = 2.0
deadzone = 300 # raw gyro units; filters small wobble
smoothing = 0.4 # 0–1; higher = smoother but more latency
invert_y = true
Omit activate to have gyro always active when mode is "mouse".
Sticks ([stick.left] / [stick.right])
Three modes:
"gamepad"(default) — pass through as normal gamepad axes"mouse"— stick controls the cursor"scroll"— stick controls scroll wheel
[stick.right]
mode = "mouse"
sensitivity = 2.5
deadzone = 100
suppress_gamepad = true # prevent duplicate gamepad axis events
Use suppress_gamepad = true with "mouse" or "scroll" to avoid sending both gamepad axes and mouse/scroll events simultaneously.
D-pad ([dpad])
[dpad]
mode = "arrows" # emit arrow key events
suppress_gamepad = true
Default is "gamepad". Set to "arrows" to make the d-pad behave as arrow keys (useful for desktop navigation).
Layers ([[layer]])
Layers are the most powerful feature. A layer is a context-sensitive override: while active, its remap/gyro/stick/dpad settings replace the base config.
Two activation modes:
"hold"— active while the trigger button is held"toggle"— press once to enter, press again to exit
The tap + hold_timeout combination lets a button do double duty: if released before hold_timeout ms, it fires tap instead of activating the layer.
# "aim" layer: hold LM to enable gyro + mouse aim
[[layer]]
name = "aim"
trigger = "LM"
activation = "hold"
hold_timeout = 200 # ms; short press fires tap action
tap = "mouse_side"
[layer.gyro]
mode = "mouse"
sensitivity = 2.0
smoothing = 0.3
[layer.stick_right]
mode = "mouse"
sensitivity = 1.0
suppress_gamepad = true
[layer.remap]
RB = "mouse_left"
RT = "mouse_right"
Layer sub-configs can also be written inline (equivalent):
[[layer]]
name = "aim"
trigger = "LM"
activation = "hold"
hold_timeout = 200
tap = "mouse_side"
gyro = { mode = "mouse", sensitivity = 2.0, smoothing = 0.3 }
stick_right = { mode = "mouse", sensitivity = 1.0, suppress_gamepad = true }
remap = { RB = "mouse_left", RT = "mouse_right" }
Toggle example — F-key row on Select:
[[layer]]
name = "fn"
trigger = "Select"
activation = "toggle"
[layer.remap]
A = "KEY_F1"
B = "KEY_F2"
X = "KEY_F3"
Y = "KEY_F4"
Layers are evaluated in declaration order. Only one layer is active at a time.
Macros ([[macro]])
Named sequences bound via macro:<name> in remap values.
[[macro]]
name = "dodge_roll"
steps = [
{ tap = "B" },
{ delay = 50 },
{ tap = "LEFT" },
]
[[macro]]
name = "shift_hold"
steps = [
{ down = "KEY_LEFTSHIFT" },
"pause_for_release",
{ up = "KEY_LEFTSHIFT" },
]
| Step | Description |
|---|---|
{ tap = "KEY" } | Press and release |
{ down = "KEY" } | Press and hold |
{ up = "KEY" } | Release |
{ delay = N } | Wait N milliseconds |
"pause_for_release" | Wait until the trigger button is released |
Bind in remap: M1 = "macro:dodge_roll"
Repeat-while-held — turbo / combo (repeat_delay_ms)
Add repeat_delay_ms = N to a [[macro]] block to make the macro restart while
the trigger button is held. Releasing the trigger lets the current iteration
finish naturally and stops further restarts. Omit the field for legacy
single-shot behaviour.
# Spam A while RM held: tap, wait 50 ms, tap again, ...
[[macro]]
name = "spam_a"
repeat_delay_ms = 50
steps = [{ tap = "A" }]
[remap]
RM = "macro:spam_a"
Trigger Threshold — analog LT / RT as digital buttons
Warning:
trigger_thresholdmust be at the top level of the mapping file. Placing it inside[[layer]]is silently ignored. To useLT/RTas remap source keys or layer triggers, set this field once at the top of your mapping file, outside any layer block.
LT / RT are analog axes by default and cannot be used directly as [remap] source keys. Once trigger_threshold is declared, padctl synthesizes digital button events from the axis values each frame, making them available for [remap] and layer triggers:
trigger_threshold = 100 # 0–255, shared by LT and RT
[remap]
LT = "KEY_LEFTSHIFT" # axis > 100 → synthesize LT press → emit Shift
RT = "mouse_right" # axis > 100 → synthesize RT press → emit right click
See Mapping Config Reference — trigger_threshold for the full field description.
LT / RT also work as down / up targets inside [[macro]] — press and release the virtual trigger from any macro step:
[remap]
M1 = "macro:aim_burst"
[[macro]]
name = "aim_burst"
steps = [
{ down = "LT" },
{ delay = 80 },
{ up = "LT" },
]
Adaptive Trigger ([adaptive_trigger]) — DualSense only
Configures the resistance profile of the DualSense L2/R2 triggers. See Mapping Config Reference for full field tables.
In-controller Chord Switch ([chord_switch])
Hold a set of modifier buttons, then tap a selector button to switch the active mapping without touching a CLI. This is configured in ~/.config/padctl/config.toml (not in a mapping file), and each selectable mapping file declares chord_index = N:
# ~/.config/padctl/config.toml
[chord_switch]
modifier = ["LM", "RM"]
selectors = ["A", "B", "X", "Y"]
hold_ms = 120
# ~/.config/padctl/mappings/desktop.toml
chord_index = 1 # LM+RM → tap A → switch to this mapping
See Mapping Config Reference — chord_switch for full field tables and a runnable example.
Full Example
A copy-paste-ready example covering every major feature is included in the repository at examples/mappings/comprehensive.toml. It covers base remaps, two layers (hold + toggle), macros, stick modes, and gyro.
Reference
Full field tables and all accepted values: Mapping Config Reference