You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Harden for beta: config restructure, validation, bundled deps, icon
- Restructure config JSON to nested format with settings + bindings keys
(auto-migrates old flat format and l3/r3 -> ls/rs)
- Add settings.logging toggle to disable log output
- Move log file from /tmp to ~/Library/Logs/CodeController.log
- Add config validation with NSAlert warnings on load/reload
(unknown buttons, types, key names, modifiers, negative mouse buttons)
- Guard against crash on negative mouse button number
- Bundle marked.min.js locally instead of loading from CDN
- Add app icon (icon.icns) matching menu bar tray icon
- Reorder menu bar: status, separator, Show README, Edit, Reload, Quit
- Title case controller status ("Connected")
- Simplify default config to keyboard overrides only (mouse fallback handles rest)
- Update README: SuperWhisper setup, log path, binding tables
- Add AGENTS.md and CLAUDE.md for project documentation
- Update .gitignore (CodeController.app/, menubar_icon.png, BACKLOG.md)
**IMPORTANT:** Always reset Accessibility permissions before deploying. The app's hash changes on every rebuild, so macOS invalidates the old permission grant. Without the reset, the app launches but silently fails to send input events.
16
+
17
+
No test suite — testing is manual with a connected controller.
18
+
19
+
## Architecture
20
+
21
+
Everything lives in `CodeController.swift` (~560 lines):
-**State tracking**: `buttonStates`, `activeBindingObjects`, scroll/mouse velocity, canonical button order and fallback mouse button numbering (10-51)
25
+
-**Event simulation**: `pressKey()`, `tapKey()`, `pressMouseButton()`, `mouseClick()`, `rightClick()`, `moveMouse()`, `scroll()` — all use CGEvent with `.privateState` event source
26
+
-**Binding dispatch**: `executeBinding()` and `handleButton()` — unified dispatch with activeBindingObjects tracking for correct release behavior. If no config entry exists, falls back to sequential mouse button
27
+
-**Polling**: `pollController()` at 60Hz via Timer — reads all buttons through `handleButton()`, left stick for mouse with momentum, right stick for scroll with hybrid decay
28
+
-**Menu bar**: `AppActions` class — NSStatusItem with icon, status, Edit Bindings, Reload Config, Show README (marked.js), Quit
29
+
-**Main**: NSApplication with `.accessory` policy, `shouldMonitorBackgroundEvents` on controller connect, Accessibility permission prompt
- Modifier layers: prefix with `lt+` or `lb+` (e.g., `lt+a`, `lb+dpad_left`)
35
+
-`ls`/`rs` not `l3`/`r3` — migration in `loadBindings()` handles old configs
36
+
- Canonical button order: A, B, X, Y, RT, RB, LS, RS, D-pad, Back, Menu
37
+
- Mouse button numbering: base 10-23, +LT 24-37, +LB 38-51
38
+
39
+
## Gotchas
40
+
41
+
-**`tapKey()` vs `pressKey()`**: `tapKey` posts explicit modifier key-down/up events around the main key — required for system shortcuts like `Cmd+Shift+[` that won't register from just setting CGEvent flags
42
+
-**`shouldMonitorBackgroundEvents`**: Must be set after a controller connects, not before (crashes on some macOS versions). Without it, gamepad input silently stops when app isn't frontmost
43
+
-**Accessibility permissions**: Reset with `tccutil reset Accessibility com.local.codecontroller` when the app stops working after rebuilds. The app prompts via `AXIsProcessTrustedWithOptions` on launch
44
+
-**CGEvent `.privateState`**: Prevents our synthetic events from contaminating the global modifier state
45
+
-**Config loading**: Runs before `freopen` redirects stdout, so load messages don't appear in `~/Library/Logs/CodeController.log`
46
+
-**Config validation**: `validateBindings()` checks for unknown button names, unknown types, missing key fields, negative mouse buttons, and unknown modifiers — warns via NSAlert on load and reload
Gamepad-to-keyboard/mouse remapper for macOS. Maps an Xbox-style controller to keyboard shortcuts, mouse movement, and scrolling — designed for terminal-centric workflows (iTerm2, Claude Code, etc.).
3
+
Write software entirely using an Xbox controller and your voice. Maps every control you need for a voice-first, multi-terminal, agentic workflow to your gamepad.
4
4
5
-
## Features
6
-
7
-
-**Left stick** — Mouse cursor with momentum and acceleration curve
8
-
-**Right stick** — Smooth pixel scrolling with momentum and hybrid decay
| Up | Arrow Up — Term: navigate | Cycle windows — macOS | Mouse 31 |
44
-
| Down | Arrow Down — Term: navigate | Cycle windows — macOS | Mouse 32 |
45
-
| Left | Arrow Left — Term: navigate | Cmd+Shift+[ — Term: prev tab | Ctrl+Left — macOS: prev Space |
46
-
| Right | Arrow Right — Term: navigate | Cmd+Shift+] — Term: next tab | Ctrl+Right — macOS: next Space |
47
-
48
-
### Modifiers
7
+
## Features
49
8
50
-
| Button | Role |
51
-
|--------|------|
52
-
| LT (Left Trigger) | Modifier 1 — held to activate LT layer |
53
-
| LB (Left Bumper) | Modifier 2 — held to activate LB layer |
9
+
-**Face buttons** — Common keys (Return, Escape, Backspace, Tab) with modifier layers
54
10
55
-
### Mouse Button Reference
11
+
-**D-pad** — Arrow keys, with modifier layers for tab and window switching
56
12
57
-
Slots marked "Mouse N" emit virtual mouse button N. These do nothing by default but can be mapped to actions in BetterTouchTool or similar.
13
+
-**Two modifier layers** — Hold LT or LB to access extended bindings on every button
58
14
59
-
| Mouse | Trigger | Mouse | Trigger |
60
-
|-------|---------|-------|---------|
61
-
| 21 | RT | 29 | LB+A |
62
-
| 22 | RB | 30 | LB+X |
63
-
| 24 | LT+B | 31 | LB+D-pad Up |
64
-
| 25 | LT+X | 32 | LB+D-pad Down |
65
-
| 26 | LT+Menu | 33 | LB+Menu |
66
-
| 27 | LT+RB | 34 | LB+RB |
67
-
| 28 | LT+RT | 35 | LB+RT |
15
+
-**Left stick** — Moves mouse cursor (fallback for when buttons won't do)
68
16
69
-
## Configuration
17
+
-**Right stick** — Smooth scrolling
70
18
71
-
Bindings are stored in `~/.config/codecontroller.json`. A default config is written on first launch. Edit the file and restart the app to apply changes.
19
+
-**Universal mouse buttons** — Any button combo without a keyboard binding automatically emits a virtual mouse button (10–51), mappable in utilities like [SuperWhisper](https://superwhisper.com) or [BetterTouchTool](https://folivora.ai)
72
20
73
-
### Binding types
21
+
-**JSON config** — All bindings stored in `~/.config/codecontroller.json`, editable without recompiling
74
22
75
-
| Type | Description | Fields |
76
-
|------|-------------|--------|
77
-
|`key`| Hold key (press on button down, release on up) |`key`, `modifiers`|
78
-
|`tap`| Fire once on press (for shortcuts like Cmd+Shift+[) |`key`, `modifiers`|
[SuperWhisper](https://superwhisper.com) is a macOS voice-to-text app. Bind controller buttons to SuperWhisper for hands-free dictation:
106
44
107
-
Prefix with `lt+` or `lb+` for modifier layers (e.g., `lt+a`, `lb+dpad_left`).
45
+
1. Install SuperWhisper
46
+
2. Open SuperWhisper Settings
47
+
3. Set Mouse Shortcut to **Mouse Button 14** (RT)
108
48
109
-
## Claude Code Setup
49
+
###Claude Code Keybindings
110
50
111
-
The default bindings map Y to Tab. Claude Code changed Tab from extended thinking toggle to autocomplete (a controversial change). To restore Tab as the thinking toggle, add this to `~/.claude/keybindings.json`:
51
+
The default bindings map Y to Tab. Claude Code changed Tab from extended thinking toggle to autocomplete. To restore Tab as the thinking toggle, add this to `~/.claude/keybindings.json`:
112
52
113
53
```json
114
54
{
@@ -123,25 +63,132 @@ The default bindings map Y to Tab. Claude Code changed Tab from extended thinkin
123
63
}
124
64
```
125
65
126
-
## Setup
66
+
## Default Bindings
127
67
128
-
1. Build:
129
-
```bash
130
-
./build.sh
131
-
```
132
-
2. Install:
133
-
```bash
134
-
cp -R CodeController.app /Applications/
135
-
open /Applications/CodeController.app
136
-
```
137
-
3. Grant **Accessibility** permission to CodeController in System Settings > Privacy & Security > Accessibility.
68
+
Every button has a default mouse button number shown in the **#** column. The config file contains keyboard overrides — remove an override to expose the underlying mouse button.
138
69
139
-
##Requirements
70
+
### Sticks
140
71
141
-
- macOS 10.15+
142
-
- Swift toolchain (Xcode or Command Line Tools)
143
-
- A gamepad supported by Apple's Game Controller framework (Xbox, PlayStation, MFi)
72
+
| Input | Action | # |
73
+
|-------|--------|---|
74
+
| Left Stick | Mouse cursor ||
75
+
| Left Stick Click | Left click | 16 |
76
+
| Right Stick | Scroll ||
77
+
| Right Stick Click | Right click | 17 |
78
+
79
+
### Modifiers
80
+
81
+
| Button | Role |
82
+
|--------|------|
83
+
|**LT** (Left Trigger) | Hold to activate +LT layer |
84
+
|**LB** (Left Bumper) | Hold to activate +LB layer |
|**LT+D-Up**|`<Accessibility>`| Cycle windows | 32 |
116
+
|**LT+D-Down**|`<Accessibility>`| Cycle windows | 33 |
117
+
|**LT+D-Left**|`Cmd+Shift+[`| Prev tab | 34 |
118
+
|**LT+D-Right**|`Cmd+Shift+]`| Next tab | 35 |
119
+
|**LT+Back**|`Ctrl+E`| Toggle show all | 36 |
120
+
|**LT+Menu**||| 37 |
121
+
122
+
### +LB
123
+
124
+
| Button | Key | Action | # |
125
+
|--------|-----|--------|---|
126
+
|**LB+A**||| 38 |
127
+
|**LB+B**|`Cmd+W`| Close tab | 39 |
128
+
|**LB+X**||| 40 |
129
+
|**LB+Y**|`Cmd+T`| New tab | 41 |
130
+
|**LB+RT**||| 42 |
131
+
|**LB+RB**||| 43 |
132
+
|**LB+LS**||| 44 |
133
+
|**LB+RS**||| 45 |
134
+
|**LB+D-Up**||| 46 |
135
+
|**LB+D-Down**||| 47 |
136
+
|**LB+D-Left**||| 48 |
137
+
|**LB+D-Right**||| 49 |
138
+
|**LB+Back**|`Ctrl+B`| Background task | 50 |
139
+
|**LB+Menu**||| 51 |
140
+
141
+
## Configuration
142
+
143
+
Bindings are stored in `~/.config/codecontroller.json`. A default config is written on first launch. Edit the file and use **Reload Config** from the menu bar to apply changes.
144
+
145
+
The config only needs entries for keyboard overrides. Any button combo not in the config automatically emits its mouse button.
146
+
147
+
### Binding Types
148
+
149
+
| Type | Description | Fields |
150
+
|------|-------------|--------|
151
+
|`key`| Hold key (press on button down, release on up) |`key`, `modifiers`|
152
+
|`tap`| Fire once on press with explicit modifier key events — use for system shortcuts (e.g. `Cmd+Shift+[`) that don't register with `key`|`key`, `modifiers`|
153
+
|`leftClick`| Left mouse click | — |
154
+
|`rightClick`| Right mouse click | — |
155
+
|`cycleWindows`| Raise next window of frontmost app | — |
0 commit comments