Build BetterTouchTool
A background agent that watches trackpad gestures, mouse buttons and keyboard shortcuts, matches them against rules scoped to the frontmost application, and runs an action: a keystroke, a shell command, a window arrangement or a sequence of them, with a conflict checker so two rules never fight over the same input.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree if you already own one
Why Gesture recognition cannot be developed against a mouse, and the whole build is macOS input handling.
Get it Any recent Mac. A laptop trackpad or a Magic Trackpad both work.
Verify
About This Mac reports macOS 14 or higher and a trackpad is present - installfree
Why Builds and signs the agent.
Get it Mac App Store or developer.apple.com/xcode. open ↗
Verify
xcodebuild -version prints 16 or higher - decidefree
Why Nothing here works without it, and it is the permission that reads every keystroke. Understand what you are granting before asking anyone else to.
Get it System Settings, then Privacy and Security, then Accessibility. During development it must be re-granted whenever the binary path or signature changes.
Verify
AXIsProcessTrusted returns true and the event tap installs - have readyfree
Why Phase 3 and 4 need real variety: a text editor, a browser and something that refuses window manipulation. Testing against one application hides most of the bugs.
Get it Anything already installed. Include at least one that ignores Accessibility window commands, because handling refusal is part of the work.
Verify
All three respond differently to the same rule and each case is handled - account99 USD per year
Why Product phase only. Without it there is no notarization and a Gatekeeper warning everywhere else.
Get it Enrol at developer.apple.com/programs. open ↗
Verify
A Developer ID Application certificate appears in Xcode settings
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- triggers(id, kind, spec_json, app_scope) · kind is gesture, mouse_button, key_combo or window_event; app_scope is a bundle identifier or all - actions(id, kind, payload_json) · kind is send_keys, run_shell, move_window, launch_app, sequence or delay - rules(id, trigger_id, action_id, enabled, priority, notes) - profiles(id, name, active) · a named rule set, for example docked versus laptop - conflicts(rule_a, rule_b, scope) · computed, not stored by the user, and surfaced before a rule is saved - Rules resolve most specific first: an application-scoped rule beats a global one, and priority breaks the remaining ties.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
RULES_PATH | required | ~/Library/Application Support/Input/rules.json | Where rules and profiles are stored. Version controlled if you want history. |
GESTURE_MIN_DISTANCE | required | 60 | Minimum travel in points before a swipe counts. Lower values cause accidental firing. |
GESTURE_COOLDOWN_MS | required | 350 | Lockout after a gesture fires, so one physical gesture cannot trigger twice. |
SHELL_TIMEOUT_MS | required | 3000 | How long a shell action may run before it is killed. |
SPARKLE_FEED_URL | optional | https://example.com/appcast.xml | Product phase only. |
The build, in order
An event tap that can consume
You can observe an input event and stop it reaching the application underneath.
Check AXIsProcessTrustedWithOptions and show your own panel before the system prompt. This permission lets you read every keystroke, so say so.
Files
Permissions.swiftCGEvent.tapCreate at the session level for keyboard and mouse events. Return nil from the callback to consume an event.
Files
EventTap.swiftHandle tapDisabledByTimeout and tapDisabledByUserInput by re-enabling. Without this the application dies quietly under load.
Files
EventTap.swift
done when · tick each as it passeswatch out- macOS disables an event tap that takes too long to return, and it does so silently. Your remaps simply stop working with no error anywhere. Listen for the tap-disabled notification and re-enable, and never do slow work inside the callback: post it to another queue and return immediately.
Trackpad gestures
Multi-finger gestures are recognised reliably and do not fire by accident.
NSEvent monitors for magnify, swipe and touches. Read touch phases directly for finger counts rather than trusting the swipe event alone.
Files
Gestures.swiftMinimum distance, maximum duration, and a cooldown after firing. These three numbers are the difference between reliable and infuriating.
Files
Gestures.swiftTwenty deliberately imprecise gestures. Careful gestures always work and tell you nothing.
done when · tick each as it passeswatch out- Without a cooldown after recognition, the tail of a swipe re-triggers the same gesture, and the user gets two window moves from one flick. Lock out the same gesture for a short interval after it fires, and test with deliberately sloppy input rather than careful demonstrations.
Actions
A recognised input runs something useful, including moving windows that belong to other applications.
Post events with a distinctive user data value and ignore that value in your tap callback.
Files
Actions/SendKeys.swiftGet the focused window of the frontmost application and set its position and size attributes. Handle applications that refuse.
Files
Actions/Window.swiftOff the main thread, with a hard timeout and the process group killed on expiry.
Files
Actions/Shell.swift
done when · tick each as it passeswatch out- Sending a keystroke from inside the event tap callback can feed straight back into your own tap and loop. Tag events you generate with a user data field and ignore them when they come back around, or the first remap you write will hang the machine.
Scoping and conflicts
Rules apply where you meant them to, and two rules never silently fight.
Application scope beats global; priority breaks remaining ties. Write it once and test it with a table of cases.
Files
Resolver.swiftCompare trigger and scope against existing rules. Show both rules and let the user choose.
Files
Conflicts.swiftA named active set. Useful the first time you plug into an external display.
Files
Profiles.swift
done when · tick each as it passeswatch out- Two rules on the same trigger in the same scope produce behaviour that depends on iteration order, which will change when you refactor. Detect the collision at save time and make the user resolve it.
Sharing configurations
A rule set can be exported, read by a human and imported without breaking anything.
A schema version in the file. Import checks it and refuses a newer version rather than guessing.
Files
Export.swiftParse the whole file, validate every rule, then apply atomically. A half-applied import is worse than a refused one.
Files
Import.swiftList every command that would be installed and require explicit approval.
Files
ImportPreview.swift
done when · tick each as it passeswatch out- An imported rule that runs a shell command is arbitrary code from whoever wrote it. Show every shell action in the import preview and require explicit approval for those specifically, because a rule set is exactly the kind of file people share without reading.
Signing, notarization and updatesproduct builder
Others can install it, and you can ship the fix the next macOS release will need.
Hardened runtime, Developer ID Application, notarytool, stapler.
terminalxcrun notarytool submit Input.zip --keychain-profile AC --wait xcrun stapler staple Input.app
EdDSA key pair with the private half outside the repository, appcast over HTTPS.
Files
appcast.xmlOne paragraph saying the application can observe every keystroke and what it does and does not do with them.
Files
README.md
done when · tick each as it passeswatch out- Signing invalidates the Accessibility grant that testers gave your unsigned build, and macOS will not prompt again. Put the removal instructions in the release notes.
That is the whole plan for BetterTouchTool. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Broad input device coverage across stream decks, tablets and controllers.
- The scripting depth accumulated over a decade.
- The community preset library.
- Guaranteed compatibility across macOS releases.
- huge input-device coverage, scripting depth, community presets, and OS maintenance
- years of OS-version edge cases
- large extension ecosystem
- cross-device sync
- commercial support and rapid compatibility fixes
- Window snapping to halves and quarters, which is the most requested action every time
- A recording mode that captures a gesture and offers to bind it
- Per-display profiles that switch when an external monitor is connected
Need the files? The project pack on the verdict page hands your agent the whole brief · more productivity utilities.