diff --git a/README.md b/README.md index 986a977..d182c99 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ If you're looking for a friendlier GUI-based controller remapper, search "Game C - **Button combos** — hold a modifier button to change what other buttons do (e.g., X+dpad_up) - **Per-app profiles** — automatic profile switching based on the frontmost application - **Modes** — multiple binding sets per profile, switchable via picker, cycling, or direct jump +- **Automatic modes** — let an external program pick the mode, e.g. the app in your terminal's focused pane ([herdr](https://herdr.dev/) plugin [available](https://github.com/vgreg/herdr-padio)) - **Custom menus** — define popup menus with labeled items that trigger any action - **Haptic feedback** — rumble on system beep, notifications, or on-demand from any binding - **Media keys** — play/pause, track skip, volume, brightness (no Accessibility permission needed) diff --git a/config.json b/config.json index 8108e7e..53c8170 100644 --- a/config.json +++ b/config.json @@ -44,7 +44,21 @@ "LB": { "type": "prev_mode" }, "RB": { "type": "next_mode" } }, + "context_modes": { + "claude": "agent", + "nvim": "nvim", + "zsh": "shell", + "ssh": "shell" + }, + "hidden_modes": ["agent"], "modes": { + "agent": { + "A": { "type": "keystroke", "key": "return" }, + "B": { "type": "keystroke", "key": "escape" }, + "X": { "type": "keystroke", "key": "`continue`" }, + "dpad_up": { "type": "keystroke", "key": "up" }, + "dpad_down": { "type": "keystroke", "key": "down" } + }, "shell": { "A": { "type": "keystroke", "key": "return" }, "B": { "type": "keystroke", "key": "c", "modifiers": ["ctrl"] }, diff --git a/docs/configuration/automatic-modes.md b/docs/configuration/automatic-modes.md new file mode 100644 index 0000000..8fddbdc --- /dev/null +++ b/docs/configuration/automatic-modes.md @@ -0,0 +1,130 @@ +# Automatic Modes + +PadIO can switch mode by itself based on what you are actually doing, instead of you picking a mode by hand every time. An external program reports a short **context token** (usually the name of the app you are working in), and each profile maps tokens to modes. + +The motivating case is a terminal multiplexer: focus a Claude Code pane and the controller is in agent mode, move to a shell pane and it is in shell mode, launch `nvim` and it becomes vim mode. But nothing about the mechanism is terminal-specific. Any program that can write a file can drive it. + +## How it works + +PadIO watches `~/.config/padio/context`, a plain text file holding one token: + +``` +claude +``` + +When the token **changes**, PadIO looks it up in the active profile's `context_modes` and switches mode if there is a match. + +``` +producer PadIO +──────── ───── +write ~/.config/padio/context ──> file watch → context_modes → mode +``` + +## Configuration + +Add `context_modes` to a profile. Keep bindings that should always be available in the profile's `global`, so they survive every mode change: + +```json +"profiles": { + "terminal": { + "apps": ["com.mitchellh.ghostty"], + "default_mode": "shell", + "global": { + "options": { "type": "mode_select" } + }, + "context_modes": { + "claude": "agent", + "nvim": "nvim", + "zsh": "shell", + "ssh": "shell" + }, + "hidden_modes": ["agent", "nvim"], + "modes": { + "shell": { }, + "agent": { }, + "nvim": { } + } + } +} +``` + +| Field | Type | Description | +|-----------------|--------|-------------| +| `context_modes` | object | Maps a context token to a mode name. Matching is exact. | + +## Resolution rules + +Four rules govern automatic switching. They exist to keep it from fighting you. + +**1. It fires only when the token changes.** A producer rewriting the same token repeatedly does nothing. + +**2. No match leaves the mode alone.** A token with no `context_modes` entry keeps the current mode. It does *not* fall back to `default_mode`. This is what makes it practical to add entries only for the apps you care about, and to ignore everything else. + +**3. A mode you picked by hand sticks.** Because switching happens only on token *change*, a mode chosen from the picker survives until you move to a different app. Pick `media` by hand inside a Claude pane and it stays `media`; move to an `nvim` pane and it becomes `nvim`. + +**4. An unknown mode name is ignored.** If `context_modes` names a mode that exists in neither the profile's `modes` nor `shared_modes`, PadIO logs it and does nothing. + +Only the active profile is consulted, so a token means whatever that profile says it means, and nothing at all in a profile with no `context_modes`. + +Returning to a profile re-applies the current token rather than snapping back to `default_mode`, so switching away from your terminal and back keeps the app-driven mode. + +## Hidden modes + +Automatic switching tends to produce one mode per app, which makes the mode picker unwieldy when only a couple of modes are ever chosen by hand. `hidden_modes` keeps them out of the way: + +```json +"hidden_modes": ["agent", "nvim"] +``` + +Hidden modes are excluded from the [`mode_select`](actions.md#mode_select) picker and from [`prev_mode` / `next_mode`](actions.md#prev_mode-next_mode) cycling. They remain fully reachable via `context_modes`, [`mode:`](actions.md#modename), and `default_mode`. + +The key can go on a profile, or at the top level for modes shared across profiles. The two lists are combined. + +```json +{ + "hidden_modes": ["media"], + "profiles": { + "terminal": { + "hidden_modes": ["agent", "nvim"] + } + } +} +``` + +If hiding would leave nothing to show, PadIO shows every mode instead, so the picker can never become an empty panel you cannot escape. + +!!! note + Cycling with `prev_mode` / `next_mode` while a hidden mode is active moves you into the visible set, since the current mode is not part of the cycle. + +## The context file + +The file is a general-purpose extension point. PadIO does not care what writes it. + +| Property | Behaviour | +|----------|-----------| +| Path | `~/.config/padio/context` | +| Content | A single token. Surrounding whitespace and newlines are stripped. | +| Empty or missing | Treated as no context. Not an error, and never a mode change. | +| Updates | Picked up immediately, whether written in place or replaced atomically. | + +Producers should write atomically, to a temp file in the same directory followed by a rename, so PadIO never reads a half-written token. PadIO ignores a rewrite of an unchanged token, so a producer that cannot easily detect changes may simply write every time. + +A minimal producer is a one-liner: + +```bash +printf 'nvim' > ~/.config/padio/context.tmp && mv ~/.config/padio/context.tmp ~/.config/padio/context +``` + +Which means anything can drive it: a shell hook on directory change, a window manager, a build script that switches you into a "watching CI" mode, or your editor. + +## herdr integration + +[herdr](https://herdr.dev/) is a terminal multiplexer for running coding agents, with workspaces, tabs and panes. The [herdr-padio](https://github.com/vgreg/herdr-padio) plugin is a ready-made producer for it: + +``` +herdr plugin install vgreg/herdr-padio +``` + +It watches the focused pane and writes the app running in it. Tokens are herdr's own agent name when it has detected one (`claude`), and otherwise the program driving the pane (`zsh`, `nvim`, `ssh`). Wrapped programs that report an unhelpful name can be renamed with substring rules; see that project's README. + +With herdr, put the workspace, tab and pane navigation bindings in the profile's `global` so they work in every mode, and let `context_modes` handle the app in the pane. diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index 4a74b13..76a0c97 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -28,12 +28,14 @@ A profile is a named set of bindings that applies when specific apps are in the } ``` -| 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. | +| 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. | +| `context_modes` | object | Optional. Maps an external context token to a mode, for [automatic mode switching](automatic-modes.md). | +| `hidden_modes` | string[] | Optional. Modes to [hide from the picker](automatic-modes.md#hidden-modes) and from mode cycling. | ### Profile resolution order @@ -78,6 +80,9 @@ Each profile can have multiple modes. Only one mode is active at a time. Switch - [`mode_select`](actions.md#mode_select) — opens a picker overlay - [`prev_mode` / `next_mode`](actions.md#prev_mode-next_mode) — cycle through modes - [`mode:`](actions.md#modename) — jump directly to a named mode +- [automatic switching](automatic-modes.md) — let an external program pick the mode from what you are working in + +Modes that are only ever selected automatically can be kept out of the picker and the cycle with [`hidden_modes`](automatic-modes.md#hidden-modes). ## Button combos diff --git a/docs/example-config.md b/docs/example-config.md index d8b878c..751932d 100644 --- a/docs/example-config.md +++ b/docs/example-config.md @@ -49,7 +49,21 @@ A complete annotated config demonstrating profiles, modes, sequences, custom men "LB": { "type": "prev_mode" }, "RB": { "type": "next_mode" } }, + "context_modes": { + "claude": "agent", + "nvim": "nvim", + "zsh": "shell", + "ssh": "shell" + }, + "hidden_modes": ["agent"], "modes": { + "agent": { + "A": { "type": "keystroke", "key": "return" }, + "B": { "type": "keystroke", "key": "escape" }, + "X": { "type": "keystroke", "key": "`continue`" }, + "dpad_up": { "type": "keystroke", "key": "up" }, + "dpad_down": { "type": "keystroke", "key": "down" } + }, "shell": { "A": { "type": "keystroke", "key": "return" }, "B": { "type": "keystroke", "key": "c", "modifiers": ["ctrl"] }, @@ -140,4 +154,6 @@ This config: - **shell mode**: return, ctrl-c, ctrl-l, git menu on Y (using `"type": "menu", "name": "git"` syntax) - **nvim mode**: vim-style hjkl navigation, with X+dpad combos for ctrl-j/ctrl-k (half-page scroll) - **tmux mode**: prefix sequences (ctrl-a + key) for pane navigation + - **agent mode**: return, escape, and `continue` as injected text — reached only via `context_modes` +- **Automatic modes**: `context_modes` switches mode from an external token (see [Automatic Modes](configuration/automatic-modes.md)), and `hidden_modes` keeps `agent` out of the picker since it is never chosen by hand - **Git menu**: quick terminal commands accessible via Y button in shell mode diff --git a/mkdocs.yml b/mkdocs.yml index 707900c..9b5fce1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - Configuration: - Overview: configuration/index.md - Profiles & Modes: configuration/profiles.md + - Automatic Modes: configuration/automatic-modes.md - Actions: configuration/actions.md - Custom Menus: configuration/menus.md - Haptics: configuration/haptics.md