Skip to content

Commit bbe1947

Browse files
committed
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)
1 parent da24bb1 commit bbe1947

8 files changed

Lines changed: 435 additions & 236 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
.DS_Store
2+
CodeController.app/
3+
menubar_icon.png
4+
BACKLOG.md

AGENTS.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CodeController
2+
3+
Single-file Swift macOS app that remaps an Xbox controller to keyboard/mouse for terminal workflows. No Xcode project, no SPM — just `swiftc`.
4+
5+
## Build & Deploy
6+
7+
```bash
8+
# Build
9+
bash build.sh
10+
11+
# Reset permissions, build, deploy, and launch
12+
tccutil reset Accessibility com.local.codecontroller && pkill -x CodeController 2>/dev/null; sleep 0.5; bash build.sh && cp -R CodeController.app /Applications/ && open /Applications/CodeController.app
13+
```
14+
15+
**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):
22+
23+
- **Binding config** (top): `Binding` struct (Codable), `defaultConfigJSON`, `keyNameToCode` lookup, `loadBindings()` reads from `~/.config/codecontroller.json`
24+
- **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
30+
31+
## Key Conventions
32+
33+
- Button names: `a`, `b`, `x`, `y`, `rt`, `rb`, `ls`, `rs`, `dpad_up/down/left/right`, `back`, `menu`
34+
- 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

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [AGENTS.md](AGENTS.md)

CodeController.swift

Lines changed: 224 additions & 132 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 151 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,54 @@
11
# CodeController
22

3-
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.
44

5-
## Features
6-
7-
- **Left stick** — Mouse cursor with momentum and acceleration curve
8-
- **Right stick** — Smooth pixel scrolling with momentum and hybrid decay
9-
- **D-pad** — Arrow keys (with modifier layers for tabs, windows, Spaces)
10-
- **Face buttons** — Common keys (Return, Escape, Backspace) with modifier layers
11-
- **Two modifier layers** — LT (Mod 1) and LB (Mod 2) for extended bindings
12-
- **JSON config** — All bindings in `~/.config/codecontroller.json`, editable without recompiling
13-
- **Virtual mouse buttons** — Unbound slots emit high-numbered mouse buttons for BetterTouchTool
14-
15-
## Default Bindings
5+
Sit, stand, or walk around while developing software faster than ever.
166

17-
### Sticks
18-
19-
| Input | Action |
20-
|-------|--------|
21-
| Left Stick | Mouse movement |
22-
| L3 (left stick click) | Left click |
23-
| Right Stick | Scroll |
24-
| R3 (right stick click) | Right click |
25-
26-
### Buttons
27-
28-
| Button | Normal | LT (Mod 1) | LB (Mod 2) |
29-
|--------|--------|-------------|------------|
30-
| A | Return — Term: submit | Space — Term: scroll page | Mouse 29 |
31-
| B | Escape — Claude: cancel | Mouse 24 | Cmd+W — Term: close tab |
32-
| X | Backspace — Term: delete | Mouse 25 | Mouse 30 |
33-
| Y | Tab — Claude: toggle thinking | Shift+Tab — Claude: toggle perm mode | Cmd+T — Term: new tab |
34-
| RB | Mouse 22 | Mouse 27 | Mouse 34 |
35-
| RT | Mouse 21 | Mouse 28 | Mouse 35 |
36-
| Back | Ctrl+O — Claude: toggle verbose | Ctrl+E — Claude: toggle perm explain | Ctrl+B — Claude: background task |
37-
| Menu | / — Claude: slash commands | Mouse 26 | Mouse 33 |
38-
39-
### D-pad
40-
41-
| Direction | Normal | LT (Mod 1) | LB (Mod 2) |
42-
|-----------|--------|-------------|------------|
43-
| 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
498

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
5410

55-
### Mouse Button Reference
11+
- **D-pad** — Arrow keys, with modifier layers for tab and window switching
5612

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
5814

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)
6816

69-
## Configuration
17+
- **Right stick** — Smooth scrolling
7018

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)
7220

73-
### Binding types
21+
- **JSON config** — All bindings stored in `~/.config/codecontroller.json`, editable without recompiling
7422

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` |
79-
| `mouse` | Virtual mouse button (for BetterTouchTool) | `button` |
80-
| `leftClick` | Real left mouse click ||
81-
| `rightClick` | Real right mouse click ||
82-
| `cycleWindows` | Raise next window of frontmost app ||
23+
## Requirements
8324

84-
### Example bindings
25+
- macOS 10.15+
26+
- Xbox One wireless controller (known supported — other controllers may work via Apple's Game Controller framework)
8527

86-
```json
87-
{ "type": "key", "key": "return" }
88-
{ "type": "key", "key": "w", "modifiers": ["command"] }
89-
{ "type": "tap", "key": "[", "modifiers": ["command", "shift"] }
90-
{ "type": "mouse", "button": 22 }
91-
{ "type": "leftClick" }
92-
{ "type": "cycleWindows" }
93-
```
28+
## Setup
9429

95-
### Available key names
30+
Check the [Releases](../../releases) page for pre-built binaries.
9631

97-
Letters (`a`-`z`), numbers (`0`-`9`), `return`, `tab`, `space`, `backspace`, `escape`, `delete`, arrow keys (`up`/`down`/`left`/`right`), function keys (`f1`-`f12`), punctuation (`/`, `[`, `]`, `-`, `=`, `;`, `'`, `,`, `.`, `` ` ``), navigation (`home`, `end`, `pageup`, `pagedown`).
32+
### Building from Source
9833

