multitouch

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

multitouch

Go Reference License Pure Go

The raw contacts on a Mac's trackpad, in pure Go, with no permission at all.

The gestures macOS publishes are the ones macOS has already decided about. A three-finger swipe means switch space only if that is switched on; it arrives as a private event the Dock consumes first; and an application that wants the gesture for something else has to fight for it — or ask a person to change a system setting so that a program can work, which is backwards.

The contacts underneath are not spoken for.

w, err := multitouch.Watch(func(f multitouch.Frame) {
    if sw, ok := swiper.Feed(f); ok && sw.Horizontal() {
        // three fingers went sideways
    }
})
defer w.Close()
  • No permission. Not Accessibility, not Screen Recording, not an event tap.
  • Every device. A Mac with a Magic Trackpad beside its built-in one has two, and listening to the wrong one hears nothing while somebody uses the other.
  • Normalised coordinates, 0 to 1 across the surface — so a gesture on a laptop trackpad and the same one on a Magic Trackpad twice the size give the same numbers.
  • Measured on an M4 Max: 2918 frames in sixty seconds, one to four contacts distinguished.

The recogniser is separate, and that is on purpose

Swiper turns frames into swipes: how many fingers, how far, within how long, and once per gesture rather than once per frame past the threshold. It is arithmetic over values, so it is tested against frames made up in a test file rather than against somebody's hand — a recogniser that can only be tried by swiping is a recogniser nobody changes. That half carries a 100% coverage gate.

A private framework, and how that is handled

MultitouchSupport publishes no header. The contact layout here is the one every open-source reader of it uses, and it is checked rather than trusted: normalised coordinates are in [0,1] by definition, so a wrong layout shows up as coordinates outside it instead of as plausible nonsense. TestTheLayoutIsRight asserts exactly that against real contacts, and passes on a machine nobody is touching — it checks that the reading is right, not that it happened.

What this will not do

It reads where fingers are, because that is what a gesture is made of. It never logs one. The tests print counts and ranges, never a position, and anything built on this should keep that discipline: a trackpad trace is a record of somebody's hands.

Documentation

Overview

Package multitouch reads the raw contacts on a Mac's trackpad.

It exists because the gestures macOS publishes are the ones macOS has already decided about. A three-finger swipe means "switch space" only if that is switched on, it is delivered as a private event the Dock consumes first, and an application that wants the gesture for something else has to fight for it -- or ask a person to change a system setting so that a program can work, which is backwards.

The contacts underneath are not spoken for. Reading them needs NO permission: not Accessibility, not Screen Recording, not an event tap. Measured on an M4 Max: 2918 frames in sixty seconds, one to four contacts distinguished.

The consumer it was written for is a virtual desktop worn on the face, where three fingers sliding sideways should turn the ribbon of screens -- something no system gesture is going to offer, on a machine where the same swipe is bound to nothing.

A private framework

MultitouchSupport publishes no header. The contact layout here is the one every open-source reader of it uses, and it is CHECKED rather than trusted: normalised coordinates are in [0,1] by definition, so a wrong layout shows up as coordinates outside it rather than as plausible nonsense. See TestTheLayoutIsRight, which asserts that on real contacts.

What this package will not do

It reads where fingers are, because that is what a gesture is made of. It never logs one. A test here prints counts and ranges, never a position, and anything built on it should keep that discipline: a trackpad trace is a record of somebody's hands.

Index

Constants

View Source
const (
	// DefaultFingers is three: two is a scroll everywhere on this platform, and
	// four is the system's own.
	DefaultFingers = 3
	// DefaultDistance is a seventh of the trackpad. Short enough to be one
	// comfortable motion, long enough that resting three fingers and shifting
	// slightly is not a gesture.
	DefaultDistance = 0.15
	// DefaultWithin bounds how long the fingers may take. A slow drift across
	// the surface is somebody resting their hand, not a swipe.
	DefaultWithin = 700 * time.Millisecond
)

Defaults for a Swiper. They are exported because a caller tuning one wants to say what it changed FROM.

Variables

View Source
var ErrUnsupported = errors.New("multitouch: only macOS has this")

ErrUnsupported is returned on platforms with no multitouch device to read.

Functions

This section is empty.

Types

type Contact

type Contact struct {
	// ID follows one finger for as long as it stays down, so a caller can tell
	// three fingers moving together from three that landed and lifted.
	ID int
	// X and Y are 0..1 across the surface.
	X, Y float32
	// VX and VY are the same units per second.
	VX, VY float32
	// Size is how much of the surface the contact covers, in the framework's
	// own arbitrary units: useful for telling a finger from a resting palm,
	// meaningless as an absolute number.
	Size float32
}

Contact is one finger on the surface.

X and Y are NORMALISED: 0 to 1 across the trackpad, origin at the bottom left, which is the framework's own convention and the reason a caller can compare a gesture on a laptop's trackpad with the same gesture on a Magic Trackpad twice the size.

type Frame

type Frame struct {
	// At is the device's own timestamp, which is monotonic and NOT wall clock:
	// use it for durations between frames, never for what time something
	// happened.
	At time.Duration
	// Device tells frames from two trackpads apart. A Mac with a Magic
	// Trackpad beside its built-in one has two, and a caller that mixes them
	// sees six fingers where there are three.
	Device   int
	Contacts []Contact
}

Frame is every contact on one device at one instant.

type Swipe

type Swipe struct {
	Fingers int
	Dx, Dy  float32
}

A Swipe is fingers that went somewhere together.

Dx and Dy are the movement in the same normalised units as Contact, so a swipe on a small trackpad and the same one on a large Magic Trackpad give the same numbers. Positive Dx is rightward, positive Dy is upward -- the framework's own axes, not a screen's.

func (Swipe) Horizontal

func (s Swipe) Horizontal() bool

Horizontal reports whether this went sideways rather than up or down.

func (Swipe) Right

func (s Swipe) Right() bool

Right reports the direction of a horizontal swipe.

type Swiper

type Swiper struct {
	// Fingers is how many contacts must move together; 0 means
	// [DefaultFingers].
	Fingers int
	// Distance is how far they must go, normalised; 0 means [DefaultDistance].
	Distance float32
	// Within bounds the gesture's duration; 0 means [DefaultWithin].
	Within time.Duration
	// contains filtered or unexported fields
}

Swiper turns frames into swipes.

It is deliberately separate from the reading: this half is arithmetic over values, so it is tested against frames made up in a test file rather than against somebody's hand. A recogniser that can only be tried by swiping is a recogniser nobody changes.

The zero value works and uses the defaults above.

func (*Swiper) Feed

func (s *Swiper) Feed(f Frame) (Swipe, bool)

Feed offers one frame and reports a swipe when the fingers have completed one.

It reports at most once per gesture: after a swipe, the fingers must LEAVE the surface before another can begin. That is not a nicety -- without it a single motion crosses the threshold on one frame and stays across it for every frame after, and a desk that changes screen per report would fly past six of them.

type Watcher

type Watcher struct{}

Watcher is the same type everywhere, so a caller compiles unchanged.

func Watch

func Watch(func(Frame)) (*Watcher, error)

Watch reports ErrUnsupported: the framework it reads is macOS's.

func (*Watcher) Close

func (w *Watcher) Close() error

Close does nothing where nothing was started.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL