Welcome to mantis
mantis lets you browse, read, and review a codebase right in your terminal — instantly. Point it at a folder and start moving through your files with the arrow keys (or your mouse), with syntax highlighting, rendered markdown, fuzzy search, and git diff/blame/history always one keystroke away.

💡 New here? You only need two things to get started: the Installation page, then the Quick Start. Everything else is optional.
What makes it nice
- ⚡ Lightweight & instant. One small binary, no runtime dependencies, and nothing to configure before your first run. It opens in milliseconds, even over SSH.
- 🌳 A real tree view. Navigate folders with the keyboard or mouse,
respecting your
.gitignore. - 🔍 Fuzzy & full-text search. Jump to any file by name (
Ctrl+T), or search across the contents of every file (Ctrl+F) — fzf-style, as you type. - 🎨 Readable files. Syntax highlighting for source code, rendered markdown, and JSON pretty-printing.
- 🔧 Git built in. Per-line blame, working-tree diffs, file history, and status-colored tree entries — no plugins required.
- ⌨️ Discoverable. Press
?for help orCtrl+Pfor a searchable command palette. You don’t have to memorize anything.
Try it in five seconds
mantis # open the current directory
mantis path/to/dir # open a specific directory
mantis file.md # open a single file directly
Press ? any time for in-app help, and Ctrl+c to quit.
Where to go next
| If you want to… | Read |
|---|---|
Understand when to reach for mantis | Why mantis? |
| Get it installed | Installation |
| Learn the basics in 5 minutes | Quick Start |
| See every key and what it does | Usage & Keybindings |
| Use blame, diffs, and history | Git Features |
| Tweak themes and keybindings | Configuration |
Why mantis?
There are a lot of ways to look at code. So why mantis?
Because mantis is built for one job — moving through a codebase and reading it,
with git context one keystroke away — and it does that job with zero setup.
It is deliberately not a full editor, and that focus is what keeps it fast and
simple.
💡 The short version: reach for
mantiswhen you want to explore a repo, read a file, or check a diff without launching a heavyweight editor. When you actually need to change something, presseto jump into your$EDITOR.
At a glance
| mantis | Vim / Neovim | VS Code | |
|---|---|---|---|
| Footprint | Single ~MB binary | Light core, heavy once configured | Electron, hundreds of MB + RAM |
| Setup to be useful | Zero — just run mantis | Hours of config & plugins | Install, extensions, indexing |
| Learning curve | Arrow keys & a mouse | Steep (modes, motions, ecosystem) | Gentle, but mouse-heavy |
| Tree view | Built in | Needs a plugin (nvim-tree, etc.) | Built in |
| Fuzzy + full-text search | Built in | Needs telescope/fzf/ripgrep glue | Built in |
| Git diff / blame / history | Built in | Needs fugitive/gitsigns/etc. | Needs extensions |
| Time to first paint | Milliseconds | Fast (slower with a big config) | Seconds |
| Works great over SSH | Yes | Yes | Awkward |
Compared to Vim / Neovim
Vim and Neovim are superb editors. But to get the everyday browsing experience
mantis gives you out of the box — a file tree, a fuzzy finder, full-text search,
git signs, inline blame, and side-by-side diffs — you have to assemble and
maintain a stack of plugins:
- a tree plugin (nvim-tree, neo-tree),
- a fuzzy finder (telescope, fzf.vim),
- git integration (vim-fugitive, gitsigns),
…plus a plugin manager and the config glue to hold it together. Curated configs like LazyVim make this easier, but they are large, opinionated systems with their own learning curve and maintenance.
And then there’s the modal learning curve itself: modes, motions, registers, and muscle memory that take real time to build.
mantis skips all of that. There’s no init.lua, no plugin manager, and no modes —
just arrow keys (or hjkl if you prefer) and your mouse. Everything listed above
already works the moment you run it.
🧭 Love Vim? Keep it.
mantisis the browser you open first;ehands the file straight to your editor when it’s time to write.
Compared to VS Code
VS Code is a great IDE. But it’s an Electron application — it bundles a whole web browser, so it’s slow to launch and memory-hungry just to glance at a file or review a diff. On a remote machine over SSH, that gets even more painful.
mantis is a tiny native binary. It opens instantly, sips memory, and runs
happily inside any terminal — local or remote. When you only need to read code
or review a change, you don’t need to boot an IDE for it.
When mantis is the right tool
- ✅ Exploring an unfamiliar repository
- ✅ Reading source, docs, or markdown
- ✅ Reviewing a diff or checking who changed a line (blame)
- ✅ Working on a remote server over SSH
- ✅ Anytime you want something that opens now
For heavy, sustained editing — refactors, LSP, debugging — use your editor. mantis
gets you there fast with e, and is waiting when you come back.
Installation
One-liner install (recommended)
Linux / macOS
The fastest way to get mantis — no Rust toolchain required — is the install
script. It detects your OS/arch, downloads the matching prebuilt binary,
verifies its SHA-256 checksum, and installs it onto your PATH:
curl -fsSL https://raw.githubusercontent.com/ansromanov/mantis/main/install.sh | sh
You can tweak the install with environment variables:
# install a specific release instead of the latest
MANTIS_VERSION=v0.2.0 curl -fsSL https://raw.githubusercontent.com/ansromanov/mantis/main/install.sh | sh
# install into a directory of your choice
MANTIS_INSTALL_DIR="$HOME/bin" curl -fsSL https://raw.githubusercontent.com/ansromanov/mantis/main/install.sh | sh
Prefer to read before piping to a shell? Download
install.sh, inspect it, then runsh install.sh.
Windows (PowerShell)
Run this in a PowerShell window — curl is not needed, Invoke-WebRequest
(irm) is built into PowerShell:
irm https://raw.githubusercontent.com/ansromanov/mantis/main/install.ps1 | iex
From cmd.exe:
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/ansromanov/mantis/main/install.ps1 | iex"
The script downloads mantis.exe, verifies its SHA-256 checksum, installs it to
%CARGO_HOME%\bin (if Rust is present) or %LOCALAPPDATA%\Programs\mantis,
and adds the directory to your user PATH. It also generates PowerShell tab
completions (install to %CARGO_HOME%\completions\mantis.ps1).
You can override the install location:
$env:MANTIS_VERSION = 'v0.2.0' # install a specific version
$env:MANTIS_INSTALL_DIR = "$HOME\bin" # install to a custom directory
irm https://raw.githubusercontent.com/ansromanov/mantis/main/install.ps1 | iex
Via cargo install
If you have the Rust toolchain installed, the simplest way to install mantis is:
cargo install mantis
This compiles and places the mantis binary in ~/.cargo/bin (which should already be on your PATH after a standard rustup install).
To install directly from the git repository without a crates.io release:
cargo install --git https://github.com/ansromanov/mantis
From source (Rust toolchain required)
git clone https://github.com/ansromanov/mantis.git
cd mantis
cargo build --release
# binary is at target/release/mantis
Or, if you have just:
just install # builds --release and copies mantis to ~/.cargo/bin
Prerequisites
- Rust (stable toolchain)
- A terminal that supports 24-bit color
Prebuilt binaries
If you’d rather not use the install script, prebuilt binaries are attached to every release:
| Platform | Architecture | File |
|---|---|---|
| Linux (musl) | x86_64 | mantis-linux-x86_64 |
| Linux (musl) | arm64 / aarch64 | mantis-linux-aarch64 |
| macOS | Apple Silicon | mantis-macos-aarch64 |
| macOS | Intel | mantis-macos-x86_64 |
| Windows | x86_64 | mantis-windows-x86_64.exe |
Download the appropriate binary for your platform from the latest release,
make it executable (chmod +x on Linux/macOS), and place it somewhere on
your PATH (e.g. /usr/local/bin or ~/.cargo/bin).
Each release also ships a SHA256SUMS file so you can verify your download:
sha256sum --check --ignore-missing SHA256SUMS
Via package managers
Homebrew (macOS / Linux)
The formula lives in this repository, so tap it by URL, then install:
brew tap ansromanov/mantis https://github.com/ansromanov/mantis
brew install mantis
The formula installs the prebuilt binary for your platform (Apple Silicon, Intel,
Linux x86_64, and Linux arm64) — no Rust toolchain required. It is bumped
automatically on every release, so brew upgrade mantis always tracks the latest
version.
The explicit URL is needed because the repository is named
mantis, nothomebrew-mantis; Homebrew’s shortbrew tap user/repoform only resolves thehomebrew--prefixed name.
The formula installs bash, zsh, and fish completions plus the man page automatically — no extra steps needed.
Manual: completions and man page
After installing mantis from any method, you can generate completions and the
man page at any time:
# Shell completions
mantis --completions bash > /usr/local/share/bash-completion/completions/mantis
mantis --completions zsh > /usr/local/share/zsh/site-functions/_mantis
mantis --completions fish > /usr/local/share/fish/vendor_completions.d/mantis.fish
mantis --completions powershell > ~/.config/powershell/completions/mantis.ps1
# Man page
mantis --print-man-page > /usr/local/share/man/man1/mantis.1
The one-liner install script (install.sh) does this automatically when it can
write to the appropriate share/ directories.
Other package managers
Coming soon — Scoop, and more are on the roadmap.
Quick Start
This page walks you through your first few minutes with mantis. No prior terminal
expertise required — if you can run one command and press arrow keys, you’re set.
📦 Don’t have it yet? See Installation first, then come back.
1. Open something
Open the folder you’re currently in:
mantis
Or point it at a folder or a single file:
mantis ~/projects/my-app # a directory
mantis README.md # one file
You’ll see two panels: the file tree on the left, and the content of the selected file on the right.
2. Move around
You don’t need to learn anything special — use the arrow keys, or your mouse.
| To do this… | Press… |
|---|---|
| Move up / down in the tree | ↑ / ↓ (or k / j) |
| Open a file / expand a folder | Enter or → |
| Collapse a folder / go up | ← |
| Jump between the tree and the file view | Tab |
| Scroll the file | ↑ / ↓, PageUp / PageDown |
| Quit | Ctrl+c (or q while the tree is focused) |
🖱️ Prefer the mouse? Click a row to select it, click a folder to fold/unfold, and use the scroll wheel in whichever panel your cursor is over.
3. Find a file fast
Two kinds of search, both fzf-style (just start typing to filter):
- Press
Ctrl+Tto search by file name. - Press
Ctrl+Fto search inside files (full-text).
Use ↑ / ↓ to pick a result and Enter to open it. Press Esc to close
search. Inside the popup, Tab switches between name and content search.
4. Peek at git
If you’re inside a git repository, you get this for free:
- Tree entries are colored by git status (new, modified, deleted).
- Press
Ctrl+bto toggle blame — see who last touched each line. - Press
H(while the tree is focused) for the history of the current file, then pick a revision to view its diff. - Press
Ctrl+Dfor git mode: show only changed files, with their diffs.
There’s a whole page on this — see Git Features.
5. When you can’t remember a key
You never have to memorize the keymap:
- Press
F1(or?) for in-app help with all the keybindings. - Press
Ctrl+Pfor the command palette — type what you want to do (e.g. “blame”, “theme”), and it shows the action and its shortcut.
6. Make it yours (optional)
- Press
t(while the tree is focused) to switch themes live (monokai, solarized, catppuccin, and more). - Want different colors or keybindings permanently? That all lives in a small
mantis.tomlfile — see Configuration.
That’s the whole core experience. From here:
- Usage & Keybindings — the complete key reference
- Git Features — blame, diffs, and history in depth
- Configuration — themes and custom keys
Release Digests
v0.17: faster navigation, richer review, and broader language support
Version 0.17 makes the command palette the quickest route into a repository, adds a commit-oriented review flow, and expands the file types that mantis can read well out of the box. Everything below is available with the default configuration.
One palette for commands, files, content, and lines
Press Ctrl+P to open the command palette. Its first character chooses what
you are looking for:
| Start the query with | Use it to |
|---|---|
(nothing) or > | Find and run a mantis command |
/ | Fuzzy-find a file |
# | Search file contents across the repository |
: | Jump to a line; for example, :120 |
The palette remembers which commands you use. With an empty query, your most
recent command and the most useful recent commands appear first; that ranking
gradually gives more weight to what you use now. Set
palette_pin_recent = false or palette_frequent_count = 0 in mantis.toml
if you prefer an unpinned palette.
Plugins can also add actions to this same palette. Plugin authors can use the
register_commands protocol action; see Plugin Development
for the message format and lifecycle rules.
Review a change since any revision
Use Compare against a revision from Ctrl+P to review the current working
tree against a branch, tag, commit hash, or revspec such as HEAD~3. Select a
suggestion or type the revision and press Enter. The tree then contains only
files changed since that revision, and selecting a file opens its diff. The
status bar displays the selected comparison base.
There is also a repository-wide commit browser. Press L while the tree has
focus, or run Browse repository commits from the palette. Type to filter
by hash, author, or subject, select a commit, and press Enter to begin a
compare review from that commit. Press Esc or Ctrl+D to leave compare mode.
See Git Features for the full git workflow.
Fold structured data and shell scripts
The bundled JSON, YAML, and shell language providers are enabled by default.
Open a .json, .yaml/.yml, .sh, .bash, or .zsh file, focus the
content pane, place the active line on a foldable section, and press Space to
collapse or expand it.
- JSON folds multi-line objects and arrays. Use the existing JSON pretty-print command first when viewing minified JSON.
- YAML folds indentation-based sections.
- Shell folding recognizes function and compound blocks while ignoring braces in comments, quoted strings, and heredocs.
The providers can be disabled individually under [plugins] in
mantis.toml; Plugins lists their names.
More files highlighted without setup
TOML, TypeScript/TSX, and Dockerfile syntax packs now ship enabled. Open a
.toml, .ts, .tsx, .mts, .cts, or .jsx file, or a Dockerfile or
Containerfile, and mantis selects the matching highlighting automatically.
This includes common configuration files such as Cargo.toml, pyproject.toml,
and mantis.toml.
For every bundled provider and syntax pack, including how to opt out, see Plugins.
v0.16: review tools and language-aware reading
Version 0.16 focused on making code review more legible while adding folding for the languages most commonly encountered in a repository.
Compare a revision and inspect blame in context
To review all changes since a branch, tag, hash, or revspec, run Compare
against a revision from Ctrl+P, choose or type the revision, and press
Enter. The changed-file tree and each file’s diff stay scoped to that base;
press Ctrl+D to return to normal browsing.
With a tracked file open, press Ctrl+b to open the full-file blame pane.
It follows the content cursor and shows the hash, author, date, and subject for
each line. Press B in the content pane when you only need the active line’s
blame summary. The Git Features guide covers both workflows.
Fold Rust, Python, and Go
The bundled Rust, Python, and Go providers add fold regions for .rs, .py,
and .go files. Focus the content pane, move to a foldable declaration or
block, and press Space to collapse or reopen it. Folding stays available
alongside the ordinary syntax highlighting and can be disabled per provider in
mantis.toml if needed.
Faster discovery and safer diagnostics
The command palette now presents categories, descriptions, match highlights,
and context when an action cannot apply. Press Ctrl+P, type an action name,
and use the displayed shortcut or Enter to run it.
Use Report a bug (save diagnostics locally) from the palette to create a local, reviewable report, or Toggle telemetry to opt into the local-only usage log. Neither feature uploads data. See Telemetry & Bug Reports for the saved locations and contents.
v0.15: navigation and compatibility polish
Version 0.15 was a focused stability release rather than a new interaction mode. Large trees avoid an unnecessary deep scan when nothing is expanded, so starting a repository and moving through its top level is more responsive.
Visual selection now remains visible on the active line, making it easier to
keep your place while copying or inspecting a range. Existing configuration
files using older git_mode and renamed keymap fields continue to load without
spurious warnings; consult Configuration when you are ready
to migrate to the current names.
v0.14: pager mode, powerful search, and familiar keys
Version 0.14 made mantis more useful in command pipelines and easier to learn for users coming from common editors.
Use mantis as a terminal pager
Pipe a diff, log, or any text into mantis to browse it interactively:
git diff | mantis
git log -p | mantis
kubectl logs pod | mantis
Diff-shaped input opens in a navigable side-by-side view. For other input, use
--language when you want to force syntax highlighting. See Pager mode
for behavior and platform details.
Tune a search without changing its query
In content search or the in-file search bar, press Ctrl+A for
case-sensitive matching, Ctrl+W for whole-word matching, and Ctrl+R for a
regular expression. The active [Aa], [\b], and [.*] indicators show
which constraints are in effect. Press the same shortcut again to turn it off.
Learn the editor-style defaults
The shipped keymap uses familiar editor-style bindings: Ctrl+P opens the
palette, Ctrl+T finds files, Ctrl+F searches contents, Ctrl+G goes to a
line, and Ctrl+D opens the changed-file review view. Press ? or F1 for
the structured help overlay, then use its tabs to browse key groups. All of
these bindings remain remappable in mantis.toml.
Plugin authors should update manifests to mantis_protocol = "3" when using
the protocol-3 request/response, key-consumption, or provider-priority
features; Plugin Development has the
compatibility details.
v0.13: a more dependable everyday viewer
Version 0.13 improved the tools used to orient yourself in a repository and made plugin failures easier to diagnose.
Preview themes and use a scrollable help overlay
Press t in the tree to open the theme picker. Moving through its entries
previews each theme immediately; press Enter to keep it or Esc to return
to the original theme. The Themes page explains persistent theme
configuration.
Press ? or F1 whenever you need a reminder. Help is scrollable, and its Git
section collects the status, diff, blame, and history shortcuts in one place.
Rely on safer sessions and clearer plugin failures
Each workspace keeps independent session state, so multiple mantis instances no longer overwrite one another’s restored view. Bundled plugins are embedded with the application rather than relying on a local build at startup, and old retired shell plugins are removed during upgrade.
If a process plugin exits unexpectedly, mantis keeps its recent stderr and
points to a local plugin log. Open the plugin picker with p in the tree to
check its status; Plugins explains how to enable, disable, and
configure plugins.
Usage & Keybindings
Basic usage
mantis # view the current directory
mantis path/to/dir # view a specific directory
mantis file.md # open a single file directly
mantis --completions bash # generate shell completions (bash, zsh, fish, powershell)
mantis --print-man-page # print the man page
mantis --language rust < file # force syntax highlighting for piped stdin
mantis --update # self-update to the latest release
mantis --help # print help (or -h)
mantis --version # print version (or -V)
Press ? or F1 for in-app help, and q to quit.
Pager mode
When no <path> is given and stdin is piped rather than a terminal, mantis
reads stdin instead of walking a directory:
git diff | mantis # navigable side-by-side diff
kubectl logs pod | mantis # highlighted, searchable log output
curl -s https://example.com/file.py | mantis --language python
Diff-shaped input (a diff --git/diff --cc header, an @@ - hunk header, or
a --- /+++ file-marker pair) renders through the same side-by-side diff
view as git mode, starting in side-by-side layout regardless of the
[git.diff] side_by_side setting. Anything else is syntax-highlighted: pass
--language <name> (e.g. --language rust) to force it, otherwise mantis
sniffs the first line the way syntect detects shebangs and mode lines.
The tree pane collapses (there is no path driving the view) and focus starts
in the content pane, but the tree is still there — drag the splitter or
press Tab to browse the working directory alongside the piped content.
Keyboard input keeps working normally even though stdin is consumed by the
piped data: mantis reads keys from the controlling terminal (Unix: /dev/tty,
Windows: CONIN$) instead, the same trick less uses. Input is read to EOF
before the UI starts, so very large piped input delays the first frame rather
than streaming incrementally.
Terminal compatibility
The main panels require at least 80 columns to remain usable. At smaller sizes, mantis shows a resize message instead of rendering clipped tree and content panes.
Keyboard enhancement
Every default binding uses plain Ctrl+letter, a bare/Shift letter, or a
named key — combinations that work identically on every terminal and OS.
Ctrl+Shift combinations are deliberately not used: kitty reserves
ctrl+shift for its own shortcuts (kitty_mod), Windows Terminal binds
Ctrl+Shift+P/Ctrl+Shift+F itself, and legacy terminals (macOS
Terminal.app, plain xterm, many SSH setups) can’t even distinguish them from
plain Ctrl. Modifier bindings are matched case-insensitively, so CapsLock
or a stray Shift never breaks a shortcut.
On terminals with the kitty keyboard protocol (CSI-u), mantis additionally matches bindings by physical key position, so shortcuts work on non-Latin keyboard layouts.
| Terminal | Keyboard enhancement (layout independence) | Full mouse support |
|---|---|---|
| kitty | ✓ Full | ✓ |
| WezTerm | ✓ Full | ✓ |
| Ghostty | ✓ Full | ✓ |
| Alacritty 0.15+ | ✓ Full | ✓ |
| Windows Terminal | ✓ Full | ✓ |
| iTerm2 | ✓ Partial¹ | ✓ |
| macOS Terminal.app | ✗ | ✓ |
| xterm (plain) | ✗ | Partial² |
| Most SSH clients | ✗ | Depends on client |
| tmux (inside any terminal) | ✗ | ✗ |
¹ iTerm2 supports CSI-u (disambiguation + event types) but may not report alternate keys for all keyboard layouts.
² xterm supports mouse events but the generic mouse protocol (SGR 1006) lacks
drag and release tracking. Enable xterm-mouse in tmux or `XTerm*decTerminalID:
280` for full SGR mouse.
On terminals without keyboard enhancement, mantis shows a one-time notice and
notes the limitation in the in-app help (? → Getting started).
The best terminals for full mantis keyboard support are kitty, WezTerm, Ghostty, Alacritty 0.15+, and Windows Terminal.
Session persistence
mantis automatically remembers your workspace state across restarts:
expanded directories, the last open file, scroll position, and git mode.
State is cached outside the project tree (~/.local/state/mantis/
or %APPDATA%\mantis\) so it survives re-clones and never writes
dotfiles into the repository. Each workspace root gets its own file under
the sessions/ subdirectory. To reset the session for a directory, quit
and delete its file from the sessions/ subdirectory in the state directory.
💡 Can’t remember a key? Press
?orF1for the help overlay, orCtrl+Pto open the command palette and search for an action by name — it shows you the shortcut too. New tomantis? Start with the Quick Start.
Bindings are editor-style (VS Code / Sublime conventions) and fully remappable
— see Keybindings for the complete list, the
macOS (Cmd) variants, and the tree:/content: scoping syntax. The tables
below cover the shipped defaults; single letters (q, p, t, …) only work
while the tree panel is focused — the content pane’s letter keyspace is
kept free, apart from the vim motions below, for future editing features. Any
action not listed with a content-pane key is still reachable from the command
palette (Ctrl+P).
Global
These work no matter which panel is focused.
| Key | Action |
|---|---|
Ctrl+c, q (tree) | Quit |
F1, ? | Toggle help |
Ctrl+P | Command palette (fuzzy-find any action) |
Tab | Switch panel |
/ | Tree filter (tree) / in-file search (content) |
Ctrl+T | Global fuzzy file-name picker |
Ctrl+F, f (tree) | Content (full-text) search |
Ctrl+r, F5, r (tree) | Reload tree |
Ctrl+e, e (tree) | Open current file in $EDITOR |
y (tree) | Copy absolute path to clipboard |
Y (tree) | Copy path relative to tree root to clipboard |
y (content) | Copy current line (or selection if any) to clipboard |
Y (content) | Copy entire file content to clipboard |
. (tree) | Toggle hidden files |
H (tree) | Git history of current file |
Ctrl+O | Recent files (jump to a recently opened file) |
p (tree) | Plugin palette (enable/disable plugins) |
Ctrl+g | Go to line |
Ctrl+b | Toggle full-file blame (dedicated pane replacing the tree) |
B (content) | Toggle single-line blame bar for the active line |
t (tree) | Theme picker |
Ctrl+D | Toggle git mode (changed files + diffs; the pickers above scope to changed files) |
F (tree) | Toggle flat / tree view in git mode |
Tree panel
| Key | Action |
|---|---|
Up/k, Down/j | Move selection |
Enter/Right/l | Expand directory / open file |
Left/h | Collapse directory / go up |
Backspace | Go up one directory (stops at the directory mantis was launched from) |
-/= | Collapse all / expand all |
g/Home, G/End | Jump to first / last entry |
B | Toggle bookmark on the open file |
b | Open the bookmarks picker |
Bookmarks are stored per workspace and restored across sessions. Use the
bookmarks picker to fuzzy-filter the pinned files, then press Enter to open
one. The default bindings are tree-scoped; you can change them in
mantis.toml.
Content panel
The content pane has a line cursor (visible as a highlighted full-width row). Use Up/Down to move it, then press B to show a single-line blame bar for the highlighted line.
When full-file blame is toggled on (Ctrl+b), the tree panel is replaced by a dedicated blame pane listing every line’s commit hash, author, date, and subject, kept in sync with the content cursor. Clicking a row in the blame pane jumps the cursor there and opens the single-line blame bar.
| Key | Action |
|---|---|
Up/k, Down/j | Scroll / move line cursor |
PageUp/PageDown | Page up / down |
Ctrl+Home/g, Ctrl+End/G | Jump to top / bottom |
Left/Right | Horizontal scroll (when wrap off) |
Home/0 | Reset horizontal scroll |
Space | Toggle fold at cursor |
Ctrl+g | Go to line |
B | Blame the active line |
/ | In-file search |
n/N | Next / previous hunk (in a diff) |
M | Toggle raw/rendered markdown (provided by markdown plugin) |
Word wrap, line numbers, JSON pretty-print, CSV/TSV table view, side-by-side diff, and the
staged/unstaged diff cycle have no default content-pane key — use the command
palette (Ctrl+P) or bind one yourself in mantis.toml.
CSV and TSV table view
.csv and .tsv files are parsed (supporting RFC 4180 quotes and escaped characters) and rendered as aligned tables with Unicode box-drawing borders and headers. Like JSON pretty-printing, table formatting respects prettify_size_limit (files exceeding the limit show as raw text). You can toggle between table view and raw text using the command palette (Ctrl+P → “Toggle CSV/TSV table view”) or by binding toggle_table_view in mantis.toml. Wide tables can be scrolled horizontally with Left/Right.
Rendered plugin content and line numbers
mantis has no built-in markdown renderer; install and enable the markdown plugin (p in-app, or [plugins.markdown] in mantis.toml) for rendered Markdown. When a plugin renders a file’s content, line numbers are hidden in the gutter. This is by design: rendered content collapses blank lines, strips code fences, and restructures formatting, so rendered-line numbers don’t correspond to source-file line numbers.
When the markdown plugin is active and rendering a file, you can press M (or run “Toggle markdown render (markdown plugin)” from the command palette) to toggle between the raw file content and the rendered view.
Git features
Tree colors
Files and folders in the tree are colored by their git status:
| Color | Meaning |
|---|---|
| Green | New / untracked |
| Yellow | Modified |
| Red | Deleted |
| Gray | Ignored |
A directory takes the color of the most significant change inside it.
Status bar
The status bar shows a git summary when inside a repository:
[branch +ahead -behind N changed]
Git mode and diff navigation
| Key | Action |
|---|---|
Ctrl+D | Toggle git mode — show only changed files; opening a file shows its diff |
F (tree) | Toggle flat list / nested tree (git mode only) |
n / N | Jump to next / previous change hunk |
B (content) | Blame the current line: hash, author, date, summary |
H (tree) | File history — pick a commit to view its diff |
Side-by-side diff and the staged/unstaged diff cycle have no default key —
use the command palette (Ctrl+P) or bind one in mantis.toml.
Search popup
Three search entry points cover different needs:
Ctrl+T— global fuzzy file-name picker. Opens the same file-name search from either panel, regardless of focus. Use this when you want to jump to any file in the project by name./— context-sensitive: in the tree panel it filters file names inline; in the content panel (with a file open) it opens the in-file search bar; otherwise it falls back to the file-name picker.Ctrl+F(orfin the tree panel) — fuzzy content search across all files (or changed files in git mode).
Open any search popup and just start typing to filter.
In git mode (Ctrl+D), searches are automatically scoped to only the
changed files — the popup title shows “(changed files)” to make this visible.
| Key | Action |
|---|---|
| (type) | Filter results |
Up/Down | Navigate results |
Tab | Switch files / content mode |
Enter | Open selected result |
Esc | Close search |
Ctrl+A | Toggle case-sensitive matching ([Aa]) |
Ctrl+W | Toggle whole-word matching ([\b]) |
Ctrl+R | Toggle regular-expression matching ([.*]) |
The toggles apply to content search (f) and the in-file search bar (/);
the active options are shown as highlighted [Aa] [\b] [.*] indicators.
Command palette
Press Ctrl+P to open a searchable list of every action, each shown
next to its current keybinding. Type to fuzzy-filter (e.g. “blame”, “theme”,
“json”), navigate with Up/Down, and press Enter to run the highlighted
command. It’s the fastest way to discover what mantis can do without
memorizing keys.
Commands that don’t apply to the current state (e.g. “Toggle JSON pretty-print” with no JSON file open, or “Toggle blame” outside a git repo) are shown dimmed with the reason in place of their description. Selecting one anyway sets a status-bar message explaining why it didn’t run, instead of silently doing nothing.
Prefix routing
The palette is also a unified quick-open: type one of these characters as the first character of the query to switch what it searches:
| Prefix | Mode |
|---|---|
| (none) | Commands (the default list above) |
> | Commands (explicit alias) |
/ | File search (fuzzy file names) |
# | Content search (grep across files) |
: | Go to line (42, +5, -3) |
In the file/content modes, Tab toggles between the two (just like the
standalone search overlay), and Enter opens the selected result. Backspace
on an empty routed query — or Esc — returns to the commands list; a second
Esc closes the palette.
Reporting a bug
Run “Report a bug (save diagnostics locally)” from the command palette to
open an interactive modal where you can write a description and preview the
collected diagnostics. Submitting the modal (Ctrl+S or Ctrl+Enter) saves the
report locally, attempts to open the GitHub issue creator in your browser
prefilled with the report, and falls back to copying the report to your clipboard
if needed. See Telemetry & Bug Reports for exactly what the
report contains.
Git mode history
H (while the tree is focused) opens the file’s git history in both normal
and git mode. The diff of a selected commit stays on screen and won’t be
replaced by live file-watcher updates. Press Esc or reload (Ctrl+r/F5)
to return to the current file (or the working-tree diff in git mode).
Open in your editor
Press Ctrl+e (or e while the tree is focused) with a file open to launch
it in your editor. mantis uses $VISUAL, then $EDITOR, preferring nano
over vim when neither is set. The TUI suspends while the editor runs and
resumes when you exit; the file is reloaded afterwards so you see your changes.
💡
$EDITORcan include arguments — e.g.export EDITOR="code --wait"opens the file in VS Code and waits for you to close the tab before returning.
To override the editor without setting environment variables, add to
mantis.toml:
[general]
editor = "code --wait"
When $VISUAL and $EDITOR are both unset and no config override exists,
mantis probes for nano, then vim, and shows a status-bar hint on
return so you know how to change it.
Status bar
The status bar at the bottom of the screen shows context-sensitive information about the open file:
Ln N— the active (highlighted) line number, 1-indexed.[Language]— the detected syntax name from syntect (e.g.[Rust],[Python],[TOML]). Hidden when the file type is not recognised or when viewing a diff.- Scroll percentage — how far through the file the content pane is scrolled.
- Encoding and line endings — shown when
I(file info) is toggled on.
Code folding
Press Space to fold or unfold the block at the cursor. A fold gutter appears
in the content pane when foldable regions are detected, and the status bar shows
fold stats. Fold regions come from two sources: a built-in YAML indentation
detector, and language plugins that supply per-file-type regions over the
plugin protocol. Plugin regions override the built-in output for
their file extension.
Note that folding for a mainstream language like Rust (.rs) or Go (.go) can be
provided by a language provider plugin — the bundled rust and go plugins register
this way. You must explicitly enable such plugins in mantis.toml (or via the plugin
manager popup) for folding to work on those files.
Objects and arrays in .json files can be folded the same way, via the bundled
json plugin (also opt-in). It folds against the pretty-printed view below, so
regions line up whether or not the file was originally minified.
JSON pretty-printing
Viewing a JSON file? Use the command palette (Ctrl+P → “Toggle JSON
pretty-print”) to reformat it with indentation for easier reading, and again
to return to the raw text. Handy for minified .json. There’s no default key
for this — bind toggle_pretty_json in mantis.toml if you want one.
JSON pretty-printing itself is always core, not a plugin concern — the json
plugin only adds fold regions on top of it.
JSON Lines
Files ending in .jsonl or .ndjson are shown as one compact object per line.
Press Space on an object to expand it into formatted JSON in place; press
Space again to collapse it. Invalid or partially-written lines remain visible
as plain text, which keeps mixed structured logs readable.
Secret masking
Credential-shaped files such as .env, .pem, credentials, and kubeconfig
are masked by default. Values use a fixed-width placeholder so their length is
not exposed. Use the command palette action Toggle secret reveal to reveal or
mask the current file for the session; the value is never persisted.
Set content.mask_secrets = false to disable this protection.
Use the command palette action Open JSON query bar to filter or project JSON
and JSONL. The supported subset includes .a.b[0], .items[], {name, image}
and select(.level == "error"). Invalid queries keep the last valid result;
press Esc to restore the unfiltered view.
Log follow mode
When viewing log files (detected via .log extension or level/timestamp sniffing), mantis automatically enables log mode.
- Press
Fto toggle log follow mode. - When follow mode is enabled and pinned (default), mantis auto-scrolls to the tail when new logs are appended to the file.
- Navigating upwards (e.g.,
Up/k,PageUp) will temporarily unpin follow mode to let you read earlier lines. Navigating to the bottom (e.g.,G/End) will re-pin follow mode. - Press
&to open the filter bar. Typing a query will filter the visible log lines to only those containing the query.
Mouse
- Click a tree row to select it — opens a file, or folds/unfolds a directory.
- Double-click a directory to make it the new tree root.
- Click a pane to focus it.
- Scroll wheel scrolls whichever pane is under the cursor.
- Double-click a breadcrumb segment to navigate to that directory.
- In the search and history popups, single-click selects an entry and double-click activates it.
- Right-click a tree row or the content pane to open a context menu at the
cursor. Tree menus offer open, open in editor / default app, reveal in file
manager, copy absolute/relative path, expand/collapse (directory), and
expand/collapse all. Content menus offer copy selection/line/file, word wrap
and raw-markdown toggles, and reveal-in-tree. Navigate with the mouse or
j/k/arrows, activate with Enter or a left-click, and dismiss withEscor a click anywhere else.
Worktree switcher
When a repository has multiple linked worktrees, use the command palette and
choose Open worktree switcher. The picker shows each worktree’s branch and
changed-file count; type to filter, then press Enter to switch the tree to that
worktree. The status bar shows the total worktree count when more than one is
available.
Git Features
mantis has comprehensive git support built in. Inside any git repository you get
status colors, blame, file history, and a dedicated diff-review mode out of the
box.
All git data — repo info for the status bar, per-path status for tree coloring,
blame annotations, and file diffs — is provided natively via the git CLI.
ℹ️ All git features need
giton yourPATHand the file to be tracked in a repository.
Git status colors
Whenever mantis can read git status, tree entries are tinted by their state — new,
modified, deleted, or ignored — so you can see at a glance what’s changed without
doing anything. Control this with git_status in your config (see
Configuration).
The status bar also shows how far the current branch is from its upstream when a
tracking branch exists, using the familiar ↑3 ↓1 summary.
Git blame
Press Ctrl+b with a file open to toggle full-file blame. The tree panel
is replaced by a dedicated blame pane listing every line’s short commit hash,
author, relative date, and subject. Navigate it with the usual tree
keys (↑/↓, PageUp/PageDown, g/G) — they move the cursor through
the file instead of the tree selection while the pane is open. Press Ctrl+b
again (or Esc) to close it and return to the tree.
Press B with the content panel focused to toggle a single-line blame
bar at the bottom of the tree panel, showing the commit hash, author, date,
and subject for the current cursor line.
Blame is disabled while you’re viewing a diff (it only annotates real file content).
Visual-line blame
For inspecting a specific range rather than the whole file, press V with a file
open to enter visual-line mode. The first visible line is selected; extend the
selection a line at a time with j/k (or ↑/↓), jump to the top/bottom of
the file with g/G, and page with PageUp/PageDown. The selected lines are
highlighted with a distinct background.
Press b while in visual-line mode to open a blame panel scoped to the
selection: each line in the range is listed with its short commit hash, author,
relative date, and content. Press b again to dismiss the panel, and Esc to
leave visual-line mode entirely. Like the inline gutter, this is unavailable
while viewing a diff.
Git file history
With a file open in the content panel, press H to open an fzf-style list of the
commits that touched it. Type to fuzzy-filter, navigate with ↑/↓, and press
Enter (or double-click) to load the diff of that revision against your current
working tree into the content panel — additions in green, deletions in red.
Repository commit log
Press L while the tree is focused, or use the command palette (Ctrl+P) to
run Browse repository commits, to search the repository-wide commit log.
The picker shows each commit’s short hash, date, author, and subject. Type to
filter by hash, author, or subject; select a commit and press Enter to review
all changes since it in compare mode. Press Esc to close the picker.
Git mode
Press Ctrl+D to switch the tree to show only files with uncommitted changes
(modified, new, deleted, or renamed). Selecting a file shows its working-tree
diff in the content panel instead of the file contents. The tree title displays a
[git] badge while active — perfect for reviewing everything you’re about to
commit.
Press F (while the tree is focused) inside git mode to toggle between the
tree view (directories intact) and a flat, depth-0 list of every changed file
with relative paths. Press F again to return to the tree view (a no-op
outside git mode).
When the working tree is clean (no uncommitted changes), the tree panel shows a
“Working tree clean” placeholder instead of an empty list, so you can tell at a
glance that there is simply nothing to review. If the current directory is not a
git repository, the placeholder says “Not a git repository” instead. Press
Ctrl+D to exit git mode in either case.
All directories containing changes are auto-expanded when entering git mode.
Diffs refresh on the 30-second auto-reload tick and on manual r.
git_status controls whether tree entries are coloured by git status at startup:
git_status = true # colour tree entries by git status (default: true)
git_show_deleted = false # show ghost nodes for deleted tracked files (default: false)
Compare mode
To review changes against something other than the working tree’s usual
baseline, open the command palette and run Compare against a revision.
A picker overlay opens with three tabs — Commits (default), Tags,
and Branches — switchable with the Left/Right arrow keys. Each tab
shows only items of that category, plus HEAD shortcuts (HEAD, HEAD~1,
HEAD~2) that appear in all tabs. Start typing to fuzzy-filter the list,
or enter any revision (a commit hash, tag, branch name, or something
like HEAD~3) freely — press Enter to select the highlighted item, or
when no items match, the typed text is used as a raw revspec. mantis
switches into git mode: the tree shows only files changed between that
revision and your working tree, and opening a file shows
git diff <rev> -- <file> instead of the usual working-tree diff. The
status bar shows a [compare: <rev>] badge while active.
Press Ctrl+D to leave compare mode and return to full browsing.
Using mantis as git’s pager
mantis can read a diff from stdin (see Pager mode),
so it works as a drop-in side-by-side pager for git diff, git show, and
git log -p:
git diff | mantis # one-off
GIT_PAGER=mantis git log -p # one-off, any pager-using command
git config --global core.pager mantis # every git command, permanently
Configuration
💡 Configuration is optional.
mantisworks great with no config at all. Come here when you want to change colors, remap a key, or set a default behavior.
mantis reads a mantis.toml file. It first looks for one in the directory being
viewed (and its ancestors), then falls back to the global config at
$XDG_CONFIG_HOME/mantis/mantis.toml (or ~/.config/mantis/mantis.toml). A project-local file
overrides the global one, so a repository can ship its own defaults.
Defaults vs. your config
Configuration has two layers:
- Built-in defaults ship inside
mantisand supply every value. You don’t have to set anything. - Your
mantis.tomloverrides only the keys you set; everything else falls through to the defaults.
On first run mantis creates a tiny stub mantis.toml (just a header comment) next to a
read-only mantis.default.toml in your config directory. mantis.default.toml lists
every option with comments and is refreshed on every upgrade, so it always
documents the current set of options. Your own mantis.toml is never modified by an
upgrade — edit it freely.
When you change a setting at runtime (e.g. switching theme), mantis saves only the
keys that differ from the defaults back to your mantis.toml, keeping it small. To
see all available options, open mantis.default.toml; to change one, copy that line
into your mantis.toml.
Live reload
mantis watches your mantis.toml while running. Saving the file re-reads it and
hot-reloads most settings (theme, keybindings, tree/content display options)
immediately — the status bar shows mantis.toml reloaded, or the specific problem
if the file failed to parse or contains unknown keys. Changes to [plugins]
entries can’t be applied to an already-running plugin process, so those show
mantis.toml changed — restart to apply instead and take effect on the next
launch.
Options
Options are grouped into tables by area. A few general keys sit at the top
level (they must appear before the first [table] header); everything else
lives under [tree], [content], [search], or [git].
Migrated from flat keys? Older configs used flat top-level keys (
show_hidden,tree_width,git_status, …). Those still load — they are folded into the grouped tables automatically — but the grouped form below is canonical.mantis.default.tomlis refreshed to the grouped layout on upgrade.
# top-level (must precede any [table])
recent_files_count = 10 # number of recently opened files to remember
palette_pin_recent = true # pin the last-used command atop the Ctrl+P palette
palette_frequent_count = 3 # most-used commands pinned below it; 0 disables
[general]
editor = "code --wait" # external editor command; overrides $VISUAL / $EDITOR
# When unset, falls back to $VISUAL, then $EDITOR,
# then probes for nano before falling back to vim
[tree]
show_hidden = false # show dotfiles / hidden entries
width = 20 # tree panel width as a percentage (5-95) of terminal width
independent_scroll = false # PageUp/Down scroll the viewport, not the selection
indent_guides = true # draw indentation guide lines (│)
icons = false # Nerd Font file-type icons (icon map from a plugin)
[content]
word_wrap = false # wrap long lines in the content pane
line_numbers = true # show the line-number gutter
scrollbar = true # show a scrollbar
scroll_percentage = true # show scroll-position percentage
watch = false # auto-reload the open file when it changes on disk
follow_tail_auto = false # auto-enable follow mode for log files
mask_secrets = true # mask credential-shaped values by default
log_colorizing = true # enable log level and timestamp colorizing
image_preview = true # render images inline on Kitty-graphics terminals (Ghostty, Kitty, WezTerm)
show_file_info = true # encoding + line-ending info in the status bar
prettify_size_limit = 10485760 # max bytes for JSON/YAML pretty-printing and CSV/TSV tables;
# larger files show as raw text (10 MiB default)
[search]
in_file_search = true # enable in-file incremental search via `/`
context_lines = 0 # trailing context lines shown after each match
keep_query = false # restore the last query when reopening search
[git]
status = true # show git status colours/markers in the tree
show_untracked = true # include untracked (??) files
show_ignored = false # include ignored (!!) files
show_deleted = false # ghost nodes for deleted tracked files
ignore_gitignore = false # respect .gitignore when listing files
[git.diff]
mode = "all" # default diff source: "all" (vs HEAD) | "staged" | "unstaged"
side_by_side = false # start the diff view in side-by-side layout
[telemetry]
enabled = false # anonymous, local-only usage log — see Telemetry & Bug Reports
Keybindings
Every keybinding is remappable. Each action takes a list of key specs, so an
action can have several shortcuts. A spec is a single character ("q", "?",
"0") or a named key (Up, Down, Left, Right, Enter, Tab, Esc,
Backspace, PageUp, PageDown, Home, End, Space, F1-F12),
optionally prefixed with modifiers: "ctrl+c", "alt+.", "cmd+p" (cmd /
super / command are all accepted, and map to the platform’s Cmd/Super key).
Panel scoping. A spec can also carry a
tree:orcontent:prefix (e.g."tree:q") to restrict it to that panel; unprefixed specs fire regardless of focus. The shipped defaults scope single-letter shortcuts to the tree panel so the content pane’s letter keyspace stays free for future editing features — only a small movement set (j k h l g G 0 n N) and modifier/F-key/named-key combos work as content-pane defaults. You can still bind a bare letter to a content-view action yourself; user overrides always take effect regardless of scope.
Keyboard layouts. Keybinding specs are written with Latin characters (e.g.
"ctrl+p"). On terminals that support the kitty keyboard protocol (kitty, WezTerm, foot, ghostty, and others),mantisautomatically uses the physical key position instead of the layout-translated character, soctrl+pworks correctly even on non-Latin layouts (Russian, Hebrew, etc.). Terminals without kitty protocol fall back to the logical character — bindings may not trigger as expected on non-Latin layouts in those terminals.
No Ctrl+Shift combinations. Ctrl+Shift shortcuts are unsupported: kitty reserves
ctrl+shiftas its own shortcut prefix (kitty_mod), Windows Terminal bindsCtrl+Shift+P/Ctrl+Shift+Ffor its palette and search, and legacy terminals can’t distinguishctrl+shift+<letter>fromctrl+<letter>at all. Modifier+letter specs are therefore case-insensitive:"ctrl+P","ctrl+shift+p", and"ctrl+p"all mean the same binding, which also fires with CapsLock on or Shift held. (Char case still encodes Shift for unmodified keys:"G"is Shift+G.)
Defaults are editor-style (VS Code / Sublime conventions), with vim motions kept as tree-panel secondaries:
[keys]
# global
quit = ["ctrl+c", "tree:q"]
help = ["F1", "?"]
command_palette = ["ctrl+p", "tree:P"]
reload = ["ctrl+r", "F5", "tree:r"]
switch_panel = ["Tab"]
toggle_hidden = ["tree:."]
theme_picker = ["tree:t"]
plugin_picker = ["tree:p"]
open_in_editor = ["ctrl+e", "tree:e"]
copy_path = ["tree:y"]
copy_relative_path = ["tree:Y"]
copy_line = ["content:y"]
copy_file = ["content:Y"]
toggle_watch = ["tree:W"]
recent_files = ["ctrl+o"]
toggle_bookmark = ["tree:B"]
bookmarks = ["tree:b"]
file_history = ["tree:H"]
goto_line = ["ctrl+g"]
git_mode_toggle = ["ctrl+d"]
git_mode_flat_toggle = ["tree:F"]
# search
search_files = ["/"] # contextual: tree filter / in-file search
find_files = ["ctrl+t"]
search_content = ["ctrl+f", "tree:f"]
# navigation (shared by tree and content panes)
nav_up = ["Up", "k"]
nav_down = ["Down", "j"]
# tree pane
tree_expand = ["Enter", "Right", "l"]
tree_collapse = ["Left", "h"]
tree_up_dir = ["Backspace"]
tree_collapse_all = ["-"]
tree_expand_all = ["="]
tree_width_grow = ["]"] # increase tree pane width
tree_width_shrink = ["["] # decrease tree pane width
fold_toggle = ["Space"]
# content pane
content_left = ["Left"]
content_right = ["Right"]
content_top = ["ctrl+Home", "g", "tree:Home"]
content_bottom = ["ctrl+End", "G", "tree:End"]
content_page_up = ["PageUp"]
content_page_down = ["PageDown"]
content_reset_col = ["Home", "0"]
# toggle_wrap, toggle_line_numbers, toggle_pretty_json, toggle_table_view,
# toggle_diff_side_by_side, and toggle_diff_staged have no default binding —
# they're reachable from the command palette (Ctrl+P); bind them here
# if you'd like a dedicated key.
toggle_blame = ["ctrl+b"]
blame_line = ["content:B"]
# with the blame pane open, opens the file as it was at the commit on the
# active line (takes precedence over open_external's "o" while blame is open)
blame_open_commit = ["o"]
# diff view
diff_hunk_next = ["n"]
diff_hunk_prev = ["N"]
# in a revision diff, toggles between the diff and the full file content
# at that revision
toggle_file_revision = ["ctrl+u"]
macOS.
Keymap::default()layers Cmd-primary bindings for the most frequent actions on top of the table above, keeping everyctrl+binding as a fallback (Terminal.app/iTerm2 intercept mostcmd+shortcuts beforemantissees them; kitty/WezTerm/Ghostty forward them):find_files = ["cmd+t", "ctrl+t"],command_palette = ["cmd+p", "ctrl+p", "tree:P"],search_content = ["cmd+f", "ctrl+f", "tree:f"],reload = ["cmd+r", "ctrl+r", "F5", "tree:r"],recent_files = ["cmd+o", "ctrl+o"],content_top = ["cmd+Up", "ctrl+Home", "g", "tree:Home"],content_bottom = ["cmd+Down", "ctrl+End", "G", "tree:End"],content_reset_col = ["cmd+Left", "Home", "0"].goto_lineandgit_mode_togglestay onctrlon every platform.
Command palette ranking
When you open the command palette with ctrl+p without typing a query, commands
are ranked by frecency — a blend of how often and how recently each command
has been used — rather than shown in a fixed order. Each use adds 1 to a
command’s score, and scores decay by 10% per day since last use, so a command
used heavily last month gradually yields to one used a few times this week.
The most recently used command is pinned at the top; the highest-scoring
commands follow it. Type any character to switch to the usual fuzzy search,
which ignores this ordering.
Two options control the ranking:
# palette_pin_recent = true # pin the last-used command at the top (default: true)
# palette_frequent_count = 3 # how many most-used commands to pin below it; 0 disables (default: 3)
Pinned entries are marked with a ★ prefix in the palette. Usage data is
persisted across sessions in the state directory alongside session history.
Status bar
Segments in the status bar can be placed on the left or right side. Left segments render at column 0; right segments are right-anchored as a block with spaces between. When the terminal is too narrow, low-priority segments are dropped from both sides.
Default mode (both left and right unset): all segments are visible.
The historical default places ["lnum", "type", "git", "version"] on the right,
everything else on the left.
Explicit allowlist mode (either left or right set): only the listed
segment ids render, on their configured side, in the order you specify.
Unlisted segments are hidden. Set both to empty lists for an empty bar.
[statusbar]
# left = ["badges", "scroll", "lnum", "type", "fileinfo", "git", "errors", "folds", "message"]
# right = ["lnum", "type", "git", "version"]
Valid ids: badges scroll lnum type jsonpath fileinfo git errors
folds message version. There is no keybinding-hint segment — the ?/F1
help overlay and the command palette are the discovery surfaces for bindings.
Theme
Themes live under a [theme] table and have their own page: see
Themes for the presets, the live picker (t), and every role you
can override.
Themes
Press t for an fzf-style picker to switch themes live, or set one in config.
Built-in presets: default, monokai, solarized, catppuccin, synthwave84.
Configure under a [theme] table in your mantis.toml. name selects a preset as
the base; each role then overrides it. A role takes a color name (cyan,
lightyellow, reset) or a hex value (#aabbcc); syntax is a
syntect theme name for file contents.
Anything left unset keeps the preset’s value.
Presets ship their own background color; the default theme leaves it transparent
(the terminal’s background). Set transparent_background = true to keep a preset’s
colors but use your terminal’s background instead.
[theme]
name = "catppuccin" # built-in preset to start from
auto_detect = true # true = pick vscode-light or default preset based on terminal background
# Optional per-role overrides on top of the preset:
transparent_background = false # true = use terminal's background instead of the preset's
background = "reset" # panel backgrounds (reset = terminal default)
accent = "cyan" # focused borders, primary highlights
accent_alt = "yellow" # popup chrome, keys, prompts
dim = "darkgray" # unfocused borders, gutters, hints, rules
text = "white" # emphasized / default text
dir = "blue" # directory entries in the tree
file = "reset" # file entries in the tree
selection_bg = "darkgray" # selected row / status bar background
selection_fg = "yellow" # selected row foreground in popups
active_line_bg = "#3a5a5a" # active line cursor highlight (default: selection_bg)
heading1 = "lightcyan" # markdown H1 / table headers
heading2 = "lightyellow" # markdown H2
heading3 = "lightgreen" # markdown H3
code = "lightyellow" # inline code / code blocks
diff_add = "green" # added lines in a diff
diff_del = "red" # removed lines in a diff
git_clean = "green" # statusbar git indicator: clean working tree
git_dirty = "yellow" # statusbar git indicator: uncommitted changes
git_conflict = "red" # statusbar git indicator: conflict / detached HEAD
git_progress = "#ff8700" # statusbar git indicator: rebase / merge in progress
breadcrumb_fg = "cyan" # breadcrumb path bar foreground (default: accent)
breadcrumb_bg = "reset" # breadcrumb path bar background (default: background)
syntax = "base16-ocean.dark"
The background role controls panel backgrounds. When set to reset (the default),
the terminal’s own background shows through. Each preset ships a preferred background
color that you can override or disable with transparent_background. On top of
presets, live theme switching instantly re-applies the current file with the new
syntax highlighting theme.
Telemetry & Bug Reports
mantis ships two local-only diagnostic features: an opt-in usage
telemetry log and an on-demand bug report command. Neither sends
anything anywhere — both write files to your machine that you can inspect,
delete, or choose to share when filing an issue.
Bug reports
Open the command palette (Ctrl+P) and run “Report a bug (save
diagnostics locally)”. This opens a modal dialog where you can write a
description of the bug and preview the diagnostic report below it.
When you submit the report (via Ctrl+S or Ctrl+Enter), mantis:
- Saves the report as markdown under the state directory
(
~/.local/state/mantis/bug-reports/on Linux/macOS,%APPDATA%\mantis\bug-reports\on Windows). - Attempts to open your default browser to create a new GitHub issue pre-filled with your description and the diagnostic report.
- If the report exceeds the URL length limit (~6KB) or if the browser fails to open, it copies the full report to your clipboard so you can paste it manually, updating the status bar with instructions.
The report contains, in full:
- app version and release date
- OS, architecture, OS version, and whether the session runs under WSL
- terminal identity: the
TERM,TERM_PROGRAM,TERM_PROGRAM_VERSION, andCOLORTERMenvironment variables, terminal size, and booleans for Windows Terminal / SSH sessions - workspace shape: visible node/file/directory counts, tree depth, number of expanded directories, walk-error count, and whether it is a git repo
- open-file facts: extension, size, line count, encoding, line endings, detected syntax name, and JSON/diff/memory-mapped flags — never the name
- which config keys differ from defaults (key paths only, never values), the theme name, and the plugin count
- whether telemetry is enabled
It deliberately contains no personal data: no absolute paths, no file or directory names, no file content, no config values, no plugin names, and no environment variables beyond the terminal whitelist above.
Telemetry
Telemetry is disabled by default. Toggle it from the command palette
(Ctrl+P → “Toggle telemetry”) — the status bar confirms the new state
and the setting persists to mantis.toml:
[telemetry]
enabled = true
You can also set this key by hand instead of using the palette.
When enabled, whitelisted events are appended to a local JSONL log under
<state dir>/telemetry/. Each session writes to a fresh, timestamped file
events-<epoch>.jsonl (one file per session), rotated at 1 MiB within a
session; at most five files (one active plus four rotated) are kept across sessions. When disabled
— the default — no events are recorded and no files or threads are created.
Data never leaves your machine; there is no upload endpoint.
Complete event schema
Events are a closed set; each line also carries ts_ms, milliseconds since
the session started (no wall-clock timestamps).
| Event | Fields | Recorded when |
|---|---|---|
session_start | app_version, os, arch, terminal, os_version, wsl, term_program, term_program_version, colorterm, windows_terminal, ssh_session, terminal_size, tree_nodes, tree_files, tree_dirs, tree_max_depth, expanded_dirs, tree_filter_active, walk_errors, git_repo, git_mode, file_open, file_extension, file_size_bytes, file_line_count, file_encoding, file_line_ending, file_syntax, file_is_json, file_is_diff, file_uses_mmap, theme, plugin_count, telemetry_enabled | mantis starts — same privacy rules as bug-report snapshot: counts and whitelisted env vars only, never paths or names |
session_end | duration_s, events_dropped | mantis exits |
action_invoked | action (canonical action id), source (palette, key, mouse) | a command palette, key binding, or mouse action runs |
overlay_opened | kind (help, about, theme_picker, command_palette, etc.) | an overlay popup is opened |
feature_used | feature (fold, diff_nav, git_history, visual_mode, git_blame) | a core feature is used |
plugin_toggled | kind (linter, formatter, etc.), enabled (bool) | a plugin is enabled/disabled |
file_opened | size_bucket (under_1kb, from_1kb_to_1mb, etc.), source_kind (tree, search, history, etc.), encoding, is_binary | a file is opened in the content pane |
perf_span | span (open_file, build_visible, highlight, etc.), duration_bucket (<1ms, 1-16ms, 16-100ms, >100ms) | an instrumented hot-path span finishes |
error_occurred | module, kind (error classification/variant name) | an error event is intercepted |
Raw keystrokes, search queries, palette query text, file names, and paths
are never recorded — the event types above cannot carry them. To stop
collecting, set enabled = false (or remove the key); to erase history,
delete the telemetry/ directory from the state dir.
Plugins
mantis supports two kinds of plugins: process plugins (subprocess-based) and
syntax plugins (syntax definitions loaded into the highlighter).
Process plugins are standalone executables that hook into app events and issue
actions back to the viewer. They run in separate processes; mantis talks to them
over stdin/stdout using newline-delimited JSON, so a plugin can be any
executable — a compiled binary, a Python script, or anything that can read
stdin and write stdout.
Syntax plugins provide .sublime-syntax files that are loaded into the built-in
syntect highlighter at startup. They add syntax highlighting for new file types
without modifying the core binary.
Installing a plugin
Plugins live in the default plugin directory:
- Linux / macOS:
~/.config/mantis/plugins/(or$XDG_CONFIG_HOME/mantis/plugins/if that variable is set) - Windows:
%APPDATA%\mantis\plugins\
Process plugins
Drop the executable there and make it executable (chmod +x my-plugin.sh on
Unix).
Syntax plugins
.sublime-syntax files placed in {plugin_dir}/syntaxes/ are auto-discovered
at startup. They are installed automatically by mantis’s first-run setup.
Registering a plugin
Process plugins
Add a [plugins] section to your mantis.toml:
[plugins]
my-plugin = { path = "my-plugin.sh", enabled = true }
path— path to the executable. Relative paths are resolved against the default plugin directory above. Absolute paths are used as-is.enabled— set tofalseto keep the entry without loading the plugin.
You can register as many plugins as you like:
[plugins]
git-stats = { path = "git-stats.sh" }
file-logger = { path = "/usr/local/bin/mantis-file-logger" }
dev-tools = { path = "dev-tools.py", enabled = false }
Syntax plugins
Syntax plugins can also be registered explicitly in mantis.toml with
kind = "syntax":
[plugins]
terraform = { kind = "syntax", syntax_file = "syntaxes/terraform.sublime-syntax",
extensions = ["tf", "tfvars"] }
toml = { kind = "syntax", syntax_file = "syntaxes/toml.sublime-syntax",
extensions = ["toml"] }
typescript = { kind = "syntax", syntax_file = "syntaxes/typescript.sublime-syntax",
extensions = ["ts", "tsx", "mts", "cts", "jsx"] }
dockerfile = { kind = "syntax", syntax_file = "syntaxes/dockerfile.sublime-syntax",
extensions = ["dockerfile"] }
nginx = { kind = "syntax", syntax_file = "syntaxes/nginx.sublime-syntax" }
justfile = { kind = "syntax", syntax_file = "syntaxes/justfile.sublime-syntax" }
kind—"syntax"for syntax-definition plugins (default:"process").syntax_file— path to the.sublime-syntaxfile. Relative paths are resolved against the plugin directory.extensions— file extensions this syntax should match (optional; the syntax definition itself also declares its own extensions).
Syntax plugins are not spawned as subprocesses. Their syntax definitions are loaded into the highlighter alongside the built-in language set.
What plugins can do
Plugins receive lifecycle and hook events from mantis and can respond with
actions:
| Action | Effect |
|—|—|—|
| show_message | Displays a message in the status bar |
| open_file | Opens a file in the content panel |
| set_content | Replaces content panel with ANSI-escaped lines |
| set_icon_map | Sets file-type icon glyphs (requires Nerd Font) |
| register_language_provider | Declares file extensions and capabilities |
| set_fold_regions | Provides fold regions for a file |
| set_status_facts | Provides a short status-bar summary for a file |
| register_commands | Adds commands to the Ctrl-P command palette |
Each action has specific parameters; see Plugin Development for the full protocol reference.
Protocol version
mantis and plugins communicate over an IPC protocol identified by a version
string. Each plugin declares its protocol version in plugin.toml via the
tv_protocol field. At startup mantis validates that every discovered plugin
matches the host protocol version — plugins with a mismatched version are
silently skipped. The init event sent to each plugin includes the host
protocol version so the plugin can verify compatibility dynamically.
Current protocol version: "2" (bumped from "1" for the 0.8 release).
Upgrading from 0.7: Plugins written for protocol
"1"must updatetv_protocol = "2"in theirplugin.tomland handle the newprotocol_versionfield on theinitevent to remain compatible with 0.8+.
Lifecycle
mantisstarts, reads[plugins]from config, and spawns each enabled plugin.- Each plugin receives an
initevent (with the active theme name and host protocol version). - As you use
mantis, plugins receive hook events (on_file_open, - When you quit, each plugin receives
on_quitthenshutdown, andmantiswaits for each subprocess to exit.
Plugin stderr is discarded (/dev/null). Write debug output to a log file instead.
Example: status-bar clock
A minimal shell plugin that shows the current time in the status bar on every file open:
#!/usr/bin/env bash
# ~/.config/mantis/plugins/clock.sh
while IFS= read -r line; do
event=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('event',''))")
case "$event" in
on_file_open|init)
ts=$(date +"%H:%M:%S")
printf '{"event":"action","action":"show_message","params":{"message":"%s"}}\n' "$ts"
;;
shutdown) exit 0 ;;
esac
done
Register it:
[plugins]
clock = { path = "clock.sh" }
Bundled plugins
mantis ships with several bundled Rust plugins. Each is a workspace member
compiled alongside mantis and installed on first run.
| Plugin | Binary | What it does |
|---|---|---|
| iconize | iconize | On init, sends a set_icon_map action with Nerd Font glyphs for ~80 file extensions. Requires icons = true in mantis.toml and a Nerd Font terminal. |
| markdown | markdown | Renders .md files using pulldown-cmark, sending the output as ANSI-escaped lines via set_content. Responds to theme changes and M keypress for raw/rendered toggle. |
| python | python | Registers as a language provider for .py files with the fold capability. On file open, computes and registers collapsible indentation-based fold regions. |
| rust | rust | Registers as a language provider for .rs files with the fold capability. On file open, computes and registers collapsible curly-brace fold regions. |
| go | go | Registers as a language provider for .go files with the fold capability. On file open, computes and registers collapsible curly-brace fold regions. |
| json | json | Registers as a language provider for .json files with the fold capability. On file open, computes fold regions for multi-line objects and arrays in the displayed JSON. |
| sh | sh | Registers as a language provider for .sh, .bash, and .zsh files with the fold capability. On file open, computes fold regions for function bodies and compound blocks, aware of # comments, quoted strings, and heredocs. |
| yaml | yaml | Registers as a language provider for .yaml/.yml files with the fold capability. On file open, computes and registers collapsible indentation-based fold regions. When enabled, its regions take precedence over the built-in YAML folding. |
| k8s | k8s | Registers as a language provider for .yaml/.yml files with the status_facts capability — coexists with the yaml plugin’s fold registration on the same extensions since they declare different capabilities. On file open, heuristically detects Kubernetes manifests (apiVersion: + kind:) and reports the first resource’s identity (Kind/name (namespace)) plus per-kind counts across ----separated documents (e.g. Deployment/nginx (default) · 3 Deployments · 2 Services · 1 ConfigMap) in the status bar. Files without both apiVersion and kind show no facts. |
Bundled syntax plugins
mantis also bundles syntax-only plugins that provide highlighting for
additional file types without spawning a subprocess.
| Plugin | Extensions | What it highlights |
|---|---|---|
| terraform | .tf, .tfvars | HashiCorp Configuration Language (HCL) used by Terraform |
| toml | .toml | TOML configuration files (Cargo.toml, mantis.toml, pyproject.toml, etc.) |
| typescript | .ts, .tsx, .mts, .cts, .jsx | TypeScript and TSX (JSX) source files |
| dockerfile | Dockerfile, Containerfile | Dockerfile instructions (matched by filename, no extension) |
| nginx | nginx.conf | Nginx server configuration (matched by filename, no extension claim) |
| justfile | justfile | Just (justfile) task recipes (matched by filename, no extension) |
All bundled plugins are compiled as workspace members and installed to the
plugin directory the first time mantis creates its global config. They are all
enabled by default. If you want to disable a bundled plugin, explicitly set
enabled = false in your mantis.toml:
[plugins.rust]
enabled = false
Note: Git features are built into
mantisnatively — no plugin required. Git status colors, blame, file history (H, while the tree is focused), and working-tree diffs all work without any plugin enabled.
Auto-removal of retired plugins
When upgrading from a pre-0.8 install (which used shell-script plugins), superseded
files (git-diff.sh, git-log.sh, iconize.sh) and their [plugins] config entries
are silently removed on first launch. User-authored plugins are never touched.
Plugin Registry
mantis supports a git-backed plugin registry for discovering and installing
third-party plugins. The registry is a plain git repository containing an
index.json file that lists available plugins. No HTTP library is used — mantis
uses git clone and git pull to sync the registry.
How it works
- On first access,
mantisclones the registry repository into the local cache at~/.config/mantis/registry/. - On subsequent access,
mantisrefreshes the cache from the pinnedmainbranch withgit pull --ff-only. - The
index.jsonfile is parsed to build the list of available plugins.
Registry refreshes that cannot be fast-forwarded are recovered by cloning a
fresh main checkout and replacing the cache only after the clone contains a
valid index.json. If recovery fails, the previous cache is preserved when
possible and the error is reported to the caller.
Bundled plugins (such as the built-in python language provider at
plugins/python/) are not listed in the registry — they ship with mantis
and are installed automatically at startup. See
Plugin Development for how bundled plugins are
structured.
Default registry
The default registry is hosted at:
https://github.com/ansromanov/mantis-plugins
To use a different registry, set the MANTIS_PLUGIN_REGISTRY environment variable:
export MANTIS_PLUGIN_REGISTRY="https://github.com/your-org/your-plugin-registry"
Cache location
The registry cache lives at:
| Platform | Path |
|---|---|
| Linux / macOS | ~/.config/mantis/registry/ (or $XDG_CONFIG_HOME/mantis/registry/) |
| Windows | %APPDATA%\mantis\registry\ |
Override with the MANTIS_PLUGIN_REGISTRY_DIR environment variable:
export MANTIS_PLUGIN_REGISTRY_DIR="/custom/path/to/registry"
index.json format
The registry repository must contain an index.json file at its root with the
following structure:
{
"plugins": [
{
"name": "git-tools",
"description": "git diff/log integration",
"repo": "https://github.com/example/tv-git-tools",
"tag": "v0.1.0",
"sha256": "<64 lowercase hexadecimal characters>"
},
{
"name": "markdown-preview",
"description": "Live markdown preview panel",
"repo": "https://github.com/example/tv-md-preview",
"tag": "v0.2.0",
"sha256": "<64 lowercase hexadecimal characters>"
}
]
}
Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Unique plugin name (used for resolution). |
description | Yes | One-line description shown in search results. |
repo | Yes | Git repository URL (HTTPS or SSH). |
tag | Yes | Git tag or branch to check out when installing. |
sha256 | Required for installation | Lowercase hexadecimal SHA-256 digest of the released artifact. Legacy entries may omit it for discovery, but installation must reject them. Uppercase letters are invalid. |
Trust and consent
Registry entries are metadata only; a plugin must be verified before its
artifact is installed. The host compares the downloaded artifact with the
entry’s sha256 value and refuses a mismatch or a missing digest.
Discovered third-party plugin manifests are disabled by default. Discovery
does not execute plugin code; the user must explicitly enable a plugin from
the plugin manager or configuration. The manifest’s permissions field is
advisory and describes requested access for review—it is not a sandbox.
Searching
Use the plugin picker in mantis (accessible via the plugin overlay) to search the
registry by name or description. The search is case-insensitive and matches
substrings in both name and description fields.
Resolution
To look up a specific plugin by name (e.g. for installation), mantis uses exact
case-sensitive matching against the name field in index.json.
Hosting your own registry
To create your own plugin registry:
- Create a new git repository.
- Add an
index.jsonfile following the format above. - Push to a git remote (GitHub, GitLab, self-hosted, etc.).
- Set
MANTIS_PLUGIN_REGISTRYto your repository URL.
Example — create a minimal registry:
mkdir my-plugin-registry
cd my-plugin-registry
git init
cat > index.json << 'EOF'
{
"plugins": [
{
"name": "my-plugin",
"description": "My custom mantis plugin",
"repo": "https://github.com/me/mantis-my-plugin",
"tag": "v1.0.0"
}
]
}
EOF
git add index.json
git commit -m "initial registry"
git remote add origin https://github.com/me/my-plugin-registry
git push -u origin main
Then point mantis at it:
export MANTIS_PLUGIN_REGISTRY="https://github.com/me/my-plugin-registry"
Tip: Make sure the repository is accessible without authentication for
mantisto clone it. Public GitHub repos work out of the box.
Development
Prerequisites
Commands
| Command | Action |
|---|---|
cargo build | Debug build |
cargo build --release | Release build |
cargo run -- [path] | Run with optional path |
cargo test | Run all tests |
cargo check | Type-check only |
cargo clippy --all-targets -- -D warnings | Lint |
cargo fmt --all | Format code |
Or use the justfile shortcuts:
| Command | Action |
|---|---|
just build | Debug build |
just run . | Run against the current directory |
just test | Run the test suite |
just clippy | Lint |
Before committing
cargo fmt --all— check formattingcargo clippy --all-targets -- -D warnings— no warningscargo test— all tests passcargo check— no type errors- No debug
println!,dbg!, or commented-out code
Project structure
src/
├── main.rs # Entry: terminal setup, event loop, dispatch
├── lib.rs # Crate root, re-exports the modules below
├── app/ # App state and input handling
│ ├── mod.rs # core App state + overlays
│ ├── key_handlers/ # keyboard dispatch (incl. open-in-editor)
│ ├── mouse_handlers.rs # click/scroll handling
│ ├── navigation.rs # tree movement & expand/collapse
│ ├── content_pos.rs # scroll / wrap position math
│ └── file_ops.rs # load files, JSON pretty-print, reloads
├── ui/ # ratatui rendering
│ ├── mod.rs # layout & frame composition
│ ├── tree.rs # tree panel
│ ├── content/ # content panel (incl. blame gutter, diffs)
│ ├── popups/ # search / history / palette / help popups
│ └── statusbar.rs # status bar
├── config/ # mantis.toml deserialization, keybinding parsing
├── command_palette.rs # Ctrl+P action list + fuzzy matching
├── search.rs # fuzzy file + full-text content search
├── selection.rs # text selection model
├── tree.rs # Flat Vec<TreeNode> from ignore::WalkBuilder
├── file.rs # Binary file detection (null-byte check)
├── virtual_file.rs # memory-mapped, lazily indexed large files
├── theme.rs # Theme struct + presets, color parsing
├── git.rs # Shells out to `git` for log/diff/blame
├── highlight.rs # syntect syntax highlighting → ratatui styles
└── release_info.rs # bundled "what's new" release metadata
End-to-End (E2E) & Functional Testing
To ensure that releases do not introduce regressions (especially regarding terminal rendering, PTY initialization, and UI interactivity across different terminals), mantis uses a combination of automated and manual E2E/functional testing.
1. Test Dataset (e2e/data/)
A dedicated test dataset is located in the e2e/data/ directory. It contains static sample files representing different edge cases and supported file types:
- rust_sample.rs: Rust code structure to verify syntax highlighting, search, and line numbers.
- json_sample.json: Minified JSON to verify automatic pretty-printing.
- yaml_sample.yml: Nested YAML with anchors (
&) and aliases (*) to verify indentation-based folding and counts. - python_sample.py: Python source file.
- markdown_sample.md: Markdown document.
- long_lines.txt: Extremely long lines of text to verify word-wrapping behavior.
- crlf_sample.txt: Text file with Windows (
\r\n) line endings to verify normalization to LF. - bom_utf8_sample.txt: UTF-8 file with a Byte Order Mark (BOM) to verify BOM detection.
- binary_sample.bin: Binary data containing NUL bytes to verify binary placeholder rendering.
2. Automated E2E Testing
Automated E2E tests are split into two parts:
A. Cargo Integration Tests (tests/e2e_tests.rs)
Runs programmatically using a simulated App state and a TestBackend to verify file parsing, encoding detection, search matching, and folding logic.
B. Whole-Binary TUI Smoke Test (scripts/ci-e2e.py)
Spawns the compiled mantis binary under a pseudo-terminal (PTY) in Python, sets the terminal size, waits for it to render the TUI screen, verifies file tree listings, and exits cleanly with a simulated q keystroke. This verifies real-world raw-mode terminal initialization.
Running Automated E2E Tests
To run all automated E2E tests locally:
just test-e2e
These tests are also run automatically in CI on every Pull Request (in ci.yml) and push to main (in main.yml).
3. Manual Testing Checklist
Run through these verification steps on your target terminals before a release:
- Windows Terminal (PowerShell & WSL)
- iTerm2
- Ghostty
- Alacritty
Launch Command
Launch mantis targeting the test dataset directory:
cargo run -- ./e2e/data
Checklist Steps
| Category | Steps to Execute | Expected Behavior |
|---|---|---|
| 1. File Tree & Nav | Navigate tree with Up/Down arrows or mouse scroll wheel. Double-click or press Enter on directories. | Smooth movement without cursor drift or overlaps. |
| 2. Binary Files | Select binary_sample.bin. | Displays the binary placeholder: [binary file — BIN file, 125 B] and shows instructions. |
| 3. JSON Pretty Print | Select json_sample.json. | The minified JSON is pretty-printed across multiple lines, highlighted, and supports folding. |
| 4. YAML Folding | Select yaml_sample.yml. Focus content pane (Tab). Move cursor to a parent line (e.g. production:) and press Space. | The block collapses. Gutter shows folding indicators (+ / -). Scrolling is adjusted correctly. |
| 5. Word Wrap | Select long_lines.txt. Toggle word wrap via the command palette (Ctrl+P and type wrap) or by pressing its keybinding (if configured). | Long lines wrap cleanly at terminal edge, line numbers align to physical lines, no horizontal scroll needed. |
| 6. Search | Select rust_sample.rs. Press / to open search. Type Rectangle. Press Enter. Press n/N to cycle. | Selection highlights and cursor jumps to each matching keyword. |
| 7. Status Bar | Check status bar while cycling files. | Correct file names, encoding (e.g., UTF-8 BOM for bom_utf8_sample.txt), line endings (CRLF for crlf_sample.txt), and syntax names. |
| 8. Git Diffs | Press Ctrl+D to enter git mode (if in git repository) or view diff history by pressing H (from the tree). Toggle side-by-side diff via the command palette. | Displays diff correctly. Side-by-side mode splits left/right panels. |
| 9. Resizing | Resize the terminal window while running mantis. | Viewport adapts immediately without crashing or breaking layout boundary lines. |
| 10. Root Clamp | With the tree focused at the launch directory, press Backspace. | No-op; status bar shows Already at root. |
| 11. Compare Mode | In a git repo, run Compare against a revision from the command palette (Ctrl+P), select or type a revision in the picker overlay, press Enter. | Tree shows A/M/D/R badges; status bar shows a [compare: <rev>] badge. |
| 12. Blame Rework | Focus content pane on a tracked file, press Ctrl+B for the full blame pane, B for the single-line bottom bar. | Blame pane shows hash/author/date/subject columns; bottom bar shows a 2-line summary. |
| 13. Bundled Language Plugins | Press p to open the plugin manager, verify rust/python/go are enabled (default). | Folding (Space) works on the corresponding language files. |
| 14. Bug Report & Telemetry | Run Report a bug (save diagnostics locally) and Toggle telemetry from the command palette. | Bug report modal opens with a scrollable diagnostic preview; telemetry toggle shows telemetry enabled/disabled in the status bar. |
| 15. First-Run Welcome Overlay | Launch mantis against a fresh state dir (MANTIS_STATE_DIR=$(mktemp -d) cargo run -- ./e2e/data). | A dismissible welcome popup shows 5 essential keybindings; does not reappear after Esc and relaunch. |
See the per-terminal checklists in e2e/checklists/*.md (section “4. New Features”) for detailed step-by-step coverage of items 10–15.
Plugin Development
This page describes how to write both kinds of mantis plugins: process
plugins (subprocess-based) and syntax plugins (.sublime-syntax files).
See Plugins for how to install and configure plugins.
Plugin manifest (plugin.toml)
Every plugin must have a plugin.toml manifest file in its own subdirectory
of the plugin directory (see Plugins for where that is). The
manifest is how mantis discovers the plugin and learns its entry point, version,
and other metadata.
Schema
name = "git-tools" # Required: plugin name (shown in picker)
version = "0.1.0" # Required: semver recommended
description = "git diff on open" # Optional: one-line description
author = "ansromanov" # Optional: author name/handle
entry = "run.sh" # Required: executable relative to this dir
mantis_protocol = "3" # Required: IPC protocol version (tv_protocol still accepted, see below)
platforms = ["linux", "macos"] # Optional: OS filter (default: all)
events = ["on_file_open"] # Optional: events to subscribe to (empty/absent = all, for back-compat)
permissions = ["run_git"] # Optional: required permissions (advisory)
Fields:
| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable name shown in the plugin picker. |
version | Yes | Plugin version. Semver recommended. |
description | No | One-line description displayed in the picker. |
author | No | Author name or handle. |
entry | Yes | Path to the executable, relative to this manifest’s directory. |
mantis_protocol | Yes | IPC protocol version ("3" for the current protocol). Plugins declaring a different version are skipped. The field was named tv_protocol through protocol 2 (pre-rename); mantis_protocol is the current name and tv_protocol remains accepted as an alias — if both are present, mantis_protocol wins. New plugins should use mantis_protocol. |
platforms | No | OS filter: list of "linux", "macos", "windows". Absent = all. |
events | No | Events this plugin subscribes to; only listed events are sent to it. Empty or absent means all events are sent (back-compat with pre-subscription plugins). |
permissions | No | Permissions the plugin needs (advisory, shown for review). |
Protocol version
The mantis_protocol field (or its tv_protocol alias) must match the host’s
expected protocol version. Plugins declaring a mismatched version are silently
skipped during discovery. The host protocol version is also sent to each
plugin on the init event (see below) so the plugin can verify compatibility
dynamically.
| Version | Release | Changes |
|---|---|---|
"1" | 0.7.x | Initial protocol. Events: init, on_file_open, on_keypress, on_selection_change, on_theme_change, on_quit, shutdown. Actions: show_message, open_file, set_content, set_icon_map. Git features (set_file_statuses, set_blame_data, set_status_bar_git_info) were removed in 0.11.22 — git is now built in only. |
"2" | 0.8.x | Language providers (register_language_provider, set_fold_regions), event subscription (events field in manifest), protocol hardening (bounded queues, line caps), protocol_version field on init event. init/on_theme_change additionally carry an optional colors object (0.13.x, additive — does not bump this version) with the active theme’s actual role colors as #rrggbb hex. |
"3" | 0.14.x | Request/response correlation (request/response events) so the host can ask a plugin for something and match the reply; a plugin_error action for reporting failures outside the request/response flow; key-consumption semantics for on_keypress (key_handled action, host waits up to one tick); priority field on register_language_provider plus a status-bar warning on conflicting registrations; manifest field renamed tv_protocol → mantis_protocol (alias kept, see above). As with every prior protocol bump, discovery requires an exact version match: a manifest still declaring "2" is silently skipped, not loaded in a reduced-compatibility mode — plugins must declare "3" (via mantis_protocol, or its tv_protocol alias) to be discovered on this host. highlight capability remains formally reserved and unimplemented: real syntax highlighting continues to flow through syntax plugins (.sublime-syntax + syntect), not language providers. A status_facts capability plus set_status_facts action (0.18.x, additive — does not bump this version) let a provider push a free-text status-bar summary for a file, gated the same way as fold/set_fold_regions; the bundled k8s plugin uses it to report Kubernetes resource identity and per-kind counts for .yaml/.yml files without conflicting with the yaml plugin’s fold registration on the same extensions. |
Discovery
On startup mantis scans every subdirectory of the plugin directory for
plugin.toml. Each discovered manifest produces a (name, PluginEntry) pair
that appears in the plugin picker. Discovered plugins default to disabled
— no code runs without explicit user opt-in via the picker or mantis.toml.
If a plugin is also declared in [plugins] in mantis.toml, the explicit config
entry takes precedence (allowing the user to override the entry path, enable
it, or set its kind).
Process plugins
The protocol for subprocess-based plugins.
Protocol overview
A process plugin is any executable that:
- Reads newline-delimited JSON objects from stdin (events from
mantis). - Writes newline-delimited JSON objects to stdout (actions back to
mantis). - Exits cleanly when it receives
shutdown(or when stdin closes).
mantis spawns each plugin as a subprocess with stdin, stdout, and
stderr all piped. A background reader thread drains each plugin’s stdout and
a background writer thread handles stdin so the mantis event loop never
blocks on plugin I/O. A third background thread drains stderr: it keeps the
most recent line in memory and appends sanitized output to a rotating log
file under the state directory (plugin-logs/<name>.log, capped at 64 KB).
If the plugin exits unexpectedly, that last line and log path are surfaced in
the “exited unexpectedly” message and as a badge in the plugin picker.
Events: tv → plugin (stdin)
Each event is one JSON object on a single line. Unknown fields are ignored.
init
Sent once immediately after spawn, before any user interaction. Includes the host protocol version so the plugin can verify it is compatible.
{
"event": "init",
"theme": "default",
"colors": {
"heading1": "#5fd7ff", "heading2": "#ffffaf", "heading3": "#afffaf",
"accent": "#00ffff", "dim": "#767676", "code": "#ffffaf", "text": "#ffffff"
},
"protocol_version": "2"
}
The protocol_version field is present only on init. If the value does
not match what the plugin expects, the plugin should exit gracefully or
fall back to a compatible subset of features.
The colors field carries the active theme’s actual colors for seven roles
(heading1, heading2, heading3, accent, dim, code, text) as
#rrggbb hex strings, resolved from the theme’s real definition — including
custom themes from mantis.toml. Plugins should use these directly (e.g. as
truecolor ANSI, \x1b[38;2;R;G;Bm) instead of hardcoding a palette per theme
name, so any theme renders correctly without the plugin needing to know it by
name. colors may be absent from an older host; fall back to a built-in
default palette in that case.
on_file_open
Sent when the user opens a file in the content panel.
{"event":"on_file_open","path":"/absolute/path/to/file"}
on_keypress
Sent on every keypress, including inside overlays. The key field uses
human-readable notation: "q", "ctrl+c", "Enter".
{"event":"on_keypress","key":"ctrl+p"}
Key consumption (protocol 3+). A plugin that has on_keypress in its
manifest events list may reply with a key_handled action to claim the
keypress:
{"event":"action","action":"key_handled","params":{"handled":true}}
When at least one subscribed plugin replies handled: true, mantis waits
up to one tick (~16ms) after dispatching on_keypress before deciding
whether to also run its own normal-mode key handling for that key. If any
reply arrives with handled: true within that window, mantis swallows the
key — no built-in binding fires for it. If multiple subscribed plugins
reply, the first handled: true response the host receives within the
window wins and the key is consumed exactly once; a plugin that doesn’t
reply within the window is treated as not having handled the key. Plugins
that never send
key_handled behave exactly as under protocol 2 — the keypress always falls
through to normal handling.
on_selection_change
Sent when the tree cursor moves to a different entry or the content cursor
moves to a different source line. path is absent if the tree is empty. When
the selection refers to an open text file, line contains its zero-based
physical source line; tree-only selections omit it.
{"event":"on_selection_change","path":"/absolute/path/to/entry","line":12}
on_theme_change
Sent when the user switches themes at runtime (via the theme picker or command
palette). The theme field carries the new theme name exactly as configured,
and colors carries its resolved colors (same shape as on init; see above).
{
"event": "on_theme_change",
"theme": "monokai",
"colors": {
"heading1": "#5fd7ff", "heading2": "#ffd787", "heading3": "#afd787",
"accent": "#af87d7", "dim": "#6c6c6c", "code": "#ffd787", "text": "#ffffff"
}
}
command (protocol 3+)
Sent when the user selects one of the plugin’s palette commands (see the
register_commands action below). params.id is the command’s stable id
exactly as the plugin registered it.
{"event":"command","params":{"id":"my-plugin.do-thing"}}
on_quit
Sent when the user initiates a quit (before shutdown). Use this to do any
final work before the process is torn down.
{"event":"on_quit"}
shutdown
Sent as the final event. mantis closes stdin immediately after sending this.
Exit cleanly in response.
{"event":"shutdown"}
Requests: mantis ⇄ plugin (protocol 3+)
Protocol 2 is one-way and fire-and-forget in both directions: the host emits events, the plugin emits actions, and neither side can ask the other for something and wait for a specific reply. Protocol 3 adds a correlated request/response pair on top of the existing event/action stream, used for capabilities that need an answer to a specific question (e.g. “fold regions for this file, now”) rather than a broadcast the plugin may or may not act on.
Host → plugin request, sent on stdin like any other event:
{"event":"request","id":42,"method":"fold_regions","params":{"path":"/absolute/path/to/file"}}
Plugin → host response, sent on stdout as its own line, alongside (but
distinct from) action lines:
{"event":"response","id":42,"result":{"regions":[[0,5],[10,20]]}}
or, on failure:
{"event":"response","id":42,"error":{"message":"failed to parse file"}}
Rules:
idis chosen by the host per outstanding request and must be echoed back unchanged in the response. IDs are not reused while a request is outstanding.- Exactly one of
result/errormust be present. - The host applies a per-plugin timeout to each request (a bounded number of
ticks). If no response arrives in time, the host treats it as an error,
logs it the same way as a
plugin_error(see below), and does not kill the plugin — a slow or missed response degrades gracefully rather than being fatal. request/responseis additive to the existing event/action stream, not a replacement:set_fold_regionspushed unprompted still works exactly as in protocol 2 for plugins that don’t implement requests. The host only sendsrequestevents to plugins that declared protocol 3 in their manifest.
This is the surface the reserved hover, diagnostics, and definition
capabilities are expected to use once implemented — each as a method name
on the same request/response pair, gated by the corresponding capability
in register_language_provider.
Actions: plugin → tv (stdout)
Respond with action objects on stdout. Each object must be on a single line.
Lines that are not valid JSON or that lack "event":"action" are silently
ignored.
show_message
Displays a message in the mantis status bar.
{"event":"action","action":"show_message","params":{"message":"hello from plugin"}}
plugin_error (protocol 3+)
Reports a failure that isn’t tied to a specific request/response pair
(for example, a subscription-only plugin that failed to act on a broadcast
event). Distinct from show_message: it is recorded in the plugin’s
rotating log file (plugin-logs/<name>.log) and surfaced with error styling
in the status bar and plugin picker, rather than treated as routine status
text.
{"event":"action","action":"plugin_error","params":{"message":"failed to parse file","context":"on_file_open"}}
Fields:
message— human-readable error description.context— optional free-form string naming the event/method that failed (advisory, shown alongside the message).
open_file
Opens a file in the content panel.
{"event":"action","action":"open_file","params":{"path":"/tmp/output.txt"}}
set_content
Replaces the content panel with the given lines. Each line is a string that may
contain ANSI escape codes for colour and styling. mantis parses the ANSI codes
with its built-in parser and displays them as styled text. Handy for plugins
that generate rich output (e.g. markdown renderers, linters).
{"event":"action","action":"set_content","params":{"lines":["\u001b[32mgreen line\u001b[0m","plain line"]}}
set_icon_map
Sets the file-type icon glyphs used in the tree. Requires icons = true in mantis.toml and a Nerd Font terminal. Keys in icons are file extensions (lowercase) or full filenames for extensionless files (e.g. "dockerfile").
{"event":"action","action":"set_icon_map","params":{"dir_open":"","dir_closed":"","fallback":"","icons":{"rs":"","py":"","dockerfile":""}}}
Fields:
dir_open— glyph for open directoriesdir_closed— glyph for closed directoriesfallback— glyph used when no extension key matchesicons— map of extension/filename → glyph
Once icons are active, the tree’s ▶/▼ expand/collapse arrows are suppressed —
the dir_open/dir_closed glyph substitutes as the expansion indicator instead.
register_commands (protocol 3+)
Contributes commands to the Ctrl-P command palette. Typically sent once in
response to init. Registered commands appear in the palette after the
built-in commands (they participate in fuzzy search but not in frecency
ranking) and are always applicable. When the user selects one, the host sends
a command event back to the plugin with the command’s id (see the events
section above).
{"event":"action","action":"register_commands","params":{"commands":[
{"id":"my-plugin.do-thing","name":"Do the thing","category":"Plugin","description":"runs the thing"}
]}}
Fields per command:
id— stable identifier, echoed back in thecommandevent on selection. Prefix it with your plugin name to avoid collisions with built-in action ids.name— display name shown in the palette.category— optional grouping label shown as a dimmed prefix.description— optional one-line description shown dim after the name.
Re-registration replaces the plugin’s previous command list entirely; sending
an empty commands array clears it.
Language providers
A process plugin can declare itself as a language provider by responding to
the init event with a register_language_provider action. This tells mantis
which file extensions the plugin handles and what capabilities it provides.
fold is implemented via push (set_fold_regions); highlight is formally
reserved (see below). The reserved capabilities (hover, diagnostics,
definition) are expected to slot in as request/response methods (see
Requests: mantis ⇄ plugin) once
implemented, without a protocol break.
Provider contract
The language provider contract between mantis and a plugin follows a strict
lifecycle:
-
Registration. Immediately after receiving
init, the plugin sendsregister_language_providerto declare its extensions and capabilities. Registration is expected once per plugin — re-registration overwrites the previous registration entirely. -
File routing. When the user opens a file whose extension matches a registered provider,
mantisroutes relevant events (on_file_open,on_selection_change) and capability-driven state requests to that provider. When exactly one provider matches an extension+capability pair, it is used. When more than one provider registers the same extension+capability (protocol 3+), the registration with the higherprioritywins; ties break by registration order (first registered wins). The first time this happens for a given extension+capability pair,mantisshows a one-time status-bar warning naming both plugins, so the conflict isn’t silent. Two providers can register the same extension without conflicting as long as they declare different capabilities — e.g. the bundledyamlplugin ownsfoldfor.yaml/.ymlwhile the bundledk8splugin ownsstatus_factsfor the same extensions.foldandstatus_factsdrive backend state (fold regions, status-bar text, respectively);highlightis reserved for future use (see below). -
Response. For each declared capability, the plugin should respond to file-related events with the corresponding action:
fold→ respond withset_fold_regionswhen a matching file is opened (see below).status_facts→ respond withset_status_factswhen a matching file is opened (see below).highlight→ reserved for future use.
-
Lifetime. Provider registrations persist for the entire plugin session. When a plugin exits or is deactivated, its registrations are removed. If the plugin sends
set_fold_regionsfor a file before it is opened,mantiscaches the regions and applies them when the file is opened later. -
Capability gating.
mantisenforces capability checks at runtime. For example,set_fold_regionsis only accepted when the sender has a registered provider with thefoldcapability for that file’s extension. Unknown capability strings are silently ignored, so existing providers remain compatible with future protocol extensions.
register_language_provider
Sent by the plugin immediately after receiving init. Declares the file
extensions and capabilities the plugin provides. mantis stores the registration
and uses it to route the correct events and display-state updates for each open
file.
{"event":"action","action":"register_language_provider","params":{
"extensions": ["py", "pyi"],
"capabilities": ["fold"],
"priority": 10
}}
Fields:
extensions— lowercase file extensions (no leading dot) this provider handles.capabilities— one or more of"highlight","fold", or"status_facts". Reserved for future use:"hover","diagnostics","definition".priority— optional signed integer, default0(protocol 3+). Used only to break ties when two providers register the same extension+capability pair; higher wins. Absent on protocol 2 plugins, which are treated as priority0.
On highlight: it remains declared but formally reserved as of protocol
3 — mantis accepts the registration and never dispatches anything for it.
Real syntax highlighting is intentionally not routed through language
providers; it flows through syntax plugins (.sublime-syntax files loaded
into syntect, see Syntax plugins below) or the built-in
highlighter. This was evaluated as part of the #296 capability audit and
re-confirmed for v3: provider-driven highlighting would mean re-deriving
styled spans over IPC per file/edit, which the syntect path already does
locally and faster. Revisit only if a concrete plugin use case needs
highlighting decisions syntect’s grammar model can’t express.
After registering, mantis sends on_file_open whenever a matching file is
opened. The plugin should respond with the appropriate action for each declared
capability (e.g. set_fold_regions for "fold").
set_fold_regions
Provides fold regions for a file. Plugin-supplied regions override the
built-in YAML indentation-based folding (the reference implementation)
for that file. Each region is a [start_line, end_line] pair (0-indexed,
inclusive). When the named file is currently open the regions are applied
immediately; when it is not yet open they are cached and applied the next
time the file is opened.
Fold regions from a plugin are only accepted when mantis has a registered
language provider with the fold capability for the file’s extension.
Regions from unregistered plugins or for unmatched extensions are silently
discarded.
{"event":"action","action":"set_fold_regions","params":{
"path": "/absolute/path/to/file",
"regions": [[0, 5], [10, 20]]
}}
set_status_facts
Provides a short, plugin-owned summary string shown in the status bar
alongside the fold-count segment when the named file is the one currently
open — e.g. 3 pubs · 12 fns for a Rust file, or a Kubernetes resource’s
identity (Deployment/nginx (default)). Unlike set_fold_regions, this
carries free text rather than a structured value, so the host does not
interpret it beyond displaying it.
Status facts from a plugin are only accepted when mantis has a registered
language provider with the status_facts capability for the file’s
extension. An empty text clears any previously set fact for that path
(useful when a provider detects the file no longer qualifies, e.g. a YAML
file that stopped looking like a Kubernetes manifest after an edit).
{"event":"action","action":"set_status_facts","params":{
"path": "/absolute/path/to/file",
"text": "Deployment/nginx (default)"
}}
Rules
- One JSON object per line. No pretty-printing, no multi-line objects.
- Stdout is for actions only. Don’t write debug output there — it will break the
protocol. Write to stderr instead:
mantiscaptures it for crash diagnostics (the last line and a rotating on-disk log), but does not otherwise act on it. - Exit on shutdown. When stdin closes or you receive
shutdown, exit. Do not loop forever waiting for more input. - Idempotent reads.
mantismay send multipleon_file_openevents for the same path if the user reopens a file. - No blocking. Actions are drained non-blockingly every tick. Sending many actions in rapid succession is fine; they are buffered and processed in order.
State teardown contract
Every set_* action a plugin sends registers a contribution in the host’s
plugin_contributions map (HashMap<String, PluginContributions>). When the
plugin is disabled via the plugin picker or its process exits unexpectedly,
the host automatically tears down all state that plugin produced — no per-plugin
special cases needed.
What gets torn down:
| Action | State cleared |
|---|---|
set_content | plugin_content / plugin_content_text entries for contributed paths |
set_icon_map | icon_map, icons_enabled, icon_dir_open/closed, icon_fallback |
set_fold_regions | plugin_fold_regions entries for contributed paths; active fold state reset |
set_status_facts | plugin_status_facts entries for contributed paths |
register_language_provider | Provider registration removed |
register_commands | Palette command registrations removed; an open palette listing them is closed |
After clearing, if the disabled plugin had rendered content for the current file, the file is reloaded from disk and falls back to core rendering (markdown, JSON pretty-print, or plain-text with syntax highlighting).
Plugin authors: There is nothing you need to do to opt in — registration
is automatic. If you write a new set_* action in the host code, you must
also stamp it in PluginContributions (in src/plugin/types.rs) and handle
its teardown in App::teardown_plugin_contributions (in src/app/mod.rs).
Without this, the disabled plugin’s output would persist on screen,
violating the invariant: disabled plugin = zero observable effect.
Minimal Rust example
All bundled plugins are Rust crates under plugins/. The simplest possible
plugin responds to init with a status message:
use std::io::{self, BufRead, Write};
fn main() {
let stdin = io::stdin();
let stdout = io::stdout();
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let msg: serde_json::Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(_) => continue,
};
match msg["event"].as_str().unwrap_or("") {
"init" => {
let response = serde_json::json!({
"event": "action",
"action": "show_message",
"params": {"message": "hello from plugin"}
});
let _ = writeln!(stdout.lock(), "{}",
serde_json::to_string(&response).unwrap());
}
"shutdown" => break,
_ => {}
}
}
}
Architecture notes
src/plugin/owns the subprocess lifecycle, the background reader thread per plugin, manifest parsing, binary install, and syntax discovery.PluginManager(src/plugin/manager.rs) collects plugin actions into an internal buffer;App::tick()drains them viadrain_plugin_actions()insrc/app/refresh.rs.- Hook dispatch (
on_file_open,on_keypress,on_selection_change) happens insrc/app/file_ops.rs,src/app/key_handlers/, andsrc/app/navigation.rsrespectively. - Plugin config deserialization lives in
src/config/mod.rsunder thepluginskey. - Bundled plugins are declared in
BUNDLED_PLUGINS(src/plugin/install.rs) as(name, binary_name)pairs and built as workspace-member Rust crates underplugins/. Theinstall_bundled_plugins()function finds and copies compiled binaries to the plugin directory on first run. Thepythonplugin is a language-provider example: it registers forpy/pyiwithfoldcapability atinitand sendsset_fold_regionsonon_file_openusing the sharedmantis::fold_detectors::indent_folddetector. Thejsonplugin follows the same shape for.jsonfiles, usingmantis::fold_detectors::brace_fold_with_brackets(abrace_foldvariant that also folds[…]arrays) run against the same pretty-printed text core renders by default, rather than the raw file — otherwise fold-region line numbers would not line up with what’s on screen for minified JSON. Theyamlplugin follows the same shape foryaml/ymlfiles, using the sharedmantis::fold_detectors::yaml_folddetector — the same algorithm the built-incrate::yaml_fold::detect_fold_regionsre-exports.
Syntax plugins
Syntax plugins provide .sublime-syntax files that extend the built-in
syntect-based highlighter. No subprocess is spawned.
How they work
A syntax plugin is simply a .sublime-syntax file (Sublime Text syntax
definition format, YAML-based). At startup:
- Files in
{plugin_dir}/syntaxes/are auto-discovered and loaded. - Explicit
[plugins]entries withkind = "syntax"are also loaded. - Each syntax definition is added to the
SyntaxSetso syntect associates its declared file extensions with highlights.
Writing a syntax definition
The .sublime-syntax format is documented in the Sublime Text
docs. A minimal syntax file
looks like:
%YAML 1.2
---
name: My Language
file_extensions: [ext1, ext2]
scope: source.my_lang
contexts:
main:
- match: '#.*'
scope: comment.line.number-sign.my_lang
- match: '\b(keyword)\b'
scope: keyword.control.my_lang
The file_extensions key tells syntect which files to highlight with this
syntax. The scope key defines the base scope for highlighting, and contexts
define the matching rules.
Bundled syntax plugins
mantis ships with a terraform.sublime-syntax file that is automatically
installed to {plugin_dir}/syntaxes/ on first run. It provides syntax
highlighting for .tf and .tfvars files (Terraform / HCL). Enable it by:
[plugins]
terraform = { kind = "syntax", syntax_file = "syntaxes/terraform.sublime-syntax",
extensions = ["tf", "tfvars"] }
Or simply leave it in the syntaxes/ directory for auto-discovery (no config
entry needed).
Architecture notes
src/highlight.rs::with_extra_syntaxes()loads extra syntax definitions into the syntax set.src/plugin.rs::load_extra_syntaxes()collects syntax plugins from config entries and thesyntaxes/directory.- Syntax definitions are loaded once at startup and shared between the main
thread’s
Highlighterand the background loader thread’sHighlighter.
Plugin Capability Matrix
Audit of the plugin protocol (v2) as of 0.12.x: what the protocol declares, what the host actually implements, and what the bundled plugins use. This is the “take stock” pass after the 0.8 plugin work (issue #296). For the protocol itself — message formats, examples, contribution/teardown rules — see Plugin Development.
Capabilities
Capabilities are declared by a plugin in its register_language_provider
action and stored as Capability variants (src/plugin/types.rs). The host
routes them via PluginManager::provider_for (src/plugin/manager.rs).
| Capability | Declared in protocol | Handled by host | Used by a bundled plugin |
|---|---|---|---|
fold | yes | yes — gates set_fold_regions in handle_plugin_set_fold_regions (src/app/refresh.rs) | yes — used by the bundled rust, go, python, json, sh, and yaml language provider plugins |
status_facts | yes (0.18.x) | yes — gates set_status_facts in handle_plugin_set_status_facts (src/app/refresh.rs) | yes — used by the bundled k8s plugin, coexisting with yaml’s fold registration on the same .yaml/.yml extensions |
highlight | yes | no — accepted at registration, never checked anywhere | no |
hover | yes (reserved) | no — unimplementable in v2 (no request/response correlation) | no |
diagnostics | yes (reserved) | no — same as hover | no |
definition | yes (reserved) | no — same as hover | no |
Note on highlight: real syntax highlighting flows through syntax plugins
(kind = "syntax", a .sublime-syntax file fed to syntect — see the bundled
terraform plugin), not through language-provider capabilities. The
highlight capability is documented as reserved for future provider-driven
highlighting; today registering it has no effect.
Actions
Every action the host accepts, dispatched in App::handle_plugin_action
(src/app/refresh.rs). Unknown actions are silently ignored.
| Action | Handled by host | Contribution tracked | Torn down | Used by a bundled plugin |
|---|---|---|---|---|
show_message | yes | no (transient status text) | n/a — plugin_message may briefly outlive the plugin; harmless | no |
open_file | yes | no (one-shot navigation) | n/a | no |
set_icon_map | yes | has_icon_map | yes — icon map/fields cleared | yes — iconize |
set_content | yes | content_paths | yes — content removed, current file re-rendered | yes — markdown |
register_language_provider | yes | provider registration in PluginManager | yes — remove_provider_registrations | yes — rust, python, json, yaml, k8s |
set_fold_regions | yes | fold_region_paths | yes — regions removed, fold state reset | yes — rust, python, json, yaml |
set_status_facts | yes | status_fact_paths | yes — facts removed for contributed paths | yes — k8s |
Teardown status: every stateful set_* action stamps PluginContributions
and is cleared by App::teardown_plugin_contributions (src/app/mod.rs).
No teardown gaps were found in this audit.
Protocol v1 git actions (set_file_statuses, set_blame_data,
set_status_bar_git_info) were removed in 0.11.22 along with the retired
shell-script git plugins; git features are built in. They are listed in the
version history in Plugin Development only.
Bundled plugins
| Plugin | Kind | Actions sent | Capabilities registered |
|---|---|---|---|
iconize | process | set_icon_map | none |
markdown | process | set_content | none |
python | process | register_language_provider, set_fold_regions | fold |
rust | process | register_language_provider, set_fold_regions | fold |
go | process | register_language_provider, set_fold_regions | fold |
json | process | register_language_provider, set_fold_regions | fold |
sh | process | register_language_provider, set_fold_regions | fold |
yaml | process | register_language_provider, set_fold_regions | fold |
k8s | process | register_language_provider, set_status_facts | status_facts |
terraform | syntax | none (no subprocess) | n/a — extends syntect directly |
Gaps and follow-ups
- Reserved capabilities (
hover,diagnostics,definition) are unimplementable in protocol v2 — they need id-correlated request/response. Tracked in the protocol v3 proposal (issue #481), which names this audit as its precursor. - The language-provider fold pipeline has bundled consumers —
register_language_provider+Capability::Fold+set_fold_regionsare used by the bundledrust(issue #599),go(issue #600),python(issue #601),json(issue #604),sh(issue #605), andyaml(issue #603) language provider plugins. Therustandgoplugins register thefoldcapability for.rsand.gofiles via the sharedbrace_folddetector (#598); thepythonplugin uses the sharedindent_folddetector; thejsonplugin usesbrace_fold_with_brackets, abrace_foldvariant that also folds[…]arrays; theshplugin usesshell_brace_fold, a shell-specific variant that handles#line comments, single/double quoted strings, and heredocs; theyamlplugin uses the sharedyaml_folddetector — the same algorithm the built-incrate::yaml_fold::detect_fold_regionsre-exports, so plugin-enabled and built-in YAML folding agree. Built-in YAML folding (compute_file_loadinsrc/app/loader.rs) is unchanged in this phase — it still dispatches toyaml_fold::detect_fold_regionsdirectly, and the plugin’s regions only take over when the plugin is enabled (existing override precedence inhandle_plugin_set_fold_regions). Retiring the built-in dispatch is a separate follow-up (issue #603 phase 2), deferred until bundled plugins have a default-enabled mechanism. Known limitation: provider routing is extension-based; extensionless scripts with a#!/bin/bashshebang won’t route to the plugin. Shebang routing is a host/protocol gap (see #605). Capability::Highlightis declared but routes to nothing. Either implement provider-driven highlighting in v3 or re-document it as reserved alongsidehover/diagnostics/definition. Not yet tracked in a dedicated issue; candidate checklist item for #481.status_facts/set_status_facts(0.18.x) resolves the protocol gap that blocked issue #606 (k8s manifest awareness): the epic’s “per-language statusbar facts” item (originally #482, merged into #602) had no protocol surface, and #606 was filed as a proposal rather than a build order for exactly that reason. The new capability is deliberately generic free text, not a structured breadcrumb — the bundledk8splugin uses it for both candidate features from #606 (resource identity and multi-doc per-kind counts) combined into one string, rather than adding a secondbreadcrumbcapability. Not yet solved: per-cursor “which resource is the viewport in right now” needs the host to send line/cursor position onon_selection_change, which the protocol still doesn’t carry — thek8splugin reports the first resource in the file instead of the one under the cursor. That’s a separate, still-open protocol gap.
Roadmap
Last updated: 2026-07-03. Living document — the epics linked below are the source of truth for status; this page explains the why and the sequencing.
Positioning: the reading tool for the AI era
mantis’s pitch has always been a fast way to read code without launching an editor. The opportunity in front of it: in agent-driven development, humans write less and read more. Every coding-agent session produces diffs someone must review, logs someone must check, YAML someone must sanity-check. Editors are optimized for writing; mantis can own the reading and reviewing half of the loop — for humans and for the agents themselves.
Vision: mantis is the review cockpit for humans working with AI agents, and the fastest repo-reading surface for the agents themselves.
Personas
- Backend engineer — reads unfamiliar services, reviews PRs and agent diffs, jumps between code and JSON/YAML fixtures.
- DevOps / SRE — lives over SSH; reads k8s manifests, Terraform,
.envs, and logs; needs instant startup on remote boxes. - AI engineer / agent operator — runs coding agents; wants to watch what the agent changes in real time, feed context into prompts, and give agents tools they can call.
Non-goals (the moat)
Every feature below stays read-only and zero-config by default. mantis will
not grow: editing, LSP/IntelliSense, an embedded terminal, a debugger, or a
plugin-language runtime. When a feature needs to write, it hands off (to
$EDITOR, to the clipboard, to stdout).
The four pillars
1. Agent review cockpit
Epic: #469
The highest-leverage new surface. The primitives already exist (file watcher, git status/diff, side-by-side rendering) — this pillar composes them:
- Prompt-ready copy — copy selection/file as a fenced markdown block with
language and
path:lineheader; the format every agent prompt wants. - Diff-first startup —
mantis --diff [base]opens straight into git mode againstHEAD, a branch, or a range (main...). - Watch mode / review dashboard —
mantis --review: a changed-files panel that refreshes while the agent edits; hunk-walking across files; per-file “seen” marks. - Session summary — on quit, a plain-text list of files/hunks reviewed.
2. Machine interface
Epic: #470
Make mantis useful to the agents themselves. mantis is already a library crate; expose it without the TUI:
- Headless JSON CLI —
mantis cat --range … --json,mantis search --json,mantis tree --json,mantis blame --json. - MCP server —
mantis mcpexposing file-slice, search, diff, blame, and tree tools to any MCP-capable client. - Remote-control socket —
mantis --listen <socket>so an external agent can drive a running viewer (“show the user this diff”). - Protocol hygiene — rename the plugin manifest’s
tv_protocolfield tomantis_protocol(with back-compat) before third-party plugins multiply.
3. DevOps reader
Epic: #471
mantis’s home turf is the terminal over SSH; teach it operational data, not just code:
- Log follow mode (tail-style) with level/timestamp colorizing and a filter bar; JSONL rendering for structured logs.
- JSON path breadcrumb and a jq-style query bar.
- CSV/TSV table view (#71).
- Secret masking for
.env/credential-shaped files with a reveal toggle. - Remote browsing over SSH (#359) — sequenced after log mode; it multiplies everything else in this pillar.
4. Reading UX polish
Epic: #472
Continuous ergonomics investment: theme live preview, scrollable help, onboarding hints, regex/case search toggles, sticky scroll, bookmarks, a context menu, a jump-back navigation stack, line wrap, search result counts.
This pillar is where the overlay count keeps growing (about, blame, and now
help all render scrollable popups; bookmarks and the context menu add more).
Each new scrollable overlay is a prompt to check whether the ad hoc
xxx_scroll field + inline key/mouse match introduced for help should
graduate into a shared ScrollState-style helper (see AGENTS.md →
Consistency & performance) rather than being copied a third time.
5. Language intelligence (Rust · Python · Go)
Epic: #482
Syntax highlighting already covers these; this pillar adds language-aware reading, built in and zero-config like the shipped YAML folding:
- Code folding for Rust/Go (brace-based) and Python (indentation-based) — starter issue #483.
- Symbol outline / go-to-symbol fuzzy picker (regex-based, deliberately not tree-sitter — binary size is a feature).
- Scope context in the breadcrumb, which also feeds sticky scroll (#199).
6. File-type coverage via plugins
Epic: #485
Beyond the three built-in languages, coverage grows through plugins in two
tiers. Syntax packs (.sublime-syntax, terraform-style): TOML,
TypeScript/TSX, and Dockerfile bundled first
(#486), then
Protobuf/GraphQL/Jinja2/nginx and friends via the registry. Rich viewers
(process plugins): a Jupyter .ipynb renderer and a GGUF/safetensors
model-metadata viewer headline the AI persona; Parquet/SQLite previews and a
PEM/JWT decoder serve DevOps. Rich viewers are blocked on the 64 KB plugin
content cap (#449).
Cross-cutting: plugin system hardening
The plugin system is the extension surface for pillars 2, 3, and 5, and a
July 2026 review filed its prerequisites: ship bundled plugins in releases
(#477), remove the
startup cargo build fallback
(#478 — security), plugin
stderr diagnostics (#479),
a registry trust model before the install UI ships
(#480), and protocol v3
(request/response, key consumption, provider priorities —
#481).
Engineering principles
Every change — feature, fix, refactor — is planned before it’s coded, not improvised diff-by-diff. Two questions guide that planning, in order:
- Does this shape already exist somewhere in the app? (an overlay, a picker, a scroll offset, a clamp). If yes, reuse or extend it instead of writing a parallel copy.
- Will this shape recur? If a second consumer is foreseeable (the next overlay, the next picker), design the shared primitive now so the second consumer extends it instead of duplicating it. If not, keep it local — this is not license to abstract on the first use case.
AGENTS.md → “Consistency & performance” is the enforced version of this
for Rust code; this section states the intent behind that rule so it stays
legible as the codebase (and this roadmap) grows.
Sequencing
| Release | Theme | Contents |
|---|---|---|
| 0.13 | Trust & polish | Code-review correctness fixes + quick UX wins — tracking issue #473; plugin packaging/security fixes #477/#478 |
| 0.14 | DevOps reader | Log follow mode + JSONL, --diff base startup, secret masking, CSV tables; code folding for Rust/Go/Python (#483) |
| 0.15 | Agent cockpit | --review dashboard, remote-control socket, session summary, protocol v3 + rename (#481); go-to-symbol picker |
| 0.16 | Machine interface | Headless JSON CLI, then the MCP server on top of it; registry trust model + install UI (#480) |
| 1.0 | AI-native viewer | SSH remote (#359), keymap refactor (#298), comprehensive help (#304), plugin registry maturity |
First two bets: prompt-ready copy (days of work, immediate daily-use hook for the AI crowd) and log follow mode (biggest persona expansion per unit of effort — the machinery is ~80% built).
North-star metrics
- Time-to-first-render stays under 50 ms — every feature is gated on this.
- Weekly installs (Homebrew / install script).
- Share of sessions using git/diff mode — are we becoming the review tool?
- MCP tool-call volume, once shipped.