99-
### Available modifiers
34+
Requires Swift toolchain (Xcode or Command Line Tools).
10035

101-
`command`/`cmd`, `shift`, `option`/`alt`, `control`/`ctrl`
36+
1. Build: `./build.sh`
37+
2. Copy to Applications: `cp -R CodeController.app /Applications/`
38+
3. Launch: `open /Applications/CodeController.app`
39+
4. Grant **Accessibility** permission when prompted (or manually in System Settings > Privacy & Security > Accessibility)
10240

103-
### Binding keys
41+
### SuperWhisper (Voice Input)
10442

105-
Base buttons: `a`, `b`, `x`, `y`, `rb`, `rt`, `back`, `menu`, `l3`, `r3`, `dpad_up`, `dpad_down`, `dpad_left`, `dpad_right`
43+
[SuperWhisper](https://superwhisper.com) is a macOS voice-to-text app. Bind controller buttons to SuperWhisper for hands-free dictation:
10644

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)
10848

109-
## Claude Code Setup
49+
### Claude Code Keybindings
11050

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`:
11252

11353
```json
11454
{
@@ -123,25 +63,132 @@ The default bindings map Y to Tab. Claude Code changed Tab from extended thinkin
12363
}
12464
```
12565

126-
## Setup
66+
## Default Bindings
12767

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.
13869

139-
## Requirements
70+
### Sticks
14071

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 |
85+
86+
### Base
87+
88+
| Button | Key | Action | # |
89+
|--------|-----|--------|---|
90+
| **A** | `Return` | Submit | 10 |
91+
| **B** | `Escape` | Cancel | 11 |
92+
| **X** | `Backspace` | Delete | 12 |
93+
| **Y** | `Tab` | Autocomplete / thinking | 13 |
94+
| **RT** | | | 14 |
95+
| **RB** | | | 15 |
96+
| **D-Up** | `Up` | Navigate | 18 |
97+
| **D-Down** | `Down` | Navigate | 19 |
98+
| **D-Left** | `Left` | Navigate | 20 |
99+
| **D-Right** | `Right` | Navigate | 21 |
100+
| **Back** | `Ctrl+O` | Toggle transcript | 22 |
101+
| **Menu** | `/` | Slash commands | 23 |
102+
103+
### +LT
104+
105+
| Button | Key | Action | # |
106+
|--------|-----|--------|---|
107+
| **LT+A** | `Space` | Scroll page | 24 |
108+
| **LT+B** | | | 25 |
109+
| **LT+X** | | | 26 |
110+
| **LT+Y** | `Shift+Tab` | Cycle permission mode | 27 |
111+
| **LT+RT** | | | 28 |
112+
| **LT+RB** | | | 29 |
113+
| **LT+LS** | | | 30 |
114+
| **LT+RS** | | | 31 |
115+
| **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 ||
156+
157+
### Example Bindings
158+
159+
```json
160+
{ "key": "return", "type": "key" }
161+
{ "key": "w", "type": "key", "modifiers": ["command"] }
162+
{ "key": "[", "type": "tap", "modifiers": ["command", "shift"] }
163+
{ "type": "leftClick" }
164+
{ "type": "cycleWindows" }
165+
```
166+
167+
### Key Names
168+
169+
- Letters: `a` through `z`
170+
- Numbers: `0` through `9`
171+
- Editing: `return`, `tab`, `space`, `backspace`, `escape`, `delete`
172+
- Arrows: `up`, `down`, `left`, `right`
173+
- Function: `f1` through `f12`
174+
- Punctuation: `/`, `[`, `]`, `-`, `=`, `;`, `'`, `,`, `.`, `` ` ``
175+
- Navigation: `home`, `end`, `pageup`, `pagedown`
176+
177+
### Modifier Names
178+
179+
- `command` or `cmd`
180+
- `shift`
181+
- `option` or `alt`
182+
- `control` or `ctrl`
183+
184+
### Binding Keys
185+
186+
Base buttons:
187+
188+
`a`, `b`, `x`, `y`, `rt`, `rb`, `ls`, `rs`, `back`, `menu`, `dpad_up`, `dpad_down`, `dpad_left`, `dpad_right`
189+
190+
Prefix with `lt+` or `lb+` for modifier layers (e.g., `lt+a`, `lb+dpad_left`).
144191

145192
## Logs
146193

147-
Output is logged to `/tmp/codecontroller.log`.
194+
Output is logged to `~/Library/Logs/CodeController.log`.

build.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ mkdir -p CodeController.app/Contents/MacOS
88
mkdir -p CodeController.app/Contents/Resources
99
mv CodeController CodeController.app/Contents/MacOS/
1010
cp icon.png CodeController.app/Contents/Resources/
11+
cp icon.icns CodeController.app/Contents/Resources/
1112
cp README.md CodeController.app/Contents/Resources/
13+
cp marked.min.js CodeController.app/Contents/Resources/
1214

1315
# Create Info.plist
1416
cat > CodeController.app/Contents/Info.plist << 'EOF'
@@ -26,6 +28,8 @@ cat > CodeController.app/Contents/Info.plist << 'EOF'
2628
<string>1.0</string>
2729
<key>LSMinimumSystemVersion</key>
2830
<string>10.15</string>
31+
<key>CFBundleIconFile</key>
32+
<string>icon</string>
2933
<key>LSUIElement</key>
3034
<true/>
3135
</dict>

icon.icns

33.6 KB
Binary file not shown.

marked.min.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)