I spend a lot of time looking at Markdown. Session diaries, READMEs, shell script documentation, notes. And for a while my workflow was: write in a text editor, switch to a browser with a Markdown extension installed, reload, check it looks right, switch back. Or use VS Code’s built-in preview, which splits the editor in half and requires the file to be open in VS Code in the first place.
Neither of these is bad, exactly. But neither is quite right either. What I wanted was something that lived outside my editor, out of the way, ready when I needed it: a persistent preview panel I could glance at from anywhere.
So I built one.
What MDMenuBar Does
MDMenuBar is a macOS menu bar app that slides a preview panel in from the right edge of your screen. It renders Markdown and HTML files with live reload — as soon as you save, the preview updates. You open it with a global hotkey (⌘⇧M) from any application, without switching focus. When you’re done, dismiss it with the same shortcut.
That’s the whole pitch. It’s deliberately small.
The panel itself has two modes. The first is file preview: open any .md, .markdown, or .html file and it watches for changes and re-renders automatically. The second is a scratch pad: daily timestamped notes in Markdown, with a built-in search across every scratch file you’ve ever created. More on that later.
Why Native Swift
The obvious path for a side project like this is Electron or a web app wrapped in a shell. It’s fast to prototype, you get a browser’s rendering engine for free, and you can reuse existing Markdown libraries.
I went the other way. MDMenuBar is pure Swift with AppKit — no Electron, no third-party dependencies, no package manager. The binary is small, startup is instant, and the memory footprint is nothing. On a machine that’s already running a JVM, three Electron apps, and Chrome with forty-two tabs, that matters.
The tradeoff is that AppKit is not exactly fun to work with. There’s no declarative UI. You’re managing frame math and view hierarchies manually. Things that take three lines in SwiftUI take thirty in AppKit. But for a menu bar app that needs fine-grained control over window behavior, positioning, and animation, AppKit is the right level of abstraction. SwiftUI’s menu bar support is still rough around the edges.
The panel uses NSPanel rather than NSWindow, configured to float above other windows and ignore the application activation model. When it appears it slides in from the right edge of the screen with a spring animation. It finds the right screen by checking which display the menu bar button lives on — critical for multi-monitor setups, where the obvious code path (using the main screen) gets it wrong.
Writing a Markdown Renderer From Scratch
This is the part I hadn’t planned on, but ended up being the most interesting to build.
I didn’t want to pull in a Swift Markdown library. Every one I evaluated had at least one of: a complex dependency graph, a license I had to think about, or rendering behavior I couldn’t control. For a project I intended to use every day, I wanted to understand every line.
So I wrote MarkdownRenderer.swift: a custom Markdown-to-HTML converter with no external dependencies. It’s about 350 lines and handles everything I actually use — headers, code fences with syntax hints, ordered and unordered lists, tables, blockquotes, inline bold/italic/strikethrough/code, links, and images.
The approach is a two-pass state machine. The first pass handles block-level elements: it tracks whether you’re inside a code fence, a list, a table, or a blockquote, and emits the appropriate HTML. The second pass handles inline elements using a series of regex substitutions applied in a carefully ordered sequence — bold-italic before bold, strikethrough before inline code, images before links. Order matters because the patterns can overlap, and applying them in the wrong sequence produces malformed HTML.
The output is wrapped in CSS that closely mirrors GitHub’s Markdown styling, with a dark mode variant that uses prefers-color-scheme: dark. Since the rendered content lives in a WKWebView, the light/dark theme switches automatically with the system appearance. I could have used a CDN-hosted stylesheet, but embedding the CSS means the renderer works completely offline.
Is my Markdown renderer spec-compliant? No. Does it handle every edge case? Definitely not. But it handles every edge case I’ve run into in six months of daily use, and I’ve been able to fix the ones I did hit in about ten minutes each.
File Watching and Atomic Saves
Live reload sounds simple until you encounter how most editors actually save files. VS Code, for example, doesn’t just overwrite the file in place — it writes to a temporary file and renames it to the target path. This is called an atomic save, and it’s genuinely better behavior (no partial writes, no corruption on crash). But it completely breaks file watchers that only listen for write events on the original file descriptor.
MDMenuBar’s file watcher handles this correctly. It opens the file with O_EVTONLY — a flag that tells the kernel you want to watch the file without counting as a reader — and creates a DispatchSourceFileSystemObject listening for .write, .rename, and .delete events. When it sees a rename or delete, it closes the old file descriptor and re-opens the path to catch the new inode. The new inode is the file the editor actually wrote to. Without this step, VS Code saves stop triggering updates after the first one.
This took me an embarrassingly long time to figure out. When it finally worked I felt clever. A few days later I read that this is just the standard approach and everyone who’s built a file watcher already knows it. Such is programming.
The Global Hotkey Problem
macOS doesn’t make it easy to register global keyboard shortcuts without asking for Accessibility permissions. The permission prompt is disruptive — it requires the user to navigate to System Settings, find the app, and toggle a switch — and I wanted MDMenuBar to work without it.
The solution is Carbon’s legacy event system. RegisterEventHotKey and InstallEventHandler let you intercept keyboard events at a low enough level that they don’t require Accessibility. The catch is that the callback has to be a C function pointer, which can’t capture Swift closures. The workaround is a global variable (_sharedDelegate) that the C callback looks up by reference.
It feels a little grubby. Carbon is deprecated and has been deprecated for a long time. But it works, it’s the same technique used by apps like Alfred and Raycast, and until Apple provides a better option, it’s the right answer.
The Scratch Pad
When I was building the file preview, I realized I wanted somewhere to keep notes that was integrated with the preview rather than separate from it. The scratch pad grew out of that.
Each day gets its own Markdown file: scratch-2026-04-07.md. When you type in the scratch input and hit ⌘↩, the entry is prepended to the file with a timestamp heading. Below the input, the current day’s file renders as a live Markdown preview. Previous days are listed as collapsible links.
The search feature indexes every scratch file you’ve ever created and does a case-insensitive substring match across all of them. Results are displayed in-panel with surrounding context. It’s not fancy — it’s essentially grep with a nicer wrapper — but it makes the scratch files genuinely useful as a log rather than just disposable notes.
The midnight rollover case is worth mentioning: when you add an entry after midnight, the watcher checks whether the current path still matches today’s filename. If it doesn’t, it creates a new daily file and re-watches it. Sounds obvious in retrospect, but failing to handle it means entries from 12:30 AM end up in yesterday’s file.
What It Replaced
Since I built this, I’ve stopped using VS Code’s preview pane for anything. I write in whatever editor is appropriate for the file and glance at the MDMenuBar panel when I need to check formatting. The panel stays pinned (there’s a pin button that keeps it visible when clicking elsewhere) when I’m doing extended Markdown editing, and I dismiss it when I don’t need it.
The scratch pad replaced a mix of sticky notes and Apple Notes that I was using for session notes. Having them in Markdown, automatically dated, and searchable is a meaningful upgrade. The fact that they integrate with the same panel I use for file preview means one less context switch.
The Code
The project is five Swift files, roughly 1,600 lines total. There’s nothing particularly novel in it — the interesting parts are all documented patterns — but if you want a reference for building a native macOS menu bar app with file watching, live reload, and a global hotkey, the code is on GitHub. It builds with Swift Package Manager, requires macOS 13 Ventura or later, and has no dependencies beyond what ships with the OS.
If you find a Markdown edge case it doesn’t handle, open an issue.