termtex
v0.9.8
Published
LaTeX math in your terminal: wraps any TUI with live typeset overlays on kitty-graphics terminals, Unicode everywhere else
Maintainers
Readme
Why this exists
Ask a coding agent — Claude Code, Codex, anything else — a question with real mathematics in the answer, and you get this:
The gradient vanishes at $A^{T}A\hat{x} = A^{T}b$, so
$\hat{x} = (A^{T}A)^{-1}A^{T}b$ whenever $A^{T}A$ is invertible.The model is writing LaTeX because LaTeX is how mathematics is written. Your terminal doesn't know that, so you read source code instead of equations. Editors and chat UIs solved this years ago; terminals never did.
termtex sits between the program and your terminal and fixes it in flight. It is a filter on a byte stream — it knows nothing about the program it wraps, so it works with any CLI, and no agent's update can break it.
termtex # just this — your shell, best renderer available
termtex claude # instead of `claude`
termtex codex # instead of `codex`Bare termtex asks nothing of you. It runs your shell (or whatever command
you configure) and works out where to put it:
| Situation | What happens | |---|---| | already in WezTerm / Kitty / Ghostty | runs right here — typeset math in place | | one of those is installed | opens it for you | | neither | opens the browser — typeset math there |
Force a path with --here (this terminal, Unicode only), --gui (open a
graphics terminal), or --web (browser), or pin one with "launch" in the
config file.
What it looks like
Inline math is rewritten to Unicode as it streams, in any terminal:
Nothing about this is agent-specific — the same wrapper, a different program:
Display math becomes a two-dimensional block; shell text like $PATH is left
strictly alone:
On a terminal that speaks the kitty graphics protocol — or in termtex web —
display equations are typeset properly and drawn over the raw source, in
place, without disturbing the program underneath:
The terminal frames above are rendered from output captured verbatim from
real termtex runs; the typeset equations come from termtex's own MathJax
pipeline. Regenerate them with node tools/make-docs-assets.mjs.
Quick start
Needs Node ≥ 18. No TeX installation, ever.
npm install -g termtex # or: npm install && npm link, from a clone
termtex doctor # what can this terminal do?
termtex claude # goExamples
# wrap a coding agent — any of them
termtex claude
termtex codex
termtex claude --continue # arguments pass straight through
termtex aider --model gpt-4o # nothing is special-cased
# keep the session alive after the terminal closes
termtex abduco -A math claude
termtex abduco -a math # reattach later, rendering included
# run it in a browser instead — any terminal, ssh-friendly, full typeset math
termtex web -- claude
termtex web -- ssh -t devbox # remote session, math rendered locally
# named profiles, so switching is one word
termtex @cc # e.g. ["abduco","-A","cc","claude"]
termtex @cx # e.g. ["codex"]
# no agent involved at all
termtex notes.md # render a file
somecmd | termtex # render a pipe
termtex --tex '\frac{a}{b}' # render one equation
termtex python3 # wrap a REPLHow it renders
Three tiers, picked automatically for what your terminal supports:
| Tier | Where | What you get |
|---|---|---|
| Unicode | every terminal | $E_n = \hbar\omega(n+1/2)$ → Eₙ = ħω(n + 1/2); display math as 2-D blocks |
| Typeset images | kitty-graphics terminals (WezTerm, Kitty, Ghostty) | MathJax-quality equations drawn over the text, in place |
| Browser | anywhere, via termtex web | the same typeset math as SVG, no special terminal needed |
Run termtex doctor to see which apply to you; it prints the exact install
command if you want the typeset tier in a terminal.
The engines: MathJax for typesetting and a WASM build of libtexprintf for Unicode. Both are in-process — no TeX, no compiler, no network.
Wrap mode: how it stays out of the way
termtex <command> runs the program on a PTY, so it sees a real terminal and
behaves exactly as it would unwrapped, while its output passes through two
layers:
- Typeset overlay (kitty-graphics terminals). A headless terminal
emulator shadows the real screen, so termtex knows where each
$$…$$span currently sits. Each is drawn on top of the raw text as a positioned, z-ordered, individually deletable image that occupies no cells. The program's geometry is untouched; when a span moves or scrolls away, its image is re-placed or removed. - Unicode stream rewriting (everywhere). Only spans complete within a single line are rewritten, so the line count never changes and the program's repaint bookkeeping stays valid.
Guardrails: only spans containing \, ^, or _ are touched by default, so
$PATH:$HOME in tool output is never eaten (--aggressive loosens this);
input composers and box-drawing chrome are skipped so typed text never
desyncs; code fences and inline code pass through; anything that fails to
render is left exactly as written; and terminal modes are restored on every
exit path, including crashes.
Browser mode: termtex web
The browser is the one pixel-capable surface everyone already has, so this mode needs no particular terminal:
termtex web -- claude
termtex web -- abduco -A math codex # detachable session in a tab
termtex web --port=9000 --no-open
termtex web --stop # end it from anywhereThe program runs on a server-side PTY; the page renders it with xterm.js and covers display math with server-typeset SVG — crisp at any zoom, and it works in scrollback.
- WSL: run it in your Linux shell; WSL2 forwards localhost to Windows and termtex opens your Windows browser.
- Over ssh: run it on the remote machine, then
ssh -L 8437:localhost:8437 user@hostand open the URL locally — math renders in your local browser regardless of what you ssh from. - Your browser's Install app turns the page into a standalone window with its own icon.
Detachable sessions
A session manager is just another command to wrap:
termtex abduco -A math claude # create-or-attach; outlives the terminal
termtex abduco -a math # reattach; rendering comes with it
termtex web -- abduco -A math codex # ... viewed in a browser tabDetaching (Ctrl-\) exits termtex and leaves the session running. Put termtex
outside (as above) and each viewer gets its own renderer, so the
long-lived session holds no termtex state. Put it inside
(abduco -A math termtex claude) and the renderer lives in the session
instead. Both work; tmux composes the same way (images there need
allow-passthrough on).
Desktop launcher
termtex install-launcher # detects macOS / WSL / Linux
termtex install-launcher --profile=cc --dir=~/work
termtex install-launcher --dry-run # print what it would write| Platform | What you get |
|---|---|
| macOS | ~/Applications/<name>.app — Spotlight, Launchpad, draggable to the Dock |
| WSL | a .cmd on your Windows Desktop — double-click, or pin to the taskbar |
| Linux | ~/.local/share/applications/<name>.desktop |
Absolute paths are baked in at install time, because GUI launchers don't
inherit your shell PATH — the usual reason a double-clicked script does
nothing. Clicking twice reuses the running session instead of starting a
second server. Stop it from anywhere with termtex web --stop.
Getting the agent to emit LaTeX
termtex can only render mathematics it can identify. If an agent writes math
as plain text (y_t = Σ_τ≤t g_out[t,p]) or tucks it inside code spans, there
is nothing to detect — and termtex deliberately leaves both alone, because
prose and code are full of underscores that are not subscripts
(multipole_fft.py, periodic_state_period, k_scaled). Rendering those
would corrupt every filename on screen.
So the single highest-leverage setting is your agent's instructions file
(~/.claude/CLAUDE.md, AGENTS.md, …):
# Math
Write mathematics as LaTeX, using \( ... \) for inline math and \[ ... \] on
its own line for display math. Keep each equation on a single line. Do not
put equations in code spans or code fences, and do not pre-convert them to
Unicode — they are rendered downstream.Why \( \) rather than $ … $: markdown treats _ as an italic marker, so a
renderer can eat the underscores in $x_i$ before termtex sees them.
termtex recovers from that where it can, but avoiding it is better. Both
delimiter styles render.
If you previously told an agent to write math as plain Unicode — earlier versions of this README suggested exactly that — remove it. That instruction prevents the agent from emitting anything termtex can render.
Configuration
Everything program- or UI-specific is configuration, not code. Optional file
at ~/.config/termtex/config.json (or $TERMTEX_CONFIG):
{
"command": ["claude"],
"profiles": {
"cc": ["abduco", "-A", "cc", "claude"],
"cx": ["codex"],
"remote": ["ssh", "-t", "devbox", "abduco -A m claude"]
},
"skipPattern": "^\\s*[>❯│╭╰]",
"mathColor": "#e8e8e8",
"mathBg": "#1e1e1e",
"mathScale": 1,
"port": 8437,
"sources": ["~/.config/termtex/claude-code.js"]
}Every key has a TERMTEX_* environment equivalent; precedence is CLI flags >
env > file > default.
command— what baretermtexruns. Defaults to$SHELL.profiles— named launch targets:termtex @cc. Extra arguments append; an unknown name lists what you have.skipPattern— rows treated as UI chrome and passed through untouched. Retune here if a program's chrome changes; no library update needed.sources— transcript readers fortermtex last. termtex ships none; register one to enable that command (see below).
termtex last
Re-renders the newest agent response for the current directory — useful as a
second pane (termtex last --watch) or over ssh. It's the only feature that
would need to read an agent's private files, so termtex ships no readers at
all. A ready-made Claude Code adapter is included as example code, never
loaded unless you ask for it:
cp examples/sources/claude-code.js ~/.config/termtex/
# then add: { "sources": ["~/.config/termtex/claude-code.js"] }Supporting another agent means writing a file like that one — id, detect,
watchTarget, lastResponse — not patching termtex.
Options
| Flag | Modes | Meaning |
|---|---|---|
| --overlay=auto\|force\|off | wrap | typeset images over display math |
| --aggressive | wrap, web | loosen math-span heuristics |
| --images=auto\|kitty\|iterm\|sixel\|off | file, pipe, last | image tier for display math |
| --math-color, --math-bg, --math-scale | all | appearance |
| --port, --no-open, --new, --new-token, --stop | web | server behaviour |
| --source=ID, --watch | last | transcript source, live pane |
| --profile, --dir, --name, --platform, --dry-run | install-launcher | launcher generation |
Security notes
termtex web binds 127.0.0.1 only, and every route — page, WebSocket,
/typeset, manifest, icon — requires an unguessable token, stored at
~/.local/state/termtex/token (mode 0600) and reused across restarts so
bookmarks and installed web apps keep working. Same model as Jupyter and
code-server; the tradeoff versus a per-run token is that anything already
running as your user could read the file while a session is up. Rotate with
termtex web --new-token.
Install
npm install -g termtexOr from a clone, so edits are live:
cd termtex && npm install && npm linkNotes:
node-ptyis an optional native dependency powering wrap and web modes. macOS needs Xcode Command Line Tools (xcode-select --install); Debian/Ubuntu needsbuild-essential. If it can't build, the install still succeeds and file/pipe/--texmodes work.- If your npm blocks install scripts:
npm install-scripts approve node-pty && npm rebuild node-pty.
Development
npm test # unit + PTY end-to-end suite
node tools/make-docs-assets.mjs # regenerate the images in this READMETests skip rather than fail where an optional native dependency isn't available, so the suite never depends on a native build succeeding.
Alternatives considered
| Tool | Approach | Limitation for this use case | |---|---|---| | utftex | LaTeX → Unicode (C CLI) | Single equations only; it's the engine behind termtex's Unicode tier | | buun-ch/termtex | MathJax → PNG → kitty protocol | Filter only (no TUI wrapping), kitty-only, no Unicode fallback. Name collision is coincidental | | LaTerM | xterm.js overlay addon | Web-embedded xterm.js only — but the architectural blueprint for termtex's overlay | | latex2sixel | LaTeX → DVI → Sixel | Needs a TeX install and a sixel terminal | | pylatexenc / unicodeit | LaTeX → flat Unicode (Python) | No 2-D layout, no document awareness |
Limitations
- The typeset overlay settles ~200 ms after output pauses, and needs a kitty-graphics terminal; everywhere else display math is Unicode.
- Inline math is always Unicode — terminals can't baseline-align inline images. Inline spans broken across a hard line wrap aren't detected.
- Sixel images are opaque; set
--math-bgto your terminal background. - Indented (4-space) code blocks are treated as prose; use fences.
License
GPL-3.0-or-later — required because the bundled Unicode engine (libtexprintf) is GPL. MathJax is Apache-2.0.
