Better Notch
BuildingA native macOS app that turns the MacBook notch into a live surface — music, battery, calendar, timers, a file shelf — built as a platform where every feature is a toggleable extension.
- Period
- 2026 — Present
- Swift 6
- SwiftUI
- AppKit
- macOS
The notch is dead space. Better Notch fills it with the things you glance at rather than open — what’s playing, battery, the next calendar event, a timer, a drop tray. One borderless panel above the menu bar, out of the Dock.
Two constraints shaped everything: it has to cost nothing when you aren’t looking at it, and it has to feel like hardware rather than an app parked on top.
The performance budget is the spec
| Metric | Budget |
|---|---|
| CPU, collapsed and idle | 0.0% — zero wakeups beyond system noise |
| Memory, steady state | under 60 MB resident |
| Timers while idle | zero |
Every task ends by re-verifying that table. Timers exist only while a HUD is up, a break countdown is under a minute away, or the panel is open. Everything else is event-driven:
- Power →
IOPSNotificationrun-loop source - Volume and output device → CoreAudio property listener
- Calendar →
EKEventStoreChanged - Long-horizon schedules → one
DispatchSourceTimerwith generous leeway, so the OS coalesces the wakeup
Even the music visualiser follows the rule — the four bars are driven by playback state, not an audio tap. A real tap costs CPU and a permission prompt to animate something nobody reads precisely.
Everything user-visible is an extension
The core is the platform: state machine, shape, window, gestures, the activity
and HUD engines, the settings shell. Music, power, audio, calendar, breaks, tray,
clipboard, weather, timer, privacy and camera mirror are each a NotchExtension.
protocol NotchExtension: AnyObject {
static var manifest: ExtensionManifest { get }
init(context: ExtensionContext)
func activate() async
func deactivate()
var expandedTab: ExtensionTab? { get }
var homeCard: AnyView? { get }
var settingsPane: AnyView? { get }
}
- Tabs, Home cards and the Settings list are derived from the registry, so a new feature is one folder plus one registry line.
- Disabled means never instantiated — no listeners, no memory. Toggling activates or deactivates live, no relaunch.
deactivate()must be provably complete: after switching a feature off, the idle budget holds as if it never existed. Much stronger than “stops updating the UI”, and it’s what stops the app accumulating cost as it grows.ExtensionContextis the only door in. Extensions never touch the window or state machine.
One shape, always morphing
Every state is the same black shape changing size — collapsed, activity, HUD, expanded. Never a second panel fading in.
- The window frame never animates. The panel is pre-padded larger than max
expanded, so only the SwiftUI shape animates. Animating an
NSWindowframe is visibly jittery; animating a shape is smooth at 120 Hz. - All sizes come from
NotchGeometry.size(for: state), so the morph is one animatable pair and geometry can’t disagree with the screen. - Bottom corners use continuous curvature; top corners flare outward into concave fillets joining the menu bar, so it reads as an extension of the hardware at every size. Both radii are animatable.
- Shape leads, content follows — content fades in after the shape grows and leaves before it shrinks.
The bug that had to be fixed first
The padded window accepted mouse events across its whole rect, silently
swallowing clicks on menu-bar items next to the notch. Fix: a hitTest override
returning nil outside the visible shape path, which the state machine exposes
as interactiveRegion: CGPath. Acceptance test is blunt — running collapsed,
every adjacent menu-bar icon stays clickable.
Reading now-playing after Apple closed the door
MediaRemote has been blocked for unentitled apps since macOS 15.4.
- Primary: vendored mediaremote-adapter — a bundled Perl script launches an Apple-entitled helper that streams now-playing JSON over stdout.
- Fallback:
DistributedNotificationCenterforcom.apple.Music.playerInfoandcom.spotify.client.PlaybackStateChanged, controls via AppleScript.
That’s a dependency on an unofficial path, so if an OS update breaks it music degrades instead of disappearing.
Smaller things
- Multi-display pins by display UUID, not name, so two identical monitors stay distinguishable. Unplugging a pinned display falls back to automatic and keeps the pin — it snaps back on reconnect.
- Privacy sets
window.sharingType = .noneby default, so the notch is invisible in screenshots and screen shares. Public API, zero cost, and the right default for something overlaying every app you demo.
The testing constraint
swift test can’t link on a Command Line Tools install — neither XCTest nor
swift-testing’s interop layer ships outside Xcode. So:
- The whole app is a library with a thin executable in front of it, and the suite imports that library directly.
- ~90 lines of assertion plumbing stand in for the framework. Non-zero exit on failure, so CI needs nothing special.
- Same principle in the icon pipeline —
sipsandiconutilship with the CLT, and the.icnsis committed so a normal build never runs it.
The barrier to contributing stays at “install Command Line Tools” instead of “install a 10 GB IDE.”