Beautiful LaTeX math, right in your terminal. 🧮
termtex renders LaTeX expressions and whole Markdown documents into the
terminal window, typeset the way MathJax typesets them and drawn with the
kitty graphics protocol. Inline math ($ ... $) and display math
($$ ... $$) sit among your prose, in real Computer Modern.
It is a single Go binary with no dependencies of any kind — no Node, no librsvg, no fonts to install, no cgo. 5 MB, and it draws the pixels itself.
A Markdown file rendered in kitty. The math is drawn at the terminal's own pixel grid — no external renderer, nothing resampled.
go install github.com/junosuarez/termtex@latestOr from source:
git clone https://github.com/junosuarez/termtex.git
cd termtex && go build -o termtex .Requirements: Go 1.21+ to build, and that's the whole list — no cgo, no system libraries, no fonts to install. 🎉
To see the math rather than the fallback text you want a terminal that speaks
the kitty graphics protocol: kitty,
Ghostty, WezTerm, or
Konsole. Anywhere else, -f text still works.
- 📄 Mixed documents: plain text, inline equations (
$V(S_t)$) and centered display equations ($$\sum_{i=1}^n x_i$$), interleaved. - 🔤 Real Computer Modern: the actual TeX outlines, taken from MathJax's own font data.
- 🖼️ Drawn at your terminal's pixel grid: kitty, Ghostty, WezTerm and friends, rasterized in-process so nothing is resampled and nothing is blurry.
- 🪶 No dependencies at all: pure Go, no cgo, one file to copy.
- 💾 SVG and PNG export:
-o paper.svg,-o hero.png, publication quality. - 🔡 Unicode fallback: no graphics terminal, no problem — it notices and writes
x² + y². - 🔬 Typeset the way MathJax typesets it: the layout is a port of MathJax's own algorithms, and both the layout and the SVG it writes are checked glyph-for-glyph — position and outline — against MathJax over tens of thousands of generated expressions. Nothing at run time calls MathJax: termtex renders on its own, and MathJax is only what that rendering is held to. See DEVELOPMENT.md for the harness.
- ➗ The math you actually write:
- Fractions:
\frac{a}{b} - Square & N-th Roots:
\sqrt{x},\sqrt[n]{x} - Subscripts & Superscripts:
x_i,e^{-x^2},\sum_{i=1}^n - Big Operators:
\sum,\prod,\int,\iint,\iiint,\oint,\lim,\max,\min - Delimiters & Braces:
\left( ... \right),\left[ ... \right],\left\{ ... \right\},\left\langle ... \right\rangle - Environments:
\begin{pmatrix},\begin{bmatrix},\begin{cases} - Greek Letters & Accents:
\alpha,\beta,\Delta,\hat{x},\vec{v},\bar{z}
- Fractions:
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
\begin{cases} x & \text{if } x > 0 \\ -x & \text{otherwise} \end{cases}
\begin{pmatrix} a & b \\ c & d \end{pmatrix}
termtex reads a formula from an argument, a file, or standard input, and writes to your terminal — or to a file, if you name one.
termtex "\frac{1}{x^2+1}" # one expression
termtex notes.md # a whole markdown file
cat notes.md | termtex # …or from a pipe
termtex "Where \$V(S_t)\$ is the value." # prose with inline math| Flag | Default | What it does |
|---|---|---|
-f, --format |
auto |
kitty, svg, png, text, or auto |
-o, --output |
— | Write to a file; .svg gives SVG, anything else a PNG |
-s, --size |
32 |
Font size in pixels |
-c, --color |
#cdd6f4 |
Foreground color |
-bg, --background |
transparent |
Background color |
-p, --padding |
16 |
Padding around the formula |
-d, --display |
true |
Display style — big operators, tall fractions |
--inline |
row |
Inline math height: row fits the text line, natural is full size |
--demo |
— | A showcase of what it can draw |
-h, --help |
— | This list |
Anything that fails — a formula it cannot parse, a file it cannot write —
exits non-zero with the reason on stderr, so set -e does what you expect. ✅
Render every formula in a directory to PNGs:
for f in equations/*.tex; do
termtex -o "out/$(basename "$f" .tex).png" -s 64 "$(cat "$f")"
donePreview math in a git hook, a pager, or a build log:
git show HEAD:paper.md | termtexNo graphics terminal? It notices. -f auto (the default) checks, and
writes a Unicode approximation instead — so a script that pipes through
less, or runs in CI, still says something useful:
$ TERM=dumb termtex "x^2 + y^2"
x² + y²Match your color scheme:
termtex -c "#f38ba8" -bg "#1e1e2e" -s 48 "e^{i\pi} + 1 = 0"Generate an SVG for the web:
termtex -o hero.svg -s 96 -c "#8b5cf6" "\nabla \times \mathbf{B} = \mu_0 \mathbf{J}"go get github.com/junosuarez/termtexEverything below is the whole API — there is not much of it. 🧩
import (
"os"
"github.com/junosuarez/termtex/pkg/render"
"github.com/junosuarez/termtex/pkg/tex"
)
opts := tex.DefaultRenderOptions()
opts.DisplayMode = true
render.RenderToTerminal(os.Stdout, `\int_0^\infty e^{-x^2}dx`, opts)RenderInlineToTerminal does the same on the line you are already on, and
steps the cursor past the image so text carries on after it. SupportsGraphics
reports whether the terminal will show it at all.
import "github.com/junosuarez/termtex/pkg/doc"
doc.RenderDocument(os.Stdout, "Given $x^2$, we have:\n\n$$e^{i\\pi} + 1 = 0$$\n", opts)Text passes through untouched; $…$ and $$…$$ become math.
svg, err := tex.RenderTeXToSVG(`\frac{a}{b}`, opts)import "github.com/junosuarez/termtex/pkg/raster"
img, err := raster.RenderTeX(`\sqrt{x}`, opts, 2) // 2 device pixels per unit
// img is an *image.RGBA — encode it, composite it, do as you likeA Renderer keeps the glyphs it has rasterized and the buffers it drew into,
so a second formula costs a fraction of the first:
r := raster.NewRenderer()
for _, formula := range formulas {
node, err := tex.Parse(formula)
if err != nil {
continue
}
page := tex.BuildPage(node, opts) // lay out, and find the canvas it needs
img := r.Image(page, opts, 2) // draw it
encode(img) // …before the next call reuses the buffer
}Two things to know: the image a Renderer hands back is only good until the
next call on that renderer — copy it if it must outlive that — and a Renderer
is not safe to share between goroutines. Give each one its own; they are cheap.
type RenderOptions struct {
FgColor string // "#cdd6f4", or "currentColor"
BgColor string // "transparent" or a color
FontSize float64 // font size in pixels
Padding float64 // around the ink
DisplayMode bool // display style rather than inline
}Pure Go, no cgo, no dependencies — the binary is everything it needs.
| Binary | 5.4 MiB (4.2 MiB stripped), including 1.1 MiB of Computer Modern glyph outlines |
| Peak memory, one formula | 6.2 MiB resident |
| Peak memory, a whole document | 6.9 MiB resident |
| One formula to the terminal | 4.9 ms end to end |
| A Markdown document (3 formulas) | 5.5 ms end to end |
Most of that is not termtex. Breaking one invocation down:
| Process spawn — what any binary costs | 2.14 ms |
| Go runtime start | 1.25 ms |
| termtex: tables, flags | 0.99 ms |
| termtex: parse, lay out, draw, emit | 0.49 ms |
| 4.86 ms |
So the program's own share of a cold invocation is about 1.5 ms, and the
work of turning TeX into pixels is 0.49 ms of it. The lookup tables are
placed in the binary rather than built at start-up, so init costs 0.16 ms and
82 KB.
In process, where the start-up is paid once, over 3,000 generated expressions (median):
| Parse | 1.7 µs |
| Lay out | 4.1 µs |
| Parse and write SVG | 14.4 µs |
| Rasterize (64k pixels, warm glyph cache) | 99 µs |
Measured on an Apple M1 Max, macOS 26.5, go 1.26; wall-clock figures are the
median of 60 runs. go test ./pkg/raster/ -bench . measures the drawing loops
directly.
The ordinary way to get TeX into a terminal is node + MathJax to make an SVG,
rsvg-convert to make a PNG, and kitty +kitten icat to display it. Measured
the same way, on the same machine, for the same formula:
| node + MathJax + rsvg + icat | termtex | |
|---|---|---|
| One formula to the screen | 175 ms | 9.8 ms |
| Peak memory | 73 MB (node) + 16 MB (rsvg) | 6 MB |
| Installed size | ~148 MB (node, mathjax-full, librsvg) | 5.4 MB, one file |
| Processes per formula | 3 | 1 |
Nearly all of that gap is start-up rather than typesetting. Inside node:
require MathJax |
67.8 ms |
| build the TeX and SVG pipelines | 6.0 ms |
| first render | 9.8 ms |
| every render after that | 0.63 ms |
So MathJax lays a formula out in well under a millisecond; it is the loading that costs, and a command-line tool pays it every time. A long-running service that keeps node warm would not — and that is the fair comparison for termtex's in-process figures: 0.63 ms for MathJax to produce the SVG, against 14 µs for termtex to produce the same geometry and ~110 µs to produce pixels.
Ten formulas in one termtex process take 14.6 ms all told, because the start-up is paid once and the glyphs are already rasterized.
One place the naive pipeline wins: it sends a PNG to the terminal, about 29 KB base64 for the formula above, where termtex sends raw RGBA — 250 KB. That is deliberate (encoding a PNG costs more than drawing the formula did) and it does not matter locally, but it would over a slow ssh link.
None of this says termtex is better at typesetting than MathJax. MathJax is the reference these numbers are checked against, and it supports things termtex does not; where termtex does support something, the geometry is identical by construction. What the numbers say is that a terminal renderer does not need to start a JavaScript runtime three times a second.
This project's TeX vector glyph data and typesetting lineage are deeply indebted to MathJax and The MathJax Consortium.
- Vector glyph paths and TeX Computer Modern font path definitions are derived from mathjax-full.
- Huge gratitude to the MathJax team and contributors for their pioneering work in open-source mathematical web typesetting!
termtex is kept in exact agreement with MathJax by a comparison harness that
lives in pkg/parity, pkg/fuzz, cmd/ and scripts/. It compares both
stages of the pipeline: the box tree the layout builds, and the SVG the writer
produces from it. If you are changing how anything is placed or drawn, start
with DEVELOPMENT.md, and docs/adr for the
decisions behind how it draws.
Licensed under the Apache License, Version 2.0. See the LICENSE file for full details.




