That Damn Notch!
I’d filed MenuTidy as “done.” That’s an embarrassing thing to admit about your own app, but it’s the truth. It’s a tiny utility — you click a chevron in the menu bar and the third-party icons to its left collapse out of sight; click again, they come back. It does that one job, it does it well (or so I thought), it’s been shipping for months without complaints. So in my head it sat in a folder marked “maintenance only,” and I’d been quietly working on shinier things instead.
Today I went back to it because of a feature I’d been postponing.
If you have a MacBook Pro with a notch — any 14” or 16”, plus the notched Airs — you’ve probably noticed that when you accumulate enough menu bar icons, the leftmost ones don’t just get squeezed; they vanish. The notch sits in the middle of the menu bar, and macOS treats it as an exclusion zone. Status items live to the right of it, growing leftward as more apps add their icons. When the queue gets too long, the items at the front of the line stop being rendered. They’re still there in the system — the apps still own them — but you can’t see them, and you can’t click them. They’re behind the notch in every functional sense.
Bartender users have been able to deal with this since the very first notched Mac shipped. I haven’t. The MenuTidy approach — collapse the entire row of third-party icons behind a chevron — sidesteps the problem rather than solving it: if your icons are collapsed, none of them are visible, so the notch doesn’t matter. But that’s not always what you want. Sometimes you specifically want certain icons available, and you’ve carefully arranged them to be the right of the spacer (always visible), and the notch then chooses one or two of those favourites to eat anyway.
So today: I added a feature called Reveal Hidden Icons. Right-click MenuTidy’s chevron, choose the menu item, get a little floating panel listing every status icon that’s currently behind the notch. Left-click an entry, the corresponding app behaves as if you’d clicked its icon directly. Right-click, it behaves as if you’d right-clicked. That was the design.
The implementation took rather longer than the design suggested it should.
I want to spend a moment on the geometry, because I had a wrong mental model for most of the morning.
I’d been thinking of the notch as a visual clip — the menu bar extends across the whole screen, and the notch is just a black rectangle drawn on top, hiding whatever’s behind it. If that were true, you could click in the notch area and the click would still land on the icon underneath; it just wouldn’t be visually obvious where the cursor was.
That isn’t how it works. The notch is a physical hole in the screen — that’s why it exists, the camera and Face ID hardware sit in there — and macOS knows it. The system menu bar is laid out around the notch, with two distinct rectangular regions, one to its left and one to its right. NSScreen exposes them as auxiliaryTopLeftArea and auxiliaryTopRightArea. The space between them is the notch, and the menu bar isn’t there in any meaningful sense. There’s no menu bar pixel to render onto. There’s no menu bar event-handler to receive clicks. It’s a gap.
When status items overflow the right region, macOS doesn’t reflow them into the left region or scroll them or compress them — it just stops rendering them. The AX tree still includes them, and they still report a frame, but the frame they report is the position they would occupy if the menu bar continued through the notch — which it doesn’t. So an item that’s “behind the notch” reports an X coordinate inside the notch’s horizontal range, where there’s nothing but air.
I figured this out the slow way.
The Accessibility API can enumerate every running app’s status items via AXExtrasMenuBar, an attribute on each application’s AX element that returns a kind of meta-menu-bar containing exactly that app’s status items. Walking NSWorkspace.runningApplications and asking each for its AXExtrasMenuBar children gave me, on a typical run, around forty status items across thirty apps. I filtered them to ones whose frame X-centre fell inside the notch range, which gave me four: the icons currently being eaten.
Showing them in a panel was easy. Activating them was the hard part.
For the first attempt I used AXUIElementPerformAction with kAXPressAction — the canonical “simulate a click on this element” call. AX bypasses the normal hit-testing path and goes straight to the receiving app’s event handlers, which is exactly what I needed. I tested with Browser Notes, which had been getting eaten by my notch all week.
The menu opened.
I moved my cursor toward it.
The menu closed.
That happened reliably. Click the row in MenuTidy’s panel, BrowserNotes’ menu would pop into view at the icon’s logical position, and the moment I started moving the cursor toward the menu, the menu would dismiss as if I’d clicked outside it.
I knew immediately what was probably happening, because I’d seen this once before with a different app. NSStatusItem-attached menus track the cursor: when the menu opens, it reads the current cursor position and starts watching for movement. If the cursor is far from the icon when the menu opens (because the user clicked some panel mid-screen, say), the very first mouseMoved event reads as “moved away from the icon, dismiss.” The menu’s tracking model assumes you’re drag-selecting from the icon; if you’re not, the model decides you’re leaving.
The fix should have been to warp the cursor onto the icon’s position before the AXPress call, so the menu’s tracking model would see a consistent origin. CGWarpMouseCursorPosition takes a point and moves the system cursor there. I added a pre-warp.
The menu still closed.
Now I was less confident in my mental model. If pre-warping didn’t fix it, maybe AXPress was opening the menu in some weird half-tracking mode that any subsequent mouse-move would dismiss regardless. So I tried a fundamentally different approach: synthesise a real mouse-down + mouse-up pair via CGEvent, posted at the icon’s frame centre. That should make the click look exactly like a real user click — cursor warps to the click position naturally, mouse-down fires, mouse-up fires, status item button receives the standard event sequence, menu opens in proper click-and-stay mode.
I removed the AXPress call, swapped in CGEvent, rebuilt, tested.
The cursor warped.
No menu appeared.
This was, briefly, baffling. The CGEvent click was definitely being posted — the cursor jump was visible — but the icon wasn’t responding. After a couple of minutes of staring, I realised what should have been obvious from the start: the icon’s frame is behind the notch, and the click is being posted at coordinates inside the notch. The CGEvent system doesn’t care that I think there should be a status item there. It posts the event into the global event stream at the given coordinate; macOS’s window-server hit-tests that coordinate against rendered windows; nothing renders there; the event goes nowhere.
The notch is a hardware clip. CGEvent is a software click. They don’t meet.
This was the moment the geometry clicked into place properly. AX isn’t just a fancier version of CGEvent — it’s a fundamentally different interaction model. CGEvent posts events into the OS’s event stream, where they’re dispatched by hit-testing against windows. AX talks directly to the application’s accessibility hierarchy, asking the app to perform an action on a specific element by reference, with no hit-testing involved. AXPress on an element doesn’t need the element to be on-screen, or visible, or even unobscured. It just asks the app to behave as if its primary action had been triggered.
That’s why my first attempt worked — the menu opened — and that’s why the CGEvent attempt couldn’t. Behind the notch, CGEvent has nothing to hit. AX bypasses the entire question.
So the right answer was AXPress, plus — I returned to my pre-warp theory, but more carefully. Maybe my earlier pre-warp hadn’t worked because I’d combined it with a post-warp: I’d been warping the cursor onto the icon, calling AXPress, then warping the cursor below the menu bar onto the freshly-opened menu. I’d been assuming both warps would help. What if the second warp was the thing dismissing the menu? Cursor warping generates a mouseMoved event; what if that event — happening 60 milliseconds after the menu opened — was the very thing the menu’s tracking interpreted as “user moved off the icon”?
I dropped the post-warp. Just AXPress, with the cursor pre-warped to the icon’s position.
The menu opened. I physically moved the mouse downward. The cursor — which was warped to the icon position, behind the notch, invisible to me — moved with my physical input, descending out from below the notch onto the menu’s drop area. The menu tracked normally. I clicked “Settings…”.
It opened.
I sat there for a moment, slightly dazed by how thoroughly the simpler version had been the right answer all along.
Right-clicks have a different complication. AX doesn’t have a standard “right-press” action; AXPress always simulates a primary click. For apps that distinguish left and right click (Jorvik apps sometimes do — left opens a popover, right shows the menu), I needed a way to fire a right-click specifically.
For right-clicks I fall back to CGEvent — rightMouseDown + rightMouseUp at the icon’s frame centre — with the same caveat as before: if the icon is currently behind the notch, the click goes into the notch’s coordinate space and finds nothing. For visible icons elsewhere on the menu bar, it works perfectly. For behind-notch icons, right-click won’t register without AX support. That’s a documented limitation of the feature, and an honest one: I can’t reach behind the notch for right-click without AX cooperation that doesn’t exist as a standard interface.
In practice this matters less than you’d think, because most apps use left-click to open their menu, with right-click as a redundant alternative. If you’ve got an app that only responds to right-click on the icon, you’ll need to wait for it to drift out from behind the notch by quitting something else. I’m not aware of any such app, but I won’t pretend they don’t exist.
The first version of the reveal panel was correct but sluggish. Every time you opened it, MenuTidy walked all forty-or-so running apps, asked each for its AXExtrasMenuBar, walked the children, computed frames, filtered to the notch range. That took roughly half a second on a quiet system; with a busier process list and a few apps that were slow to respond to AX queries, longer.
Half a second isn’t catastrophic, but it doesn’t feel right for a panel that’s meant to drop down instantly when you click a menu item. So I added a live cache. The cache populates on launch, refreshes whenever NSWorkspace fires didLaunchApplicationNotification or didTerminateApplicationNotification (with a small delay so the launching app has time to actually create its status item), and gets read instantly when the panel opens. There’s a background refresh on every panel open too, just in case something drifted between events — the panel doesn’t wait for that, but the next open will reflect any changes.
The result is that opening the reveal panel feels responsive. Click the menu item; it’s already there. Which, for a feature you might use a dozen times a day, is the difference between “tolerable” and “feels native.”
This part is much shorter, mostly because the bug was much smaller, but I want to mention it because it’s the kind of thing you only notice when you’ve been quietly tolerating it for a long time.
MenuTidy creates two NSStatusItems: a chevron (visible) and an invisible spacer that you can drag around to define which icons get hidden when collapsed. Both items have autosaveName set, which means macOS persists their menu bar positions across launches in the standard NSStatusItem Preferred Position UserDefaults keys. If you command+drag the spacer to a new position, that position should be remembered next time you launch the app. The README has been claiming so since the first release.
The README was lying. Not deliberately — I’d genuinely intended for positions to persist — but the actual code did this in applicationDidFinishLaunching:
UserDefaults.standard.set(150, forKey: "NSStatusItem Preferred Position MenuTidyChevron")
UserDefaults.standard.set(300, forKey: "NSStatusItem Preferred Position MenuTidySpacer")
Setting the autosave keys, on every launch, to fixed values, before the NSStatusItems were created and could read them. Whatever the user had dragged the spacer to last session was overwritten with 300 before macOS got a chance to use it. The spacer would dutifully appear at position 300 every time, and the user’s carefully-arranged divider would silently reset, week after week.
The fix is a one-shot flag:
if !UserDefaults.standard.bool(forKey: didSeedDefaultPositionsKey) {
UserDefaults.standard.set(150, forKey: "NSStatusItem Preferred Position MenuTidyChevron")
UserDefaults.standard.set(300, forKey: "NSStatusItem Preferred Position MenuTidySpacer")
UserDefaults.standard.set(true, forKey: didSeedDefaultPositionsKey)
}
Seed the initial positions once, on the first ever launch, then never touch the keys again. Autosave handles the rest. The README isn’t lying anymore.
I’m a little embarrassed this had been wrong since the first release. Nobody had reported it as a bug, presumably because the symptom — “the spacer keeps reverting to where it always was” — reads, to a casual user, as just how the app works. They were dragging it back into position every Monday and treating that as part of the experience. That’s not a feature; that’s a tax. I’m glad it’s gone.
The code change is bounded: a single Swift file, a few hundred lines of new logic, one bug fix that’s not even thirty lines including the comment explaining it. The conceptual change is bigger.
I’d treated MenuTidy as a small, simple utility that did its small simple thing. Today I treated it like an app that ought to meet users where they actually are, which means dealing with the Mac they actually own — the one with the notch. The notch is ugly, both literally and as an engineering problem. It’s easier to pretend it’s not your concern when your app has a different focus. But it absolutely is the user’s concern, every single day, and a tool that lives in the menu bar can’t reasonably ignore the menu bar’s most distinctive feature.
I’d also been treating the position-memory bug as a small thing. It wasn’t. It was the README saying one thing and the app doing another, every launch, for months. The fact that nobody had noticed loudly enough to file an issue isn’t because it wasn’t real; it’s because users had absorbed the cost.
There’s a thing that happens in long-running software where you stop seeing it the way users do. You know how it works, you know its quirks, you mentally route around them, and after a while you’re no longer experiencing the friction. That’s how an app gets filed in your head as “done” while still containing several rough edges that users encounter daily and patiently work around because what else are they going to do. The only real cure is to use your own software the way a stranger would, every now and again, and let the friction surprise you.
MenuTidy 1.3.0 ships today — .pkg and .zip, signed and notarised as always. The reveal feature is opt-in (only appears in the menu when you have a notched display, which I’m happy about because half my readers are on Studio Displays where it would just be visual noise). Position memory works the way the README has always claimed it did. The Settings panel grew a Permissions section so the AX requirement is upfront, not surprising.
I no longer think of MenuTidy as the “also-ran” app in our collection. It’s funny how much of that change was actually about how I was thinking, rather than what the app does.