A macOS menu bar app that counts down to — and up from — the moments you care about. Ship dates, deadlines, birthdays, the day you joined.
Your next event lives in the menu bar as a coloured dot and a short countdown. Click it for the full list, each one ticking down to the second.
● 3d <- the menu bar: your closest event
┌──────────────────────────────────┐
│ TimeBar + │
├──────────────────────────────────┤
│ ● Design review 02:05:10 │
│ Sun, Aug 30 at 16:56 remaining│
│ ● Ship v1.0 3d 04:12:00 │
│ Wed, Sep 2 at 19:03 remaining│
│ ● Joined the team 9y 363d … │
│ Thu, Sep 1, 2016 elapsed│
├──────────────────────────────────┤
│ Add Event About · Quit │
└──────────────────────────────────┘
scripts/build-app.sh
open build/TimeBar.appDrag build/TimeBar.app to your Applications folder to keep it around. TimeBar
has no Dock icon and no window — it lives entirely in the menu bar, and you quit
it from the popover footer.
To start it automatically, add it under System Settings → General → Login Items → Open at Login.
Add an event with + in the header, or Add Event in the footer. Give it a
name, a date and time, and a colour. +1h / +1d / +1w / +1mo / +1y jump
the date without typing it. Press Return to save, Escape to cancel.
Edit an event by clicking its row. Delete it by hovering the row and clicking the bin, or from Delete in the editor.
About in the footer shows the app version and links to the source code and the author's site.
Events are sorted with the soonest first. Once an event passes it drops to the bottom of the list, greyed out and labelled elapsed, and keeps counting upward — so it doubles as a "time since" tracker.
The menu bar shows a single unit — just enough to know where you stand at a glance. The popover shows the whole breakdown.
| menu bar | popover | |
|---|---|---|
| An hour out | 2h |
02:05:10 |
| A few days out | 3d |
3d 04:12:00 |
| Years past | +9y |
9y 363d 00:00:04 |
| suffix | y |
d |
h |
m |
s |
|---|---|---|---|---|---|
| years | days | hours | minutes | seconds |
There is no month unit, so m always means minutes and never needs a second
look. Two months out simply reads as 59d.
A + prefix means the event has already passed and the number is counting up.
The menu bar tracks your next upcoming event; once everything has passed, it
shows whichever passed most recently.
Events are stored on your Mac in UserDefaults under sh.mdr.TimeBar, and
reload when the app starts. Nothing leaves the machine and there is no network
access of any kind.
Built with Swift Package Manager rather than an Xcode project, so it compiles with Command Line Tools alone — no full Xcode install required.
swift build # build
swift test # 32 tests (swift-testing)
scripts/build-app.sh [debug|release] # -> build/TimeBar.appSwiftPM produces a bare executable, but a menu bar app needs a real bundle for
Info.plist (notably LSUIElement, which suppresses the Dock icon) to be
honoured. scripts/build-app.sh assembles that bundle and ad-hoc signs it —
enough to launch locally. Swap in a Developer ID identity before distributing.
Requires macOS 14+ (the Observation framework) and Swift 6.
Sources/TimeBarCore/ UI-free domain layer — the whole test surface
Models/ TimeEvent, EventColor, Countdown, CountdownFormatter
Services/ EventStoring (UserDefaults + in-memory), TimeProviding
ViewModels/ MenuBarViewModel
Sources/TimeBar/ SwiftUI shell, kept thin
Views/ MenuBarLabel, MenuContentView, EventListView,
EventRowView, EventEditorView, ColorPalettePicker,
AboutView
Support/ AppComposition (composition root), AppDelegate,
AppInfo
Tests/TimeBarCoreTests/ swift-testing suites
MenuBarViewModel takes its store and clock as protocols, so tests drive the
countdown with a stub clock and an in-memory store instead of waiting on real
time. Everything worth testing lives in TimeBarCore; the SwiftUI layer holds
no logic of its own.
No months, so m means minutes. Months are the one unit whose natural
abbreviation collides with another, so they are dropped entirely. This falls out
of the component set CountdownFormatter asks for — leave .month out of the
request and Calendar rolls whole months into the day count for you, so there
is no formatting branch that could reintroduce one. A test sweeps 800 days of
offsets asserting no label ever contains mo.
Calendar arithmetic, not fixed multiples. Every interval goes through
Calendar, so year lengths, leap days and DST transitions come out right —
2028 is a leap year, so 2028-01-01 to 2028-12-31 is 365d where the same
dates in 2027 are 364d.
The app sleeps until the label would change. CountdownFormatter.nextChange
computes the exact instant the visible text next differs, so an event three days
out wakes the process about once an hour rather than 86,400 times a day.
Per-second ticking happens only while the popover is open, and a model with no
events schedules nothing at all. A wake-from-sleep observer catches the clock up
so a long sleep can't strand the label.
The colour dot is a non-template NSImage. MenuBarExtra rasterises its
label as a template image, which flattens SwiftUI shapes and SF Symbols to a
single tint — a Circle().fill(.orange) renders white. Drawing into an
NSImage with isTemplate = false is the only way to keep the event's colour.
The editor replaces the list in place rather than presenting a sheet, and
uses .stepperField date pickers and a fixed colour palette. Sheets, calendar
overlays and ColorPicker's system colour panel all take key focus away from a
MenuBarExtra window and dismiss it mid-edit.