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
What each release changed, in the order you would meet it. Every feature below works with the default configuration unless the text says otherwise.
The release workflow defaults to a SemVer bump selected from Conventional
Commits. It generates CHANGELOG.md and the GitHub release notes from commits
since the preceding version tag. The curated digests on this page remain the
human-readable feature tour for each release.
v0.21: menus, a clickable status bar, and tabs that scale
Released 11 September 2026.
Version 0.21 is about pointing at things. Everything mantis could already do from the keyboard became reachable by mouse, and the tab strip grew up.
Find any action without knowing its key
Press F10 for the action menu: every action grouped by what it does, each
showing the key that runs it. Actions that cannot apply right now — a git
command outside a repository, a fold command in a file with no fold regions —
appear dimmed rather than missing, so the menu doubles as an explanation of
why something is unavailable.
Set [ui] menu_bar = true in mantis.toml to keep the menu row permanently
visible; hover a menu name to open it and click an action to run it. F10
works either way.
Click the status bar
The status bar became an instrument panel rather than a readout. Click the git, worktree, line, language, fold, error, update, or file-info segment to open its picker or run its action — the branch segment opens the revision picker, the worktree count opens the worktree switcher, the line number opens go-to-line. Segments hidden because the terminal is narrow are not clickable, since they are not there to click.
Tabs at any count
Tab handling now survives having a lot of them:
| Key | Action |
|---|---|
Ctrl+1 … Ctrl+9 | Select tab 1–9 |
Ctrl+0 | Select the last tab |
Ctrl+[ / Ctrl+] | Move the current tab left / right |
Ctrl+Tab | Search open tabs |
Ctrl+Backspace | Reopen the last closed tab |
When tabs outgrow the strip, labels shrink toward a minimum and the strip
scrolls to keep the active tab visible. Click the ‹/› edge affordances or
use the mouse wheel over the strip to reach the rest.
Overlays that close when you expect
A batch of interaction fixes: the theme picker closes on a single Esc,
compare mode exits reliably, and search navigation keys work again while a
query is being typed. See Usage & Keybindings for the full map.
v0.20: one mantis, several projects
Released 8 September 2026.
Version 0.20 made mantis multi-project. Until now one process meant one repository; now it means as many as you want, each with its own tree, content pane, and remembered position.
Open more than one project
Pass several paths at once (mantis dir1 dir2), or press Ctrl+n and type a
directory to open it as a new tab. A tab strip appears above the panes as soon
as a second project is open — click a tab to switch, or its × to close.
Each tab is a full, independent mantis: its own expanded directories, open
file, scroll position, and mantis.toml. The set of open projects is restored
when you relaunch, so a morning’s layout survives a reboot.
Folding for six more formats
Bundled language providers now fold TypeScript and TSX, Go templates, CSS,
TOML, INI, and SQL. Open a file, put the cursor on a foldable construct, and
press Space. Every provider can be disabled individually under [plugins]
in mantis.toml; Plugins lists them.
A worktree switcher that opens instantly
The worktree picker no longer waits for git before drawing. It appears immediately with every worktree listed, and changed-file counts fill in as the background scan finishes.
v0.19: structured data, inline images, and a safer plugin registry
Released 6 September 2026.
Version 0.19 went after the files that are technically text but miserable to read as text — big JSON documents, Kubernetes manifests — and taught mantis to show pictures.
Navigate JSON by path
Open a JSON file and the status bar shows the path to the value under the
cursor, so you always know where you are inside a deep document. To narrow the
view, run Open JSON query bar from Ctrl+P and filter or project with a
jq-flavoured subset: .a.b[0], .items[], {name, image}. It works on JSONL
as well.
Know what a manifest is before you read it
A bundled k8s plugin recognises Kubernetes manifests and summarises them in
the status bar: the first resource’s identity and per-kind counts across
----separated documents, for example
Deployment/nginx (default) · 3 Deployments · 2 Services · 1 ConfigMap. Files
without both apiVersion and kind are left alone.
It rides on a new plugin capability, status_facts, which any language
provider can use to contribute a short status-bar summary — see
Plugin Development.
Deployment/nginx (default) — in the status bar, next to the fold count.See images without leaving the terminal
On terminals that speak the Kitty graphics protocol — Ghostty, Kitty, WezTerm
— images render inline in the content pane. Set content.image_preview = false
to turn it off. Everywhere else, the binary-file placeholder now reports the
image’s real dimensions instead of only its size.
Pin the files you keep coming back to
Press B to bookmark the open file and b to open the bookmarks picker.
Bookmarks are per workspace and restored across sessions, so each project keeps
its own shortlist.
Terraform folding and a stricter registry
HCL joins the folding set: .tf, .tfvars, and .hcl files fold blocks
correctly through comments, quoted strings, and heredocs.
The plugin registry gained a trust model. Installing a plugin now requires a
sha256 digest in the registry index, and mantis refuses an artifact whose
digest is missing or does not match. Plugin Registry
documents the format.
v0.18: logs, tables, worktrees, and a right mouse button
Released 28 August 2026.
Version 0.18 pushed mantis past source code into the other files a working day is made of — logs, exports, credentials, manifests — and made the mouse a first-class way to drive it.
Follow a log while it is being written
Press F to follow a file: mantis stays pinned to the tail as new lines
arrive. Scroll up to read back and follow quietly unpins; return to the bottom
with G or End and it re-pins. Press & to open the filter bar and reduce
the view to lines containing a query.
Read CSV and TSV as tables
.csv and .tsv files render as aligned tables with headers and box-drawing
borders, quotes and escapes handled per RFC 4180. Wide tables scroll sideways
with Left/Right; Toggle CSV/TSV table view in the palette returns to
raw text. Files past prettify_size_limit stay raw, as with JSON.
JSONL files render one row per record, each expandable in place, so a structured log reads as a list instead of a wall.
Credentials stay masked unless you ask
Credential-shaped values in .env and similar files are masked by default,
with a fixed-width placeholder so the length of a secret is not exposed
either. Toggle secret reveal in the palette unmasks the current file for
the session only — nothing is persisted. Set content.mask_secrets = false to
disable the behaviour entirely.
Move between worktrees
Open worktree switcher lists every linked worktree of the current
repository with its branch and changed-file count, and Enter re-roots mantis
there. Each worktree keeps its own session state, so switching back returns you
to where you were. When a repository has more than one worktree, the status bar
says so.
Right-click anywhere useful
Right-click a tree row or the content pane for a context menu at the cursor:
open, open in editor or the default application, reveal in the file manager,
copy absolute or relative path, expand or collapse, and the word-wrap and
raw-markdown toggles. Navigate with the mouse or j/k, activate with Enter
or a click, dismiss with Esc.
Smaller things that add up
nginx.conf and Justfile get syntax highlighting. Narrow terminals explain
themselves instead of rendering a broken frame. Blame mode and git mode stay
visible in the status bar when the content pane has focus. Piped input
(git diff | mantis) is fully interactive.
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, F10 for the action menu, and q to quit.
The menu groups palette actions by area and shows each action’s current keybinding.
Set [ui] menu_bar = true in mantis.toml to keep its row visible for mouse use.
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.
Tabs
Open more than one project at once: pass multiple paths on the command line
(mantis dir1 dir2), or use the command palette’s Open project as new tab
(type a directory path, Enter to open it, Esc to cancel). A Workspaces
list appears at the top of the left panel, above the file tree, whenever more
than one tab is open. Each row shows the project name with its git branch and
changed-file count, and the active workspace is highlighted. Click a row to
switch to it, or its × to close it; drag a row onto another to reorder. The
list takes at most a third of the panel height and scrolls vertically to keep
the active workspace visible; click the ▴/▾ border affordances or use the
mouse wheel on the list to scroll through hidden workspaces.
| Key | Action |
|---|---|
Ctrl+n | Open project as new tab |
Ctrl+w | Close the current tab |
Ctrl+PageDown | Next tab |
Ctrl+PageUp | Previous tab |
Ctrl+1 … Ctrl+9 | Select tab 1 … 9 |
Ctrl+0 | Select the last tab |
Ctrl+[ / Ctrl+] | Move the current tab left / right |
Ctrl+Tab | Search open tabs |
Ctrl+Backspace | Reopen the last closed tab |
Tabs can also be reordered by dragging their labels in the strip. Duplicate root names expand to the shortest unique path suffix, and Git repositories show their branch and changed-file count, truncating long labels or badges when the strip is narrow. Reordering is saved with the workspace manifest; closing a tab saves its per-root session so reopening it restores that state.
Each tab is fully independent — its own tree, open file, scroll position, and
git mode — using the same per-root session persistence
as a single-root launch. The set of open tabs and which one was active are
also remembered and restored automatically the next time you launch mantis
with no path arguments; disable this with [tabs] restore_on_launch = false
in mantis.toml (see Configuration).
The menu row and tree-pane width are workspace chrome, so they stay fixed while switching between tabs even when projects have different UI settings. The active tab still supplies the colors and menu actions.
💡 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.
The help overlay groups actions using the same categories shown in the command palette and action menu.
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 |
F10 | Open the action menu |
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+L | Force a full redraw if the terminal frame is corrupted |
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 |
O (content) | Open the symbol outline (when a provider supplies symbols) |
! (content) | Open diagnostics for the current file (when available) |
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) |
@ | Symbols in the open file |
! | Diagnostics in the open file |
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.
With a language provider that supports symbols enabled, type @ after opening
the palette to fuzzy-search the current file’s functions, types, and other
symbols. Press Enter to jump to the declaration. Press O in the content
pane to open the same outline with the symbol containing the cursor selected.
The status bar shows the enclosing symbol as a scope breadcrumb.
With a language provider that supports diagnostics enabled, type ! to search
the current file’s diagnostics. Press Enter to jump to the selected location.
The content gutter marks diagnostic lines, while the status bar shows diagnostic
counts and the active line’s message.
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.
Click the git, worktree, line, language, fold, error, update, or file-info segment to open its related picker or perform its action. Segments hidden by the status-bar width limit cannot be clicked.
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.
Terraform / HCL files (.tf, .tfvars, .hcl) fold via the bundled
terraform language provider plugin, which detects HCL blocks while ignoring
#/////* */ comments, quoted strings, and heredocs.
INI-family files (.ini, .service, .timer, .conf, .properties, .cfg,
.desktop) fold by section. SQL files (.sql) fold multiline statements and
common procedural blocks such as BEGIN, CASE, IF, and LOOP; comments and
quoted strings are ignored.
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
When [ui] menu_bar = true, hover over a menu name to open its dropdown and
click an action to run it. Unavailable actions appear dimmed. F10 opens the
same menu even when the persistent row is disabled.
- 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.
- Click a status-bar segment to open its related picker or perform its action. The git, worktree, line, language, fold, error, update, and file-info segments are clickable when visible.
- 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, content line, breadcrumb, blame annotation, diff
hunk, or tab to open its context menu.
MenuorShift+F10opens the menu for the focused tree row or content cursor, and Open context menu is also available in the command palette. Tree menus include file and directory operations, bookmarks, scoped search, copying, and a Git submenu when the action applies. Content menus include copy, go-to-line, blame, fold, and revision actions. Breadcrumb, tab, and status-bar menus offer navigation and picker actions. Status-bar menus use the clicked visible segment to offer its related navigation action, such as go-to-line on the cursor segment or Git history on the branch segment. Navigate with the mouse orj/k/arrows, open a submenu with Enter or Right, return with Left or Esc, 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 picker opens immediately; changed-file counts are filled in as
the background scan completes. The status bar shows the total worktree count
when more than one is available.
Press Ctrl+Enter on a worktree to open it in a new tab. If that worktree
already has a tab, Mantis activates it instead of creating a duplicate. The
command palette also offers Open worktree in new tab, where Enter opens the
selected worktree in its own tab. Each tab keeps its own session state.
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 & badges
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. In addition to colors, tree items show explicit badges:
?untracked fileAstaged new fileSstaged changesMunstaged changesSMfile with both staged and unstaged modificationsDdeleted fileRrenamed file
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 diff in the
content panel instead of the file contents.
While git mode is active:
- The tree header displays
[Git mode · Ctrl+D to exit]. - The status bar displays a
[git mode · Ctrl+D]badge. - Clicking the tree top border/header or the status bar badge toggles git mode off.
- Right-clicking the status bar git indicator offers quick access to “Toggle git mode”.
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.
Diff viewing & interactive controls
When viewing any diff (in git mode, compare mode, or file history):
- An informative diff header bar is shown at the top of the content pane:
- Mode indicator:
Diff: all,Diff: staged, orDiff: unstaged. - Hunk statistics: Displays total hunk count in the file (e.g.
3 hunks). - Layout indicator:
[unified]or[side-by-side]. - Quick shortcuts hint:
[s] stage [S] split [n/N] hunks.
- Mode indicator:
- Keyboard shortcuts:
s: Cycle diff mode (all->staged->unstaged->all).S: Toggle between side-by-side and unified diff layout.n/N: Jump to the next / previous diff hunk (@@ ... @@).
- Mouse interactivity:
- Click the diff mode badge on the diff header or status bar to cycle diff modes.
- Click the layout toggle on the diff header or status bar to switch side-by-side view.
- Click any hunk header row to jump or right-click to access hunk actions.
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 outside git mode (default: false)
Git mode always includes deleted tracked files so the change list remains complete, regardless of this setting.
Compare mode
Click the branch name in the status bar to open the revision picker on its Branches tab. Choose a branch to compare it with the working tree. This is read-only: mantis browses another branch without checking it out or changing the working tree. To inspect a file at that revision, use Toggle file at revision while viewing its diff.
You can also open the command palette and run Compare against a revision
to compare against a commit, tag, or branch. The picker has 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). These files are
layered from global to local: a nearer project file overrides only the keys it
sets, so a repository can ship its own defaults without hiding global preferences.
Defaults vs. your config
Configuration has three layers:
- Built-in defaults ship inside
mantisand supply every value. You don’t have to set anything. - Global and project
mantis.tomlfiles override the built-in values. They are merged from global to project-local, with nearer ancestors taking priority only for keys they set.
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], [git], or [ui].
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 outside git mode
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
[ui]
menu_bar = false # keep the action menu row visible; F10 opens it either way
[telemetry]
enabled = false # anonymous, local-only usage log — see Telemetry & Bug Reports
[tabs]
restore_on_launch = true # reopen last session's tabs when launched with no path
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"]
context_menu = ["Menu", "shift+F10"]
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"]
diagnostics_picker = ["content:!"]
goto_line = ["ctrl+g"]
git_mode_toggle = ["ctrl+d"]
git_mode_flat_toggle = ["tree:F"]
# tabs
new_tab = ["ctrl+n"]
close_tab = ["ctrl+w"]
next_tab = ["ctrl+PageDown"]
prev_tab = ["ctrl+PageUp"]
select_tab = ["ctrl+1", "ctrl+2", "ctrl+3", "ctrl+4", "ctrl+5", "ctrl+6", "ctrl+7", "ctrl+8", "ctrl+9", "ctrl+0"]
move_tab_prev = ["ctrl+["]
move_tab_next = ["ctrl+]"]
tab_picker = ["ctrl+Tab"]
reopen_tab = ["ctrl+Backspace"]
# 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, and toggle_table_view
# 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"]
toggle_diff_staged = ["s"]
toggle_diff_side_by_side = ["S"]
# 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"]
separator = " " # drawn before every segment (default: " ")
height = 1 # 1 = single row, 2 = spread the two groups over two rows
Valid ids: badges worktrees scroll lnum type jsonpath scope fileinfo git errors
folds pluginfacts message pluginerror version update. There is no
keybinding-hint segment — the ?/F1 help overlay and the command palette are
the discovery surfaces for bindings.
Separator
separator is the string drawn before every segment. The default " " matches
the historical single-space layout. A wider separator (e.g. " | ") consumes
width from the same elision budget as the segments themselves, so on a narrow
terminal more or different segments may be dropped to make room.
Colors
Per-segment foreground overrides assign any theme color role to a segment id; the override replaces only the foreground, keeping the bar background and the segment’s modifiers:
[statusbar.colors]
git = "accent" # git segment in the accent color
version = "dim"
errors = "diff_del"
Unknown segment ids or theme roles are reported by config validation on reload.
Two rows (height = 2)
With height = 2 the left-aligned group renders on the top row and the
right-aligned group on the bottom row, right-anchored. Instead of dropping low
priority segments from one crowded line, a narrow terminal spreads them across
the second row. Elision still applies per row, so each row independently drops
its lowest-priority segments when they don’t fit. Overlay hints (search, theme
picker, …) render on a single line regardless of this setting. The value is
clamped to 1 or 2.
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).
The bundled toml, css, ini, and sql process plugins provide folding for
configuration sections, CSS-family rulesets, and SQL statements. TOML syntax
highlighting remains available through the bundled toml-syntax syntax plugin.
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-syntax = { kind = "syntax", syntax_file = "syntaxes/terraform.sublime-syntax",
extensions = ["tf", "tfvars"] }
toml-syntax = { kind = "syntax", syntax_file = "syntaxes/toml.sublime-syntax",
extensions = ["toml"] }
typescript-syntax = { kind = "syntax", syntax_file = "syntaxes/typescript.sublime-syntax",
extensions = ["ts", "tsx", "mts", "cts", "jsx", "js", "mjs", "cjs"] }
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_content_chunk / set_content_end | Streams a large rendered document into the content panel |
| 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 |
| set_symbols | Provides a file’s outline symbols for navigation |
| register_commands | Adds commands to the Ctrl-P command palette and Plugins menu |
| register_context_items | Contributes items to context menus (tree, content, tabs, blame) |
| register_status_segments | Contributes persistent segments to the status bar |
Each action has specific parameters; see Plugin Development for the full protocol reference. Plugin commands also appear under the Plugins dropdown in the menu bar.
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: "3". Chunked content actions are an additive
protocol-3 feature and do not require another version bump.
Plugins must declare mantis_protocol = "3" (or the legacy tv_protocol
alias) to be discovered by the current host.
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, streaming ANSI-escaped lines into the content panel. 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 fold and symbols capabilities. On file open, provides curly-brace folds and a symbol outline for functions, types, modules, and methods. |
| 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, reports the first Kubernetes resource’s identity (Kind/name (namespace)) plus per-kind counts across ----separated documents; after the debounced on_content_cursor_change event, it reports the resource containing the cursor. Files without both apiVersion and kind show no facts. |
| terraform | terraform | Registers as a language provider for .tf, .tfvars, and .hcl files with the fold capability. Folds HCL blocks using the shared Terraform detector. |
| toml | toml | Registers as a language provider for .toml files with the fold capability. Folds TOML tables ([table]) and arrays of tables ([[table]]) through the next section or end of file. |
| css | css | Registers as a language provider for .css, .scss, and .less files with the fold capability. Folds nested rulesets, media queries, and keyframes while ignoring comments and quoted strings. |
| typescript | typescript | Registers as a language provider for TypeScript, TSX, JavaScript, and JSX extensions with the fold capability. Disabled by default because the existing syntax pack owns these extensions; enable it explicitly to add folding. |
| template | template | Registers as a language provider for Go-template, Helm, and Jinja2 extensions with the fold capability. |
| ini | ini | Registers as a language provider for .ini, .service, .timer, .conf, .properties, .cfg, and .desktop files. Folds sections through their last meaningful line. |
| sql | sql | Registers as a language provider for .sql files. Folds multiline statements and common BEGIN, CASE, IF, and LOOP blocks while ignoring comments and quoted strings. |
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-syntax | .tf, .tfvars | HashiCorp Configuration Language (HCL) used by Terraform |
| toml-syntax | .toml | TOML configuration files (Cargo.toml, mantis.toml, pyproject.toml, etc.) |
| typescript-syntax | .ts, .tsx, .mts, .cts, .jsx, .js, .mjs, .cjs | TypeScript, TSX, JavaScript, and JSX source files |
| template-syntax | .tpl, .tmpl, .template, .j2, .jinja, .jinja2 | Go-template, Helm, and Jinja2 templates |
| 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
enabled by default, except the TypeScript/JavaScript folding provider, which is
opt-in to avoid changing existing syntax-plugin behavior. Set enabled = false
or enabled = true explicitly in your mantis.toml as needed:
[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. Optional filenames and shebangs selectors for register_language_provider (0.21.x, additive — see #836) also do not change the protocol version. The on_content_cursor_change event (0.21.x, additive — does not bump this version) sends debounced, one-based line and column coordinates to subscribers. Chunked rendered content (set_content_chunk + set_content_end, 0.21.x) is also additive. The diagnostics capability (0.21.x, additive — see #834) uses request/response without changing the protocol version. |
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_content_cursor_change
Sent to subscribers after the content cursor has stayed still for 100 ms. A
burst of cursor moves is coalesced into one event for the latest position.
line and column are one-based. Mantis’ content cursor selects a source
line, so column is currently always 1 (the start of that line). This event
is separate from on_selection_change, whose existing tree and selection
semantics and zero-based line field remain unchanged.
{"event":"on_content_cursor_change","path":"/absolute/path/to/file.yaml","line":13,"column":1}
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, menu bar
entries (see the register_commands action below), or context-menu items
(see register_context_items below). params.id is the command’s stable id
exactly as the plugin registered it. When activated from a context menu,
params.target is also provided:
{"event":"command","params":{"id":"my-plugin.format","target":{"kind":"content","path":"/path/to/file.rs","line":42}}}
target.kind is "tree_file", "tree_dir", "content", "tab", or "blame".
target.path is the target path (or current file path) if available.
target.line is the 1-based physical line number when applicable (content or blame), or null.
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 300 ms timeout to each request (5 seconds in test builds).
If no response arrives in time, the host treats it as an error and
does not kill the plugin. Non-diagnostics requests are logged like a
plugin_error; a missed diagnostics response is silent and leaves the file without diagnostic UI. 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 hover and definition capabilities can use in the
future. Diagnostics uses the same pair today, gated by the diagnostics
capability in register_language_provider.
For diagnostics, the host sends a diagnostics request with the current file
path whenever that file is opened or reloaded:
{"event":"request","id":43,"method":"diagnostics","params":{"path":"/repo/main.py"}}
The plugin responds with a result object containing a diagnostics array:
{"event":"response","id":43,"result":{"diagnostics":[
{"line":4,"column":8,"end_line":4,"end_column":13,
"severity":"error","message":"Undefined name `value`","source":"ruff:F821"}
]}}
Coordinates are zero-based physical source positions; end coordinates are
optional. severity is error, warning, info, or hint; message and
source are strings. The host ignores malformed entries, caps accepted
results at 10,000 items, and ignores late replies after a newer reload.
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"]}}
For large rendered documents, use set_content_chunk followed by
set_content_end. Each chunk is a bounded action, so the host can display
early lines while later chunks are still arriving. content_id identifies one
render pass and must be unique for each new render of a path. index is a
zero-based chunk number; chunks may arrive out of order, and duplicate indexes
are ignored. set_content_end is the explicit terminator and reports the total
number of chunks. The host appends contiguous chunks as they become available
and waits for any gaps before marking the stream complete.
{"event":"action","action":"set_content_chunk","params":{"path":"/tmp/report.md","content_id":"render-17","index":0,"lines":["first line","second line"]}}
{"event":"action","action":"set_content_chunk","params":{"path":"/tmp/report.md","content_id":"render-17","index":1,"lines":["third line"]}}
{"event":"action","action":"set_content_end","params":{"path":"/tmp/report.md","content_id":"render-17","total_chunks":2}}
The host accepts chunks up to 1 MiB and caps a streamed document at 64 MiB,
1,000,000 lines, and 65,536 chunks. These limits complement the existing
4 MiB stdout-line limit and bounded plugin action queue. A stream that exceeds
a limit, has an invalid terminator, or is idle for ten seconds keeps its
already-delivered lines and is marked incomplete in the status bar. A later
render should use a new content_id.
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. Commands registered here also appear under
the Plugins dropdown in the menu bar.
register_context_items (protocol 3+)
Contributes items to context menus across the application. Context menu items are scoped by target kind and optional file extensions, rendered in a dedicated plugin group below built-in items, and never interleaved with host actions.
{"event":"action","action":"register_context_items","params":{"items":[
{
"id": "my-plugin.format",
"label": "Format document",
"target": "content",
"category": "Format",
"extensions": ["rs", "toml"],
"weight": 10
}
]}}
Fields per item:
id— stable identifier, dispatched back to the plugin in acommandevent when activated.label— display label shown in the context menu (truncated to 40 characters).target— context scope kind:"tree_file","tree_dir","content","tab", or"blame".category— optional submenu category (items sharing a category are nested into a submenu).extensions— optional list of file extensions (case-insensitive) the item applies to.weight— optional sort weight (lower numbers sort first; ties are broken alphabetically by label).
Limits and invariants:
- A plugin may contribute at most 16 context items. Registrations exceeding this limit or containing malformed parameters report a
plugin_error. - Re-registration replaces the plugin’s previous items; an empty
itemsarray clears them. - Selecting an item dispatches a
commandevent containingidandtargetinformation (kind,path,line) back to the plugin.
register_status_segments (protocol 3+)
Contributes persistent segments to the status bar. Segments participate in the status bar
layout, elision ladder, and user color customization via [statusbar] in mantis.toml.
{"event":"action","action":"register_status_segments","params":{"segments":[
{
"id": "git-stats",
"text": "±3",
"priority": 3,
"side": "right"
}
]}}
Fields per segment:
id— unique identifier for the segment. Can be referenced in[statusbar]left/rightlists and styled in[statusbar.colors.<id> = "<role>"].text— text content to display (truncated to 40 characters).priority— optional priority for the elision ladder (integer from 1 to 5, default: 2; 5 is highest priority / last to be dropped on narrow screens).side— optional alignment preference:"left"or"right"(default:"right").
Limits and invariants:
- A plugin may contribute at most 8 status segments. Registrations exceeding this limit or containing malformed parameters report a
plugin_error. - Re-registration replaces the plugin’s previous segments; an empty
segmentsarray clears them.
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 and symbols are implemented via push (set_fold_regions and
set_symbols); diagnostics uses a request/response; highlight is formally
reserved (see below). hover and definition can use the same request
transport later 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.foldstatus_facts,symbols, anddiagnosticsdrive backend state (fold regions, status-bar text, symbol outlines, and diagnostics, 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).symbols→ respond withset_symbolswhen a matching file is opened (see below).diagnostics→ respond to the host’sdiagnosticsrequest with adiagnosticsresult array (see above).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, filenames, shebang interpreters, and capabilities the plugin
provides. mantis stores the registration and uses it to route display-state
updates for each open file.
{"event":"action","action":"register_language_provider","params":{
"extensions": ["py", "pyi"],
"filenames": ["Makefile", "Dockerfile*"],
"shebangs": ["python3"],
"capabilities": ["fold"],
"priority": 10
}}
Fields:
extensions— lowercase file extensions (no leading dot) this provider handles.filenames— optional exact filenames or filename globs this provider handles. Globs support*(any sequence) and?(one character); matching is case-sensitive.shebangs— optional interpreter executable names, case-insensitive, such asbash,sh, orpython3. Forenvshebangs the host skips common env flags and assignments before reading the interpreter name.capabilities— one or more of"highlight","fold","status_facts","symbols", or"diagnostics". Reserved for future use:"hover"and"definition".priority— optional signed integer, default0(protocol 3+). Used only to break ties between providers with the same match specificity and capability; higher wins. Match precedence is exact filename, filename glob, extension, then shebang. Equal priority keeps the first registered provider. Absent on protocol 2 plugins, which are treated as priority0.
Shebang detection uses the first line already read by the background file loader and is cached with the loaded path; the UI thread does not reopen files to route providers. Binary files do not match shebang registrations. Conflicts on the same selector and capability produce a one-time status-bar warning.
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 when a file is opened. The host
accepts capability output only from the provider selected by the precedence and
priority rules above (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 that plugin wins routing for
the file with the fold capability, by exact filename, filename glob,
extension, or shebang. Regions from unregistered or non-winning plugins 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 that plugin wins routing for
the file with the status_facts capability. 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)"
}}
set_symbols
Provides a flat symbol list for a file. Symbols power the Ctrl+P @ route,
the O outline picker, and the scope breadcrumb beside the cursor line.
Line numbers are zero-based; end_line is inclusive when supplied. The host
accepts this action only from a provider that declared the symbols capability
for the file extension.
{"event":"action","action":"set_symbols","params":{
"path":"/absolute/path/to/file.rs",
"symbols":[
{"name":"Service","kind":"struct","line":0,"end_line":3},
{"name":"run","kind":"method","line":5,"end_line":8,"parent":"Service"}
]
}}
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_content_chunk / set_content_end | Stream assembly state and partial plugin_content 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 |
set_symbols | plugin_symbols entries for contributed paths |
response (diagnostics) | Diagnostic entries, line index, status-bar segments, and picker data for contributed paths |
register_language_provider | Provider registration removed |
register_commands | Palette command registrations removed; an open palette listing them is closed |
register_context_items | Context menu item registrations removed; an open context menu is closed |
register_status_segments | Status segment registrations removed |
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.rs.on_content_cursor_changeis queued from cursor movement and emitted after its debounce insrc/app/refresh.rs. - 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, .tfvars, and .hcl files (Terraform / HCL).
The terraform syntax is auto-discovered: because it lives in the
syntaxes/ directory and is not tied to a [plugins] entry, it is loaded
unconditionally. Folding for the same extensions is provided separately by
the bundled terraform process plugin, which registers the fold
capability via register_language_provider and answers on_file_open
using the shared mantis::fold_detectors::hcl_brace_fold detector.
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 (v3) as of 0.21.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, yaml, terraform, ini, and sql 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 | yes — sends a correlated diagnostics request to the winning provider, validates results, and clears contributed state on teardown | yes — bundled python plugin uses Ruff when installed |
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
toml/typescript/dockerfile/nginx/justfile plugins), 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 |
response (diagnostics) | yes (protocol 3) | diagnostic_paths | yes — markers, line index, badge, and picker data removed | yes — python / Ruff |
register_commands | yes | command_ids | yes — commands removed, open palette closed | no |
register_context_items | yes | context_item_ids | yes — context items removed, open context menu closed | no |
register_status_segments | yes | status_segment_ids | yes — status segments removed | no |
Teardown status: every stateful registration and 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, response | fold, diagnostics |
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 | process | register_language_provider, set_fold_regions | fold |
ini | process | register_language_provider, set_fold_regions | fold |
sql | process | register_language_provider, set_fold_regions | fold |
Gaps and follow-ups
- Reserved capabilities (
hover,definition) still need concrete UI and contribution models.diagnosticsuses the protocol 3 request/response transport and is implemented by the host; the bundled Python provider uses Ruff when available. - 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),yaml(issue #603), andterraform(issue #782) 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; theterraformplugin useshcl_brace_fold, an HCL-specific variant that handles#/////* */comments, double-quoted strings, and heredocs for.tf,.tfvars, and.hclfiles. 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. Provider routing supports exact filenames, filename globs, extensions, and shebang interpreters. The host reads and caches the shebang during file load; filename specificity wins over extension and shebang matches (see #836). Capability::Highlightis declared but routes to nothing. Either implement provider-driven highlighting in v3 or re-document it as reserved alongsidehover/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. The additiveon_content_cursor_changeevent closes the per-cursor gap: it sends debounced, one-based line and column coordinates, and the bundledk8splugin now reports the resource containing the cursor.
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 & format intelligence
Epic: #602 (absorbing #482 & #485); Roadmap Spec
Language smarts and format viewing are delivered via the v3 plugin architecture and shared core detectors (fold_detectors):
- Code folding across core languages (Rust, Go, Python, Shell, JSON, YAML, Terraform/HCL shipped; TypeScript/JS, INI/Systemd, SQL, PowerShell, Templates next).
- Symbol outline / go-to-symbol fuzzy picker (regex-based heuristics in core, surfaced via provider capability).
- Scope context in the breadcrumb, which also feeds sticky scroll (#199).
- Per-language statusbar facts (protocol shipped in 0.18.x with
plugins/k8s; language fact summaries next).
6. File-type coverage via plugins
Epic: #602
Coverage expands through bundled and registry plugins in two tiers:
- Syntax packs (
.sublime-syntax): TOML, TypeScript/TSX, Dockerfile, Nginx, Justfile, Terraform bundled; PowerShell and Jinja2/Go-template next. - Rich viewers (process plugins): Markdown shipped; SQLite/DB and CSV/TSV table views (#71) planned following the 64 KB plugin buffer enhancement (#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.