State of Affairs
If you use third-party mice with your Mac, you've surely noticed just how useless the side buttons are. By default, they act as a sort of crippled middle click, consigned to opening new links out from under you when you least expect it. Compare to Windows, where those same buttons allow you to fluidly navigate back and forward in practically any window with a history. Once you've gotten used to this feature, it's hard to get by without it.
Here's a demo of how the side buttons work in macOS by default. The colored circles stand for button clicks, with orange representing the bottom/back side button (M4) and blue representing the top/forward side button (M5). Each of the four windows already has a history.
As you can see, this binding is completely useless. Except for opening new links in browsers, there's no response in any of the apps. (And unlike with an actual middle click, you can't even click to close the tabs afterwards.) Frankly, it's not clear to me why these buttons do anything at all!
The most common fix for this silly behavior is to rebind the side buttons to
⌘+] — standard, OS-wide shortcuts for navigating history. This behavior can't be configured natively in System Preferences, but it's used in everything from Logitech's mouse software to third-party tools like USB Overdrive. Unfortunately, while this works fine in most applications, it still doesn't feel very natural. Whenever you click one of the side buttons, you either get a distracting menu bar blink or an obnoxious alert sound. In contrast to every other mouse function, each side click is sent to the entire foreground application instead of the specific view under your cursor. Behavior can be unpredictable in some contexts depending on how the foreground application has its keyboard shortcuts configured. In other words, you can't rely on this approach.
Here's a demo of the keyboard shortcut binding in action.
Decidedly less useless, but still frequently confusing. For instance: if you have lots of windows open in an application, how do you know from a glance which one your navigation click is going to? In the video, you can see the navigation commands going to Finder even when the cursor is hovering over a different window. Or, if you're unfamiliar with an app, how can you be sure that the side buttons won't trigger some state-changing shortcut, as they do with indentation in Xcode? Keyboard shortcuts are unpredictable, and this makes the side buttons feel unsafe in practice. You learn to avoid them unless necessary.
In contrast to the above two approaches, SensibleSideButtons makes your side buttons behave like bona fide navigation keys. Navigation now works in practically any application with a history, even if there aren't any keyboard shortcuts bound to forward and back. Navigation commands are sent directly to the view under your cursor, alleviating mistakes and even (hypothetically) allowing multiple navigable sections within a single window to be controlled independently. There's no blinking menu bar, no annoying noises, no fear of errant keyboard shortcuts messing with your workflow. Best of all, the code is native, clean, and very simple. All we're doing is dusting off some slightly forgotten parts of macOS.
Here's a demo of the side button behavior when using SensibleSideButtons.
Pretty much perfect! All four windows, including Xcode and Documentation, perform exactly as one would hope. And there's no more confusion if the cursor is over a non-navigable or out-of-focus window when the side button is clicked: the command simply resolves to nothing instead of triggering an application-wide shortcut.
SensibleSideButtons lives in your menu bar. That's it: while it's up there, you'll get the sensible back/forward behavior, and when it's gone there's no trace of the behavior left. (You can also open the app menu and selectively enable or disable it as you please.) There's no CPU overhead or any weird polling — just some system-wide events getting sent out whenever M4 or M5 is clicked.
I've suffered with poorly-behaving side buttons on all my third-party macOS mice for years, to the point where I just assumed that the problem was unfixable. But then I got my hands on a Logitech MX Master, and I was surprised to discover that the side buttons on this particular mouse (and no others!) behaved in that ideal, universal, Windows-like way. No blinking menu bars... navigation localized to the view under your cursor... consistent behavior in practically every app. It was the first time I'd seen it on a Mac!
Curious about the unusual behavior, I whipped out Xcode and wrote a quick application to capture mouse click events and do some analysis. First question: what was the standard, expected macOS behavior for the side buttons in generic mice? Testing several models, I discovered that the side buttons emitted standard M4 and M5 commands just as they did in Windows. It just so happened that macOS didn't want much to do with these commands, and they also couldn't be rebound in any native way.
Inferring, then, that the Master was doing something special with its side buttons, I tried capturing them as well. To my surprise, they appeared to emit nothing! I guessed that perhaps the Logitech driver was doing something interesting to make them work this way. But what was the secret? The Master's side buttons behaved in a completely consistent way across all my software, so it probably wasn't some special-purpose Logitech code. (Although I couldn't rule this out.) The buttons were definitely not emitting keyboard shortcuts since they were missing the trademark menu bar blink and only affected views under the cursor. There had to be a global set of back/forward events in macOS that was being emitted here, but hours of bitter Googling couldn't yield evidence of such events existing. I also looked into AppleScript tricks, but those lead to a similar dead end.
A bit stumped, I decided to capture all the events emanating from the MX Master. And there was a hit! The side buttons weren't being seen by the OS as clicks at all, but fake three-finger swipe gestures.
For several years, the standard navigation gesture in macOS has been the two-finger drag: snazzy, but mostly limited to Safari and a few other apps. However, a legacy three-finger swipe can be additionally enabled in Trackpad preferences, and while it's not nearly as flashy as its younger brother, it works far more universally across the OS. Under the hood, this gesture is basically free to implement: all you have to do is include a
swipeWithEvent: selector in your view controller or responder and you're good to go. Then, if the user performs a three-finger swipe with the cursor over your view, it triggers the selector and presumably navigates in the corresponding direction.
In other words, this gesture is as close to a global back/forward event as exists in macOS. Unfortunately, the type of event that it emits —
NSEventTypeSwipe — cannot be generated programmatically.
I really wanted to port this behavior to my own third-party mice, but macOS was stubbornly standing in my way! How was Logitech creating these events? Did they have access to some private frameworks? Was there some reverse-engineering involved? I thought I'd have to either capture the binary event data and clone it for my own use, or alternatively send a contraband message to whatever Logitech process was handling this behavior. (I figured it was the Logitech Options Daemon, since CPU usage spiked significantly when mashing the side buttons.) But I lucked out, as someone else had already done the hard work in this area.
In order to create the gesture code for an application called Sesamouse, developer Nathan Vander Wilt reverse-engineered the data structure of gesture-based NSEvents and released his work as open source. Playing around with his functions, I quickly discovered the winning combination that would get me the same gesture events that the Master was sending out. The swipe event didn't even require a mouse position, so there was no need to reason about coordinates. It was as simple as a
NSEventPhaseBegan event followed immediately by a
The other half of the puzzle was stifling the M4 and M5 commands and replacing them with this falsified gesture. Fortunately, macOS provides a very easy way to do this through the use of event taps. With a simple call to
CGEventTapCreate and a filter of
kCGEventOtherMouseDown, a callback is scheduled for each numbered mouse button click. The callback is allowed to return an event other than the default, so I simply returned
NULL for M4 and M5 and then sent out my fake events instead. As far as I can tell, event taps have almost no overhead.
You can get the source code (GPLv2) for SensibleSideButtons on Github or as a zip.
SensibleSideButtons is made by me — Alexei Baboulevitch. You can find me on Twitter @archagon, and e-mail me at email@example.com. My blog can currently be found at http://beta-blog.archagon.net.
A huge thanks to Nathan Vander Wilt for the tremendous work involved in reverse-engineering event handling in macOS. I would have surely given up if his Touch project hadn't existed!
SensibleSideButtons is free because I believe it's an optimal solution to a longstanding issue with macOS. I'd rather treat it as a bugfix than a product and make it available for anyone to download and enjoy! However, if you find that the app improves your productivity as much as I think it will, please consider leaving a small donation or buying something through my Amazon affiliate link. I'd greatly appreciate it, and it will help fund this and many other useful software endeavors.
Thank you for visiting!