Skip to content

catokolas/HS_ModulesContrib-multitouch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hs._ckol.multitouch

Native Hammerspoon helper that streams per-touch events from connected Magic Mouse and Trackpad devices to a Lua callback. Wraps Apple's private MultitouchSupport.framework; no public macOS API exposes multi-finger touch position or finger count on a per-device basis.

Designed as the input side of MouseTrackpadTweaks.spoon: the Spoon owns the hs.eventtap and per-touch decisions (scroll inversion, middle-click synthesis); this module owns device enumeration and the per-frame MTSF callback.

What it actually does

On start(callback):

  1. Enumerates every multitouch device via MTDeviceCreateList.
  2. Registers a contact-frame callback on each (MTRegisterContactFrameCallback).
  3. Calls MTDeviceStart on each so MTSF begins delivering touch frames.

For every touch frame MTSF delivers (on its private thread), the module:

  1. Filters the frame to states 3 (began), 4 (moved), 5 (ended) and snapshots into a thread-safe POD buffer.
  2. Flips the Y coordinate so the Lua side gets (0, 0) at top-left.
  3. Marshals the snapshot to the main thread via dispatch_async.
  4. Invokes the registered Lua callback once per touch transition.
  5. Synthesizes "ended" for any touch id that disappeared from a frame without going through state=5 first — defensive against MTSF dropping the terminal frame under load or on device disconnect.

Lua API

local mt = require("hs._ckol.multitouch")

mt.start(function(deviceId, deviceKind, touchId, phase, nx, ny, timestamp)
  -- deviceId    integer  -- per-session id (MTDeviceGetDeviceID)
  -- deviceKind  string   -- "magicMouse" | "trackpad"
  -- touchId     integer  -- stable from began → moved → ended
  -- phase       string   -- "began" | "moved" | "ended"
  -- nx, ny      number   -- normalized 0..1, (0, 0) at top-left
  -- timestamp   number   -- MTSF frame timestamp (seconds)
end)

mt.stop()            -- unregisters every device; safe to call repeatedly
hs.inspect(mt.devices())
--> { { id=1234567890, kind="trackpad",  familyId=99 },
--    { id=2345678901, kind="magicMouse", familyId=113 } }

start registers / replaces the callback; calling it twice swaps the callback without re-enumerating devices. stop undoes everything, including releasing the Lua callback ref.

Family-ID → kind mapping

Family IDs 112, 113, and 218 are mapped to "magicMouse". Every other ID maps to "trackpad" — that covers the built-in MacBook trackpads, the Magic Trackpad 1/2/3, and any unrecognized future device. If you have a new Magic Mouse generation that reports a different family ID, extend kindForFamilyID in internal.m.

Install without compiling (pre-built universal binary)

Each release ships a multitouch-<version>-macos-universal.zip containing a fat internal.so (arm64 + x86_64) plus init.lua, built against -mmacosx-version-min=13.0. Pull the latest release artifact and unzip straight into ~/.hammerspoon:

# 1. Download (replace <version> with whatever's current on the Releases page).
curl -L -o multitouch.zip \
  https://github.com/catokolas/HS_ModulesContrib-multitouch/releases/latest/download/multitouch-<version>-macos-universal.zip

# 2. macOS may quarantine a downloaded .so; clear the flag so dlopen accepts it.
xattr -dr com.apple.quarantine multitouch.zip 2>/dev/null || true

# 3. Unzip into ~/.hammerspoon. The archive's top-level is hs/, so this
#    lands at ~/.hammerspoon/hs/_ckol/multitouch/.
unzip -o multitouch.zip -d ~/.hammerspoon

# 4. Quit and relaunch Hammerspoon (Reload Config will NOT pick up a fresh .so).
#    Then verify in the Console:
#      hs.inspect(require("hs._ckol.multitouch").devices())

If dlopen still complains about the quarantine after step 2, repeat the xattr after step 3 on the unpacked .so: xattr -dr com.apple.quarantine ~/.hammerspoon/hs/_ckol/multitouch.

Build & install from source

cd multitouch
make install       # copies into ~/.hammerspoon/hs/_ckol/multitouch/
# or for development:
make link          # symlinks instead, so future `make` picks up automatically

Then quit and relaunch Hammerspoon (Reload Config does not refresh native modules — already-loaded .so files stay pinned in package.loaded).

To produce a release artifact (universal binary zip) yourself:

cd multitouch
VERSION=0.1
make dist $VERSION   # → dist/multitouch-0.1-macos-universal.zip

If you have access — publish the artifact as a GitHub Release with the gh CLI:

# From the repo root (the parent of the `multitouch/` subdir):
gh release create v$VERSION \
  dist/multitouch-$VERSION-macos-universal.zip \
  --title "v$VERSION" \
  --notes "Initial release. Universal arm64 + x86_64 binary built against macOS 13.0+."

To remove an installed copy:

make uninstall

Smoke-test

After installing and relaunching Hammerspoon, run this from the Console:

local mt = require("hs._ckol.multitouch")
print(hs.inspect(mt.devices()))

mt.start(function(devId, kind, tid, phase, nx, ny, ts)
  print(string.format("[%s %d] touch %d %s (%.2f,%.2f) @ %.3f",
    kind, devId, tid, phase, nx, ny, ts))
end)

Touch the trackpad or Magic Mouse and watch the console. Expect a began, several moved, then a single ended per finger; nx, ny should stay in [0, 1] with (0, 0) at the top-left of the device surface.

mt.stop() ends the stream.

Known limitations

  • Private framework. MultitouchSupport.framework is not part of Apple's public SDK. The symbols used here have been stable since OS X 10.5, but a future macOS release could break this module without notice. Re-validate on each major macOS bump.
  • No hot-plug. Devices connected after start() are not seen. Call stop() then start() again to re-enumerate.
  • Magic Mouse 1 multi-finger. The first-generation Magic Mouse tracks ≤ 2 simultaneous fingers reliably; 3+ finger gestures may not register at all. Magic Mouse 2 / 3 don't have this limitation.
  • Click events not provided. This module only exposes touch state. Physical clicks ride macOS's normal mouse-button event stream and should be intercepted via hs.eventtap. See MouseTrackpadTweaks.spoon for the correlation pattern.
  • No per-touch pressure / size / angle. The Lua callback only receives position and phase. The MTSF struct exposes more (size, angle, majorAxis, minorAxis, zDensity), but nothing in the current consumer needs them — adding new fields is straightforward if a future use case warrants it.

What's here

multitouch/
├── multitouch/
│   ├── init.lua          # Lua loader
│   ├── internal.m        # MultitouchSupport wrapper + Lua bridge
│   └── Makefile          # build + make link / make install / make dist
├── LICENSE               # MIT
└── README.md             # this file

License

MIT — see LICENSE.

About

Personal Hammerspoon native module

Resources

License

Stars

Watchers

Forks

Contributors