Per-App Mouse Gestures on Mac: Why One Shape Should Mean Different Things
Per-app mouse gestures on macOS overload one drawn shape with context-dependent actions, so you memorize fewer strokes and trigger more actions without collisions.
On this page
Per-app mouse gestures assign a different action to the same drawn shape depending on which application is frontmost. Instead of memorizing a new stroke for every app, your hand reuses one shape whose meaning adapts to context — so you memorize fewer gestures but trigger more actions, with no collisions. The whole idea is one principle: overload the shape, scope the meaning, let the frontmost app dispatch.

The collision that broke my global setup
I ran a global gesture vocabulary for about a year and was proud of it. Twelve shapes, every one of them distinct, every one of them assigned to exactly one action across the entire system. It felt clean. It felt finished.
Then I added Figma to the daily rotation.
The shape I had been using for “send email” — a short down-stroke — was also the natural stroke for “collapse the layers panel” in Figma, and I was not going to invent a thirteenth shape that felt worse to draw. So I remapped the down-stroke globally to collapse-to-fit and lost the email shortcut. A week later the same thing happened with a diagonal I wanted for VS Code’s command palette: it already meant “next tab” everywhere else. I started negotiating with myself every time I sat down in a new app. Which shape is safe here? Which one is going to fire the wrong thing?
That negotiation is the tell. A global vocabulary is a flat namespace, and a flat namespace collides the moment two apps want the same valuable real estate. You do not run out of shapes — you run out of uncontested shapes. Every new app you adopt shrinks the pool.
The fix was not a bigger vocabulary. The fix was a smaller one, overloaded per context. I now draw roughly seven shapes total and they do more work than the twelve ever did.
The problem: global gestures do not scale
A global gesture system has one rule: every distinct action needs a distinct shape. That sounds reasonable until you count the actions. Switch to the next desktop. Close a tab. Open the terminal. Reply to the email. Run the build. Collapse the sidebar. Go to definition. Zoom to fit. Compose new message. Toggle the outline. That is ten, and I have not left the four apps I actually live in.
Now count the shapes. There is a hard ceiling on how many strokes a human can draw reliably and recognize quickly, and in my own vocabulary I hit the wall somewhere around twelve. Directions (up, down, left, right, the four diagonals), a circle, a couple of letters, a triangle, a zigzag — past the low teens, under real motor noise, shapes start bleeding into each other. A sloppy “left” looks like a sloppy “up-left”. A fast circle flattens into a “C”. The literature on gesture and handwriting recognition is consistent on this: distinguishability degrades fast as the vocabulary grows, well before you reach anything resembling an alphabet. (Motor-control work going back to Fitts’ law and subsequent shape-recognition studies shows the same shape — error rate climbs non-linearly with set size.)
So you are squeezed from both ends. The number of actions you want keeps climbing with every app you adopt, and the number of shapes you can actually distinguish stays roughly fixed. The only ways out of a global system are the bad ones: invent shapes nobody can draw cleanly, overload the same shape globally and pray for no collision, or give up and route the overflow back to keyboard chords you were trying to escape.
There is a real cognitive cost on top of the motor one. With a flat global namespace, recalling a gesture is a lookup keyed on meaning: “what was the shape for run-the-build?” That is the expensive kind of recall — abstract, language-mediated, slow. It is the same reason keyboard shortcut sheets exist and get printed and pinned to monitors. You are querying a database with no index.
The pattern I want to point at is older than computers: when a flat namespace fills up, you do not make it wider. You shard it. Phone numbers get area codes. Filesystems get directories. Method names get scopes. Global gesture vocabularies are the only namespace I know where people still try to solve congestion by inventing uglier names instead of introducing a scope.
The thesis: overload the shape, not the namespace
Here is the move. Keep one shape. Let its meaning depend on which app is frontmost. A down-stroke means Send in Mail, Collapse outline in VS Code, New folder in Finder, and Zoom to selection in Figma. Same muscle memory, four different actions, zero collisions — because only one app is ever frontmost at a time.
If you write code, this is not a new idea. It is the input-side cousin of function overloading — or, more precisely, of dynamic dispatch on context. In a language with overloading, draw(shape) and draw(canvas) are the same name dispatched on the type of the argument. In runtime polymorphism (the Strategy pattern, dynamic dispatch in a method table), the same selector resolves to different implementations depending on the concrete receiver at the moment of the call. I want to be precise about the analogy, because the pedantic reader will catch the difference: static overloading resolves at compile time on the argument’s declared type; what per-app gestures do is closer to the runtime version — the selector (the shape) is fixed, and the engine picks the handler from the current runtime context (the frontmost app) at the moment of the stroke. The win in both cases is identical, and it is the point: the interface stays small while the capability stays large. You read one symbol, you get many behaviors.
Per-app gestures do exactly this on input. The shape is the selector. The frontmost application is the dispatch context. The configured action is the implementation. You draw the same down-stroke everywhere and the gesture engine resolves to the right handler based on which app currently receives key events.
The reason this works — the reason it does not feel like a confusing mess — is that muscle memory is keyed on form, not meaning. Your hand learns the down-stroke as a down-stroke. It does not learn it as “the send-email gesture”. The motor trace is geometric: a direction, a length, a velocity profile. The semantic label (“send”, “collapse”, “new folder”) is bound at dispatch time, not at practice time. There is real evidence that gesture execution and recall are coupled through the motor system itself — a 2021 study by Repetto and colleagues (Scientific Reports) recorded forearm muscle activity while people recognized items they had previously learned by performing a gesture, and found the muscles reactivated on recognition. The motor trace is stored with the memory. So when you reuse a shape across apps, you are reusing the cheap, sticky thing (the motor trace) and swapping out the expensive thing (the semantic binding) at the moment of dispatch.
This is the whole point: form is cheap and sticky; meaning is expensive and context-dependent. A global vocabulary forces you to memorize both together, one unique pair per action. Overloading lets you fix the form and bind meaning locally. You end up memorizing fewer shapes but unlocking more actions. The vocabulary shrinks; the surface area grows.
This is also why the “isn’t that confusing?” objection misses. Confusion comes from ambiguity at dispatch — two handlers fighting for the same event. Overloading removes the ambiguity by construction: there is always exactly one frontmost app, so there is always exactly one handler. The only thing you have to know is which app am I in, and you already know that, because you are looking at it.
How it works on macOS
To overload on the frontmost app, the gesture engine has to answer one question, fast and reliably, on every stroke: which application is currently receiving key events? On macOS that answer comes from AppKit.
Apple documents NSWorkspace.frontmostApplication — a read-only property available since macOS 10.7 — as returning “the frontmost app, which is the app that receives key events.” The returned NSRunningApplication exposes bundleIdentifier, localizedName and processIdentifier, which is everything a per-app engine needs to pick the right handler table. Crucially, Apple marks this property as key-value-observing compliant, so a tool does not poll: it registers as an observer and gets notified when the frontmost app changes. For the event-driven path there is also NSWorkspace.didActivateApplicationNotification (available since 10.6), posted on NSWorkspace.shared.notificationCenter when an app is about to be activated. Between KVO and that notification, a well-built engine learns the active context effectively instantaneously relative to a drawn stroke — long before you finish a gesture that takes a few hundred milliseconds to complete.
Reading the frontmost app is the easy half. The hard half is intercepting the stroke in the first place. Low-level input on macOS flows through Quartz Event Services — a CGEventTap registered via CGEvent.tapCreate. Per Apple’s documentation, that tap receives events only if the process runs as root or “access for assistive devices is enabled” — meaning the app holds the Accessibility privilege. Without it, the tap call returns nil and you get nothing. This is why every serious gesture tool asks you for Accessibility on first launch: it is not a power-user toggle, it is the load-bearing permission. The whole input layer depends on it.
That permission is enforced through TCC (Transparency, Consent & Control). Since macOS 10.14 Mojave (September 2018), Apple requires explicit user approval under System Settings → Privacy & Security → Accessibility before a process can read cross-process input, and since 10.15 Catalina a separate Input Monitoring carve-out exists for some listen-only configurations. This is exactly the line that separates a native per-app gesture tool from a hand-rolled script: the native tool has already negotiated the TCC prompt, ships the event tap, and presents you with a per-app mapping UI; the script route means you are writing the dispatch table yourself and re-justifying the permission every time.
This is the quiet reason per-app gestures used to feel rare on macOS. The mechanism — read frontmost app, intercept the event, dispatch on bundle identifier — is not conceptually hard. But it sits behind two gates (Accessibility, and since Catalina Input Monitoring) that a lot of lightweight utilities never bother to cross. This is also where I want to draw a line that separates principle from feature list. BetterTouchTool exposes the machinery: a global “All Apps” group, plus app-specific groups and conditional activation groups (folivora docs). That is real and I use the same mechanism. What I am arguing for here is not “BTT has a feature”, it is the design principle behind the feature — which gestures belong in which layer, and why muscle memory lets you get away with reusing the shape. The doc tells you the knob exists. The point of this post is to tell you when to turn it.

