Skip to content

NullPointerDepressiveDisorder/MiddleDrag

Folders and files

NameName
Last commit message
Last commit date

Latest commit

3834973 · · Mar 5, 2026

History

356 Commits
Feb 18, 2026
Jan 14, 2026
Feb 13, 2026
Mar 5, 2026
Jan 17, 2026
Jan 2, 2026
Jan 4, 2026
Dec 7, 2025
Feb 3, 2026
Dec 12, 2025
Feb 12, 2026
Feb 3, 2026
Jan 2, 2026
Signed-off-by: Karan Mohindroo <[email protected]> * fix: use windowID for bundle ID lookup in WindowHelper When looking up the bundle identifier after finding a window, use the windowID from the initial match instead of re-matching by point and bounds. This prevents returning the wrong bundle ID when multiple windows overlap at the same point. * fix: use animation completion handler in MenuBarController Revert to NSAnimationContext.runAnimationGroup with completion handler instead of hardcoded asyncAfter delay. This ensures the alpha restoration timing stays in sync if animation duration changes. * fix: remove unnecessary MainActor.assumeIsolated wrapper AppDelegate is @MainActor and showGestureConfigurationPromptOnFirstLaunch is @MainActor, so calling it from DispatchQueue.main.asyncAfter doesn't need the extra isolation wrapper. * test: add coverage for windowID-based bundle ID lookup Add 6 new tests verifying the windowID-based lookup logic: - testGetWindowAt_WithMockData_OverlappingWindows_UsesWindowIDForBundleLookup - testGetWindowAt_WithMockData_WindowIDMatchEnsuresCorrectWindow - testGetWindowAt_WithMockData_MultipleWindowsSameSize - testGetWindowAt_WithMockData_WindowIDUsedNotPointReMatch - testGetWindowAt_NonisolatedMethod_ReturnsNilBundleID These tests ensure that when looking up bundle identifiers for overlapping windows, we use the windowID from the initial match rather than re-searching by point, which could return wrong results. * test: add coverage for getBundleIdentifier lookup Add injectable bundleIdentifierLookup parameter to getWindowAt for testing. The production code passes the real getBundleIdentifier function, while tests can inject mock lookups. New tests verify: - Bundle ID is populated when lookup returns a value - Correct PID is passed to the lookup function - Bundle ID is nil when lookup returns nil - Overlapping windows use correct PID (from first match via windowID) - Windows without ownerPID don't call lookup - Different PIDs can return different bundle IDs * test: add coverage for MainActor.assumeIsolated thread-safety patterns Add 8 tests verifying thread-safety of WindowHelper calls in MultitouchManager: - testIgnoreDesktopCheckFromMainThread: isCursorOverDesktop from main thread - testIgnoreDesktopCheckFromBackgroundThread: isCursorOverDesktop via sync - testWindowSizeFilterCheckFromMainThread_Drag: windowAtCursorMeetsMinimumSize - testWindowSizeFilterCheckFromBackgroundThread_Drag: same via sync - testWindowSizeFilterCheckFromMainThread_Tap: tap variant - testWindowSizeFilterCheckFromBackgroundThread_Tap: tap variant via sync - testCombinedIgnoreDesktopAndWindowSizeFilterFromBackgroundThread: both - testMainActorIsolatedCallsDoNotDeadlock: stress test for deadlock prevention These tests verify that MainActor.assumeIsolated calls work correctly whether called from main thread (direct) or background thread (via DispatchQueue.main.sync). --------- Signed-off-by: Karan Mohindroo <[email protected]> Co-authored-by: Copilot <[email protected]>[email protected]>hQueue.main.sync). --------- Signed-off-by: Karan Mohindroo <[email protected]> Co-authored-by: Copilot <[email protected]>" class="Link--secondary" href="https://github.com/NullPointerDepressiveDisorder/MiddleDrag/commit/56753a62e931a505caeed96e3b696e8a9df1685c">Refactor/documentation (#75 Signed-off-by: Karan Mohindroo <[email protected]> * fix: use windowID for bundle ID lookup in WindowHelper When looking up the bundle identifier after finding a window, use the windowID from the initial match instead of re-matching by point and bounds. This prevents returning the wrong bundle ID when multiple windows overlap at the same point. * fix: use animation completion handler in MenuBarController Revert to NSAnimationContext.runAnimationGroup with completion handler instead of hardcoded asyncAfter delay. This ensures the alpha restoration timing stays in sync if animation duration changes. * fix: remove unnecessary MainActor.assumeIsolated wrapper AppDelegate is @MainActor and showGestureConfigurationPromptOnFirstLaunch is @MainActor, so calling it from DispatchQueue.main.asyncAfter doesn't need the extra isolation wrapper. * test: add coverage for windowID-based bundle ID lookup Add 6 new tests verifying the windowID-based lookup logic: - testGetWindowAt_WithMockData_OverlappingWindows_UsesWindowIDForBundleLookup - testGetWindowAt_WithMockData_WindowIDMatchEnsuresCorrectWindow - testGetWindowAt_WithMockData_MultipleWindowsSameSize - testGetWindowAt_WithMockData_WindowIDUsedNotPointReMatch - testGetWindowAt_NonisolatedMethod_ReturnsNilBundleID These tests ensure that when looking up bundle identifiers for overlapping windows, we use the windowID from the initial match rather than re-searching by point, which could return wrong results. * test: add coverage for getBundleIdentifier lookup Add injectable bundleIdentifierLookup parameter to getWindowAt for testing. The production code passes the real getBundleIdentifier function, while tests can inject mock lookups. New tests verify: - Bundle ID is populated when lookup returns a value - Correct PID is passed to the lookup function - Bundle ID is nil when lookup returns nil - Overlapping windows use correct PID (from first match via windowID) - Windows without ownerPID don't call lookup - Different PIDs can return different bundle IDs * test: add coverage for MainActor.assumeIsolated thread-safety patterns Add 8 tests verifying thread-safety of WindowHelper calls in MultitouchManager: - testIgnoreDesktopCheckFromMainThread: isCursorOverDesktop from main thread - testIgnoreDesktopCheckFromBackgroundThread: isCursorOverDesktop via sync - testWindowSizeFilterCheckFromMainThread_Drag: windowAtCursorMeetsMinimumSize - testWindowSizeFilterCheckFromBackgroundThread_Drag: same via sync - testWindowSizeFilterCheckFromMainThread_Tap: tap variant - testWindowSizeFilterCheckFromBackgroundThread_Tap: tap variant via sync - testCombinedIgnoreDesktopAndWindowSizeFilterFromBackgroundThread: both - testMainActorIsolatedCallsDoNotDeadlock: stress test for deadlock prevention These tests verify that MainActor.assumeIsolated calls work correctly whether called from main thread (direct) or background thread (via DispatchQueue.main.sync). --------- Signed-off-by: Karan Mohindroo <[email protected]> Co-authored-by: Copilot <[email protected]>[email protected]>hQueue.main.sync). --------- Signed-off-by: Karan Mohindroo <[email protected]> Co-authored-by: Copilot <[email protected]>" class="Link--secondary" href="https://github.com/NullPointerDepressiveDisorder/MiddleDrag/commit/56753a62e931a505caeed96e3b696e8a9df1685c">)
Jan 21, 2026
Feb 17, 2026
Dec 12, 2025
Jan 16, 2026
Jan 16, 2026
Jan 7, 2026
Jan 14, 2026

