Smoothing mouse movements for a demo video
The problem statement: I was trying to capture a demo video of my Mac app. But I wanted the mouse movements in that video to look smooth and precise, not an easy job for a human.
I found cliclick
, a command-line program which can smoothly move
the mouse around and inject clicks and keystrokes, and installed it from
Homebrew. You can write a single cliclick
command which seqences mouse
movements, clicks of different types, keypresses, and pauses, or you can build
a small shell script to sequence multiple cliclick
actions with other
things. (It works great to start the script with a pause, so you can kick it
off and then hide everything but the app you’re demoing.)
This meant that I needed a sequence of click coordinates on the screen. You
can get these a few different ways: position the mouse and run cliclick p
,
for example, or run the Digital Color Meter app and enable Show Mouse Location
(under the View menu).
Writing code to intercept mouse input
But this gave me an excuse to play around with Quartz Event Services, an API built into macOS which lets you monitor, filter, transform, or synthesize user input events—mouse actions, keyboard actions, and more. All I cared about was intercepting mouse actions and printing them out so I could compose my demo script.
The core API of interest, defined in Core Graphics, is:
CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:)
This accepts a callback (C-style plain function pointer plus optional untyped
user pointer) and a set of event types (defined by the enumeration
CGEventType
), and returns a Mach port object which you must
add to a run loop. That run loop will then dispatch callbacks for every
intercepted event.
(This tap will only be enabled if the user allowed it in System Preferences, under the Input Monitoring settings. The first time your program runs, macOS will prompt the user; if those settings change, your program must restart.)
EventMonitor is a new Swift wrapper library I wrote to encapsulate this API safely and set up a passive event tap. It’s published as an SPM package but it’s really just a single, small file with embedded documentation comments.
To use it, create an EventMonitor
object, register one or more handler
functions or closures by their desired event types, and call start()
. It
will set up the tap, validate that the tap is active (allowed by the user),
route messages to your handlers, and clean up when deallocated.
Here’s a little command-line tool I made, based on that library. It sets up an
EventMonitor, reporting each mouse-up’s coordinates as c:X,Y
, which is how
cliclick
expects click events. I also detect a press of a number key and
print out a little divider, so as I navigated the screen, I could delimit the
output. (As a command-line tool, I needed to end it with a call to
RunLoop.run
, or nothing would happen; GUI apps would of course already have
an active run loop).
import Foundation
import EventMonitor
var tap = EventMonitor()
tap.handle(.leftMouseUp) { event in
print("c:\(Int(event.location.x)),\(Int(event.location.y))")
}
tap.handleKeyDown { str, _ in
if let first = str.first, first.isNumber {
print("--- \(str) ---")
}
}
try! tap.start()
print("event tap installed")
RunLoop.current.run()
Pretty simple. Event taps are much more powerful than this, of course, but this was a nice foray into an aspect of the Mac API I hadn’t touched before.