A menu bar that knows about my meetings.
When I'm heads down, notifications are off, and a calendar reminder is exactly what gets silenced. So my menu bar shows the next meeting whatever Focus mode is doing, and treats everything it reads from my calendar and my screen as private data.

01 — The situationHeads down
The best part of my day is the stretch where I'm deep in a diff or a design problem and nothing interrupts me. To protect it, I switch notifications off, either with macOS Focus or by starting a 25- or 50-minute focus timer in my own menu bar. Then I get on with the work.
The catch is obvious once you say it out loud. A calendar reminder is a notification, so the one thing Focus mode reliably silences is the thing I can't afford to miss: the meeting where a client, a teammate or a product decision is waiting for me. I wanted deep focus and a guarantee that the important meeting still reaches me.
The second requirement came straight after. Anything that can see my calendar and my screen is handling personal data, mine and the people I meet with, so it has to be built with the care I'd expect from any product feature that touches personal data. Privacy and security have been interests of mine for a long time, and a personal tool doesn't get a pass.
Both answers ended up in the same place: the menu bar. My desktop runs on AeroSpace, a tiling window manager that keeps its own workspaces, so macOS's own bar can't show them anyway. I cover the rest of the setup in My setup, and how I got here. The bar is SketchyBar, an empty strip where every item is a small script. In June it was SketchyBar's sample config. By September it had become a small, tested piece of software built around these two ideas.
02 — FocusThe meeting that matters
The trick is that the meeting pill isn't a notification at all. Focus mode filters notifications; it doesn't hide the menu bar. The MTG pill is drawn by SketchyBar from data a small calendar helper keeps up to date, so it appears whether Focus is on, off or halfway through a 50-minute session. The bar doesn't know or care what Focus is doing, and that's the point.
- It shows up early. MTG appears 30 minutes before a meeting starts, with a countdown in minutes (
12m) that becomesNOWat the start time. - It stays until it stops being useful. The pill goes away five minutes after the start or when the meeting ends, whichever comes first, so a late join is still one click away.
- It sits next to the focus timer, not under it. Starting a focus session adds a
FOCUScountdown to the bar and hides nothing. When a meeting comes into range, both pills are visible side by side. The timer ends with a short chime and DONE, and clicking DONE starts the break. - One meeting at a time. If two meetings overlap, it shows the one that starts first.
| click | JoinOpens the call link: Google Meet, Zoom, Teams, Webex, Whereby, Jitsi or a Slack huddle. If the event has no call link, it switches to my calendar workspace instead, so I still land on the details. |
| right-click | DetailsShows the meeting's time, the same join action and a shortcut to record. |
| mic | Record with GranolaA microphone next to MTG starts a new Granola note with transcription on. It's a launcher, not a recording indicator: Granola owns capture, pause and stop. |
The last rule matters as much as the pill itself: a wrong meeting is worse than no meeting. If the calendar helper hasn't refreshed in the last three minutes, or calendar access hasn't been granted, the pill simply isn't drawn. It won't offer a link from a cache that has gone stale. The test for it is named exactly that: stale meeting never opens old link.
03 — PrivacyA calendar is personal data
Behind the pill is a native helper, written in Swift, that reads the calendars macOS already syncs. It refreshes every minute and whenever a calendar changes. A calendar holds names, client meetings, private appointments and call links, so I held the helper to the same standard as a feature that handles someone else's data:
- Read-only.macOS calls the permission "Full Access", but the helper only reads events. It never creates, edits or deletes one, and the permission prompt says exactly that.
- As little as possible, for as short as possible.It only looks at the next 24 hours and skips all-day, cancelled and declined events. For each meeting it keeps four things: start, end, title and a vetted call link. Notes and locations are scanned for a link, but never stored.
- Owner-only storage.The cache is written atomically into an owner-only folder under
~/Library/Caches, and no credentials live in the dotfiles. - Links are data, not commands.A join link must be HTTPS, on a known meeting domain, with no embedded username or password. It's checked once when the helper stores it and again before it's opened, then passed to
openas a single argument. The controller's first line says it plainly: "External names and URLs never become shell code." - Quiet logs.When something fails, the log records the type of error and nothing else: no titles, no URLs, no arguments.
None of this is exotic. It's data minimisation, least privilege and input validation, the same rules I'd ask for in a code review. They just apply to a menu bar this time.
05 — Design ruleQuiet by default
A bar that shouts all day would undo the focus it's meant to protect. The rule that shapes everything is a comment at the top of the config:
“Ordinary information sits directly on the bar. Only focus and alerts get a surface.”
- Metric cards stay hidden until they matter. CPU only appears above 70%, in amber, and turns red at 90%. Memory also appears above 70%, but its colour follows the kernel's memory-pressure level, which is what actually predicts slowdowns. Clicking either opens
btop. With coding agents building in the background, that's my early warning. - Battery only speaks when it has something to say. It hides while charging unless it's low, shows time left on battery, and shows the charge rate while charging.
- One accent colour means "attention". Orange marks the focused workspace, the meeting pill and presentation mode, and it matches the outline JankyBorders draws around the focused window.
Getting here took a round trip. In August I added network and disk cards; in September I took them out again, along with the list of apps in every workspace. If it doesn't help me decide something right now, it doesn't get a place.
06 — Under the hoodBuilt like a product
There's no framework and nothing exotic. The pieces are small, and each has one job:
sketchybarrc # the bar, its defaults, which items exist items/*.sh # declare each item and when it updates plugins/*.sh # fill items in from AeroSpace, pmset, vm_stat… plugins/toolbar.py # one controller: menus, timers, meetings, privacy native/ToolbarBridge # Swift: calendars, audio outputs, screens bin/capture_state # Swift: is the mic or camera live? tests/test_toolbar.py # privacy, meetings, launching, timers, profiles
- Source, not binaries. The Swift helpers are compiled from source on first run and whenever that source changes, so the repo only carries code I can read.
- One owner for shared state. The Python controller takes a file lock, writes its state atomically with owner-only permissions, and is the only thing that decides what's visible. A display change can't interrupt a timer or switch presentation mode off.
- Cheap when idle. The controller only ticks every second while a timer is running; otherwise it checks in every few seconds.
- Works on any desk. The bar measures the camera notch instead of hard-coding it, gives the cards that won't fit beside it a twin on the other side, and switches layout profiles when I dock or undock.
- Tested like a feature. The test suite reads like a spec for the two promises in this post. A few of its names:
- meeting window
- meeting excludes stale ended and old
- join link validation
- stale meeting never opens old link
- presentation survives refresh and hides titles
- granola visibility and render never start recording
A menu bar with a test suite sounds like overkill until you remember what it touches: my calendar, my microphone and whatever is on my screen when I share it.
07 — IterationReproducible, and still changing
The bar lives in mac-setup next to the rest of my dotfiles, and GNU Stow symlinks it into ~/.config. Editing the bar is editing the repo, and a new Mac gets it with the same single command as everything else. AeroSpace starts SketchyBar and JankyBorders at login.
- JuneThe sample config. Workspaces with their app names, the date, volume, battery and CPU, always on.
- AugustAwareness. Notch detection and twin cards, the mic/camera indicator, colour thresholds, and network and disk cards.
- SeptemberFocus and privacy. The meeting pill and Granola shortcut, the focus timer, presentation mode, the read-only calendar helper, display profiles and the tests. Network, disk and the per-workspace app lists went back out.
- NextWhatever gets in my way. That's been the only roadmap so far.
The honest trade-offs
- It's a lot of machinery for a menu bar. Shell, Python and Swift in one folder is powerful, but I'm the only maintainer.
- It depends on the calendar being right. The pill is only as good as what's synced to macOS. A meeting that isn't on my calendar won't show up, and a stale sync hides the pill on purpose.
- It leans on other people's behaviour. Granola's desktop link could change in a future version, and macOS permission prompts shift between releases.
Standing on shoulders
- SketchyBarthe bar itself
- AeroSpacetiling window manager
- JankyBordersfocus outline
- btopwhere the cards lead
- Granolameeting notes
- SF Symbolsthe icons
If you want to borrow one idea, take this one: put the signal you can't miss somewhere that Focus mode doesn't reach, and treat everything it reads as private.
Show me your menu bar.
Questions, pushback or your own take? I read every email.


