mdr — Markdown Reader
A lightweight, fast Markdown viewer with Mermaid diagram support and live reload. Built in Rust.
Why mdr?
Built for the LLM era. AI tools generate Markdown constantly — code documentation, technical specs, analysis reports — packed with diagrams, tables, and structured content. You need a fast way to read them.
Most developers end up previewing Markdown in VS Code, pasting into a browser, or squinting at raw text in the terminal. mdr renders Mermaid diagrams itself, and opens the file you give it without a project, a server or an extension.
- One command —
mdr file.md and you're reading, not editing
- Native Rust binary — no Electron, no Node.js, no npm
- Mermaid diagrams — flowcharts, sequence diagrams, pie charts rendered as SVG natively (no headless browser)
- Three backends — a native window (
gui), the system webview (web), or a terminal UI (tui) over SSH
- Live reload — edit your file or let your AI tool regenerate it, see changes instantly
- In-document search — Ctrl+F /
/ to find text across all backends
- Fully keyboard-driven — every backend quits, scrolls, searches and navigates from the keyboard
Backends
mdr offers multiple rendering backends, selectable at runtime:
| Backend | Stack | Strengths |
|---|
gui | Pure Rust GPU rendering (egui) | Native window, no JavaScript engine, cross-platform |
web | OS native WebView (WebKit/WebView2) | GitHub-quality HTML/CSS rendering, full CSS support |
| tui | Terminal UI (ratatui + crossterm) | Works over SSH, no GUI needed, keyboard-driven |
--backend and the backend key of the config file pick one. The default is
auto, which chooses among the backends this binary was built with: tui over
SSH or with no display, and otherwise the first of gui and web that is
compiled in.
Install
From crates.io
cargo install mdr
# or, to download the release binary instead of compiling
cargo binstall mdr
> Note: cargo install compiles the three backends; on Linux that needs the
> system dependencies below. cargo binstall downloads
> the release binary instead.
From source
git clone https://github.com/CleverCloud/mdr.git
cd mdr
cargo install --path .
Build with specific backends only
# gui only (smaller binary, no WebView dependency)
cargo install --path . --no-default-features --features egui-backend
# web only
cargo install --path . --no-default-features --features webview-backend
Homebrew (macOS/Linux)
brew install CleverCloud/misc/mdr
Snap (Linux)
sudo snap install --edge mdr-markdown-renderer
> Note: The snap command is mdr-markdown-renderer, not mdr. You can create an alias: sudo snap alias mdr-markdown-renderer mdr
Scoop (Windows)
scoop bucket add clevercloud https://github.com/CleverCloud/scoop-bucket
scoop install mdr
Chocolatey (Windows)
choco install mdr
WinGet (Windows)
winget install CleverCloud.mdr
Nix
nix run github:CleverCloud/mdr
Pre-built binaries
Download from the Releases page for macOS, Linux, and Windows.
Usage
# Open with the backend mdr picks for this session
mdr README.md
# Open with the web backend
mdr --backend web README.md
# Open in terminal (TUI)
mdr --backend tui README.md
# Never touch the network (remote images are left unresolved)
mdr --offline README.md
# Render in the light colour scheme
mdr -t light README.md
# Show help
mdr --help
In web, clicking an http(s) link opens it in your system browser, and a
link to another local .md file opens that file in mdr. gui and tui have
no such routing.
gui keybindings
| Key | Action |
|---|
q, Esc, Ctrl/Cmd+Q, Ctrl/Cmd+W | Quit |
Ctrl/Cmd+F | Search in the document |
Esc | Close the search (quits when no search is open) |
F10 | Show or hide the table of contents |
t | Switch between the light and dark theme |
j / ↓, k / ↑ | Scroll down / up |
Space / PgDn, PgUp | Page down / up |
g / Home, G / End | Go to top / bottom |
On macOS the shortcuts use ⌘, not ⌃.
web keybindings
Press ? in the web backend for this list.
| Key | Action |
|---|
Ctrl/Cmd+Q | Close the window |
Ctrl/Cmd+F | Search in the document |
n / N | Next / previous search match |
Esc | Close search, help or the expanded image |
j / ↓, k / ↑ | Scroll down / up |
Space / PgDn, PgUp | Page down / up |
g / Home, G / End | Go to top / bottom |
Ctrl/Cmd + + / - / 0 | Zoom in / out / reset |
Ctrl/Cmd+B | Show or hide the table of contents |
t | Switch between the light and dark theme |
Ctrl/Cmd+P | Print or export to PDF |
? | Show or hide the shortcut list |
t flips the colour scheme of the current window, whether it came from
prefers-color-scheme or from --theme. It is a bare key on purpose: Ctrl/Cmd+D
is a split-pane shortcut in most terminals. Mermaid diagrams are the exception:
one rendered to SVG carries its own colours from the start, and one drawn in the
page is not recoloured once it is on screen.
TUI keybindings
| Key | Action |
|---|
q / Esc / Ctrl+C | Quit |
j / ↓ | Scroll down |
k / ↑ | Scroll up |
Space / PgDn | Page down |
PgUp | Page up |
g / Home | Go to top |
G / End | Go to bottom |
Tab | Switch focus between TOC and content |
Enter | Navigate to selected TOC heading |
/ or Ctrl+F | Open search |
n | Next search match |
N | Previous search match |
t | Switch between the light and dark theme |
The terminal owns its own background, so t here switches the colours code
blocks are highlighted in. Each block paints the theme's own background, so both
themes stay legible whatever the terminal is set to.
Features
- GFM — tables, task lists, strikethrough, footnotes, autolinks.
gui draws
tables itself
- One parser for the structure — comrak produces the HTML
web renders,
the lines tui draws, and the headings every table of contents is built
from. gui draws through egui_commonmark, which reads the Markdown again
with its own parser, so its typography — and, for tables and raw HTML, its
rendering — is its own
- Raw HTML in a document —
web hands it to a real engine. gui has none,
so a short, explicit set of tags (headings, paragraphs, images) is rewritten
as the Markdown that means the same thing, and anything else keeps its text
and loses its tags. Only blocks at the top level of the document are
converted; one nested in a quote or a list is left as written. Attributes
with no Markdown equivalent, such as align="center", are dropped, ``
becomes a space, and a declared width is honoured for vector images. A
heading written in HTML becomes a real heading, so it appears in the gui
table of contents where web does not list it. tui shows HTML as the text
it is
- Syntax highlighting — code blocks with language detection (via syntect), in
the terminal too. The palette follows the terminal background when it says what
it is (
COLORFGBG), and falls back to a dark one; --theme dark|light or
theme "light" in the config file settles it when the terminal stays silent —
Terminal.app and Alacritty do. The same setting picks the palette in gui and
web.
- Mermaid diagrams — flowcharts, sequence diagrams, pie charts, and more (via mermaid-rs-renderer)
- Table of Contents — auto-generated sidebar from headings with click-to-navigate
- Live reload — file watching with 300ms debounce, updates on save
- Dark/Light theme — follows the OS by default;
--theme dark|light (or
theme in the config file) settles it. In gui and web it picks the whole
palette; in tui the terminal owns its own colours, so it selects the syntax
highlighting of code blocks and nothing more. t flips the scheme live in
every backend
- YAML front matter — recognised as metadata, so it is neither rendered nor listed in the TOC
- Unique heading anchors — repeated headings get
setup, setup-1, … as GitHub does
Images
In gui and web, images are inlined into the document before rendering, so
nothing is fetched while you read. tui loads an image when it draws it, and
has its own path for that: no cache, and none of the size ceiling described
below.
- Local images resolve relative to the Markdown file, and may live anywhere
inside the enclosing project — the nearest ancestor directory holding a
.git, .hg, .svn or .jj. That makes the usual docs/page.md →
 layout work. The search for that marker stops at