MiddleDrag

Three-finger trackpad gestures for middle-click and middle-drag on macOS.

The middle mouse button your Mac trackpad is missing.

macOS 15+ Swift 6.2+ License: MIT Homebrew Cask Version MacPorts Version GitHub release Downloads FOSSA Status codecov

MiddleDrag Demo

The Problem

Mac trackpads don't have a middle mouse button. Many apps expect one.

MiddleDrag fixes this. Three-finger tap for middle-click. Three-finger drag for middle-drag. Works alongside Mission Control and other system gestures.

Use Cases

Browsers

  • Open links in new background tabs
  • Close tabs with a click
  • Open bookmarks/history in new tabs

Design & Creative Toolsools

  • Pan canvas in Figma, Photoshop, Illustrator, GIMP
  • Navigate large documents in PDF viewers
  • Scroll in any direction without modifier keys

Development

  • Close editor tabs in VS Code, Sublime Text, IDEs
  • Middle-click paste in terminals (where supported)
  • Pan around large codebases in code visualization tools

3D & CAD Softwareware

  • Orbit and pan viewports in Blender, FreeCAD, Fusion 360, SketchUp, Maya, ZBrush, OnShape
  • Navigate Google Earth and mapping applications
  • Essential for apps with broken or missing trackpad support

Productivity

  • Autoscroll in supported applications
  • Any workflow that expects middle-mouse input

Features

  • Three-finger tap → Middle mouse click
  • Three-finger drag → Middle mouse drag (pan/orbit in 3D apps)
  • Works with system gestures — Mission Control, Exposé, and other macOS gestures remain functional
  • Native macOS app — Menu bar interface, no terminal configuration required
  • Configurable — Adjust sensitivity and smoothing to your preference
  • Launch at login — Set it and forget it

Installation

Homebrew (Recommended)

brew install --cask middledrag

MacPorts

sudo port install MiddleDrag

