Minimalist macOS app that uses the physical notch to show progress on the current calendar event.
- Pick one or more calendars (checkboxes in Settings).
- If a tracked calendar is deleted or unshared, NotchBar won't auto-pick a replacement — reselect one in Settings.
- English and French, following the system language.
- Respects Reduce Motion: with it on, the panel crossfades in place — no scale, offset or spring — and the progress bar's shimmer is frozen.
- At rest NotchBar draws nothing in the notch — the physical notch shows through untouched (so it never slides with the desktop during Space switches).
- Menu-bar countdown (on by default, toggle in Settings): while an event is running, the time left shows next to the menu-bar icon —
23 min, then1h05past the hour. No event running, or the toggle off, and it's the icon alone. - Event notifications (off by default, toggle in Settings): a notification 5 minutes before a tracked event starts, and 5 minutes before it ends. Turning it on is what asks macOS for notification permission. Events shorter than 5 minutes only get the start one.
- On hover, the panel expands and shows one of seven contextual states, computed across the events of every tracked calendar:
| State | Trigger | Shown |
|---|---|---|
| In progress | Event overlaps now | Title, start–end times, animated progress bar, elapsed / remaining + Join button when a meeting link is detected (Zoom, Meet, Teams, Webex) |
| Starting soon | Next event in ≤ 5 minutes | Starts in Xm — <title> + Join button when a meeting link is detected (Zoom, Meet, Teams, Webex) |
| Upcoming today | Next event later today | Next: <title> in Xh Ymin |
| Upcoming | Next event is beyond today (up to 7 days out) | Next event in: DD:HH:MM:SS (live countdown) |
| Empty today | No events found | No event today |
| No calendar | No calendars selected | Pick a calendar in Settings |
| Access off | Calendar access denied or revoked | Calendar access is off — re-enable in Settings |
Sources/
├── NotchBarApp.swift # @main + AppDelegate + the menu-bar status item
├── NotchPanel/ # NSPanel windows, hover tracking, SwiftUI rendering, motion style
├── Calendar/ # EventKit access, snapshot model, meeting links, notifications
├── Settings/ # UserDefaults-backed preferences + settings UI
├── Utilities/ # ScreenHelper (notch geometry), Localized helper, os.Logger categories
└── Resources/ # en.lproj/ + fr.lproj/ Localizable.strings, processed natively by SwiftPM
Tests/
└── NotchBarTests/ # XCTest target (@testable import NotchBar)
Supporting/
├── Info.plist # LSUIElement, calendars usage description, bundle metadata
└── NotchBar.entitlements # Sandbox + calendars entitlement
NotchBar is intentionally simple — no external dependencies, no framework layers.
SPM-only (no .xcodeproj)
Swift Package Manager is the sole build system. This keeps builds fully reproducible and eliminates Xcode project file merge conflicts in team workflows.
NSPanel over NSWindow
The notch overlay is an NSPanel configured as borderless + nonactivatingPanel. This combination keeps the panel visible at the correct screen layer without stealing keyboard focus from the active app.
Seven-state snapshot model
EventProgressModel holds an EventProgressSnapshot — an immutable, Equatable value derived from live EventKit data. CalendarManager maps EventKit into plain CalendarEvent values; SnapshotBuilder turns those into a snapshot; the view renders whatever snapshot it receives. All conditional logic is isolated in SnapshotBuilder.computeSnapshot, making each state independently testable without a running EventKit store.
Data flow
EventKit → CalendarManager → EventProgressSnapshot → NotchPanelView
CalendarManager owns EKEventStore, publishes the fetch window as [CalendarEvent] values, and polls every 30s — paused while the screens are asleep, with one catch-up refresh on wake — plus reacts to EKEventStoreChanged. If Calendar access is granted after launch — e.g. from System Settings, without restarting NotchBar — the store-changed notification and each panel open re-check authorization and pick up the change automatically. NotchPanelView observes EventProgressModel via @ObservedObject.
Idle-first performance
The 1-second refresh tick and the 60fps progress-bar shimmer run only while the panel is open (hover). While collapsed, the only live work is the menu-bar countdown's tick — one snapshot recompute every 30 seconds, matching CalendarManager's polling cadence, and only while the toggle is on. Turn the countdown off and NotchBar does no live work at all when collapsed. Snapshots are Equatable, so redundant recomputes never trigger a SwiftUI invalidation.
LSUIElement
Set in Info.plist, this flag hides the app from the Dock and the Cmd-Tab app switcher. NotchBar runs as a pure background UI layer with no Dock presence.
NotchBar is ad-hoc signed, not Apple-notarized — notarization needs a paid Apple Developer ID and is not planned. macOS refuses to open such an app whenever the copy carries a quarantine flag, and browsers and Homebrew both attach one. Only the curl route below never sets it; the other two clear it in one extra step.
brew tap periicles/tap
brew trust periicles/tap # third-party casks need an explicit trust (Homebrew 6+)
brew install --cask notchbar
xattr -dr com.apple.quarantine /Applications/NotchBar.appHomebrew always quarantines a cask: --no-quarantine was removed in Homebrew 6 and now fails with Error: invalid option: --no-quarantine. The last line clears the flag from the copy just installed, which is what lets an ad-hoc signed app open; without it macOS blocks the first launch until you allow it under System Settings → Privacy & Security → Open Anyway.
Remove with brew uninstall --cask notchbar (add --zap to also delete its data).
curl -fsSL -o /tmp/NotchBar.dmg https://github.com/Periicles/Notchapp/releases/latest/download/NotchBar.dmg &&
hdiutil attach -quiet -nobrowse -mountpoint /tmp/NotchBar.mount /tmp/NotchBar.dmg &&
cp -R /tmp/NotchBar.mount/NotchBar.app /Applications/ &&
hdiutil detach -quiet /tmp/NotchBar.mount && rm /tmp/NotchBar.dmgNothing is piped into a shell — every step is visible above. curl does not set the quarantine flag, so the app opens with a normal double-click afterwards.
- Download NotchBar.dmg, or get it from the NotchBar website.
- Open the
.dmgand drag NotchBar into your Applications folder. - Open NotchBar. The first time, macOS blocks it — expected for an app Apple has not notarized, downloaded through a browser.
- Open System Settings → Privacy & Security, scroll down, and click Open Anyway, then confirm.
Why the extra step? The app is ad-hoc signed, so Gatekeeper rejects the quarantine flag your browser attached to the download. You do this once; afterwards it opens with a normal double-click. The
curlroute above avoids the step entirely.
On first launch, grant Calendar access when prompted, then hover over the notch and click the settings icon to choose which calendars to track.
NotchBar has no network access and does not check for new versions on its own — take the route matching how you installed it:
| Installed with | Update with |
|---|---|
| Homebrew | brew upgrade --cask notchbar, then xattr -dr com.apple.quarantine /Applications/NotchBar.app again |
| The one-command install | Quit NotchBar, then re-run the exact same command — it overwrites the copy in /Applications |
The .dmg |
Quit NotchBar, download the latest .dmg and drag the app over the old one |
The cask is bumped automatically whenever a release is published, so Homebrew is the route that keeps you closest to the latest version. It does ask for the xattr line every time: Homebrew carries an unquarantined app's state across an upgrade only while its signing identity is unchanged, and an ad-hoc signature is designated by the binary's cdhash, which every build changes. Your settings and calendar selection survive an update — they live in the app's container, not in the bundle — but macOS may ask for Calendar access again after the app bundle is replaced.
- If you turned on Launch at login, hover the notch → open settings → toggle it off first (this removes the login item cleanly). You can also remove it later under System Settings → General → Login Items.
- Quit NotchBar: click the NotchBar icon in the menu bar → Quit, or hover the notch → open settings → Quit NotchBar.
- Drag NotchBar from Applications to the Trash.
- Optional — remove leftover settings: delete
~/Library/Containers/com.periicles.NotchBar/. - Optional — revoke Calendar access: System Settings → Privacy & Security → Calendars.
Prerequisites
- macOS 14 or later
- Xcode full install (required for XCTest and the Swift 6.1 toolchain)
Clone, build, and run
git clone https://github.com/Periicles/Notchapp.git
cd Notchapp
swift build
swift runOn first launch, grant Calendar access when the system prompt appears (or later via System Settings → Privacy & Security → Calendars). Then hover over the physical notch and click the settings icon to choose which calendars to track.
Running tests
The test target depends on XCTest, which ships with full Xcode (not Command Line Tools). If xcode-select -p points at /Library/Developer/CommandLineTools, set DEVELOPER_DIR for the test invocation:
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift testLinting
Install SwiftLint and run it from the project root:
brew install swiftlint
swiftlintThe CI pipeline runs swiftlint on every PR. Fix all errors before pushing.
Branch naming
| Prefix | Use for |
|---|---|
feature/<topic> |
New functionality |
fix/<topic> |
Bug fixes |
docs/<topic> |
Documentation-only changes |
refactor/<topic> |
Code changes with no behavior change |
Commit style — Conventional Commits
feat: add weekly agenda view
fix: correct progress bar overflow at event boundary
refactor: extract snapshot helpers into static methods
test: cover upcomingToday state with same-day boundary
docs: document five-state model in README
Before opening a PR
swift test— all tests passswiftlint— zero errors- New non-trivial logic is covered by tests
- One concern per PR — avoid mixing features with refactors
- Add an entry under
## [Unreleased]in CHANGELOG.md when behavior changes
The CI pipeline (lint → build → test) runs automatically on every PR. A PR cannot be merged with a failing CI.
Releases are cut by pushing a tag. A GitHub Actions workflow (.github/workflows/release.yml) then lints, builds, tests, packages the .dmg, and publishes a GitHub Release automatically.
git tag v0.3.0
git push origin v0.3.0The version comes from the tag (vX.Y.Z → X.Y.Z) and is injected into the app at build time — no need to edit Info.plist. The CFBundleShortVersionString checked into Supporting/Info.plist is only a placeholder for local swift run builds; every packaged build overwrites it. Releases are published as stable, which is what keeps /releases/latest/download/NotchBar.dmg resolving — that URL skips pre-releases.
Publishing a release also bumps the Homebrew cask. The bump-cask job computes the sha256 of the released .dmg, opens a pull request on the tap and turns on auto-merge, so the bump lands as soon as the tap's brew test-bot is green and brew upgrade --cask notchbar serves the new version with no manual step. It needs one repository secret:
| Secret | For |
|---|---|
TAP_TOKEN |
fine-grained PAT with contents, pull requests and issues write access on Periicles/homebrew-tap |
To rehearse the rewrite without pushing anything: scripts/bump-cask.sh 0.3.0 "$(brew --repository periicles/tap)" --dry-run.
Notarization is not planned — it needs a paid Apple Developer ID, and clearing the quarantine flag is a one-line workaround for the routes that set one. Kept here in case that ever changes: add these repository secrets and follow the commented hooks in release.yml / scripts/package.sh.
| Secret | For |
|---|---|
MACOS_CERT_P12_BASE64 |
Developer ID Application cert (.p12), base64-encoded |
MACOS_CERT_PASSWORD |
the .p12 password |
APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD |
notarytool credentials |
NotchBar is released under the MIT License.