your home directory, so a document outside any project is restricted to its
own directory. Anything outside the root that comes out of this is refused.
- A document read from stdin has no directory of its own —
cat README.md | mdr writes it to a temp file — so its relative image paths resolve from
the directory mdr was run in. cd docs && cat page.md | mdr therefore
resolves them against docs, whatever directory page.md itself lives in.
Links to other local files are not redirected this way: in web they are
still resolved next to the temp file, so a piped document cannot follow them.
- Remote images (
http/https, typically README badges) are downloaded
once, cached for the lifetime of the process, and embedded as data: URIs.
Responses larger than 16 MB are ignored.
mdr --offline file.md disables every network access; remote images are then
left unresolved. The same can be set permanently with offline #true in the
config file.
Configuration
mdr writes a commented config file with its defaults the first time it runs, so
there is nothing to scaffold and no flag to know about. Where it lands follows
the platform:
| Order | Path |
|---|
| 1 | ~/.config/mdr/config.kdl, if it already exists |
| 2 | $XDG_CONFIG_HOME/mdr/config.kdl, when that variable holds an absolute path |
| 3 | %APPDATA%\mdr\config.kdl, on Windows |
| 4 | ~/.config/mdr/config.kdl |
The order is the same on every platform; only step 3 is Windows-only. HOME
gives the home directory, except on Windows where %USERPROFILE% comes first,
since Git Bash sets HOME to a POSIX path a native binary cannot resolve.
Step 1 is a deliberate departure from the XDG spec, which says the variable
wins: every mdr before 0.6 read ~/.config/mdr/config.kdl and nothing else, so
letting XDG_CONFIG_HOME take precedence would silently ignore the config of
everyone who has both. A relative XDG_CONFIG_HOME is ignored with a warning,
as the spec requires. -c, --config PATH points somewhere else; a path given
there must exist, since a typo is a mistake rather than a request to create a
file.
If no environment variable names a home directory, mdr says so and reads
./.config/mdr/config.kdl if it happens to exist — but does not create one
there, rather than leaving a .config/ behind in whatever directory it was
started from. A file that is already there is treated like any other config,
old backend name included.
The file is KDL v2. Four keys are recognised, each mirroring
the command line option of the same name:
mdr -s web writes the backend into the file for you, leaving comments and
every other setting alone; it refuses a backend the binary was not built with.
The backends were called egui and webview before 0.6. A config file holding
one of the old names is corrected in place the first time it is read — comments
and every other setting kept — and the run says so once. A file mdr cannot
write, because it is read-only or a symlink, is left alone with a warning and
still read with the old name understood. On the command line there is no such
mapping: --backend egui is not a backend any more, and the error lists the
ones that are.
backend auto // auto, gui, tui or web
verbose #true // same as -v
offline #true // same as --offline
theme "auto" // auto, dark or light
Mermaid Support
Mermaid code fences are rendered as SVG diagrams:
```mermaid
graph TD
A[Input] --> B[Process]
B --> C[Output]
```
Supported diagram types: flowchart, sequence, pie, class, state, ER, gantt.
> Note: Diamond/decision nodes ({text}) are not yet supported by the underlying renderer. Use square brackets as a workaround.
Architecture
src/
├── main.rs # CLI (clap), backend dispatch
├── core/
│ ├── markdown.rs # GFM parsing (comrak) + CSS
│ ├── mermaid.rs # Mermaid → SVG rendering
│ ├── toc.rs # Heading extraction for TOC
│ ├── slug.rs # Heading anchors, shared by the renderer and the TOC
│ ├── sanitize.rs # Strips scripts and event handlers from raw HTML
│ ├── paths.rs # Which directory tree images may be read from
│ ├── net.rs # Remote image fetching (respects --offline)
│ └── watcher.rs # File watching (notify, 300ms debounce)
└── backend/
├── egui.rs # `gui` backend (egui/eframe)
├── tui.rs # ratatui/crossterm TUI backend
└── webview.rs # `web` backend (wry/tao)
Building
Requires Rust 1.95 or later (the floor comes from kdl; the MSRV is checked in CI).
# All backends (default)
cargo build --release
# Run tests
cargo test
# Run clippy exactly as CI does
cargo clippy --all-features --all-targets -- -D warnings
Linux dependencies
sudo apt-get install libgtk-3-dev libwebkit2gtk-4.1-dev libxdo-dev libgl1-mesa-dev
Releases
Pre-built binaries are available on the Releases page for:
- macOS (Apple Silicon + Intel)
- Linux (x86_64 + aarch64)
- Windows (x86_64)
Each release publishes to crates.io, and updates the Homebrew tap, the Scoop
bucket, the Chocolatey package and the Snap Store (edge channel) for whichever
of those channels is enabled — each one is gated on its own variable and needs
its own secret, so a release still succeeds when a channel is not configured.
See PACKAGING.md for the setup.
Release notes are the matching section of CHANGELOG.md, so add
it before pushing the tag.
To create a release, push a version tag:
git tag v0.4.0
git push origin v0.4.0
License
MIT
Contributing
Issues and PRs welcome at github.com/CleverCloud/mdr.