When I wrote about MDMenuBar in April, I described it as a lightweight tool for one thing: previewing Markdown files from the menu bar. The app I’m running today is different enough that I gave it a new name.
It’s called Beacon now.
Why the Rename
The original name was literally descriptive: Markdown in a Menu Bar. That description stopped fitting when I found myself loading a local web dashboard in the panel instead of individual .md files, and then wiring the app to respond to signals from that dashboard by changing its menu bar icon.
A Markdown previewer that also serves as an alert indicator and a remote control for a web app is not well described as “MDMenuBar.” Beacon fits better. The beacon icon (a lighthouse-style SF Symbol I replaced with a custom SVG) also looks better at 18pt than anything I’d been using.
Becoming a Proper App Bundle
The SPM build I described in the original post still works, but distributing a binary and adding it to Login Items is awkward. Since May the project has an Xcode project alongside the SPM source, producing a proper .app bundle that behaves like any other menu bar app.
Getting there surfaced a few things. Swift 6 strict concurrency required wrapping main.swift in MainActor.assumeIsolated { }. The sandbox had to come off entirely: the app loads a local web server and follows links across your filesystem, and security-scoped bookmarks are too much infrastructure for a personal productivity tool that never leaves your machine. And WKWebView’s WebProcess crashes immediately if network.client is absent from the entitlements, because it communicates internally over network sockets.
None of these were obvious from the documentation.
Interactive HTML
The original HTML support had a quiet limitation: loading a local file via loadHTMLString(_:baseURL:) gives the WKWebView an about:blank null origin. This means localStorage is scoped per-load and cleared every time the file watcher fires on save.
For a live dashboard with checkboxes, sort state, and collapsible sections, this is a problem. The fix was switching HTML loading from loadHTMLString to webView.load(URLRequest(url: fileURL)), which gives the page a stable file:// origin. State persists across hot-reloads. It sounds like a small change, but it’s the difference between a dashboard that works and one that resets itself every time you save.
URL Source Mode
Beacon is no longer limited to local files. A source sheet (accessed via the toolbar’s link button) lets you point the panel at any HTTP or HTTPS URL, with a configurable auto-refresh interval. When in URL mode, in-panel links stay in the panel rather than opening a browser tab.
This is how I actually use it: the source is a locally-running Node.js server, and the panel is a persistent window into that server. The file watcher isn’t involved at all.
The Alert System
The feature I’m most pleased with is the alert integration. Any page loaded in the panel can call window.beaconSignal(cardId, payload) to change the menu bar icon from the normal beacon to a red-tinted notification variant, and window.beaconClear() to revert it. When you open the panel, the icon clears automatically.
On the Swift side: Beacon registers beaconAlert and beaconClear as WKScriptMessageHandler names, injects a small shim at document start that maps window.beaconSignal and window.beaconClear to those handlers, and connects the icon-switching logic in AppDelegate.
The icon change uses NSImage.SymbolConfiguration(paletteColors: [.systemRed]) rather than contentTintColor. This matters: the macOS menu bar overrides contentTintColor on status item buttons. Baking the color into the image via SymbolConfiguration is the only approach that actually renders correctly.
Alert behavior is configurable: a right-click submenu lets you independently toggle the icon change and the sound (an NSSound.beep()), or disable both at once. Settings persist across launches via UserDefaults. Beacon also delivers UNUserNotificationCenter banners when alerts fire, so you see them even when the panel is closed.
Quick Actions
Command Center (the dashboard Beacon loads) registers keyboard shortcuts for quick-entry modals: ⌘. for a scratch note, ⌘K for the chat overlay, ⌘I for a quick issue. These are document.keydown listeners inside the web app.
Beacon exposes these from anywhere on your machine via global hotkeys: ⌘⇧., ⌘⇧K, ⌘⇧I. When triggered, Beacon opens the panel and dispatches a synthetic KeyboardEvent to the WKWebView’s document with metaKey: true and the corresponding key. A 150ms delay before dispatch lets the WebView settle before the event fires.
The same approach works for any shortcut in the loaded page: anything registered as a document.keydown listener can be driven from a global macOS hotkey. The dispatchMetaKey(_:) method in PreviewPanel.swift is the only bridge needed.
All four global hotkeys (panel toggle, note, chat, issue) are configurable. A “Shortcuts…” item in the right-click menu opens a popover with four ShortcutField views, each of which records a keypress when clicked. You have to include a modifier key to avoid accidentally binding a bare letter key system-wide. Defaults are ⌘⇧M, ⌘⇧., ⌘⇧K, ⌘⇧I.
JumpCloud SSO
This one took an afternoon.
WKWebView instances are not system browsers. The macOS Extensible SSO extension fires at the system browser stack level and does not apply to third-party WKWebViews. For services protected by JumpCloud conditional access, the standard WKWebView login flow simply fails.
The fix is ASWebAuthenticationSession: Apple’s API for authenticating against web services using the system browser session, which does participate in the Extensible SSO stack. When Beacon detects a JumpCloud SSO redirect in decidePolicyFor, it starts an ASWebAuthenticationSession, lets the system browser handle authentication, then syncs HTTPCookieStorage.shared cookies back to the WKWebView’s httpCookieStore and reloads the target URL.
For SAML-based flows the sheet stays open until you dismiss it manually. For OIDC, register beacon-sso://callback as a redirect URI in the JumpCloud admin console and the sheet closes automatically on completion.
What Came Out
The Scratch pad tab is gone. Scratch functionality moved to a dedicated card inside Command Center, which is the app that’s actually loaded in the panel. There was no reason to maintain a second implementation in Swift. Removing it simplified the toolbar and eliminated about a hundred lines of wiring.
The Code
The project is on GitHub at github.com/maparker/MDMenuBar (the repo retains its original name). It builds from both Xcode (for a proper .app) and SPM (swift run still works). macOS 13 Ventura or later, no external dependencies.