leaf: a full-screen terminal Markdown reader with a width trap in its generated config
Terminal Markdown previewer — GUI-like experience.
At a glance
- What is it?
- leaf is a Rust Markdown previewer that takes over the terminal screen instead of printing a pipe. Its install channels disagree on the package name, its generated config pins content width at 80, and its file history ships switched off.
- Who is it for?
- leaf fits a developer who previews Markdown over SSH, inside a container or on Termux, where a browser is not an option and a pager pipe is too thin. Skip it if you need HTML output or a document tree.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 25 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A ratatui full-screen reader instead of a pager in the pipe
A terminal Markdown reader usually arrives as a filter: the file goes in, ANSI text comes out, and whatever pager wraps around it can scroll back but cannot draw a tree or keep a cursor over the document. leaf takes the other route and claims the whole screen, which is what the repository topics `markdown-viewer` and `terminal` describe.
The parsing layer and the drawing layer are separate crates in one manifest. `pulldown-cmark` turns bytes into Markdown events, `ratatui` and `crossterm` turn those events into cells on the screen, `syntect` handles fenced code, and `unicode-width` with `unicode-segmentation` keep CJK and emoji columns aligned. Fifteen crates are listed in total, each pinned to a minor line, so `ratatui` sits at 0.30, `crossterm` at 0.29 and `pulldown-cmark` at 0.12.
Watch mode is what turns the viewer into a live preview, which is the form almost every integration below uses:
# Watch mode: reloads automatically on save
leaf --watch TESTING.md
leaf -w TESTING.mdTwo build details deserve more attention than they first get. The manifest adds a second `crossterm` entry carrying the `use-dev-tty` feature only under `cfg(target_os = "macos")`, so the macOS build compiles terminal handling that the Linux and Windows builds never compile. The topics list also names `termux` next to `terminal`, matching an install script that covers Android and Termux, where no graphical viewer exists at all. The last push was on 2026-09-28 and the manifest version is 1.28.3.
Seven install paths, two package names, one binary named leaf
Seven install routes reach the same executable, and they do not agree on its name. The `leaf` binary is declared as a `[[bin]]` target pointing at `src/main.rs`, while the distributable packages are called `leaf-markdown-viewer` on every channel except npm, where the name is `@rivolink/leaf`.
For macOS, Linux, Android and Termux the README points at a shell installer:
curl -fsSL https://raw.githubusercontent.com/RivoLink/leaf/main/scripts/install.sh | shWindows gets a PowerShell one-liner instead:
irm https://raw.githubusercontent.com/RivoLink/leaf/main/scripts/install.ps1 | iexThe registry channels are three commands, and each one has a matching upgrade command further down:
npm install -g @rivolink/leaf
brew install leaf-markdown-viewer
cargo install leaf-markdown-viewerThe two Windows and Arch package managers use the same hyphenated name, with the Arch line written as the README asks, through an AUR helper such as yay:
scoop install leaf-markdown-viewer
yay -S leaf-markdown-viewerAfter any of them, one command confirms what landed:
leaf --versionTab completion is opt-in and ships with the tree, which lists its own `completions/` directory beside `npm/`, `scripts/` and `src/`:
leaf --auto-completeCompletion covers bash, zsh, fish, Nushell and PowerShell, and a shell restart is what makes it take effect.
The generated config.toml writes width 80 and calls the terminal width the default
`leaf --config` opens `config.toml` in your editor and creates the file with documented defaults when it is missing, so a first run hands over a populated file rather than an empty directory.
theme = "ocean" # arctic, forest, ocean, solarized-dark, or a custom theme file
editor = "nano" # any editor in PATH
watch = false # auto-reload when opening a file
width = 80 # maximum content width (min: 20, default: terminal width)
extras = ["txt", "rs"] # extra file types shown in the picker
file-picker-width = "75%" # width of the file picker (fuzzy + file browser)
main-line-numbers = false # show line numbers in the main document by default
code-line-numbers = true # show line numbers inside fenced code blocks
tab-title-length = -1 # terminal tab title truncation (min: 20, -1: no truncation)
file-history-length = 0 # recent file history length (0 disables, max: 50)
hyper-link-prefix = "#" # single character before link text ("" for none)The fourth line is the contradiction. The generated file assigns `width = 80`, and the comment sitting on that same line calls the terminal width the default. Once the file exists the key holds an explicit value, so anyone who never edits it gets 80 columns whatever the window size, which is the opposite of what the comment promises. The per-run override is `leaf --width 100 TESTING.md`, the documented floor is 20, and `leaf --config reset` or `leaf --config remove` are the two documented ways out.
Every key is optional and command line arguments always win over the file, which is the right order of precedence. Two keys have odd corners: `hyper-link-prefix` accepts a single character or an empty string, and `tab-title-length` takes either a real length or a negative one, which disables truncation.
History pickers ship disabled while their flags stay documented
The history commands are the clearest case of documentation running ahead of defaults. `leaf --history` and `leaf -H` open a file history picker, `leaf --last` and `leaf -l` jump straight to the most recent entry, yet the generated config carries `file-history-length = 0`, which the same file describes as disabling the history with a ceiling of 50. On a fresh install both flags open an empty list until that number moves. The two combine cleanly, since `leaf --last --watch` previews the most recent file and keeps reloading it.
Two different pickers sit behind those flags. Calling `leaf` with no path opens the fuzzy picker and accepts a pre-filled filter whose keyword is alphanumeric and capped at 15 characters, while `leaf --picker` switches to the classic directory browser, and either can be launched in watch mode to follow whatever file you land on. `file-picker-width` sets the width for both, and the generated value is 75%.
Non-Markdown files join the same picker through `extras`, which the generated file fills with two extensions and which the later example widens to six entries covering text, csv, Rust, java, json and yaml. Code files there get syntax highlighting and text files render as plain Markdown. A path given on the command line opens regardless, so `leaf main.rs` works without touching `extras` at all.
A fifth theme is hiding in the tree. `theme` accepts a path and relative paths resolve from the config file directory, while the comment names only arctic, forest, ocean and solarized-dark as built-ins, yet the repository root carries `gruvbox.toml`, which is not on that list.
Inline mode drops the TUI and becomes a stdout renderer for fzf
Everything above needs a real terminal and a whole screen. `--inline` is the escape hatch, and it changes what the program is: rather than drawing a TUI it writes rendered Markdown to stdout, which turns leaf into a renderer inside another program's pane.
# Render to terminal with colors
leaf --inline README.md
# Force plain text, no ANSI codes (no colors)
leaf --inline plain README.md
# Force ANSI colors even when piping
leaf --inline ansi README.mdWidth is a word in the mode rather than a separate flag, since `leaf --inline 60 README.md` and `leaf --inline ansi:60 README.md` both cap the output. The plain and ansi keywords exist because a redirected stream and a colour-capable terminal are opposite problems, and the mode lets you say which one you are in.
That is what makes the fzf pairing the serious alternative to leaf's own picker:
fzf --preview 'leaf --inline ansi {}'
fzf --preview 'leaf --inline ansi:$FZF_PREVIEW_COLUMNS {}'In that setup fzf owns navigation and leaf paints only the preview pane, with the second form reading the pane width from fzf's own environment variable. Stdin works in both modes, so `cat TESTING.md | leaf` previews a local file and `claude "explain Rust lifetimes" | leaf` feeds output from another command straight into the full-screen reader.
The editor string expands {$line} and {$path} before it runs
Since the viewer is a preview surface, handing the file back to an editor is part of the design. `Ctrl+E` opens the current file in whatever `editor` names, and that setting is a template rather than a bare command.
editor = 'nano +{$line}'
editor = 'nvim +{$line} +"normal! zz"'
editor = 'code -g {$path}:{$line}'`{$line}` expands to the line currently visible in leaf, so editing picks up where the reading stopped, and `{$path}` supplies the file path for editors that want both. Without `{$line}` the editor opens at the top of the file, which is the behaviour of the default `editor = "nano"`.
Vim users close the loop with one mapping in `~/.vimrc`:
" Preview the current Markdown file in a vertical split using leaf
nnoremap <Leader>md :vertical botright terminal leaf -w %<CR>The md mapping then opens a live preview in a vertical split and `Ctrl+w,h` returns focus to the buffer. The mapping deliberately confines leaf to a `botright` split instead of the full screen, and the manual step back to the buffer is part of the documented workflow rather than an oversight.
Self-update checks checksums.txt SHA256, and Windows can block it
`leaf --update` is the only channel that verifies what it downloads. The update flow fetches the matching published asset, checks it against the published `checksums.txt` SHA256, and installs only after that check passes. The dependency list matches that description: `reqwest` arrives with `blocking`, `json` and `rustls-tls` and no default features, and `sha2` and `semver` sit beside it.
leaf --updateEvery other channel updates itself, each with different wording:
npm update -g @rivolink/leaf
brew upgrade leaf-markdown-viewer
cargo install leaf-markdown-viewer --force
scoop update leaf-markdown-viewerThe cargo line is the ongoing cost for Rust users: `--force` is what replaces the installed binary, and no checksum step appears on that path. The documented Windows failure is concrete as well. If the running `.exe` cannot be replaced because the operating system holds it, the answer is not a retry but rerunning the PowerShell installer from the install section, so an upgrade can drop back to the install route.
Cargo.toml declares `license = "MIT"` for the `leaf-markdown-viewer` crate, the npm package `@rivolink/leaf` and the `leaf-markdown-viewer` formula all point back at the same repository, and no channel adds separate licence terms. One thing the README does not document is rollback: nothing there moves an installation back to an earlier version except pinning a package manager by hand.
Editorial conclusion
leaf fits a developer who previews Markdown over SSH, inside a container or on Termux, where a browser is not an option and a pager pipe is too thin. Skip it if you need HTML output or a document tree. Before adopting, open `leaf --config` and read the `width` key: the file leaf generates sets 80 columns while its own comment names the terminal width as the default, and both history pickers stay empty until `file-history-length` moves off 0. Upgrades differ per channel, and only `leaf --update` checks `checksums.txt` before it swaps the binary.
Frequently asked questions
How do I install leaf on macOS or Linux?
The README gives a shell installer for macOS, Linux, Android and Termux: `curl -fsSL https://raw.githubusercontent.com/RivoLink/leaf/main/scripts/install.sh | sh`. Registry channels are `npm install -g @rivolink/leaf`, `brew install leaf-markdown-viewer` and `cargo install leaf-markdown-viewer`. Confirm the result with `leaf --version`.
Can leaf render Markdown when the output is piped?
Yes. `cat TESTING.md | leaf` streams a file into the full-screen viewer, and `--inline` renders straight to stdout. `--inline plain` forces plain text and `--inline ansi` forces colours, which is what makes `fzf --preview 'leaf --inline ansi {}'` work.
Why does leaf --history show an empty list on a fresh install?
The generated `config.toml` sets `file-history-length = 0`, and its own comment marks 0 as disabling the history, with a maximum of 50. Until that number is raised, `leaf --history` and `leaf --last` have nothing recorded to show.
How wide is the preview in leaf by default?
The `config.toml` created by `leaf --config` assigns `width = 80` while the comment on that same line calls the terminal width the default, so the created file fixes the wrap at 80 columns. `leaf --width 100 TESTING.md` overrides it for a single run, and the documented floor is 20.
Does leaf verify the file it downloads during an update?
`leaf --update` downloads the matching published asset, verifies it against the published `checksums.txt` SHA256, and then installs it. On Windows a running `.exe` can block the replacement, and the fix given is to rerun the PowerShell installer.
Which shells get tab completion in leaf?
`leaf --auto-complete` enables Tab completion for all arguments across bash, zsh, fish, Nushell and PowerShell. Restarting the shell is what activates it.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rivolink-leaf)Was this useful?