A terminal markdown reader that actually looks good.
ink renders markdown in your terminal with syntax highlighting, inline images, mermaid diagrams, themes, tabs, search, and a table of contents. It's fast on large documents, safe with untrusted files, and works as both an interactive reader and a bat-style pager. One binary. No dependencies. Built in Rust.
curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | shThe installer picks the right binary for your OS, CPU and libc (static musl build on Alpine, Raspberry Pi and older-glibc systems), checks it against SHA256SUMS and installs to /usr/local/bin (using sudo only if it has to). Pin a version with INK_VERSION=v0.8.0 or change the target with INK_INSTALL_DIR="$HOME/.local/bin":
curl -fsSL https://raw.githubusercontent.com/borghei/ink/main/install.sh | INK_VERSION=v0.10.0 INK_INSTALL_DIR="$HOME/.local/bin" shirm https://raw.githubusercontent.com/borghei/ink/main/install.ps1 | iexInstalls ink.exe (x64 or ARM64) to %LOCALAPPDATA%\Programs\ink and adds it to your user PATH. Set $env:INK_VERSION = "v0.10.0" first to pin a version.
brew tap borghei/tap
brew trust borghei/tap # recent Homebrew requires trusting third-party taps
brew install inkThe community-maintained conda-forge package can be installed with:
conda install -c conda-forge ink-md
# or
pixi global install ink-mdcargo install ink-md # build from source
cargo binstall ink-md # or grab the prebuilt binary (needs cargo-binstall)mise use -g cargo:ink-md # via crates.io
mise use -g ubi:borghei/ink # or straight from GitHub releasesDownload from the releases page, then:
sudo apt install ./ink-md_*_amd64.deb # Debian/Ubuntu
sudo dnf install ./ink-md-*.x86_64.rpm # Fedora/openSUSEscoop bucket add borghei https://github.com/borghei/scoop-bucket
scoop install inkGrab the latest binary for your platform from the releases page:
| Platform | Asset |
|---|---|
| Linux x86_64 / arm64 (glibc 2.35+: Ubuntu 22.04, Debian 12 and newer) | ink-linux-amd64, ink-linux-arm64 |
| Linux x86_64 / arm64 / armv7, static (any distro: Alpine, Debian 11, Ubuntu 20.04, RHEL 8, NixOS, containers, Raspberry Pi) | ink-linux-amd64-musl, ink-linux-arm64-musl, ink-linux-armv7-musl |
| macOS Intel / Apple Silicon | ink-macos-amd64, ink-macos-arm64 |
| Windows x64 / ARM64 | ink-windows-amd64.exe, ink-windows-arm64.exe |
The glibc builds need glibc 2.35 or newer; older and musl-based systems use the static builds, and the install script picks for you. The Windows builds link the C runtime statically, so no Visual C++ redistributable is needed. Each binary also comes as an archive (ink-<version>-<os>-<arch>.tar.gz, .zip on Windows) bundling the man page and bash/zsh/fish completions, and the .deb/.rpm packages install those too.
SHA256SUMS is published alongside, and every asset carries a signed build provenance attestation you can check with the GitHub CLI:
gh attestation verify ink-linux-amd64 --repo borghei/inkThe static musl builds and Windows ARM64 binaries are new in v0.9.0; earlier releases only have the glibc, macOS and Windows x64 binaries.
git clone https://github.com/borghei/ink.git
cd ink
cargo build --release
# binary is at ./target/release/ink# Read a file
ink README.md
# Browse all markdown files in a directory
ink .
# Read from a URL
ink https://raw.githubusercontent.com/borghei/ink/main/README.md
# Pipe from stdin (or name it explicitly with -)
cat notes.md | ink
cat notes.md | ink -
# Plain output (no TUI, pipe-friendly)
ink --plain README.mdHeadings, bold, italic, strikethrough, links, blockquotes, lists, task lists, tables, footnotes, horizontal rules — all rendered with proper styling and colors.
Tables follow their column alignment (:--, :-:, --:), wide CJK and emoji cells included. HTML superscript x<sup>2</sup> and subscript H<sub>2</sub>O come out as x² and H₂O (x^(2) and H_(2)O with --ascii); when a character has no Unicode form, or the tag holds links or other markup (a <sub> caption), its content is shown as written. Carets and double bars in prose (2^10, a||b) are shown as written: ^x^ and ||x|| are not GitHub markdown. Single and double tildes, ~x~ and ~~x~~, strike through as on GitHub, and definition lists (Term then : definition) get a bold term with its definitions indented underneath.
Language-aware highlighting for every major language. Code blocks get clean borders with the language label shown at the top.
Images in your markdown render directly in the terminal. On terminals with a graphics protocol (Kitty, iTerm2, WezTerm, Ghostty, or Sixel) ink draws real pixels — auto-detected at startup; everywhere else it falls back to Unicode half-blocks, which work in any true-color terminal. Images scroll with the document and clip cleanly at the edges. If an image can't load, ink shows a placeholder instead of crashing.
Pick a renderer explicitly with --image-protocol <auto|kitty|iterm2|sixel|halfblocks> (default auto).
Supported formats: PNG, JPEG, GIF, WebP, BMP, TIFF, ICO, TGA, QOI, PNM, HDR, OpenEXR, farbfeld — and SVG (including gzipped .svgz), rasterized at load time so vector diagrams render like any other image.
Remote (http/https) images are not fetched by default — a document you didn't write shouldn't be able to phone home or probe your network. Pass --remote-images to enable them (private, loopback, and cloud-metadata addresses stay blocked even then).
Flowcharts (every node shape, link style and direction, with subgraphs drawn as frames), state diagrams, class diagrams and ER diagrams are laid out as real box-and-arrow drawings that fit your terminal width; Gantt charts get a scaled time axis, mindmaps a tree, and sequence diagrams and pie charts a compact text form. Other diagram types show their source in a labelled box. Box-drawing characters by default, plain ASCII with --ascii. No external tools needed.
[!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION] blocks render with distinct colors and icons. A custom title (> [!NOTE] Read this first) replaces the type name, and Obsidian callouts (> [!info]- Title, [!bug], [!example], …) map onto the same five colours, fold markers ignored.
YAML (---) and TOML (+++) frontmatter is hidden by default. With --frontmatter (or frontmatter = true) it shows as a small key/value box at the top of the document — lists comma-joined, nested values as written — instead of being read as markdown. A leading JSON object ({ on the first line) is never hidden: with --frontmatter it gets the same box when markdown follows it, and otherwise it is shown as document text.
[[page]] and [[page|display text]] syntax works out of the box. Great for browsing Obsidian vaults and personal wikis.
Run ink with no arguments or point it at a directory. You'll get an interactive file picker that lists every .md file, with filtering and keyboard navigation.
Open multiple files at once:
ink README.md CHANGELOG.md docs/guide.mdSwitch between them with Tab and Shift+Tab.
Press / to search within a document. Matches highlight inline; press Enter to lock in the search, then n/N to cycle forward and backward through results. Esc clears the highlights.
ink captures the mouse for wheel-scroll, which used to mean giving up your terminal's own text
selection. Now it does the job itself, and does it better — it knows where a code block starts and
where ink's own decoration ends. If you'd rather keep your terminal's own selection and link
clicking, run ink --no-mouse (one session) or set mouse_capture = false (always); both apply to
the reader and the file browser.
vstarts a character-wise selection,Va line-wise one. Move withh j k l, arrows,w/b(word),0/$(line ends),g/G(document ends),Ctrl+d/Ctrl+u(half page).ycopies and exits;Esccancels.- Drag with the mouse to select, release to copy. Double-click takes the word, triple-click the line. A single click on a link follows it instead (see below).
clabels every code block on screen — press its letter to copy the block's raw source: no borders, no line numbers, no syntax-highlighting escapes.Ycopies the markdown source of the section you are reading (heading included, down to the next heading of the same level). Inside link-hint mode (f),Yswitches the labels from "open this link" to "copy this URL".
Copied text reaches the clipboard two ways at once: an OSC 52 escape sequence, which works over
SSH and inside tmux, and a native helper (pbcopy, wl-copy, xclip, xsel, clip.exe) when one
is installed locally. No extra dependencies, nothing to configure. Set clipboard in your config to
narrow it down or turn it off.
v and V copy lines the way they are drawn, ink's heading bars and code-box borders included —
they select what you can see. Reach for c and Y when you want the source instead.
Press f to label every link on screen with a letter; the popup lists each link's text next to its URL, so you can tell which is which. Press that letter to open web and mail links in your browser, or to follow a relative .md link or #heading anchor right inside ink ([ goes back).
Clicking works too: a plain click on a link (press and release without moving) does exactly what its letter would. Dragging across a link still selects it. With --no-mouse ink leaves clicks to your terminal, which opens links its own way (usually Cmd- or Ctrl-click).
Press ? any time for a popup listing every active keybinding — including your own overrides.
Press t to toggle a sidebar showing every heading in the document. Tracks your position as you scroll.
Press o to move into it (it opens if it was closed). j/k or the arrows move, g/G go to the first and last heading, Ctrl+d/Ctrl+u page, and Enter jumps there — [ brings you back. h/l fold and unfold a section's subheadings in the sidebar, and / filters the list as you type (every word you type must appear, case doesn't matter). Esc clears the filter; Esc again, or o, goes back to the document without moving. With the mouse, click a heading to jump to it; the wheel over the sidebar scrolls the sidebar, not the page.
Dark, Light, Dracula, Catppuccin, Nord, Tokyo Night, Gruvbox, and Solarized. Press T to open the theme picker and preview each one live.
With no theme set, ink asks the terminal for its background colour (an OSC 11 query, answered by nearly every modern terminal and passed through by tmux) and picks Light or Dark to match, falling back to COLORFGBG and then Dark. An explicit --theme or theme = in the config always wins, and never triggers the query.
Themes adapt to what the terminal can show: 24-bit colour where it is advertised, the 256-colour palette on *-256color terminals, and the 16 ANSI colours on the Linux console and other 16-colour terminals (TERM=linux, xterm, vt100, screen, …) — there ink uses your terminal's own palette, so your colour scheme applies. With --color=never or NO_COLOR the reader draws without colour at all, using bold, underline and reverse video for headings, links, search hits and selections.
On the Linux console, a non-UTF-8 locale, a legacy console font or a screen reader, box-drawing and symbol characters come out as junk. ink --ascii (or ascii = true under [behavior]) draws every border, bullet, task box, quote bar, rule and status-bar marker in plain 7-bit ASCII instead — +--+, |, *, [x], # — and turns off smart quotes and emoji shortcodes, so an ASCII document prints as pure ASCII. It switches on by itself for TERM=linux and non-UTF-8 locales; ascii = false in the config keeps Unicode regardless.
Inline $E=mc^2$, block $$...$$ and ```math render as Unicode text: $x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}$ reads x = (-b ± √(b² - 4ac))/2a. Greek letters, operators, \mathbb{R} → ℝ, super- and subscripts, fractions, roots, and matrix, cases and aligned environments (drawn on several lines with brackets) are covered; anything else is shown as written. A lone $ in prose (costs $5 and $10) stays a dollar sign. With --ascii the LaTeX source is shown instead. :emoji: shortcodes resolve to their glyph (:rocket: → 🚀).
Split any markdown file into slides on --- separators and navigate with ←/→/Space:
ink --slides deck.mdPoint ink --plain at a long document on an interactive terminal and it pages the output through $PAGER (default less -R) — a drop-in markdown replacement for cat/less. Piped or redirected output prints straight through, so it stays friendly for scripts, fzf previews, and git. Use --no-pager to always print directly. When stdout is not a terminal, ink file.md behaves like ink --plain file.md, as bat and glow do.
--line-range works like bat's: ink --line-range 40:80 README.md renders only those lines of the markdown source (1-based, inclusive; also 40:, :80, a single line, or several --line-ranges). A range that cuts through a fenced code block keeps the block intact and highlighted, inside a blockquote too. It always prints plain output.
Color follows --color <auto|always|never> (default auto). In auto mode, --plain and ink diff emit color and OSC 8 hyperlinks only when stdout is a terminal, so redirects, pipes, and git textconv get clean text. Precedence: an explicit --color wins; then NO_COLOR turns color off; then CLICOLOR_FORCE=1 or FORCE_COLOR turns it on; then TERM=dumb turns it off; then the terminal check. Tools that display ANSI from a pipe need --color=always:
fzf --preview 'ink --plain --color=always {}'
ink --plain --color=always notes.md | less -R
# Render markdown in git diffs (clean text, no escapes)
git config --global diff.markdown.textconv "ink --plain"
echo '*.md diff=markdown' >> ~/.gitattributes24-bit colors are downgraded to the 256-color palette on terminals that don't advertise truecolor (COLORTERM, Windows Terminal, VS Code, Ghostty, WezTerm and iTerm2 are detected).
Auto-reload when the file changes on disk. Edit in another terminal, see the rendered view update within ~100ms. Scroll position is preserved.
ink --watch draft.mdWorks with editors that save by replacing the inode (vim, IntelliJ) — ink watches the parent directory, not the file handle.
# Heading structure
ink outline README.md
# Word count, reading time, element counts
ink stats README.md
# Diff two markdown files
ink diff old.md new.md| Key | Action |
|---|---|
j / k / ↑ / ↓ |
Scroll up/down |
Space / Page Down |
Page down |
Page Up |
Page up |
G / End |
Jump to end |
Home |
Jump to start |
Ctrl+f / Ctrl+b |
Page down / up |
Ctrl+d / Ctrl+u |
Half-page down / up |
n / N |
Next / previous heading (cycles search matches after a search) |
/ |
Search |
f |
Link-hint mode (open links by letter, Y to copy one instead) |
v / V |
Select text — character-wise / line-wise (y copies, Esc cancels) |
c |
Copy a code block by letter |
Y |
Copy the current section as markdown |
e |
Edit the file in $VISUAL / $EDITOR (opens at the heading you are reading, reloads when you quit the editor) |
t |
Toggle table of contents |
o |
Focus the table of contents (j/k move, Enter jumps, h/l fold, / filters, Esc returns) |
T |
Theme picker (choice is saved to config) |
? |
Help overlay |
Enter |
Follow first visible link |
[ / ] |
Navigation back / forward |
Tab / Shift+Tab |
Next / previous tab |
Shift+B |
Back to file browser (when launched via browser) |
q / Esc / Ctrl+C |
Quit ink (first press clears an active search) |
Pick a preset or override individual bindings in your config file (see Configuration):
[keybindings]
preset = "emacs" # default | vim | emacs
[keybindings.bindings]
# Per-action overrides, applied on top of the preset.
toggle_toc = ["ctrl-t"]
toc_focus = ["ctrl-o"]
edit = ["alt-e"]The emacs preset binds Ctrl+N/Ctrl+P (line nav), Ctrl+V/Alt+V (page nav), Ctrl+A/Ctrl+E (home/end), Ctrl+S (search), Ctrl+F/Ctrl+B (next/prev heading), and Ctrl+X Ctrl+C (chord exit).
Two-key chord bindings work — write them with a space: "ctrl-x ctrl-c".
Run ink keybindings to print the active map. Run ink config init to drop a commented starter config in the right place for your platform.
ink reads $XDG_CONFIG_HOME/ink/config.toml when XDG_CONFIG_HOME is set, otherwise ~/.config/ink/config.toml on Linux, ~/Library/Application Support/ink/config.toml on macOS, and %APPDATA%\ink\config.toml on Windows. If you already have a config in the platform folder and nothing under $XDG_CONFIG_HOME/ink yet, ink keeps using the existing one until you move it. Run ink config path to print the resolved location.
Create the file:
# Default theme (dark, light, dracula, catppuccin, nord, tokyo-night, gruvbox, solarized)
theme = "catppuccin"
# Max rendering width in columns
width = 90
# Line spacing: compact, normal, relaxed
spacing = "normal"
# Show table of contents on startup
toc = false
# Show YAML/TOML/JSON frontmatter as a metadata box at the top
frontmatter = false
# Behavior
[behavior]
# Set true to restore the old "return to file browser after closing a doc" behavior
browser_loop = false
# Set false to let your terminal handle the mouse (click-to-open links, text
# selection) instead of ink capturing it for wheel-scroll. Default: true
mouse_capture = true
# How copied text reaches the clipboard:
# auto - OSC 52 escape (crosses SSH and tmux) plus a native helper, if present
# osc52 - escape sequence only
# native - pbcopy / wl-copy / xclip / xsel / clip.exe / termux-clipboard-set only
# off - copying is disabled
clipboard = "auto"
# Plain-ASCII borders, bullets and markers (Linux console, legacy fonts,
# screen readers). Unset: automatic on TERM=linux and non-UTF-8 locales.
# ascii = falseDrop a .toml file in the themes/ folder next to your config file (e.g. $XDG_CONFIG_HOME/ink/themes/ or ~/.config/ink/themes/) and use it by name:
ink --theme mytheme README.mdEvery color is customizable — headings, code, links, blockquotes, admonitions, status bar, and more. Check any built-in theme in src/theme/builtin.rs for the full list of color keys.
ink shell-setup bash # or zsh, fishPrints config snippets you can add to your shell profile — aliases, fzf preview, git pager setup.
ink completions zsh > ~/.zfunc/_ink # bash | zsh | fish | powershell | elvish
ink man > /usr/local/share/man/man1/ink.1 # man page (troff)ink [OPTIONS] [FILE|URL]...
Options:
-t, --theme <THEME> Color theme [default: auto]
-w, --width <WIDTH> Max width (columns, or: narrow, wide, full)
-s, --slides Presentation mode
-p, --plain Plain output (no TUI); automatic when stdout is not a terminal
--color <WHEN> auto | always | never [default: auto]
--watch Watch file for changes
--toc Show table of contents on startup
--no-images Disable image rendering
--remote-images Allow fetching remote (http/https) images
--image-protocol <P> auto | kitty | iterm2 | sixel | halfblocks
--list-themes List available themes and exit
--no-pager Never page --plain output, even on a TTY
--frontmatter Show YAML/TOML/JSON frontmatter as a metadata box
--line-range <START:END> Render only these source lines (N, START:, :END;
repeatable; implies --plain)
--spacing <MODE> Line spacing: compact, normal, relaxed
--no-mouse Don't capture the mouse (overrides mouse_capture in config)
--ascii Draw borders, bullets and markers in plain ASCII
Subcommands:
outline Show document heading structure
stats Show document statistics
diff Diff two markdown files
shell-setup Print shell integration snippets
completions Generate shell completions
man Generate a man page
config Config helpers (init, path)
keybindings Print the active keybinding map
doctor Print a diagnostic report to attach to an issue
| Feature | ink | glow | mdcat | frogmouth |
|---|---|---|---|---|
| Interactive TUI | Yes | Yes | No | Yes |
| Multi-tab | Yes | No | No | No |
| In-document search | Yes | No | No | No |
| Table of contents | Yes | No | No | Yes |
| Inline images (graphics protocols) | Yes | No | Yes | No |
| Inline images (half-block fallback) | Yes | No | No | No |
| Mermaid diagrams | Yes | No | No | No |
| Admonitions | Yes | No | No | No |
| Wikilinks | Yes | No | No | No |
| File browser | Yes | Yes | No | Yes |
| Watch mode | Yes | No | No | No |
| Presentation mode | Yes | No | No | No |
| Pager mode | Yes | Yes | No | No |
| Shell completions + man page | Yes | Yes | Yes | No |
| Math + emoji | Yes | No | No | No |
| Themes | 8 | 2 | 0 | 0 |
| Single binary | Yes | Yes | Yes | No |
Terminal graphics support varies wildly. If an image is blank, garbled, or missing:
- Try the universal renderer:
ink --image-protocol halfblocks file.md. If the image appears as coarse colored blocks, decoding is fine — the problem is your terminal's pixel graphics protocol. - Try a specific protocol:
--image-protocol iterm2(iTerm2, WezTerm, VS Code, Warp and friends) or--image-protocol sixel. Some terminals advertise protocols they only partially implement — auto-detection already works around the known cases (e.g. iTerm2 claiming kitty support), but new terminal versions ship new quirks. - Collect a diagnostic report: run
ink doctorin the affected terminal — it prints your platform and terminal identity, the colour depth and theme ink picked (and which signal decided each), the clipboard helper and OSC 52 wrapping it would use, mouse capture, config warnings, what the graphics negotiation chose, and decoder self-tests. It leaves out anything that identifies you (no hostnames, user names, IPs or session IDs; your home directory shows as~), so it is safe to paste into an issue.ink doctor --save ink-doctor.txtwrites it to a file. The same report helps with colour, theme and clipboard problems. - Send it to us: open an image rendering issue with the report attached. That output usually pinpoints the problem immediately.
ink treats every document as untrusted input: control bytes and escape
sequences are stripped before anything reaches the terminal, link schemes are
restricted to http/https/mailto, and remote image fetching is off by
default (--remote-images opts in). Local images referenced by a document
— absolute paths included — are read and rendered, but only ever as pixels:
image bytes must decode as an image and are never shown as text, so a hostile
document cannot use an image tag to put file contents on screen.
Note for Windows authors: markdown treats \ as an escape character, so
a path like C:\pics\.cache\x.png loses its separators in any CommonMark
renderer. Write image paths with forward slashes (C:/pics/.cache/x.png) —
they work everywhere, including on Windows.
Contributions are welcome. Check out CONTRIBUTING.md for guidelines.
Free to use, modify, and distribute. Cannot be sold — not the original, not forks, not derivatives. See LICENSE for the full text.
Made by borghei — who got tired of reading raw markdown like a caveman.
Thanks to everyone who has sent a pull request:
- @mariusrueve — Conda and Pixi install instructions (#9), and the conda-forge package behind them
