Profiles & Modes
Profiles
A profile is a named set of bindings that applies when specific apps are in the foreground.
"profiles": {
"terminal": {
"apps": ["com.mitchellh.ghostty", "com.apple.Terminal"],
"default_mode": "shell",
"global": {
"options": { "type": "mode_select" }
},
"modes": {
"shell": { ... },
"nvim": { ... }
}
},
"default": {
"apps": [],
"default_mode": "general",
"global": {},
"modes": {
"general": { ... }
}
}
}
| Field | Type | Description |
|---|---|---|
apps |
string[] | Bundle IDs this profile applies to. Empty list = default catch-all profile. |
default_mode |
string | Mode to activate when the profile is first entered. |
global |
object | Bindings that apply in all modes of this profile. Overridden by top-level global. |
modes |
object | Named modes, each containing button bindings. |
Profile resolution order
- First profile whose
appslist contains the frontmost app's bundle ID. - The profile named
"default"(fallback for unmatched apps).
Shared Modes
Modes defined in the top-level shared_modes object are available to all profiles without redefinition. Any profile can reference a shared mode by name — it works the same as a profile-defined mode.
{
"shared_modes": {
"media": {
"LB": { "type": "keystroke", "key": "play_pause" },
"RB": { "type": "keystroke", "key": "next_track" },
"dpad_up": { "type": "keystroke", "key": "volume_up" },
"dpad_down": { "type": "keystroke", "key": "volume_down" }
}
}
}
Now every profile that supports mode switching can switch to "media" — the bindings are resolved from shared_modes when the active mode name isn't found in the profile's own modes dictionary.
If a profile defines a mode with the same name as a shared mode, the profile's mode takes priority.
Modes
A mode is a flat object mapping button names to action objects:
"shell": {
"A": { "type": "keystroke", "key": "return" },
"dpad_up": { "type": "keystroke", "key": "up" },
"dpad_down": { "type": "keystroke", "key": "down" }
}
Each profile can have multiple modes. Only one mode is active at a time. Switch between modes using:
mode_select— opens a picker overlayprev_mode/next_mode— cycle through modesmode:<name>— jump directly to a named mode
Button combos
Hold one button as a modifier to change what another button does. Use the syntax "<modifier>+<button>" as a binding key:
"nvim": {
"dpad_up": { "type": "keystroke", "key": "k" },
"dpad_down": { "type": "keystroke", "key": "j" },
"X+dpad_up": { "type": "keystroke", "key": "k", "modifiers": ["ctrl"] },
"X+dpad_down": { "type": "keystroke", "key": "j", "modifiers": ["ctrl"] }
}
In this example, pressing dpad_up sends k, but holding X and pressing dpad_up sends ctrl-k instead.
Combo keys work in top-level global, profile global, and mode bindings — anywhere regular button keys work. The modifier button still fires its own action when first pressed; users who want a "pure modifier" button simply don't bind it to any action.
Binding resolution order
When a button is pressed, PadIO checks combo keys first (if any other buttons are held), then falls back to plain keys:
- Combo key in active mode bindings (highest priority)
- Combo key in profile
global - Combo key in top-level
global - Plain key in active mode bindings (highest priority)
- Plain key in profile
global - Plain key in top-level
global
Mode bindings have the highest priority, so switching to a mode can override any button — including those defined in top-level global. This lets you define cross-profile defaults in top-level global (stick mappings, click buttons) and override specific buttons per mode when needed.
If multiple buttons are held simultaneously, PadIO tries them in ButtonID order and uses the first match.
Hold inheritance
When a mode overrides a button that has a hold action defined at a lower priority level (e.g., profile global), the hold behavior is inherited automatically. Only the press (tap) action is overridden — the hold persists unless explicitly cleared.
"global": {
"L3": { "type": "left_click", "hold": { "type": "left_click_hold" } }
},
"modes": {
"special": {
"L3": { "type": "keystroke", "key": "space" }
}
}
In special mode, tapping L3 sends space, but holding L3 still holds the left mouse button (inherited from profile global).
To explicitly clear an inherited hold, set the hold to "none":