Build BetterTouchTool

YESback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

LanguageSwift 6 and SwiftUI in XcodeApp typeA background agent with LSUIElement trueInput captureA CGEvent tap for keyboard and mouse, NSEvent monitors for gesturesWindow controlThe Accessibility API against other applicationsStorageJSON in Application Support, versioned

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

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

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

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

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

  5. 99 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.

VariableNeededExampleWhere the value comes from
RULES_PATHrequired~/Library/Application Support/Input/rules.jsonWhere rules and profiles are stored. Version controlled if you want history.
GESTURE_MIN_DISTANCErequired60Minimum travel in points before a swipe counts. Lower values cause accidental firing.
GESTURE_COOLDOWN_MSrequired350Lockout after a gesture fires, so one physical gesture cannot trigger twice.
SHELL_TIMEOUT_MSrequired3000How long a shell action may run before it is killed.
SPARKLE_FEED_URLoptionalhttps://example.com/appcast.xmlProduct phase only.

The build, in order

  1. An event tap that can consume

    You can observe an input event and stop it reaching the application underneath.

    1. Check AXIsProcessTrustedWithOptions and show your own panel before the system prompt. This permission lets you read every keystroke, so say so.

      Files Permissions.swift

    2. CGEvent.tapCreate at the session level for keyboard and mouse events. Return nil from the callback to consume an event.

      Files EventTap.swift

    3. Handle tapDisabledByTimeout and tapDisabledByUserInput by re-enabling. Without this the application dies quietly under load.

      Files EventTap.swift

    done when · tick each as it passes
    watch 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.
  2. Trackpad gestures

    Multi-finger gestures are recognised reliably and do not fire by accident.

    1. NSEvent monitors for magnify, swipe and touches. Read touch phases directly for finger counts rather than trusting the swipe event alone.

      Files Gestures.swift

    2. Minimum distance, maximum duration, and a cooldown after firing. These three numbers are the difference between reliable and infuriating.

      Files Gestures.swift

    3. Twenty deliberately imprecise gestures. Careful gestures always work and tell you nothing.

    done when · tick each as it passes
    watch 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.
  3. Actions

    A recognised input runs something useful, including moving windows that belong to other applications.

    1. Post events with a distinctive user data value and ignore that value in your tap callback.

      Files Actions/SendKeys.swift

    2. Get the focused window of the frontmost application and set its position and size attributes. Handle applications that refuse.

      Files Actions/Window.swift

    3. Off the main thread, with a hard timeout and the process group killed on expiry.

      Files Actions/Shell.swift

    done when · tick each as it passes
    watch 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.
  4. Scoping and conflicts

    Rules apply where you meant them to, and two rules never silently fight.

    1. Application scope beats global; priority breaks remaining ties. Write it once and test it with a table of cases.

      Files Resolver.swift

    2. Compare trigger and scope against existing rules. Show both rules and let the user choose.

      Files Conflicts.swift

    3. A named active set. Useful the first time you plug into an external display.

      Files Profiles.swift

    done when · tick each as it passes
    watch 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.
  5. Sharing configurations

    A rule set can be exported, read by a human and imported without breaking anything.

    1. A schema version in the file. Import checks it and refuses a newer version rather than guessing.

      Files Export.swift

    2. Parse the whole file, validate every rule, then apply atomically. A half-applied import is worse than a refused one.

      Files Import.swift

    3. List every command that would be installed and require explicit approval.

      Files ImportPreview.swift

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more productivity utilities.