Manual Download

  1. Download the latest .pkg installer from Releases
  2. Open the installer and follow the prompts
  3. Launch MiddleDrag from your Applications folder
  4. Grant Accessibility permissions when prompted

Verify Download Integrity

Downloads are cryptographically attested via GitHub Artifact Attestations.

# Requires GitHub CLI: brew install gh
gh attestation verify  ~/Downloads/MiddleDrag-{VERSION}.pkg --repo NullPointerDepressiveDisorder/MiddleDrag

Usage

MiddleDrag runs in your menu bar as a hand icon.

Gesture Action
Three-finger tap Middle click
Three-finger drag Middle drag (pan/orbit)

Settings

  • Enabled — Toggle gesture recognition
  • Drag Sensitivity — Cursor speed during drag (0.5x – 2x)
  • Require Exactly 3 Fingers — Ignore 4+ finger touches
  • Launch at Login — Auto-start with macOS

Why MiddleDrag?

vs. BetterTouchTool ($10-24)

BetterTouchTool is powerful but overwhelming. Hundreds of options, complex interface, middle-click buried among features you'll never use. MiddleDrag does one thing well.

vs. Middle ($8)

Middle costs $8 for functionality that should be free. It's also closed-source. MiddleDrag is MIT-licensed and community-maintained.

vs. MiddleClick (open source)

MiddleClick requires terminal commands for all configuration — no GUI. MiddleDrag provides a native macOS settings interface. Both are open source, but MiddleDrag is actively maintained for modern macOS versions.

Requirements

  • macOS 15.0 (Sequoia) or later
  • Built-in trackpad or Magic Trackpad
  • Accessibility permissions

How It Works

MiddleDrag uses Apple's private MultitouchSupport framework to intercept raw touch data before the system gesture recognizer processes it. This allows three-finger gestures to generate middle-mouse events while leaving Mission Control and other system gestures intact.

Technical flow:

  1. MultitouchSupport framework provides raw touch coordinates
  2. GestureRecognizer detects three-finger tap/drag patterns
  3. Accessibility API generates synthetic middle-mouse events
  4. CGEventTap suppresses conflicting system click events

Building from Source

git clone https://github.com/NullPointerDepressiveDisorder/MiddleDrag.git

cd MiddleDrag
./build.shh

Or open MiddleDrag.xcodeproj in Xcode 16+.

Project Structure
MiddleDrag/
├── Core/
│   ├── GestureRecognizer.swift    # Gesture detection logic
│   ├── MouseEventGenerator.swift  # Mouse event synthesis
│   └── MultitouchFramework.swift  # Private API bindings
├── Managers/
│   ├── DeviceMonitor.swift        # Trackpad monitoring
│   └── MultitouchManager.swift    # Main coordinator
├── Models/
│   ├── GestureModels.swift        # Configuration types
│   └── TouchModels.swift          # Touch data structures
├── UI/
│   ├── AlertHelper.swift          # Dialog utilities
│   └── MenuBarController.swift    # Menu bar interface
└── Utilities/
    ├── LaunchAtLoginManager.swift # Login item management
    └── PreferencesManager.swift   # Settings persistence
ngs persistence

Compatibility

macOS Version Status
macOS 15 (Sequoia) ✅ Supported
macOS 26 beta (Tahoe) ✅ Compatible

Works with built-in MacBook trackpads and external Magic Trackpads.

Troubleshooting

Gestures not working
  1. Check Accessibility permissions: System Settings → Privacy & Security → Accessibilitylity
  2. Toggle "Enabled" in the menu bar
  3. Restart the app
After updating, gestures stopped

macOS treats each app version as a new application. Re-grant permissions:

  1. System Settings → Privacy & Security → Accessibilitylity
  2. Toggle MiddleDrag off then on
  3. Restart MiddleDrag
Conflicts with system gestures

Use soft taps instead of physical clicks. The app is designed to coexist with system gestures, but pressing down hard may still trigger Mission Control.

Contributing

Contributions welcome. See CONTRIBUTING.md.

Acknowledgements

MiddleDrag uses Sentry for crash reporting and Sparkle for auto-updates. See THIRD_PARTY_LICENSES for full license texts.

Trademark

MiddleDrag™ is used as a trademark by Karan Kunal Mohindroo. The MIT License grants rights to the code, not the name. You may use the name "MiddleDrag" for:

  • Forks intended for contribution back to this project
  • Describing compatibility (e.g., "works with MiddleDrag")
  • Factual references and reviews

If you distribute a modified version as a separate project, we ask that you choose a different name to avoid user confusion.

License

MIT License


FOSSA Status Star History Chart


The middle mouse button your Mac trackpad is missing.