Fast Markdown reading for the terminal.
A pure-Rust, paginated TUI reader with inline ASCII Mermaid. No browser, no Chrome, no subprocess.
Created by @guiwohl
veol is a single-binary terminal Markdown reader for people who want less to render their .md files instead of dumping them. It opens a file, paginates it, syntax-highlights the fences, draws the tables, and renders every Mermaid diagram inline as ASCII — all in-process, all synchronous, all instant. The companion to reedo: reedo edits, veol reads.
- One binary. No
mmdc, no Node, no headless Chrome, no image protocol negotiation. - Mermaid renders as text. 18 diagram types, drawn in pure Rust, instant, never blocks paint.
- Paginated reader workflow — keyboard-first,
j/k,]/[,/,t. Mouse scroll too. - Themed: 9 bundled themes ported from reedo + drop-in TOML.
- Always-on file watching (500ms poll) — edit elsewhere, veol re-renders.
- No async runtime. Pure
std::thread+mpsc::channel. ~50 fewer deps.
veol --plain examples/kitchen-sink.md — actual output, no Photoshop.
A simple flowchart:
┌─────────────────┐ ┌───────┐ ┌─────────────────┐ ┌─────────────┐ ┌───────┐
│ │ │ │ │ │ │ │ │ │
│ Markdown Source ├─────► Parse ├─────► Layout ├─────► Viewport ├─────► Draw │
│ │ │ │ │ │ │ │ │ │
└─────────────────┘ └───┬───┘ └─────────────────┘ └──────▲──────┘ └───────┘
│ │
│ ┌─────────────────┐ ┌─────────────┐ ┌───────┐
└─────────► Extract Mermaid ├─────► mmdc Worker ├──┤ Cache │
└─────────────────┘ └─────────────┘ └───────┘
A sequence diagram:
┌───────┐ ┌─────┐
│ Alice │ │ Bob │
└───┬───┘ └──┬──┘
│ Hello │
├───────────►│
│ Hi back │
│◄┈┈┈┈┈┈┈┈┈┈┈┤
│ │
A pie chart:
Renderer share
Text █████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░ 60.0%
Code ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 25.0%
Diagrams █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15.0%
A mindmap:
● Veol
├── · Markdown
├── · Mermaid
└── · Themes
Requires a recent stable Rust toolchain (edition 2021) and a UTF-8 terminal.
git clone https://github.com/guiwohl/veol.git
cd veol
cargo install --path .Or run from source:
cargo run --release -- README.md
cat notes.md | cargo run --release -- -veol README.md # open the TUI reader
veol --plain README.md # dump rendered output to stdout (pipeable)
veol --theme dracula notes.md # pick a theme
veol --no-mouse docs/spec.md # disable mouse capture (text-select friendly)
veol --no-watch CHANGELOG.md # skip the 500ms mtime poll
veol --toc README.md # open with the TOC modal up
cat - | veol - # read from stdin (watch auto-disabled)| Flag | Purpose |
|---|---|
FILE |
path to a .md file, or - for stdin |
--pager / --no-pager |
force TUI mode / force stdout mode (default: auto by TTY) |
--plain |
render to stdout, never enter the TUI |
--theme NAME |
pick a bundled theme or a custom one from ~/.config/veol/themes/ |
--theme-list |
list available themes and exit |
--width COLS |
cap the rendering width (min 20 cols) |
--no-mermaid |
render mermaid fences as plain source code blocks |
--no-watch |
disable the 500ms file-mtime poll |
--no-mouse |
disable mouse capture (lets the terminal own click + drag selection) |
--toc |
open with the Table of Contents modal already up |
--line-numbers |
show line numbers in the viewport |
--config PATH |
use a non-default config file |
--debug-render |
dump internal layout markers for renderer debugging |
-V, --version |
print version |
-h, --help |
print help |
Reader-only. No modes. No leader keys. No vim-isms beyond h j k l–style scroll.
| Key | Action |
|---|---|
q |
Quit |
j, ↓ |
Scroll down one line |
k, ↑ |
Scroll up one line |
Space |
Page down |
b |
Page up |
d |
Half-page down |
u |
Half-page up |
PgDn |
Next paragraph |
PgUp |
Previous paragraph |
g |
Jump to top |
G |
Jump to bottom |
] |
Next heading |
[ |
Previous heading |
/ |
Start search |
n |
Next match |
N |
Previous match |
t |
Toggle Table of Contents modal |
m |
Toggle Mermaid render ↔ source view (global) |
f |
Toggle frontmatter card |
r |
Reload file from disk |
o |
Open link under cursor in $BROWSER / xdg-open |
? |
Help overlay |
Ctrl+E |
Toggle modal file browser (.md-only) |
Ctrl+T |
Toggle theme switcher |
Esc |
Close popup / cancel search |
Mouse wheel scrolls the viewport (and tree / TOC inside popups). Shift+drag selects text using your terminal's native selection — pass --no-mouse if your terminal needs full ownership of pointer events.
veol renders Mermaid diagrams inline as Unicode box-drawing text. Dispatcher in src/mermaid/ascii/mod.rs:
pub fn render_mermaid(source: &str, max_width: u16) -> Vec<String>Pipeline per diagram: parse → AST → layout → Canvas → Vec<String>. No panics. On any parse failure or unknown diagram kind, the source is preserved as a fenced fallback block prefixed with // mermaid: <reason>.
| Mermaid keyword | Status | Notes |
|---|---|---|
flowchart / graph |
full | TD / TB / LR / BT / RL, subgraphs, A* edge routing |
sequenceDiagram |
full | participants, sync / async / dotted arrows, alt / opt / loop / par |
classDiagram |
full | members, visibility, relationships |
stateDiagram / stateDiagram-v2 |
full | [*] start/end, composite states, transitions |
erDiagram |
full | cardinalities, attributes |
pie |
full | slices + legend |
gantt |
partial | tasks and sections; durations approximated |
journey |
full | actors and scores |
timeline |
full | events along a single axis |
mindmap |
full | hierarchical tree |
gitGraph |
full | commits, branches, merges |
quadrantChart |
full | 4-quadrant scatter |
requirementDiagram |
full | requirements + verifications |
sankey-beta |
full | flows between named nodes |
xychart-beta |
full | bar / line chart in text |
block-beta |
full | grid of labeled blocks |
architecture-beta |
full | groups, services, edges |
packet-beta |
full | byte-range packet diagram |
| anything else | fallback | rendered as a fenced mermaid code block |
m toggles all diagrams between rendered and source view for the current document. --no-mermaid skips the dispatcher entirely. Full notes: docs/mermaid.md.
Nine bundled themes, ported from reedo. Pick one with --theme, switch live with Ctrl+T, persist via the switcher's Enter key.
Default— inherits terminal palettereedo-darkreedo-lightcatppuccindraculagruvboxnordrose-pinesolarized-dark
Drop custom TOML themes into ~/.config/veol/themes/*.toml and they appear in --theme-list and the switcher. Markdown-specific color fields cover heading_1..heading_6, link, quote_marker, quote_text, code_bg, code_border, table_border, table_header_fg, hr, task_done, task_pending, mermaid_caption, search_match.
Config lives at ~/.config/veol/config.toml (XDG via the directories crate). Minimal example:
theme = "dracula"
wrap = true
show_statusline = true
frontmatter = true
[cache]
max_size_mb = 128CLI flags always win over config; the theme switcher writes back to this file.
Pure-Rust crate, single binary, no runtime dependencies beyond a UTF-8 terminal.
src/
├── main.rs # entry, raw mode, alternate screen, popup dispatch
├── app.rs # central App state + Popup enum + Mode dispatch
├── cli.rs # clap derive — every flag from --pager to --no-mouse
├── config.rs # ~/.config/veol/config.toml load/save
├── watcher.rs # 500ms mtime polling via filetime
├── markdown/ # pulldown-cmark events → block model → wrapped lines
├── render/ # syntect code highlight, table shrink, theme, inline spans
├── tui/ # viewport, draw, keymap, search, toc, browser, theme switcher, help
└── mermaid/
├── extract.rs # pulldown events → mermaid sources + cache keys
├── display.rs # per-document render cache
└── ascii/ # pure-Rust ASCII renderer — 18 diagram types
├── mod.rs # dispatcher + fallback
├── canvas.rs / charset.rs / coord.rs / astar.rs / label.rs # shared primitives
└── flowchart/ sequence/ class/ state/ er/ pie/ gantt/ journey/
timeline/ mindmap/ gitgraph/ quadrant/ requirement/ sankey/
xychart/ block/ architecture/ packet/
Threading: std::thread + mpsc::channel for the file watcher. Mermaid rendering is synchronous and runs on the render path because it's measured in microseconds. No tokio, no async ecosystem, no future-soup.
Every other terminal Markdown reader either dumps the file unrendered, shells out to mmdc (which pulls Chrome through puppeteer), or assumes your terminal speaks Kitty / Sixel / iTerm2 image protocols. veol's bet is the opposite: Mermaid is text, so render it as text — in-process, deterministic, and fast enough that you don't notice it ran.
The result is a reader that boots in <30ms, paints instantly, has no network surface, no subprocess footprint, and works the same in tmux, mosh, SSH, and CI logs as it does on your desktop. It also reads stdin (claude -p "..." | veol -), which makes it a half-decent terminal alternative to opening a browser tab every time an agent emits Markdown at you.
| Topic | Link |
|---|---|
| Mermaid renderer | docs/mermaid.md |
| Full spec | docs/tasks/guiwohl-veol-spec-2026-05-26.md |
| ASCII pivot spec | docs/tasks/guiwohl-veol-ascii-mermaid-2026-05-27.md |
| AI instructions | .claude/CLAUDE.md |
- reedo — sibling project. veol borrows reedo's TOML theme schema, modal file browser UX, and the nine bundled themes.
- mermaid-ascii — the Go reference renderer that proved Mermaid-as-text was a tractable problem and informed the canvas + A*-edge approach.
- pulldown-cmark, syntect, ratatui, crossterm — the load-bearing crates underneath everything.
MIT.