A minimal, self-contained cross-platform markdown viewer with TUI and GUI. One executable, no installation, no dependencies, runs everywhere.
For most users
Open Markdown like any other document - cleanly, safely, and without editing tools getting in the way.
For experts and developers
Browse an entire local Markdown documentation set like a website, with linked navigation, rich rendering, and cross-document search - without building or serving anything.
mdv adapts to wherever it runs:
- GUI - a native-webview window with full rendering (default on desktops).
- TUI - a rich terminal UI when no graphical environment is available.
- Console - plain rendered output to stdout when piped or non-interactive.
Honestly? This might be the missing tool that should ship pre-installed on every OS, with
.mdfiles associated to it out of the box. Or - even better - every OS should just bake this kind of instant markdown preview straight into its native file manager (File Explorer, Finder, Nautilus, you name it). Until that glorious day arrives, there'smdv. 😉
It is built so it always starts, including inside headless Docker containers
over SSH: the distributed binary is a pure-Go launcher with zero webview
linkage, so missing WebKitGTK/GUI libraries never cause a failure. The GUI
is a separate helper embedded in the binary and only spawned when a graphical
environment is actually present.
Linux GUI requirement: the embedded GUI helper needs GTK >= 4.14 and WebKitGTK 6.0 (e.g. Debian 13+, Ubuntu 24.04+, Fedora 39+). On older systems mdv detects this and runs in the TUI/console instead; the installer prints a note when the GUI cannot run.
No package managers needed - the install scripts download a single executable from GitHub Releases.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/thgossler/mdv/main/scripts/install.sh | shWindows (PowerShell):
irm https://raw.githubusercontent.com/thgossler/mdv/main/scripts/install.ps1 | iexThe PowerShell installer is cross-platform - with PowerShell 7+ it also works on macOS and Linux:
pwsh -c "irm https://raw.githubusercontent.com/thgossler/mdv/main/scripts/install.ps1 | iex"Silent install with automatic .md file association (for unattended setups
or chaining from another installer):
curl -fsSL https://raw.githubusercontent.com/thgossler/mdv/main/scripts/install.sh | sh -s -- --silent --associate-md-file-extension$s = irm https://raw.githubusercontent.com/thgossler/mdv/main/scripts/install.ps1
& ([scriptblock]::Create($s)) -Silent -AssociateMdFileExtensionOr download a binary directly from the Releases page:
| Platform | Asset |
|---|---|
| macOS (universal) | mdv-macos-darwin-universal.tar.gz |
| Windows (x64) | mdv-windows-x64.zip |
| Windows (arm64) | mdv-windows-arm64.zip |
| Linux (x64) | mdv-linux-x64.tar.gz |
| Linux (arm64) | mdv-linux-arm64.tar.gz |
| Platform | Default location | PATH handling |
|---|---|---|
| Windows | %LOCALAPPDATA%\Programs\mdv\mdv.exe |
Added to your user Path (persisted for new terminals) and prepended to the current session. |
| macOS / Linux | /usr/local/bin/mdv if it is on your PATH and writable, otherwise ~/.local/bin/mdv |
If the chosen directory isn't already on PATH, the installer appends it to your shell profile (.zshrc, .bashrc, .bash_profile, or .profile). |
Set the MDV_INSTALL environment variable to install somewhere else (e.g.
MDV_INSTALL=$HOME/bin), and MDV_VERSION to pin a specific release tag.
On Windows and macOS the installer also asks whether to associate .md files
with mdv so you can open Markdown by double-clicking. Set
MDV_ASSOCIATE_MD=1 to associate without being prompted (or =no to skip it
silently in non-interactive installs). See
Open Markdown files from File Manager
for how association works and how to change the default by hand.
For unattended installs (e.g. chaining the script from another installer or
tool), pass --silent (-Silent in PowerShell) to suppress all prompts; this
leaves the file association untouched. Add
--associate-md-file-extension (-AssociateMdFileExtension in PowerShell) to
request the .md association even in silent mode (see the
silent install example above).
Both installers make mdv usable in the same shell, with no restart or
manual source needed:
- The PowerShell installer, when run with the
irm … | iexone-liner, executes in your current session and prepends the install directory to$env:PATHimmediately. - The POSIX installer prefers a directory that is already on your
PATH, so the binary is found right away. If it has to fall back to~/.local/bin, it updates your profile for future shells and prints the one-lineexportto enable it in the current one (or run it sourced -. install.sh- to have thePATHupdate applied directly to your shell).
mdv README.md # open a single document
mdv ./docs # open a folder (sidebar lists all markdown files)
mdv README.md --remote --sidepanel --ignore "*,\!/README.md,\!docs/" # open
# document, show sidebar with related markdown files, but only within docs/
mdv --tui README.md # force the terminal UI
mdv --console README.md # render to stdout and exit
cat README.md | mdv --console # render Markdown piped on stdin (see note below)
mdv --pdf out.pdf README.md # render to a PDF and exit (headless-friendly)
mdv --pdf out.pdf --force README.md # overwrite an existing PDF without asking
mdv --pdf out.pdf --remote README.md # allow downloading remote images/assets
mdv --version # show current SemVer version number
mdv --init-config # write a default settings.jsonc| Flag | Description |
|---|---|
--tui |
Force the interactive terminal UI |
--gui |
Force the graphical UI |
--console, -c |
Render to stdout and exit |
--pdf PATH |
Render the input to a PDF at PATH (file or folder) and exit; see PDF export |
--force |
With --pdf, overwrite an existing output file without prompting |
--remote |
With --pdf, allow downloading remote (http/https) images/assets (blocked by default) |
--no-color |
Disable ANSI colors in console output |
--max-width N |
Cap the render width to N columns |
--images MODE |
Image rendering: auto, graphics, blocks, off |
--sidepanel |
Force the document navigator panel to start visible (GUI and TUI) |
--version |
Print version and exit |
--init-config |
Write a default settings file and exit |
Note
On Windows the prompt returns before mdv prints. The shipped mdv.exe
is a GUI-subsystem binary, which is exactly what lets you double-click a
.md file in Explorer without a console window ever flashing up. A small,
harmless side effect of that feature: Windows command-line shells
(PowerShell and cmd.exe) do not wait for GUI-subsystem programs, so after a
run such as mdv --console README.md you get a fresh prompt immediately and
mdv's output is drawn just after it. The command finished successfully -
press Enter for a clean prompt if the redraw looks untidy. This
never affects double-clicking, the GUI, or the TUI.
mdv reads Markdown piped on stdin, so you can render the output of another command without a temporary file:
cat README.md | mdv --console # macOS / Linux
type README.md | mdv --console # Windows (cmd.exe)Note
Windows PowerShell only: the shipped mdv.exe is a GUI-subsystem binary
(so double-clicking a .md file in Explorer never flashes a console window).
As a side effect, PowerShell does not wait for or capture its stdout unless a
downstream command consumes the pipeline, so a bare
type README.md | mdv --console prints nothing. Pipe the result through any
consumer to force PowerShell to drain the output - Out-String (then
Write-Host) preserves the rendering:
Get-Content README.md | mdv --console | Out-String | Write-HostThis affects only piped stdin in PowerShell. cmd.exe, PowerShell on
macOS/Linux, passing a file path (mdv --console README.md), and redirecting
to a file in cmd.exe all work without the extra step.
mdv can register itself as a handler for .md files so you can open Markdown by
double-clicking it in your file manager - the document opens in the mdv GUI. The
installer script sets this up for you when you opt in (it asks during an interactive
install, or pass --associate-md-file-extension / -AssociateMdFileExtension,
or set MDV_ASSOCIATE_MD=1). Either way, the plain mdv command stays the way
to use mdv from the terminal.
Two platform specifics are worth knowing:
- macOS - Finder's Open With only lists application bundles, so the
release archive also ships a small
mdv.appwrapper that forwards opened files tomdv --gui. The installer copies it into/Applications, registers it, and sets it as the default handler usingduti(installingdutivia Homebrew first if needed). To set the default by hand, either pickmdvvia a.mdfile's Get Info → Open with → Change All…, or runduti -s de.thomas-gossler.apps.mdv net.daringfireball.markdown all. The wrapper runs as a background agent, so only the mdv GUI icon appears in the Dock. - Linux - automatic
.mdassociation is not performed; set up your desktop environment's file association manually if you want it.
On Windows the installer registers a per-user handler (no admin rights needed), so association works out of the box once you opt in.
The document navigator can search inside your documents, not just filter by filename:
- GUI - click the ⌕ toggle next to the navigator filter box. When enabled, each matching document is shown with its matching lines nested beneath it; click a match to open the document and jump straight to that line, highlighted like in-document search. Toggle it off again to return to plain filename/title filtering.
- TUI - press
Tab(orCtrl+B) to open the document navigator. It lists every markdown file in the folder even when you opened a single file. In the list, press/to filter by name, or type//to switch to content search. Matches appear indented under each document; press Enter on a match to open the document and jump to it. PressEsc(orCtrl+B) to hide the navigator again. You can also start content search straight from the content view: press/to search the current page, or type/again (//) to search across all documents.
Search is case-insensitive and treats your query as a smart fuzzy phrase: the words must appear in order and close together - but minor differences are tolerated, so "client approvals" also matches "Client-side Approvals". It also forgives small typos (edit-distance matching) and matches a query word inside a longer word ("approval" finds "approvals"). Content search even spans a single line break, so a multi-word term that is hard-wrapped across two source lines (e.g. at ~80 columns) is still found, while words separated by a blank line are treated as different paragraphs and not matched together.
The same smart matching powers the navigator's filename/title filter (when content search is off), so filtering documents by name behaves just like searching their content. Only documents with a filename or content match remain in the list. The search runs entirely in-memory with no external dependencies.
mdv can export a document to PDF from both the GUI and the command line. To keep behaviour predictable, both surfaces prefer the highest-fidelity engine that is available and silently fall back to a self-contained engine otherwise. A4 portrait is always used.
- GUI - click the PDF button in the toolbar. mdv asks where to save the file and opens the result in your OS default PDF viewer.
- CLI -
mdv --pdf <path> <file>renders and exits without opening a window, so it works over SSH, in CI, and inside containers.<path>may be a file (out.pdf, or any name -.pdfis appended if missing) or an existing directory / trailing-slash path (the PDF is named after the source document). The input may be a Markdown file or piped on stdin (cat doc.md | mdv --pdf out.pdf); a folder as input is not supported for--pdf. The output path is printed on success, along with the engine that was used.
If the output file already exists, mdv asks for confirmation before overwriting
it (when run interactively); pass --force to overwrite without prompting. With
non-interactive input (e.g. piped stdin) mdv refuses to overwrite unless
--force is given. When the offline goldmark-pdf engine has to drop content it
cannot render (HTML tags outside code blocks, or remote/SVG/WebP images), it
prints a warning: line to stderr describing what was skipped.
For safety, PDF export never loads anything from a remote location by
default - in every engine and on every surface. A document that references
remote (http/https) images or assets is rendered with those resources
blocked, so generating a PDF cannot trigger network requests or leak that a file
was opened. To opt in:
- CLI - pass
--remotealongside--pdfto allow remote images/assets to be downloaded and embedded. - GUI - turn on the remote-images toggle in the toolbar (the same one that controls remote images in the live view). PDF export then mirrors what you see on screen. The toggle is off after every restart.
Which engine renders the PDF depends on the surface and on whether a browser is installed, so the result can differ. This is by design - the fallbacks let PDF export work everywhere, including fully offline and headless - but it is worth knowing the trade-offs:
| Surface | Tier 1 (preferred) | Tier 2 (fallback) | Fallback used when… |
|---|---|---|---|
| GUI | Installed browser, printToPDF | html2pdf.js (in the webview) | no browser found / release bundle absent |
| CLI | Installed browser, printToPDF | goldmark-pdf (pure Go, offline) | no browser found / release bundle absent |
| Engine | Fidelity | Selectable text | Needs a browser | Notes |
|---|---|---|---|---|
| Browser printToPDF | Highest - matches the on-screen render (Mermaid, KaTeX math, syntax highlighting, fonts) | Yes | Yes (Chrome/Chromium/Edge) | Uses a browser already installed on the machine; mdv never downloads one. Set MDV_CHROME (or CHROME_BIN) to point at a specific binary. |
| html2pdf.js | High - rasterised snapshot of the rendered page | No (image) | No | GUI-only fallback; diagrams and math look right but the text is an image, and very long pages may break across pages imperfectly. |
| goldmark-pdf | Basic - clean text layout with inbuilt PDF fonts | Yes | No | CLI-only fallback; works fully offline with a 1 cm page margin. HTML tags outside code blocks are stripped (HTML inside code blocks is kept verbatim); Mermaid and KaTeX are not rendered as graphics; SVG and WebP are always skipped, and remote images are skipped unless --remote is given (even then this offline engine embeds them only on a best-effort basis - it reliably embeds only local PNG/JPEG/GIF). Dropped content is reported as a stderr warning. Limited Unicode coverage. |
Notes:
- The browser path is only compiled into official release builds (it embeds
a small print harness). Plain
go build/go runbuilds therefore use the Tier 2 fallback even when a browser is installed; build with-tags pdf_bundledafter staging the frontend to enable it locally. - Browser detection looks for Chrome, Chromium, Microsoft Edge and Brave in the
usual per-OS locations. The browser runs headless with
--no-sandboxso it also works as root inside containers. - By default no PDF engine reaches out to the network: the browser path loads
only local content from a temporary loopback server and blocks every remote
request, while the goldmark fallback embeds only local images. Pass
--remote(CLI) or enable the GUI remote-images toggle to allow remote downloads.
- GitHub Flavored Markdown (tables, task lists, strikethrough, autolinks)
- GitHub alerts (
> [!NOTE],[!TIP],[!IMPORTANT],[!WARNING],[!CAUTION]) - Extended inline syntax (opt-in): math via KaTeX (
$inline$,$$block$$,```mathblocks), subscript~x~, superscript^x^, highlight==x==, inserted++x++- off by default; see Extended syntax - Mermaid diagrams (theme-aware)
- Syntax highlighting with 6 themes (Glyph, GitHub, Monokai, Nord, Solarized Light/Dark)
- Inline images in the console and terminal UI
- Wikilinks
[[doc]],[[doc|alias]],[[doc#heading]]with a backlinks panel - Smart link resolution: case-insensitive, directory-index fallback, symlink-aware
- Table-of-contents sidebar with scroll-spy, heading anchors
- CSV/TSV fenced blocks rendered as tables
- YAML frontmatter metadata block, emoji shortcodes
- Azure DevOps constructs (
[[_TOC_]],:::video:::,#123work items, image sizing) - Sanitized inline HTML (DOMPurify)
- In-document search (Cmd/Ctrl+F), toggleable live reload, drag-and-drop
- Document content search in the navigator (smart fuzzy-phrase matching)
- Entry-point highlighting in the document navigator (e.g. README)
- Export to PDF from the GUI and CLI, headless-friendly; see PDF export
- Read-only "View raw Markdown" toggle in the GUI
- Recently opened files and folders list in the GUI
- Open the current document in your associated external app
- OS file-manager integration: "Open with mdv" context-menu entry and
.mdassociation - Zoom (Cmd/Ctrl + wheel / +/-), light/dark/system themes, configurable fonts
- History navigation, link target preview in the status bar
- "Open in new window"
- Automatic update checks
mdv works with zero configuration. To customize, create
~/.config/mdv/settings.jsonc (or run mdv --init-config). The file is JSONC
(JSON with comments and trailing commas) and is merged over the built-in
defaults. On Windows/macOS the location follows XDG_CONFIG_HOME if set.
A handful of inline extensions reuse characters that appear in ordinary prose,
so enabling them globally can silently misrender plain text - for example $5 to $10 becoming math, or ~note~ becoming a subscript. These are therefore
off by default and grouped behind a single "extended syntax" toggle:
- Math via KaTeX:
$inline$,$$block$$, and```mathfenced blocks - Subscript
~x~, superscript^x^ - Highlight
==x==, inserted++x++
All other constructs (tables, alerts, wikilinks, footnotes, emoji, CSV blocks, Azure DevOps syntax, etc.) use distinctive delimiters and stay on at all times.
Enable extended syntax in one of three ways:
- Set
"enableExtendedSyntax": trueinsettings.jsonc(default for new windows) - In the GUI, click the ∑ toolbar button to toggle it live
- In the terminal UI, press
xto toggle it
The runtime choice is remembered in ~/.config/mdv/state.jsonc and shared
between the GUI and TUI. Note that the terminal renderer cannot display these
constructs, so the TUI toggle only updates the shared preference for the GUI;
the terminal output is unchanged.
Requires Go 1.26+, Node.js 18+, and the Wails v3
CLI (go install github.com/wailsapp/wails/v3/cmd/wails3@latest).
scripts/build.sh # macOS/Linux -> build/mdv
pwsh scripts/build.ps1 # Windows -> build/mdv.exeThe script builds the frontend, compiles the GUI helper, compresses and embeds
it into the launcher, and produces a single self-contained executable. On macOS
the result is a universal (arm64 + amd64) binary, and the script additionally
produces build/mdv.app (the Finder wrapper described under
Open Markdown files from File Manager).
cmd/mdv pure-Go launcher (no webview linkage) - picks GUI/TUI/console
internal/core shared logic: config, links, slugs, backlinks, updates
internal/console glamour-based stdout rendering
internal/tui Bubble Tea terminal UI
internal/launcher environment detection + embedded GUI extraction/spawn
gui/ Wails v3 GUI helper (Go bridge + TypeScript frontend)
The launcher embeds the GUI helper (gzip-compressed) and extracts it to a per-version cache directory on first GUI launch, then runs it detached. Because the launcher itself links no native UI libraries, it starts cleanly in any environment and degrades gracefully to TUI or console.
The Go test suite covers both unit logic (config parsing, link/wikilink
resolution, slugging, backlinks, folder listing, version comparison) and
end-to-end CLI behavior (the built binary's --version, --console,
--init-config, and no-arg usage paths). It is the automated quality gate for
every pull request.
go test ./... # unit + end-to-end tests
go test -short ./... # skip the slower e2e build test
go test -race -coverprofile=coverage.out ./... # what CI runs
go tool cover -html=coverage.out # browse coverageIn VS Code, press Cmd/Ctrl+Shift+P →
Tasks: Run Test Task, or pick any of the test* / coverage report tasks
from the Command Palette.
Pull requests are warmly welcome - whether it's a one-character typo fix or a
whole new rendering feature. mdv is a small codebase on purpose, so it's an
approachable place to make your first open-source contribution. 🌱
The quick path:
- Fork the repo and create a branch:
git checkout -b feature/amazing-thing. - Hack away. Keep the launcher webview-free - that headless-safety guarantee
is the whole point of the project, so anything touching native UI belongs in
gui/, never incmd/mdvorinternal/launcher. - Test your change:
go test ./...must stay green, and please add a test for anything you fix or add. The CI quality gate runsgo vet,gofmt, the race detector, and the full suite - running them locally first saves a round trip. - Format:
gofmt -w .for Go andnpx tsc --noEmitingui/frontendfor the TypeScript side. - Open a PR with a clear description of the why, not just the what. Screenshots or a short clip for UI changes earn you bonus goodwill. ✨
Good first issues: new syntax-highlight themes, additional markdown
extensions, emoji shortcode coverage, TUI keybindings, and documentation polish
are all great starting points. Look for the
good first issue
label.
House rules: be kind, assume good intent, and remember there's a human on the other side of every review. By participating you agree to uphold a welcoming, harassment-free environment for everyone.
mdv is free, MIT-licensed, and built in spare evenings fueled by curiosity
(and a non-trivial amount of coffee ☕). If it saves you a few clicks every day,
consider giving a little back:
- ⭐ Star the repo - it's free, it takes two seconds, and it genuinely helps others discover the project.
- 💬 Spread the word - blog about it, tell a colleague, or drop it in your team's tooling channel.
- 🐛 Report bugs and ideas - high-signal issues are worth their weight in gold.
- 💖 Back it financially via
-
every tier, down to "buy the maintainer a coffee," keeps the lights on and the commits coming.
Sponsorships directly fund maintenance time, code-signing certificates, and the occasional release-day pizza. Thank you for keeping independent open source alive. 🙏
Released under the MIT License - do what you like, just keep the copyright notice.
Copyright © 2026 Thomas Gossler
{ // "system" | "light" | "dark" "theme": "system", "codeTheme": "github", "fontFamily": "", "fontSizePx": 16, "lineHeight": 1.6, "contentWidthPx": 860, "navLabelMode": "filename", // or "title" "liveReload": false, // initial state of the active-document auto-reload toggle "enableExtendedSyntax": false, // math, sub/sup, highlight, inserted (GUI only) "checkForUpdates": true, "images": "auto", // "auto" | "graphics" | "blocks" | "off" "imagesRemote": true, // fetch http(s) images in console/TUI (falls back to alt text) }