batty
A from-scratch Rust clone of bat — cat with syntax highlighting, git diff markers, and a pager. Plus:
- Bundled Rhai grammar —
.rhaiscripts highlight out of the box, including template strings with${interpolation},#{}map literals,??/?.operators,::module paths, and a broad builtin list. - Interactive TUI mode (
-i) — vim-style navigation, line by line. - Vim-style relative line numbers — center distances on a cursor.
- Markdown rendering (
-m) — render Markdown likeglow, with amtoggle in interactive mode. - Tail / follow mode (
-f) —tail -fsemantics with syntax highlighting. - TOML config —
~/.config/batty/config.toml. - Small release binary — ~2.8 MB with full grammar/theme bundles.
Targets macOS and Linux.
Installation
Homebrew (macOS / Linux)
The formula builds from source via cargo, so a Rust toolchain is pulled in as a build-time dependency and removed afterwards.
Cargo (crates.io)
The crate is published as batty-cat on crates.io because the batty and batty-cli names were already taken — the installed binary is still batty.
Build from source
&&
# Binary lands at target/release/batty
Move it onto your $PATH however you like (e.g. cp target/release/batty ~/.local/bin/).
Requires Rust 1.86 or newer.
Quick start
|
Flag reference
batty accepts the flags below. Many can also be set in the config file.
Input
| Flag | Description |
|---|---|
<FILES>... |
One or more files. Use - for stdin (default when no files given). |
-l, --language <NAME> |
Override syntax detection. Use any name shown by --list-languages. |
--encoding <ENC> |
Input file encoding: auto (default), utf-8, iso-8859-1 (alias latin1). auto decodes as UTF-8 when valid and silently falls back to ISO-8859-1 otherwise; utf-8 is strict and errors on invalid byte sequences; iso-8859-1 always decodes byte-for-byte (each byte 0x00–0xFF → U+0000–U+00FF). Applies to file reads and stdin. |
Display
| Flag | Description |
|---|---|
-p, --plain |
No decorations (header / grid / numbers / changes). Equivalent to --style=plain. |
-n, --number |
Show line numbers. Equivalent to --style=numbers. |
-d, --diff |
Show git diff markers in the gutter (+ / ~ / -). |
--diff-context <N> |
Lines of context for diff display. Default: 2. (Parsed but currently doesn't filter to changed regions; see Limitations.) |
-A, --show-all |
Show non-printable characters: → for tab, · for space, • for control chars. |
--style <SPEC> |
Comma-separated style components: full, plain, numbers, grid, header, rule, changes, snip. Default: full. |
--gutter / --no-gutter |
Force the left-side gutter (line numbers + diff markers + grid bar) on or off, regardless of --style. --no-gutter is a "cleaner reading" preset that's less aggressive than --plain (header / rule / snip stay on). --gutter cancels no-gutter = true from config. |
--line-range <RANGE> |
Show only the given range, e.g. 10:20, :15, 30:, 42. Rejects 0 and inverted ranges. |
-H, --highlight-line <N> |
Highlight specific line(s) with inverse video. Repeatable. The first one acts as the cursor reference for relative numbering. |
--tabs <N> |
Tab expansion width. Default: 4. |
--wrap <MODE> |
never / character / auto. Default: auto. character and auto break long lines at the terminal-width boundary with a continuation prefix that preserves the gutter (line numbers / cursor / change marker remain blank on continuation rows; the grid bar repeats). Wide CJK / emoji chars count their actual display width. --wrap=never emits each source line in one shot, letting the terminal soft-wrap (or truncate). Forced to never in interactive mode. |
--decorations <WHEN> |
always / auto / never. Default: auto. never collapses to plain output. |
Theme & color
| Flag | Description |
|---|---|
--theme <NAME> |
Color theme. Default: Monokai Extended. See --list-themes. |
--color <WHEN> |
always / auto / never. Default: auto. auto enables color when stdout is a TTY and NO_COLOR is unset. |
--line-numbers <STYLE> |
absolute (default) or relative. With relative, the cursor line shows its absolute number, others show distance. Falls back to absolute if no cursor is set. |
Markdown rendering
| Flag | Description |
|---|---|
-m, --markdown |
Render Markdown to terminal escapes instead of showing the raw source. Uses termimad under the hood. Works on any file, not just .md. |
--markdown-on-extension |
Render as Markdown only when the file extension is .md / .markdown / .mdown / .mkd. Lower priority than --markdown (which forces on for any file) and --no-markdown (which disables). Useful as a config default — set markdown-on-extension = true and .md files auto-render while source files stay raw. |
--no-markdown |
Disable Markdown rendering. Overrides markdown = true and markdown-on-extension = true in the config. |
When --markdown is on, the gutter shows the source-line number of each top-level block on its first rendered row, with continuation rows blank in the gutter (matching how raw view handles wrapped lines). The grid bar repeats on every row when --style includes grid. Use --no-gutter to strip the gutter and read flush against the left margin. Diff markers (changes) and the cursor glyph (▶) don't appear in markdown view — block-granular mapping doesn't make them meaningful, and the status bar covers position info in interactive mode. The header prints with the language label Markdown (rendered).
Interactive mode
| Flag | Description |
|---|---|
-i, --interactive |
Enter the TUI. See Interactive mode for keys. |
--no-interactive |
Disable interactive mode. Overrides interactive = true in the config. |
--top-pad <N> |
Reserve N rows at the top of the screen. Default: 0. Use 2 in Warp to dodge its UI overlay. |
Tail / follow
| Flag | Description |
|---|---|
-f, --follow |
tail -f semantics: render the last --tail-lines lines, then poll the file every 200 ms and render appended content as it arrives. Single file only; no stdin; bypasses the pager. Mutually exclusive with --interactive. Ctrl-C exits. |
--no-follow |
Disable follow mode. Overrides follow = true in the config. |
--tail-lines <N> |
Number of trailing lines to show on launch. Default: 10. |
Truncation / rotation: when the file shrinks (e.g. > error.log or logrotate), batty prints a one-line notice and re-renders the last --tail-lines of the new content.
Pager
| Flag | Description |
|---|---|
--paging <WHEN> |
always / auto / never. Default: auto. Uses $PAGER or less -RF. never also disables interactive mode — treat it as a global "flat output" override. |
Discovery
| Flag | Description |
|---|---|
-L, --list-languages |
Print every supported language (one per line) and exit. |
--list-themes |
Print every bundled theme and exit. |
-h, --help |
Short help. |
--help |
Long help with full descriptions. |
-V, --version |
Print version. |
Flag-conflict semantics
- All boolean flags use
overrides_withso config + CLI can both set a flag withoutcannot be used multiple timeserrors. --interactiveand--no-interactiveare mutual overrides — last occurrence wins.--paging=neverimplies--no-interactive.
Config file
~/.config/batty/config.toml (XDG-style on every platform, including macOS).
Top-level keys mirror CLI long flag names with hyphens preserved:
# ~/.config/batty/config.toml
= "Dracula"
= 2
= 2
= "relative"
= true
= false # true → render every file as markdown
= true # true → render only .md / .markdown files
= false # true → hide line numbers / changes / grid by default
= false # set true to default to tail mode
= 10
= [10, 20]
= "auto" # "utf-8" | "iso-8859-1" | "auto" (default)
Mapping rules
| TOML | argv |
|---|---|
key = "string" |
--key=string |
key = 42 |
--key=42 |
key = true |
--key |
key = false |
(omitted) |
key = [a, b] |
--key=a --key=b |
- Comments (
#) are allowed. - Malformed TOML logs a warning to stderr and proceeds without config.
- Unknown keys are forwarded to clap, which rejects them with a clear error — typos surface as
unrecognized argument.
BATTY_CONFIG_PATH
Set this env var to override the default location:
BATTY_CONFIG_PATH=/path/to/other.toml
BATTY_CONFIG_PATH=/dev/null
Interactive mode
Enters raw mode in the alternate screen. A ▶ glyph in the gutter marks the cursor line; the bottom row is a status bar (file line N/M (abs|rel mode) vim-keys: ... q quit).
| Key | Action |
|---|---|
j / Down |
Cursor down one line |
k / Up |
Cursor up one line |
g / Home |
Jump to first line |
G / End |
Jump to last line |
Ctrl-d |
Half-page down |
Ctrl-u |
Half-page up |
PageDown |
Full page down |
PageUp |
Full page up |
m |
Toggle rendered Markdown view ↔ raw source. Active when the file has a .md / .markdown / .mdown / .mkd extension, or when --markdown was passed on launch. Status bar shows [md] while in rendered mode. The toggle preserves your scroll position: pressing m from raw view lands you on the corresponding block in the rendered output, and pressing m again returns to the source line of the block you were viewing. The status bar in rendered mode shows rendered N/M ↔ src K. |
n |
Toggle the gutter (line numbers + cursor glyph) on/off. Initial state follows --gutter / --no-gutter. Status bar shows no-gutter when off. |
+ / = |
Increase --top-pad by 1 row (live). Status bar shows pad=N when nonzero. |
- |
Decrease --top-pad by 1 row (saturates at 0). |
q / Esc / Ctrl-c |
Quit |
If your terminal hides the top of the alt-screen behind its own UI (Warp does this in some panes / after resizing), the live + / - bindings let you tune top-pad until content is visible — no need to exit, edit config, and retry. The value resets to whatever --top-pad (or top-pad in config) said next time you launch.
Restrictions:
- One file at a time — multiple files are rejected with an error.
- No stdin input — interactive mode requires a real file path.
- Pager is bypassed.
- Color is forced on (interactive mode in a TTY always wants color).
If the top of your screen is hidden behind a terminal-host overlay (e.g. Warp), pass --top-pad=2 (or whatever number works) — or set top-pad = 2 in your config.
Environment variables
| Variable | Effect |
|---|---|
BATTY_CONFIG_PATH |
Override the config file path. /dev/null disables config entirely. |
PAGER |
Pager binary (default less). |
LESS |
Pager arguments. batty sets LESS=-RF if unset before spawning the pager. |
NO_COLOR |
When set (any value), disables color output in --color=auto mode. |
Examples
# Render a README in your terminal
# Mix: render markdown AND open it interactively (m toggles to raw)
# Tail a log file with syntax highlighting
# Tail with the last 50 lines visible on launch
# Highlight a Rhai script
# Show lines 50–80 with line numbers, no other decorations
# Cursor-centric relative numbering for line 42
# Show only the modified portion of a file with diff markers
# Render plain text from stdin as Python
|
# Force interactive even though config says interactive = false
# One-off non-interactive run despite `interactive = true` in config
# Skip the user's config for one run
BATTY_CONFIG_PATH=/dev/null
# List bundled themes
Limitations
The full list lives in OUT-OF-SCOPE.md. Highlights:
--diff-contextdoesn't restrict output to changed regions; it only sets the diff window passed to git2.--wrapbreaks at column boundaries, not word boundaries — fine for source code, less ideal for prose. Forced off in interactive mode (cursor / viewport / status-bar all assume 1 source line = 1 visual row).- No Windows support (POSIX pager invocation, etc.).
- Interactive mode is keyboard-only: no search, no mouse, no persistent cursor across runs.
- Follow mode polls every 200 ms; a real
inotify/kqueuewatcher would be lower-latency but adds platform code. - Follow mode rebuilds the syntax highlighter on every poll, so multi-line constructs (block comments, multi-line strings) that span a poll boundary may briefly miscolor.
License
MIT.