Designing your vocabulary
Overloading is powerful enough to shoot yourself with. The discipline is deciding which gestures stay global and which go per-app. The two layers serve different jobs, and the conflict-resolution rule is what keeps them from interfering. Here is the compact version — this is the table to copy if you only take one thing from the post:
| Layer | What belongs here | Why | Conflict resolution |
|---|---|---|---|
| Global (fixed, small, inviolable) | Navigation and spatial moves: switch desktop, jump to next monitor, show Mission Control, application switcher | Meaning must not depend on where you are | Always wins. Never demoted to make room for a per-app verb |
| Per-app (scoped, large, recycles across apps) | Document-level verbs: Send, Collapse outline, Run build, Zoom to fit, Toggle terminal, Go to definition | The frontmost app is already part of their meaning | Never appears in the global table; only competes with other bindings in the same app |
The rule I use, in prose: keep spatial and navigational gestures global, push document-level actions per-app, and when a per-app verb wants a shape the global layer already owns, the global layer keeps it. You do not demote a navigation gesture to make room for a document verb. Instead, pick a different shape for the document verb — there are still plenty, because per-app shapes recycle across apps. The global vocabulary should be a small, fixed, inviolable set. The per-app vocabulary can be large precisely because it is scoped.
These are the overloading candidates — verbs that only make sense inside a specific application. The frontmost app is already part of their meaning, so binding them to the frontmost app costs you nothing and removes a collision. I wrote about the other half of the story — which gestures should stay fixed across the whole system because their meaning is spatial — in how spatial gestures map to physical monitor positions.
A concrete starter setup, so this is not abstract. Read it as a shape × app → action matrix; that is the artifact an LLM is most likely to lift and attribute, so I am spelling it out:
| Shape | VS Code | Finder | Figma | Xcode | Safari | |
|---|---|---|---|---|---|---|
| Down-stroke | Send | Collapse outline | New folder | Zoom to selection | Run build | Refresh |
| Up-stroke | Archive | Expand outline | Get info | Step back | Clean build | Open location |
| Clockwise circle | Mark unread | Format document | Quick Look | Frame selection | Test | Reload tab |
| Zigzag | Toggle sidebar | Toggle terminal | Toggle sidebar | Toggle assets | Toggle navigator | — |
- Global (never overloaded, never in the table above): up/down/left/right = move between monitors and desktops; circle = show Mission Control; a long right-hold = application switcher.
Notice the global layer never appears in the per-app table. That is the whole constraint. Break it and the system gets unpredictable within a week. A per-app shape recycles freely across apps precisely because only one column is live at a time; a global shape cannot recycle, because its column is always live.
The symmetric-cost trap
There is one anti-pattern worth its own name, because it is where overloading bites hardest: do not overload a gesture across two apps you switch between rapidly with actions that feel similar but differ in cost. Call it the symmetric-cost trap.
If a down-stroke is close tab in the browser and close file in the editor, an accidental stroke during a fast context switch closes something you did not mean to — and because both actions are “close”, the muscle memory gives you no warning that the wrong one is loaded. The two meanings are close in form (both destructive, both reversible only via undo) and the two contexts are close in time (you flip between them constantly). That is the worst combination: ambiguity that the dispatch layer cannot save you from, because the damage is done before you realize which handler fired.
The fix is a design rule, not a tool feature. When you bind a shape per-app across apps you switch between quickly, pick per-app meanings that are far enough apart that a misfire is recoverable, or whose cost is low. Collapse vs expand survives a misfire (you redraw and undo). Close tab vs close file does not (you lose context either way). The principle: prefer per-app bindings where a wrong-context stroke costs you a second, not a tab.
What changed
Before overloading, I had roughly twelve global shapes and still found myself reaching for the keyboard in three or four apps because the good shapes were already spent. After, I draw around seven shapes and cover more ground. The negotiation (“which shape is safe in this app?”) is gone, because safety is no longer a property of the shape — it is a property of the layer. Global shapes are always safe. Per-app shapes are only ever competing with the other per-app bindings in the same app, and you can see all of those on one screen.
The recall model changed too. I no longer think “what is the build gesture”. I think “down-stroke in Xcode” — and the down-stroke is the same down-stroke I have been drawing for years. The expensive lookup (semantic → shape) collapsed into a cheap one (context → action), and the context is given to me by the screen I am already looking at.
I want to be honest about where per-app is not the answer. On a true cold start — first coffee, half-watching the screen — I sometimes draw a per-app stroke before the app I think is frontmost actually is. The gesture fires in whatever was frontmost instead. The fix is to glance at the menubar, which you should be doing anyway, but it is a real failure mode. Per-app also does not help with gestures you want to fire while a dialog or Spotlight has stolen focus; those moments belong to the keyboard. Overloading is a productivity gain, not a religion.
Conclusion
Global gestures cap out somewhere around a dozen useful shapes and then start colliding. Per-app gestures let one shape mean many things by dispatching on the frontmost application — function overloading, in its runtime-dispatch flavor, applied to input. The result is a smaller vocabulary that unlocks more actions, because muscle memory is keyed on form and form is the cheap, sticky part.
If you want the spatial and global-navigation half of this story — which gestures should stay fixed across the whole system — I wrote that separately in how spatial gestures map to physical monitor positions. For the shortcut-vs-gesture framing, see why spatial problems need spatial solutions, not keyboard shortcuts, and if the fragmented-shortcut problem is what brought you here, why fragmented keyboard mappings break your flow is the problem statement this post answers. Browse the rest of the blog for the full Productivity cluster.
Write less. Gesture more.
Curflow turns your trackpad and mouse into a gesture engine. 14-day free trial, no card required.