A portable, zero-dependency CLI tool that renders Markdown files in a browser window with live reload.
macOS / Linux — install the latest release into /usr/local/bin (falls back to ~/.local/bin):
curl -fsSL https://raw.githubusercontent.com/simonerom/mdview/main/install.sh | shIf the install directory isn't on your PATH, the script detects your shell (zsh, bash, fish, ksh)
and prints a ready-to-paste command that adds it permanently to the right startup file.
Pin a version or change the install directory:
curl -fsSL https://raw.githubusercontent.com/simonerom/mdview/main/install.sh | MDVIEW_VERSION=v0.1.0 MDVIEW_INSTALL=~/bin shOr download an archive for your platform from the releases page and move the mdview binary somewhere on your PATH. Windows builds are published as .zip.
With the Go toolchain installed (1.21+):
go install github.com/simonerom/mdview@latestThis installs mdview into $(go env GOPATH)/bin — make sure that directory is on your PATH.
To build from a clone instead:
git clone https://github.com/simonerom/mdview.git
cd mdview
go build -o mdview .
sudo install -m 0755 mdview /usr/local/bin/mdviewCheck the installed version with:
mdview --versionmdview file.md # Open a single file
mdview file1.md file2.md # Concatenate and view multiple files
cat file.md | mdview # Read from stdin
mdview --window file.md # Open a standalone window instead of a tabmdview exits when you close the page, so it leaves nothing running behind you. It waits a few seconds first, because following a link drops the connection the same way a closed window does — and it never exits before a browser has shown up at all, so opening the printed URL by hand still works.
--window uses Chrome, Brave, Edge or Chromium to open a window with no tabs
and no address bar, which is what makes mdview behave like an application:
close the window and the process is gone. Safari and Firefox have no equivalent,
so it falls back to a normal tab there.
Finder can only "Open With" an application bundle, never a bare binary.
macos-app.sh builds a small one in ~/Applications:
./macos-app.shThen set it as the default: Get Info on any .md file → Open with → mdview →
Change All…
Each file you open gets its own window and its own process, and closing the window ends it.
Three things in that script are easy to get wrong on your own, and each fails in a way that does not point at its cause:
- A bundle wrapping a plain shell script never receives the file — macOS
delivers it as an Apple Event, which only an AppleScript
on openhandler picks up. osacompilewrites noCFBundleIdentifierand only a legacy wildcard type declaration. "Change All…" binds a default handler to a content type keyed by bundle identifier, so with neither in place Finder quietly settles for remembering that one file — and the setting looks like it will not stick.- The binary path is resolved and baked in at build time, because an app
launched from Finder does not read your shell profile and will not find
mdviewonPATH.
The page opens read-only. Press Edit to swap in a split view — the
Monaco editor on the left, the
live preview on the right — and Ctrl/Cmd+S or Save to write the file.
Monaco is bundled into the binary and only fetched when you press Edit, so
viewing stays as fast as it was, and editing works offline.
The Edit button appears only when there is exactly one file to save to. It is hidden for stdin (there is no file) and for multiple files (they are concatenated into one buffer, with no way to split them back apart), and for files that are not writable or not valid UTF-8.
If the file changes on disk while you are editing, mdview says so rather than picking a winner: an untouched buffer is refreshed silently, an edited one gets a prompt, and a save that would clobber a newer version on disk is refused and offered as a choice. Writes go through a temporary file and a rename, keeping permissions, symlink targets and CRLF line endings intact.
- Editing — Monaco with a live preview, off by default, behind an Edit button
- Live reload — File watcher + SSE pushes reload events to the browser
- GitHub-flavored Markdown — Tables, task lists, strikethrough, autolinks
- Syntax highlighting — Fenced code blocks with language detection
- Dark/light mode — Respects
prefers-color-scheme, with a toggle button - Clean typography — GitHub-like CSS embedded in binary
- Portable — Single binary, cross-compile for macOS/Linux/Windows
- Reads the Markdown file(s)
- Converts to HTML using goldmark with GFM extensions
- Starts a local HTTP server on a random port
- Opens the default browser
- Watches the source file for changes and auto-reloads via SSE
- Exits a few seconds after the last page is closed, or on Ctrl+C
The server binds loopback and is unauthenticated, which is not on its own enough
once a write endpoint exists: any page in your browser can reach it. Every route
checks the Host header — that is what stops DNS rebinding — and the editing
endpoints additionally require a per-session token that a cross-origin caller
cannot send. The vendored editor lives in monaco/; see
monaco/VENDOR.md for what was pruned and how to update it.
Releases are built automatically by GitHub Actions. Push a version tag and the release workflow cross-compiles for macOS, Linux and Windows (amd64 + arm64), packages the archives with checksums, and publishes a GitHub release with auto-generated notes:
git tag v0.1.0 && git push origin v0.1.0The tag name is baked into the binary as its --version string.
Here's a table:
| Feature | Status |
|---|---|
| Tables | ✅ |
| Task lists | ✅ |
| Code highlighting | ✅ |
| Dark mode | ✅ |
| Live reload | ✅ |
And a task list:
- Parse Markdown
- Render HTML
- Live reload
- World domination
package main
import "fmt"
func main() {
fmt.Println("Hello from mdview!")
}This is a blockquote to test styling.
Built with Go and goldmark.