CreativeNotch
Turns the MacBook notch into something useful, without the battery drain.
All projectsExplore the projectSource on GitHub


- Problem
- A notch app runs all day, but you hardly ever look at it. So the only cost that really matters is what it uses while it sits there doing nothing.
- Decision
- I set one rule before writing any features, and enforced it in one place. Nothing runs unless something needs it. That means no checking on a timer when macOS can send an event instead, no input monitor left running, and no listening to your audio.
- Result
- Thirteen features, nine of which never ask for a permission, and each one truly stops when you switch it off. The one repeating timer exists only because macOS has no way to say the clipboard changed.
- Role
- Sole developer
- Period
- 2026 to now
- Stack
- Swift, SwiftUI, macOS 26+
Checked against the source
- 2,367
- tests passing in CICI run 40175097659
- 1
- repeating Timer in the app sourceClipboardPoller.swift, line 36
- 1
- global event monitor, installed only while the panel is openAppDelegate.swift, line 702
- 0
- event taps, so nothing asks for Accessibilitycode search
- v0.14.0
- latest releaseGitHub releases
Each figure is read from the page it links to, not typed in. Source figures are at commit 4b56a55. Checked .
How it started
My brother wanted a personalised MacBook notch. I started building CreativeNotch for that, and now I use it throughout my own work. I wanted something simple that made an existing part of the screen useful without taking up much of the computer’s resources.
The problem
A notch app lives in the background. It sits under the menu bar the whole time your Mac is on, and you spend almost none of that time looking at it. So the cost that matters is what it uses while idle. The easy way to build one is to keep everything running all the time. You watch the mouse with a global monitor, refresh system stats on a repeating timer, and listen to the audio to drive a visualiser. Each of those is cheap on its own, but none of them ever stops.
The rule
Before I wrote the first feature, I wrote down one rule. Nothing runs when it
isn’t needed, and that rule is enforced in one central place instead of being
left to each feature. docs/ARCHITECTURE.md bans four things by name. They are
a Timer that always runs, a global event monitor that is always installed,
checking where the cursor is on a loop, and tapping the audio. The idea behind
the list is simple. If you think you need one of these, there is probably a
notification you haven’t found yet. There is one animation the rule allows,
the moving equaliser in the music view, and it only runs while that view is
open.
What the rule decided
It ended up shaping most of the app. Hovering works through an
NSTrackingArea on the panel, so nothing runs while your cursor is somewhere
else. Battery and Low Power Mode updates arrive through
IOPSNotificationCreateRunLoopSource. The keyboard shortcut is registered with
Carbon’s RegisterEventHotKey, which hands the key combination to macOS
itself, so the app does nothing between presses. The capture light, which tells
you when another app is using your camera or microphone, listens for the
DeviceIsRunningSomewhere property on audio and video devices instead of
asking every few seconds.
The same thinking shows up in the small things. The progress bar for a song
has no clock behind it. It runs a single animation to the end of the track and
lets Text(timerInterval:) count the time, so nothing ticks while music
plays. Trackpad swipes on the notch use the scroll events macOS already sends
to the panel, rather than a monitor.
Launch at login comes at the idea from another angle. The switch asks
SMAppService for the real answer every time instead of remembering one. If
you turn the login item off in System Settings, the app shows it as off,
because there’s no saved setting left to disagree with the system.
You don’t have to take my word for any of this, because you can check it in
the code. The whole app has exactly one repeating Timer, the clipboard
checker, because NSPasteboard really has no change notification. It slows
down after two quiet minutes and pauses completely while your screen is locked
or your Mac is asleep. There is also exactly one addGlobalMonitorForEvents,
and it’s only installed while the panel is open.
What it cost me
A feature that worked. Showing volume and brightness in the notch shipped in
v0.2.0, ran for a month, and came out in v0.7.0. Nothing was wrong with it, and
I’d fixed its last bug the day before. But it needed a CGEventTap running all
the time to catch the media keys. That was the one exception to the rule, and
it was the only reason the app asked for Accessibility access. Taking it out
removed 3,397 lines and added back 428. Now the rule has no asterisk, and the
app asks for nothing when you first open it. Every prompt that’s left comes
from something you just did, like opening the camera or switching a feature on.
Getting cleanup right, which was harder than I expected. When nothing polls,
every feature works by signing up for events, and every sign up has to be
undone when the feature stops. Three features shipped a stop() that missed
something start() had set up. The switch said off, the feature looked
stopped, and the listener kept running anyway. Reading the code didn’t catch
any of them. Counting the active sign ups and testing that count did, so every
observer in the app now works that way. A fourth bug turned up in v0.10, and
it was the kind a count can’t see. An observer was freed without stop()
being called, macOS kept a pointer to it, and the next battery change crashed
the app. The callback now holds the observer weakly, so that can’t happen
again.
Features I chose not to build. There’s no audio visualiser and there won’t be, because the rule bans tapping the audio by name. When the notch is closed, the now playing badge shows the album cover and four bars that stay still. The bars only move in the music view, which is only on screen while your cursor is over it. An equaliser in the closed notch would keep redrawing for as long as music plays.
Sometimes avoiding a poll costs more than polling would. Since macOS 15.4, only
apps with certain signing identities can read what’s playing, and an app signed
the way mine is can’t qualify. So the app runs a small helper through the
system’s own /usr/bin/perl and reads its answers back as JSON. Running a
helper process is heavier than a timer, so it only runs while your Mac is awake
and unlocked.
Where it is now
v0.14.0 has thirteen features. They are a file shelf with Quick Look and
AirDrop, clipboard history with search and pins, what’s playing, media
controls, battery, timers and a stopwatch, a keyboard shortcut, a camera
mirror, the capture light, Bluetooth alerts, a calendar glance, a colour picker
and a downloads ring. Nine of them need no permission at all. The camera asks
the first time you open it. Bluetooth alerts, the calendar and downloads each
need a grant, so they ship switched off and ask only when you turn them on.
You can switch off any one of them, and switching it off really stops it
instead of just hiding it. All of that is handled by one ModuleSwitchboard.
The app’s core library only uses Foundation, CoreGraphics and Observation,
which means its layout logic, its states and the rules for which view wins can
all be tested without a screen. The whole suite, 2,367 tests, runs in CI on every
push.