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.
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.
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
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
Press ? or F1 for in-app help, and q to quit.
Pager mode
When no <path> is given and stdin is piped rather than a terminal, mantis
reads stdin instead of walking a directory:
git diff | mantis # navigable side-by-side diff
kubectl logs pod | mantis # highlighted, searchable log output
curl -s https://example.com/file.py | mantis --language python
Diff-shaped input (a diff --git/diff --cc header, an @@ - hunk header, or
a --- /+++ file-marker pair) renders through the same side-by-side diff
view as git mode, starting in side-by-side layout regardless of the
[git.diff] side_by_side setting. Anything else is syntax-highlighted: pass
--language <name> (e.g. --language rust) to force it, otherwise mantis
sniffs the first line the way syntect detects shebangs and mode lines.
The tree pane collapses (there is no path driving the view) and focus starts
in the content pane, but the tree is still there — drag the splitter or
press Tab to browse the working directory alongside the piped content.
Keyboard input keeps working normally even though stdin is consumed by the
piped data: mantis reads keys from the controlling terminal (Unix: /dev/tty,
Windows: CONIN$) instead, the same trick less uses. Input is read to EOF
before the UI starts, so very large piped input delays the first frame rather
than streaming incrementally.
Terminal compatibility
Keyboard enhancement
Every default binding uses plain Ctrl+letter, a bare/Shift letter, or a
named key — combinations that work identically on every terminal and OS.
Ctrl+Shift combinations are deliberately not used: kitty reserves
ctrl+shift for its own shortcuts (kitty_mod), Windows Terminal binds
Ctrl+Shift+P/Ctrl+Shift+F itself, and legacy terminals (macOS
Terminal.app, plain xterm, many SSH setups) can’t even distinguish them from
plain Ctrl. Modifier bindings are matched case-insensitively, so CapsLock
or a stray Shift never breaks a shortcut.
On terminals with the kitty keyboard protocol (CSI-u), mantis additionally matches bindings by physical key position, so shortcuts work on non-Latin keyboard layouts.
| Terminal | Keyboard enhancement (layout independence) | Full mouse support |
|---|---|---|
| kitty | ✓ Full | ✓ |
| WezTerm | ✓ Full | ✓ |
| Ghostty | ✓ Full | ✓ |
| Alacritty 0.15+ | ✓ Full | ✓ |
| Windows Terminal | ✓ Full | ✓ |
| iTerm2 | ✓ Partial¹ | ✓ |
| macOS Terminal.app | ✗ | ✓ |
| xterm (plain) | ✗ | Partial² |
| Most SSH clients | ✗ | Depends on client |
| tmux (inside any terminal) | ✗ | ✗ |
¹ iTerm2 supports CSI-u (disambiguation + event types) but may not report alternate keys for all keyboard layouts.
² xterm supports mouse events but the generic mouse protocol (SGR 1006) lacks
drag and release tracking. Enable xterm-mouse in tmux or `XTerm*decTerminalID:
280` for full SGR mouse.
On terminals without keyboard enhancement, mantis shows a one-time notice and
notes the limitation in the in-app help (? → Getting started).
The best terminals for full mantis keyboard support are kitty, WezTerm, Ghostty, Alacritty 0.15+, and Windows Terminal.
Session persistence
mantis automatically remembers your workspace state across restarts:
expanded directories, the last open file, scroll position, and git mode.
State is cached outside the project tree (~/.local/state/mantis/
or %APPDATA%\mantis\) so it survives re-clones and never writes
dotfiles into the repository. Each workspace root gets its own file under
the sessions/ subdirectory. To reset the session for a directory, quit
and delete its file from the sessions/ subdirectory in the state directory.
💡 Can’t remember a key? Press
?orF1for the help overlay, orCtrl+Pto open the command palette and search for an action by name — it shows you the shortcut too. New tomantis? Start with the Quick Start.
Bindings are editor-style (VS Code / Sublime conventions) and fully remappable
— see Keybindings for the complete list, the
macOS (Cmd) variants, and the tree:/content: scoping syntax. The tables
below cover the shipped defaults; single letters (q, p, t, …) only work
while the tree panel is focused — the content pane’s letter keyspace is
kept free, apart from the vim motions below, for future editing features. Any
action not listed with a content-pane key is still reachable from the command
palette (Ctrl+P).
Global
These work no matter which panel is focused.
| Key | Action |
|---|---|
Ctrl+c, q (tree) | Quit |
F1, ? | Toggle help |
Ctrl+P | Command palette (fuzzy-find any action) |
Tab | Switch panel |
/ | Tree filter (tree) / in-file search (content) |
Ctrl+T | Global fuzzy file-name picker |
Ctrl+F, f (tree) | Content (full-text) search |
Ctrl+r, F5, r (tree) | Reload tree |
Ctrl+e, e (tree) | Open current file in $EDITOR |
y (tree) | Copy absolute path to clipboard |
Y (tree) | Copy path relative to tree root to clipboard |
y (content) | Copy current line (or selection if any) to clipboard |
Y (content) | Copy entire file content to clipboard |
. (tree) | Toggle hidden files |
H (tree) | Git history of current file |
Ctrl+O | Recent files (jump to a recently opened file) |
p (tree) | Plugin palette (enable/disable plugins) |
Ctrl+g | Go to line |
Ctrl+b | Toggle full-file blame (dedicated pane replacing the tree) |
B (content) | Toggle single-line blame bar for the active line |
t (tree) | Theme picker |
Ctrl+D | Toggle git mode (changed files + diffs; the pickers above scope to changed files) |
F (tree) | Toggle flat / tree view in git mode |
Tree panel
| Key | Action |
|---|---|
Up/k, Down/j | Move selection |
Enter/Right/l | Expand directory / open file |
Left/h | Collapse directory / go up |
Backspace | Go up one directory (stops at the directory mantis was launched from) |
-/= | Collapse all / expand all |
g/Home, G/End | Jump to first / last entry |
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, 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.
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.
Reporting a bug
Run “Report a bug (save diagnostics locally)” from the command palette to save an anonymous diagnostic report (app version, OS, terminal, workspace shape — no paths, names, or content) under the state directory, then attach it to a GitHub issue. 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, falling back to vim. 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.
Status bar
The status bar at the bottom of the screen shows context-sensitive information about the open file:
Ln N— the active (highlighted) line number, 1-indexed.[Language]— the detected syntax name from syntect (e.g.[Rust],[Python],[TOML]). Hidden when the file type is not recognised or when viewing a diff.- Scroll percentage — how far through the file the content pane is scrolled.
- Encoding and line endings — shown when
I(file info) is toggled on.
Code folding
Press Space to fold or unfold the block at the cursor. A fold gutter appears
in the content pane when foldable regions are detected, and the status bar shows
fold stats. Fold regions come from two sources: a built-in YAML indentation
detector, and language plugins that supply per-file-type regions over the
plugin protocol. Plugin regions override the built-in output for
their file extension.
Note that folding for a mainstream language like Rust (.rs) or Go (.go) can be
provided by a language provider plugin — the bundled rust and go plugins register
this way. You must explicitly enable such plugins in mantis.toml (or via the plugin
manager popup) for folding to work on those files.
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.
Mouse
- Click a tree row to select it — opens a file, or folds/unfolds a directory.
- Double-click a directory to make it the new tree root.
- Click a pane to focus it.
- Scroll wheel scrolls whichever pane is under the cursor.
- Double-click a breadcrumb segment to navigate to that directory.
- In the search and history popups, single-click selects an entry and double-click activates it.
Git Features
mantis has comprehensive git support built in. Inside any git repository you get
status colors, blame, file history, and a dedicated diff-review mode out of the
box.
All git data — repo info for the status bar, per-path status for tree coloring,
blame annotations, and file diffs — is provided natively via the git CLI.
ℹ️ All git features need
giton yourPATHand the file to be tracked in a repository.
Git status colors
Whenever mantis can read git status, tree entries are tinted by their state — new,
modified, deleted, or ignored — so you can see at a glance what’s changed without
doing anything. Control this with git_status in your config (see
Configuration).
The status bar also shows how far the current branch is from its upstream when a
tracking branch exists, using the familiar ↑3 ↓1 summary.
Git blame
Press Ctrl+b with a file open to toggle full-file blame. The tree panel
is replaced by a dedicated blame pane listing every line’s short commit hash,
author, relative date, and subject. Navigate it with the usual tree
keys (↑/↓, PageUp/PageDown, g/G) — they move the cursor through
the file instead of the tree selection while the pane is open. Press Ctrl+b
again (or Esc) to close it and return to the tree.
Press B with the content panel focused to toggle a single-line blame
bar at the bottom of the tree panel, showing the commit hash, author, date,
and subject for the current cursor line.
Blame is disabled while you’re viewing a diff (it only annotates real file content).
Visual-line blame
For inspecting a specific range rather than the whole file, press V with a file
open to enter visual-line mode. The first visible line is selected; extend the
selection a line at a time with j/k (or ↑/↓), jump to the top/bottom of
the file with g/G, and page with PageUp/PageDown. The selected lines are
highlighted with a distinct background.
Press b while in visual-line mode to open a blame panel scoped to the
selection: each line in the range is listed with its short commit hash, author,
relative date, and content. Press b again to dismiss the panel, and Esc to
leave visual-line mode entirely. Like the inline gutter, this is unavailable
while viewing a diff.
Git file history
With a file open in the content panel, press H to open an fzf-style list of the
commits that touched it. Type to fuzzy-filter, navigate with ↑/↓, and press
Enter (or double-click) to load the diff of that revision against your current
working tree into the content panel — additions in green, deletions in red.
Git mode
Press Ctrl+D to switch the tree to show only files with uncommitted changes
(modified, new, deleted, or renamed). Selecting a file shows its working-tree
diff in the content panel instead of the file contents. The tree title displays a
[git] badge while active — perfect for reviewing everything you’re about to
commit.
Press F (while the tree is focused) inside git mode to toggle between the
tree view (directories intact) and a flat, depth-0 list of every changed file
with relative paths. Press F again to return to the tree view (a no-op
outside git mode).
When the working tree is clean (no uncommitted changes), the tree panel shows a
“Working tree clean” placeholder instead of an empty list, so you can tell at a
glance that there is simply nothing to review. If the current directory is not a
git repository, the placeholder says “Not a git repository” instead. Press
Ctrl+D to exit git mode in either case.
All directories containing changes are auto-expanded when entering git mode.
Diffs refresh on the 30-second auto-reload tick and on manual r.
git_status controls whether tree entries are coloured by git status at startup:
git_status = true # colour tree entries by git status (default: true)
git_show_deleted = false # show ghost nodes for deleted tracked files (default: false)
Compare mode
To review changes against something other than the working tree’s usual
baseline, open the command palette and run Compare against a revision.
A prompt appears at the bottom of the content pane — type any revision
(a commit hash, a tag, a branch name, or something like HEAD~3) and press
Enter. mantis switches into git mode, but the tree now shows only files
changed between that revision and your working tree, and opening a file
shows git diff <rev> -- <file> instead of the usual working-tree diff. The
status bar shows a [compare: <rev>] badge while active.
Press Ctrl+D to leave compare mode and return to full browsing.
Using mantis as git’s pager
mantis can read a diff from stdin (see Pager mode),
so it works as a drop-in side-by-side pager for git diff, git show, and
git log -p:
git diff | mantis # one-off
GIT_PAGER=mantis git log -p # one-off, any pager-using command
git config --global core.pager mantis # every git command, permanently
Configuration
💡 Configuration is optional.
mantisworks great with no config at all. Come here when you want to change colors, remap a key, or set a default behavior.
mantis reads a mantis.toml file. It first looks for one in the directory being
viewed (and its ancestors), then falls back to the global config at
$XDG_CONFIG_HOME/mantis/mantis.toml (or ~/.config/mantis/mantis.toml). A project-local file
overrides the global one, so a repository can ship its own defaults.
Defaults vs. your config
Configuration has two layers:
- Built-in defaults ship inside
mantisand supply every value. You don’t have to set anything. - Your
mantis.tomloverrides only the keys you set; everything else falls through to the defaults.
On first run mantis creates a tiny stub mantis.toml (just a header comment) next to a
read-only mantis.default.toml in your config directory. mantis.default.toml lists
every option with comments and is refreshed on every upgrade, so it always
documents the current set of options. Your own mantis.toml is never modified by an
upgrade — edit it freely.
When you change a setting at runtime (e.g. switching theme), mantis saves only the
keys that differ from the defaults back to your mantis.toml, keeping it small. To
see all available options, open mantis.default.toml; to change one, copy that line
into your mantis.toml.
Live reload
mantis watches your mantis.toml while running. Saving the file re-reads it and
hot-reloads most settings (theme, keybindings, tree/content display options)
immediately — the status bar shows mantis.toml reloaded, or the specific problem
if the file failed to parse or contains unknown keys. Changes to [plugins]
entries can’t be applied to an already-running plugin process, so those show
mantis.toml changed — restart to apply instead and take effect on the next
launch.
Options
Options are grouped into tables by area. A few general keys sit at the top
level (they must appear before the first [table] header); everything else
lives under [tree], [content], [search], or [git].
Migrated from flat keys? Older configs used flat top-level keys (
show_hidden,tree_width,git_status, …). Those still load — they are folded into the grouped tables automatically — but the grouped form below is canonical.mantis.default.tomlis refreshed to the grouped layout on upgrade.
# top-level (must precede any [table])
recent_files_count = 10 # number of recently opened files to remember
palette_pin_recent = true # pin the last-used command atop the Ctrl+P palette
palette_frequent_count = 3 # most-used commands pinned below it; 0 disables
[tree]
show_hidden = false # show dotfiles / hidden entries
width = 28 # tree panel width in columns
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
show_file_info = true # encoding + line-ending info in the status bar
prettify_size_limit = 10485760 # max bytes for JSON/YAML pretty-printing;
# larger files show as raw text (10 MiB default)
[search]
in_file_search = true # enable in-file incremental search via `/`
context_lines = 0 # trailing context lines shown after each match
keep_query = false # restore the last query when reopening search
[git]
status = true # show git status colours/markers in the tree
show_untracked = true # include untracked (??) files
show_ignored = false # include ignored (!!) files
show_deleted = false # ghost nodes for deleted tracked files
ignore_gitignore = false # respect .gitignore when listing files
[git.diff]
mode = "all" # default diff source: "all" (vs HEAD) | "staged" | "unstaged"
side_by_side = false # start the diff view in side-by-side layout
[telemetry]
enabled = false # anonymous, local-only usage log — see Telemetry & Bug Reports
Keybindings
Every keybinding is remappable. Each action takes a list of key specs, so an
action can have several shortcuts. A spec is a single character ("q", "?",
"0") or a named key (Up, Down, Left, Right, Enter, Tab, Esc,
Backspace, PageUp, PageDown, Home, End, Space, F1-F12),
optionally prefixed with modifiers: "ctrl+c", "alt+.", "cmd+p" (cmd /
super / command are all accepted, and map to the platform’s Cmd/Super key).
Panel scoping. A spec can also carry a
tree:orcontent:prefix (e.g."tree:q") to restrict it to that panel; unprefixed specs fire regardless of focus. The shipped defaults scope single-letter shortcuts to the tree panel so the content pane’s letter keyspace stays free for future editing features — only a small movement set (j k h l g G 0 n N) and modifier/F-key/named-key combos work as content-pane defaults. You can still bind a bare letter to a content-view action yourself; user overrides always take effect regardless of scope.
Keyboard layouts. Keybinding specs are written with Latin characters (e.g.
"ctrl+p"). On terminals that support the kitty keyboard protocol (kitty, WezTerm, foot, ghostty, and others),mantisautomatically uses the physical key position instead of the layout-translated character, soctrl+pworks correctly even on non-Latin layouts (Russian, Hebrew, etc.). Terminals without kitty protocol fall back to the logical character — bindings may not trigger as expected on non-Latin layouts in those terminals.
No Ctrl+Shift combinations. Ctrl+Shift shortcuts are unsupported: kitty reserves
ctrl+shiftas its own shortcut prefix (kitty_mod), Windows Terminal bindsCtrl+Shift+P/Ctrl+Shift+Ffor its palette and search, and legacy terminals can’t distinguishctrl+shift+<letter>fromctrl+<letter>at all. Modifier+letter specs are therefore case-insensitive:"ctrl+P","ctrl+shift+p", and"ctrl+p"all mean the same binding, which also fires with CapsLock on or Shift held. (Char case still encodes Shift for unmodified keys:"G"is Shift+G.)
Defaults are editor-style (VS Code / Sublime conventions), with vim motions kept as tree-panel secondaries:
[keys]
# global
quit = ["ctrl+c", "tree:q"]
help = ["F1", "?"]
command_palette = ["ctrl+p", "tree:P"]
reload = ["ctrl+r", "F5", "tree:r"]
switch_panel = ["Tab"]
toggle_hidden = ["tree:."]
theme_picker = ["tree:t"]
plugin_picker = ["tree:p"]
open_in_editor = ["ctrl+e", "tree:e"]
copy_path = ["tree:y"]
copy_relative_path = ["tree:Y"]
copy_line = ["content:y"]
copy_file = ["content:Y"]
toggle_watch = ["tree:W"]
recent_files = ["ctrl+o"]
file_history = ["tree:H"]
goto_line = ["ctrl+g"]
git_mode_toggle = ["ctrl+d"]
git_mode_flat_toggle = ["tree:F"]
# search
search_files = ["/"] # contextual: tree filter / in-file search
find_files = ["ctrl+t"]
search_content = ["ctrl+f", "tree:f"]
# navigation (shared by tree and content panes)
nav_up = ["Up", "k"]
nav_down = ["Down", "j"]
# tree pane
tree_expand = ["Enter", "Right", "l"]
tree_collapse = ["Left", "h"]
tree_up_dir = ["Backspace"]
tree_collapse_all = ["-"]
tree_expand_all = ["="]
fold_toggle = ["Space"]
# content pane
content_left = ["Left"]
content_right = ["Right"]
content_top = ["ctrl+Home", "g", "tree:Home"]
content_bottom = ["ctrl+End", "G", "tree:End"]
content_page_up = ["PageUp"]
content_page_down = ["PageDown"]
content_reset_col = ["Home", "0"]
# toggle_wrap, toggle_line_numbers, toggle_pretty_json,
# toggle_diff_side_by_side, and toggle_diff_staged have no default binding —
# they're reachable from the command palette (Ctrl+P); bind them here
# if you'd like a dedicated key.
toggle_blame = ["ctrl+b"]
blame_line = ["content:B"]
# diff view
diff_hunk_next = ["n"]
diff_hunk_prev = ["N"]
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 recency and frequency rather than shown in a fixed order. The most
recently used command is pinned at the top; the most frequently used commands
follow it. Type any character to switch to the usual fuzzy search, which ignores
this ordering.
Two options control the ranking:
# palette_pin_recent = true # pin the last-used command at the top (default: true)
# palette_frequent_count = 3 # how many most-used commands to pin below it; 0 disables (default: 3)
Pinned entries are marked with a ★ prefix in the palette. Usage data is
persisted across sessions in the state directory alongside session history.
Status bar
Segments in the status bar can be placed on the left or right side. Left segments render at column 0; right segments are right-anchored as a block with spaces between. When the terminal is too narrow, low-priority segments are dropped from both sides.
Default mode (both left and right unset): all segments are visible.
The historical default places ["lnum", "type", "git", "version"] on the right,
everything else on the left.
Explicit allowlist mode (either left or right set): only the listed
segment ids render, on their configured side, in the order you specify.
Unlisted segments are hidden. Set both to empty lists for an empty bar.
[statusbar]
# left = ["badges", "scroll", "lnum", "type", "fileinfo", "git", "errors", "folds", "message"]
# right = ["lnum", "type", "git", "version"]
Valid ids: badges scroll lnum type fileinfo git errors
folds message version. There is no keybinding-hint segment — the ?/F1
help overlay and the command palette are the discovery surfaces for bindings.
Theme
Themes live under a [theme] table and have their own page: see
Themes for the presets, the live picker (t), and every role you
can override.
Themes
Press t for an fzf-style picker to switch themes live, or set one in config.
Built-in presets: default, monokai, solarized, catppuccin, synthwave84.
Configure under a [theme] table in your mantis.toml. name selects a preset as
the base; each role then overrides it. A role takes a color name (cyan,
lightyellow, reset) or a hex value (#aabbcc); syntax is a
syntect theme name for file contents.
Anything left unset keeps the preset’s value.
Presets ship their own background color; the default theme leaves it transparent
(the terminal’s background). Set transparent_background = true to keep a preset’s
colors but use your terminal’s background instead.
[theme]
name = "catppuccin" # built-in preset to start from
auto_detect = true # true = pick vscode-light or default preset based on terminal background
# Optional per-role overrides on top of the preset:
transparent_background = false # true = use terminal's background instead of the preset's
background = "reset" # panel backgrounds (reset = terminal default)
accent = "cyan" # focused borders, primary highlights
accent_alt = "yellow" # popup chrome, keys, prompts
dim = "darkgray" # unfocused borders, gutters, hints, rules
text = "white" # emphasized / default text
dir = "blue" # directory entries in the tree
file = "reset" # file entries in the tree
selection_bg = "darkgray" # selected row / status bar background
selection_fg = "yellow" # selected row foreground in popups
active_line_bg = "#3a5a5a" # active line cursor highlight (default: selection_bg)
heading1 = "lightcyan" # markdown H1 / table headers
heading2 = "lightyellow" # markdown H2
heading3 = "lightgreen" # markdown H3
code = "lightyellow" # inline code / code blocks
diff_add = "green" # added lines in a diff
diff_del = "red" # removed lines in a diff
git_clean = "green" # statusbar git indicator: clean working tree
git_dirty = "yellow" # statusbar git indicator: uncommitted changes
git_conflict = "red" # statusbar git indicator: conflict / detached HEAD
git_progress = "#ff8700" # statusbar git indicator: rebase / merge in progress
breadcrumb_fg = "cyan" # breadcrumb path bar foreground (default: accent)
breadcrumb_bg = "reset" # breadcrumb path bar background (default: background)
syntax = "base16-ocean.dark"
The background role controls panel backgrounds. When set to reset (the default),
the terminal’s own background shows through. Each preset ships a preferred background
color that you can override or disable with transparent_background. On top of
presets, live theme switching instantly re-applies the current file with the new
syntax highlighting theme.
Telemetry & Bug Reports
mantis ships two local-only diagnostic features: an opt-in usage
telemetry log and an on-demand bug report command. Neither sends
anything anywhere — both write files to your machine that you can inspect,
delete, or choose to share when filing an issue.
Bug reports
Open the command palette (Ctrl+P) and run “Report a bug (save
diagnostics locally)”. This collects an anonymous diagnostic snapshot,
saves it as markdown under the state directory
(~/.local/state/mantis/bug-reports/ on Linux/macOS,
%APPDATA%\mantis\bug-reports\ on Windows), and shows the saved path in the
status bar. Review the file, then attach or paste it into a
GitHub issue.
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/ (events.jsonl, rotated at 1 MiB, at most five
files kept). 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 (the $TERM value) | mantis starts |
session_end | duration_s, events_dropped | mantis exits |
action_invoked | action (canonical action id), source (palette) | a command palette entry runs |
Raw keystrokes, search queries, palette query text, file names, and paths
are never recorded — the event types above cannot carry them. To stop
collecting, set enabled = false (or remove the key); to erase history,
delete the telemetry/ directory from the state dir.
Plugins
mantis supports two kinds of plugins: process plugins (subprocess-based) and
syntax plugins (syntax definitions loaded into the highlighter).
Process plugins are standalone executables that hook into app events and issue
actions back to the viewer. They run in separate processes; mantis talks to them
over stdin/stdout using newline-delimited JSON, so a plugin can be any
executable — a compiled binary, a Python script, or anything that can read
stdin and write stdout.
Syntax plugins provide .sublime-syntax files that are loaded into the built-in
syntect highlighter at startup. They add syntax highlighting for new file types
without modifying the core binary.
Installing a plugin
Plugins live in the default plugin directory:
- Linux / macOS:
~/.config/mantis/plugins/(or$XDG_CONFIG_HOME/mantis/plugins/if that variable is set) - Windows:
%APPDATA%\mantis\plugins\
Process plugins
Drop the executable there and make it executable (chmod +x my-plugin.sh on
Unix).
Syntax plugins
.sublime-syntax files placed in {plugin_dir}/syntaxes/ are auto-discovered
at startup. They are installed automatically by mantis’s first-run setup.
Registering a plugin
Process plugins
Add a [plugins] section to your mantis.toml:
[plugins]
my-plugin = { path = "my-plugin.sh", enabled = true }
path— path to the executable. Relative paths are resolved against the default plugin directory above. Absolute paths are used as-is.enabled— set tofalseto keep the entry without loading the plugin.
You can register as many plugins as you like:
[plugins]
git-stats = { path = "git-stats.sh" }
file-logger = { path = "/usr/local/bin/mantis-file-logger" }
dev-tools = { path = "dev-tools.py", enabled = false }
Syntax plugins
Syntax plugins can also be registered explicitly in mantis.toml with
kind = "syntax":
[plugins]
terraform = { kind = "syntax", syntax_file = "syntaxes/terraform.sublime-syntax",
extensions = ["tf", "tfvars"] }
kind—"syntax"for syntax-definition plugins (default:"process").syntax_file— path to the.sublime-syntaxfile. Relative paths are resolved against the plugin directory.extensions— file extensions this syntax should match (optional; the syntax definition itself also declares its own extensions).
Syntax plugins are not spawned as subprocesses. Their syntax definitions are loaded into the highlighter alongside the built-in language set.
What plugins can do
Plugins receive lifecycle and hook events from mantis and can respond with
actions:
| Action | Effect |
|—|—|—|
| show_message | Displays a message in the status bar |
| open_file | Opens a file in the content panel |
| set_content | Replaces content panel with ANSI-escaped lines |
| set_icon_map | Sets file-type icon glyphs (requires Nerd Font) |
| register_language_provider | Declares file extensions and capabilities |
| set_fold_regions | Provides fold regions for a file |
Each action has specific parameters; see Plugin Development for the full protocol reference.
Protocol version
mantis and plugins communicate over an IPC protocol identified by a version
string. Each plugin declares its protocol version in plugin.toml via the
tv_protocol field. At startup mantis validates that every discovered plugin
matches the host protocol version — plugins with a mismatched version are
silently skipped. The init event sent to each plugin includes the host
protocol version so the plugin can verify compatibility dynamically.
Current protocol version: "2" (bumped from "1" for the 0.8 release).
Upgrading from 0.7: Plugins written for protocol
"1"must updatetv_protocol = "2"in theirplugin.tomland handle the newprotocol_versionfield on theinitevent to remain compatible with 0.8+.
Lifecycle
mantisstarts, reads[plugins]from config, and spawns each enabled plugin.- Each plugin receives an
initevent (with the active theme name and host protocol version). - As you use
mantis, plugins receive hook events (on_file_open, - When you quit, each plugin receives
on_quitthenshutdown, andmantiswaits for each subprocess to exit.
Plugin stderr is discarded (/dev/null). Write debug output to a log file instead.
Example: status-bar clock
A minimal shell plugin that shows the current time in the status bar on every file open:
#!/usr/bin/env bash
# ~/.config/mantis/plugins/clock.sh
while IFS= read -r line; do
event=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin).get('event',''))")
case "$event" in
on_file_open|init)
ts=$(date +"%H:%M:%S")
printf '{"event":"action","action":"show_message","params":{"message":"%s"}}\n' "$ts"
;;
shutdown) exit 0 ;;
esac
done
Register it:
[plugins]
clock = { path = "clock.sh" }
Bundled plugins
mantis ships with several bundled Rust plugins. Each is a workspace member
compiled alongside mantis and installed on first run.
| Plugin | Binary | What it does |
|—|—|—|—|
| iconize | iconize | On init, sends a set_icon_map action with Nerd Font glyphs for ~80 file extensions. Requires icons = true in mantis.toml and a Nerd Font terminal. |
| markdown | markdown | Renders .md files using pulldown-cmark, sending the output as ANSI-escaped lines via set_content. Responds to theme changes and M keypress for raw/rendered toggle. |
| rust | rust | Registers as a language provider for .rs files with the fold capability. On file open, computes and registers collapsible curly-brace fold regions. |
| go | go | Registers as a language provider for .go files with the fold capability. On file open, computes and registers collapsible curly-brace fold regions. |
All bundled plugins are compiled as workspace members and installed to the
plugin directory the first time mantis creates its global config. Enable them by
adding entries in mantis.toml:
[plugins]
iconize = { path = "iconize" }
markdown = { path = "markdown" }
rust = { path = "rust" }
go = { path = "go" }
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 withgit pull --ff-only. - The
index.jsonfile is parsed to build the list of available plugins.
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"
},
{
"name": "markdown-preview",
"description": "Live markdown preview panel",
"repo": "https://github.com/example/tv-md-preview",
"tag": "v0.2.0"
}
]
}
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. |
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. |
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 at install). |
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. |
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. path is absent if the
tree is empty.
{"event":"on_selection_change","path":"/absolute/path/to/entry"}
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"
}
}
on_quit
Sent when the user initiates a quit (before shutdown). Use this to do any
final work before the process is torn down.
{"event":"on_quit"}
shutdown
Sent as the final event. mantis closes stdin immediately after sending this.
Exit cleanly in response.
{"event":"shutdown"}
Requests: mantis ⇄ plugin (protocol 3+)
Protocol 2 is one-way and fire-and-forget in both directions: the host emits events, the plugin emits actions, and neither side can ask the other for something and wait for a specific reply. Protocol 3 adds a correlated request/response pair on top of the existing event/action stream, used for capabilities that need an answer to a specific question (e.g. “fold regions for this file, now”) rather than a broadcast the plugin may or may not act on.
Host → plugin request, sent on stdin like any other event:
{"event":"request","id":42,"method":"fold_regions","params":{"path":"/absolute/path/to/file"}}
Plugin → host response, sent on stdout as its own line, alongside (but
distinct from) action lines:
{"event":"response","id":42,"result":{"regions":[[0,5],[10,20]]}}
or, on failure:
{"event":"response","id":42,"error":{"message":"failed to parse file"}}
Rules:
idis chosen by the host per outstanding request and must be echoed back unchanged in the response. IDs are not reused while a request is outstanding.- Exactly one of
result/errormust be present. - The host applies a per-plugin timeout to each request (a bounded number of
ticks). If no response arrives in time, the host treats it as an error,
logs it the same way as a
plugin_error(see below), and does not kill the plugin — a slow or missed response degrades gracefully rather than being fatal. request/responseis additive to the existing event/action stream, not a replacement:set_fold_regionspushed unprompted still works exactly as in protocol 2 for plugins that don’t implement requests. The host only sendsrequestevents to plugins that declared protocol 3 in their manifest.
This is the surface the reserved hover, diagnostics, and definition
capabilities are expected to use once implemented — each as a method name
on the same request/response pair, gated by the corresponding capability
in register_language_provider.
Actions: plugin → tv (stdout)
Respond with action objects on stdout. Each object must be on a single line.
Lines that are not valid JSON or that lack "event":"action" are silently
ignored.
show_message
Displays a message in the mantis status bar.
{"event":"action","action":"show_message","params":{"message":"hello from plugin"}}
plugin_error (protocol 3+)
Reports a failure that isn’t tied to a specific request/response pair
(for example, a subscription-only plugin that failed to act on a broadcast
event). Distinct from show_message: it is recorded in the plugin’s
rotating log file (plugin-logs/<name>.log) and surfaced with error styling
in the status bar and plugin picker, rather than treated as routine status
text.
{"event":"action","action":"plugin_error","params":{"message":"failed to parse file","context":"on_file_open"}}
Fields:
message— human-readable error description.context— optional free-form string naming the event/method that failed (advisory, shown alongside the message).
open_file
Opens a file in the content panel.
{"event":"action","action":"open_file","params":{"path":"/tmp/output.txt"}}
set_content
Replaces the content panel with the given lines. Each line is a string that may
contain ANSI escape codes for colour and styling. mantis parses the ANSI codes
with its built-in parser and displays them as styled text. Handy for plugins
that generate rich output (e.g. markdown renderers, linters).
{"event":"action","action":"set_content","params":{"lines":["\u001b[32mgreen line\u001b[0m","plain line"]}}
set_icon_map
Sets the file-type icon glyphs used in the tree. Requires icons = true in mantis.toml and a Nerd Font terminal. Keys in icons are file extensions (lowercase) or full filenames for extensionless files (e.g. "dockerfile").
{"event":"action","action":"set_icon_map","params":{"dir_open":"","dir_closed":"","fallback":"","icons":{"rs":"","py":"","dockerfile":""}}}
Fields:
dir_open— glyph for open directoriesdir_closed— glyph for closed directoriesfallback— glyph used when no extension key matchesicons— map of extension/filename → glyph
Once icons are active, the tree’s ▶/▼ expand/collapse arrows are suppressed —
the dir_open/dir_closed glyph substitutes as the expansion indicator instead.
Language providers
A process plugin can declare itself as a language provider by responding to
the init event with a register_language_provider action. This tells mantis
which file extensions the plugin handles and what capabilities it provides.
fold is implemented via push (set_fold_regions); highlight is formally
reserved (see below). The reserved capabilities (hover, diagnostics,
definition) are expected to slot in as request/response methods (see
Requests: mantis ⇄ plugin) once
implemented, without a protocol break.
Provider contract
The language provider contract between mantis and a plugin follows a strict
lifecycle:
-
Registration. Immediately after receiving
init, the plugin sendsregister_language_providerto declare its extensions and capabilities. Registration is expected once per plugin — re-registration overwrites the previous registration entirely. -
File routing. When the user opens a file whose extension matches a registered provider,
mantisroutes relevant events (on_file_open,on_selection_change) and capability-driven state requests to that provider. When exactly one provider matches an extension+capability pair, it is used. When more than one provider registers the same extension+capability (protocol 3+), the registration with the higherprioritywins; ties break by registration order (first registered wins). The first time this happens for a given extension+capability pair,mantisshows a one-time status-bar warning naming both plugins, so the conflict isn’t silent. Currently onlyfoldcapability drives backend state (fold regions);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).highlight→ reserved for future use.
-
Lifetime. Provider registrations persist for the entire plugin session. When a plugin exits or is deactivated, its registrations are removed. If the plugin sends
set_fold_regionsfor a file before it is opened,mantiscaches the regions and applies them when the file is opened later. -
Capability gating.
mantisenforces capability checks at runtime. For example,set_fold_regionsis only accepted when the sender has a registered provider with thefoldcapability for that file’s extension. Unknown capability strings are silently ignored, so existing providers remain compatible with future protocol extensions.
register_language_provider
Sent by the plugin immediately after receiving init. Declares the file
extensions and capabilities the plugin provides. mantis stores the registration
and uses it to route the correct events and display-state updates for each open
file.
{"event":"action","action":"register_language_provider","params":{
"extensions": ["py", "pyi"],
"capabilities": ["fold"],
"priority": 10
}}
Fields:
extensions— lowercase file extensions (no leading dot) this provider handles.capabilities— one or more of"highlight"or"fold". Reserved for future use:"hover","diagnostics","definition".priority— optional signed integer, default0(protocol 3+). Used only to break ties when two providers register the same extension+capability pair; higher wins. Absent on protocol 2 plugins, which are treated as priority0.
On highlight: it remains declared but formally reserved as of protocol
3 — mantis accepts the registration and never dispatches anything for it.
Real syntax highlighting is intentionally not routed through language
providers; it flows through syntax plugins (.sublime-syntax files loaded
into syntect, see Syntax plugins below) or the built-in
highlighter. This was evaluated as part of the #296 capability audit and
re-confirmed for v3: provider-driven highlighting would mean re-deriving
styled spans over IPC per file/edit, which the syntect path already does
locally and faster. Revisit only if a concrete plugin use case needs
highlighting decisions syntect’s grammar model can’t express.
After registering, mantis sends on_file_open whenever a matching file is
opened. The plugin should respond with the appropriate action for each declared
capability (e.g. set_fold_regions for "fold").
set_fold_regions
Provides fold regions for a file. Plugin-supplied regions override the
built-in YAML indentation-based folding (the reference implementation)
for that file. Each region is a [start_line, end_line] pair (0-indexed,
inclusive). When the named file is currently open the regions are applied
immediately; when it is not yet open they are cached and applied the next
time the file is opened.
Fold regions from a plugin are only accepted when mantis has a registered
language provider with the fold capability for the file’s extension.
Regions from unregistered plugins or for unmatched extensions are silently
discarded.
{"event":"action","action":"set_fold_regions","params":{
"path": "/absolute/path/to/file",
"regions": [[0, 5], [10, 20]]
}}
Rules
- One JSON object per line. No pretty-printing, no multi-line objects.
- Stdout is for actions only. Don’t write debug output there — it will break the
protocol. Write to stderr instead:
mantiscaptures it for crash diagnostics (the last line and a rotating on-disk log), but does not otherwise act on it. - Exit on shutdown. When stdin closes or you receive
shutdown, exit. Do not loop forever waiting for more input. - Idempotent reads.
mantismay send multipleon_file_openevents for the same path if the user reopens a file. - No blocking. Actions are drained non-blockingly every tick. Sending many actions in rapid succession is fine; they are buffered and processed in order.
State teardown contract
Every set_* action a plugin sends registers a contribution in the host’s
plugin_contributions map (HashMap<String, PluginContributions>). When the
plugin is disabled via the plugin picker or its process exits unexpectedly,
the host automatically tears down all state that plugin produced — no per-plugin
special cases needed.
What gets torn down:
| Action | State cleared |
|---|---|
set_content | plugin_content / plugin_content_text entries for contributed paths |
set_icon_map | icon_map, icons_enabled, icon_dir_open/closed, icon_fallback |
set_fold_regions | plugin_fold_regions entries for contributed paths; active fold state reset |
register_language_provider | Provider registration 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.rsrespectively. - Plugin config deserialization lives in
src/config/mod.rsunder thepluginskey. - Bundled plugins are declared in
BUNDLED_PLUGINS(src/plugin/install.rs) as(name, binary_name)pairs and built as workspace-member Rust crates underplugins/. Theinstall_bundled_plugins()function finds and copies compiled binaries to the plugin directory on first run. Thepythonplugin is a language-provider example: it registers forpy/pyiwithfoldcapability atinitand sendsset_fold_regionsonon_file_openusing the sharedmantis::fold_detectors::indent_folddetector.
Syntax plugins
Syntax plugins provide .sublime-syntax files that extend the built-in
syntect-based highlighter. No subprocess is spawned.
How they work
A syntax plugin is simply a .sublime-syntax file (Sublime Text syntax
definition format, YAML-based). At startup:
- Files in
{plugin_dir}/syntaxes/are auto-discovered and loaded. - Explicit
[plugins]entries withkind = "syntax"are also loaded. - Each syntax definition is added to the
SyntaxSetso syntect associates its declared file extensions with highlights.
Writing a syntax definition
The .sublime-syntax format is documented in the Sublime Text
docs. A minimal syntax file
looks like:
%YAML 1.2
---
name: My Language
file_extensions: [ext1, ext2]
scope: source.my_lang
contexts:
main:
- match: '#.*'
scope: comment.line.number-sign.my_lang
- match: '\b(keyword)\b'
scope: keyword.control.my_lang
The file_extensions key tells syntect which files to highlight with this
syntax. The scope key defines the base scope for highlighting, and contexts
define the matching rules.
Bundled syntax plugins
mantis ships with a terraform.sublime-syntax file that is automatically
installed to {plugin_dir}/syntaxes/ on first run. It provides syntax
highlighting for .tf and .tfvars files (Terraform / HCL). Enable it by:
[plugins]
terraform = { kind = "syntax", syntax_file = "syntaxes/terraform.sublime-syntax",
extensions = ["tf", "tfvars"] }
Or simply leave it in the syntaxes/ directory for auto-discovery (no config
entry needed).
Architecture notes
src/highlight.rs::with_extra_syntaxes()loads extra syntax definitions into the syntax set.src/plugin.rs::load_extra_syntaxes()collects syntax plugins from config entries and thesyntaxes/directory.- Syntax definitions are loaded once at startup and shared between the main
thread’s
Highlighterand the background loader thread’sHighlighter.
Plugin Capability Matrix
Audit of the plugin protocol (v2) as of 0.12.x: what the protocol declares, what the host actually implements, and what the bundled plugins use. This is the “take stock” pass after the 0.8 plugin work (issue #296). For the protocol itself — message formats, examples, contribution/teardown rules — see Plugin Development.
Capabilities
Capabilities are declared by a plugin in its register_language_provider
action and stored as Capability variants (src/plugin/types.rs). The host
routes them via PluginManager::provider_for (src/plugin/manager.rs).
| Capability | Declared in protocol | Handled by host | Used by a bundled plugin |
|---|---|---|---|
fold | yes | yes — gates set_fold_regions in handle_plugin_set_fold_regions (src/app/refresh.rs) | yes — used by the bundled rust, go, and python language provider plugins |
highlight | yes | no — accepted at registration, never checked anywhere | no |
hover | yes (reserved) | no — unimplementable in v2 (no request/response correlation) | no |
diagnostics | yes (reserved) | no — same as hover | no |
definition | yes (reserved) | no — same as hover | no |
Note on highlight: real syntax highlighting flows through syntax plugins
(kind = "syntax", a .sublime-syntax file fed to syntect — see the bundled
terraform plugin), not through language-provider capabilities. The
highlight capability is documented as reserved for future provider-driven
highlighting; today registering it has no effect.
Actions
Every action the host accepts, dispatched in App::handle_plugin_action
(src/app/refresh.rs). Unknown actions are silently ignored.
| Action | Handled by host | Contribution tracked | Torn down | Used by a bundled plugin |
|---|---|---|---|---|
show_message | yes | no (transient status text) | n/a — plugin_message may briefly outlive the plugin; harmless | no |
open_file | yes | no (one-shot navigation) | n/a | no |
set_icon_map | yes | has_icon_map | yes — icon map/fields cleared | yes — iconize |
set_content | yes | content_paths | yes — content removed, current file re-rendered | yes — markdown |
register_language_provider | yes | provider registration in PluginManager | yes — remove_provider_registrations | yes — rust, python |
set_fold_regions | yes | fold_region_paths | yes — regions removed, fold state reset | yes — rust, python |
Teardown status: every stateful set_* action stamps PluginContributions
and is cleared by App::teardown_plugin_contributions (src/app/mod.rs).
No teardown gaps were found in this audit.
Protocol v1 git actions (set_file_statuses, set_blame_data,
set_status_bar_git_info) were removed in 0.11.22 along with the retired
shell-script git plugins; git features are built in. They are listed in the
version history in Plugin Development only.
Bundled plugins
| Plugin | Kind | Actions sent | Capabilities registered |
|---|---|---|---|
iconize | process | set_icon_map | none |
markdown | process | set_content | none |
python | process | register_language_provider, set_fold_regions | fold |
rust | process | register_language_provider, set_fold_regions | fold |
go | process | register_language_provider, set_fold_regions | fold |
terraform | syntax | none (no subprocess) | n/a — extends syntect directly |
Gaps and follow-ups
- Reserved capabilities (
hover,diagnostics,definition) are unimplementable in protocol v2 — they need id-correlated request/response. Tracked in the protocol v3 proposal (issue #481), which names this audit as its precursor. - The language-provider fold pipeline has bundled consumers —
register_language_provider+Capability::Fold+set_fold_regionsare used by the bundledrust(issue #599),go(issue #600), andpython(issue #601) language provider plugins. Therustandgoplugins register thefoldcapability for.rsand.gofiles via the sharedbrace_folddetector (#598); thepythonplugin uses the sharedindent_folddetector. Capability::Highlightis declared but routes to nothing. Either implement provider-driven highlighting in v3 or re-document it as reserved alongsidehover/diagnostics/definition. Not yet tracked in a dedicated issue; candidate checklist item for #481.
Roadmap
Last updated: 2026-07-03. Living document — the epics linked below are the source of truth for status; this page explains the why and the sequencing.
Positioning: the reading tool for the AI era
mantis’s pitch has always been a fast way to read code without launching an editor. The opportunity in front of it: in agent-driven development, humans write less and read more. Every coding-agent session produces diffs someone must review, logs someone must check, YAML someone must sanity-check. Editors are optimized for writing; mantis can own the reading and reviewing half of the loop — for humans and for the agents themselves.
Vision: mantis is the review cockpit for humans working with AI agents, and the fastest repo-reading surface for the agents themselves.
Personas
- Backend engineer — reads unfamiliar services, reviews PRs and agent diffs, jumps between code and JSON/YAML fixtures.
- DevOps / SRE — lives over SSH; reads k8s manifests, Terraform,
.envs, and logs; needs instant startup on remote boxes. - AI engineer / agent operator — runs coding agents; wants to watch what the agent changes in real time, feed context into prompts, and give agents tools they can call.
Non-goals (the moat)
Every feature below stays read-only and zero-config by default. mantis will
not grow: editing, LSP/IntelliSense, an embedded terminal, a debugger, or a
plugin-language runtime. When a feature needs to write, it hands off (to
$EDITOR, to the clipboard, to stdout).
The four pillars
1. Agent review cockpit
Epic: #469
The highest-leverage new surface. The primitives already exist (file watcher, git status/diff, side-by-side rendering) — this pillar composes them:
- Prompt-ready copy — copy selection/file as a fenced markdown block with
language and
path:lineheader; the format every agent prompt wants. - Diff-first startup —
mantis --diff [base]opens straight into git mode againstHEAD, a branch, or a range (main...). - Watch mode / review dashboard —
mantis --review: a changed-files panel that refreshes while the agent edits; hunk-walking across files; per-file “seen” marks. - Session summary — on quit, a plain-text list of files/hunks reviewed.
2. Machine interface
Epic: #470
Make mantis useful to the agents themselves. mantis is already a library crate; expose it without the TUI:
- Headless JSON CLI —
mantis cat --range … --json,mantis search --json,mantis tree --json,mantis blame --json. - MCP server —
mantis mcpexposing file-slice, search, diff, blame, and tree tools to any MCP-capable client. - Remote-control socket —
mantis --listen <socket>so an external agent can drive a running viewer (“show the user this diff”). - Protocol hygiene — rename the plugin manifest’s
tv_protocolfield tomantis_protocol(with back-compat) before third-party plugins multiply.
3. DevOps reader
Epic: #471
mantis’s home turf is the terminal over SSH; teach it operational data, not just code:
- Log follow mode (tail-style) with level/timestamp colorizing and a filter bar; JSONL rendering for structured logs.
- JSON path breadcrumb and a jq-style query bar.
- CSV/TSV table view (#71).
- Secret masking for
.env/credential-shaped files with a reveal toggle. - Remote browsing over SSH (#359) — sequenced after log mode; it multiplies everything else in this pillar.
4. Reading UX polish
Epic: #472
Continuous ergonomics investment: theme live preview, scrollable help, onboarding hints, regex/case search toggles, sticky scroll, bookmarks, a context menu, a jump-back navigation stack, line wrap, search result counts.
This pillar is where the overlay count keeps growing (about, blame, and now
help all render scrollable popups; bookmarks and the context menu add more).
Each new scrollable overlay is a prompt to check whether the ad hoc
xxx_scroll field + inline key/mouse match introduced for help should
graduate into a shared ScrollState-style helper (see AGENTS.md →
Consistency & performance) rather than being copied a third time.
5. Language intelligence (Rust · Python · Go)
Epic: #482
Syntax highlighting already covers these; this pillar adds language-aware reading, built in and zero-config like the shipped YAML folding:
- Code folding for Rust/Go (brace-based) and Python (indentation-based) — starter issue #483.
- Symbol outline / go-to-symbol fuzzy picker (regex-based, deliberately not tree-sitter — binary size is a feature).
- Scope context in the breadcrumb, which also feeds sticky scroll (#199).
6. File-type coverage via plugins
Epic: #485
Beyond the three built-in languages, coverage grows through plugins in two
tiers. Syntax packs (.sublime-syntax, terraform-style): TOML,
TypeScript/TSX, and Dockerfile bundled first
(#486), then
Protobuf/GraphQL/Jinja2/nginx and friends via the registry. Rich viewers
(process plugins): a Jupyter .ipynb renderer and a GGUF/safetensors
model-metadata viewer headline the AI persona; Parquet/SQLite previews and a
PEM/JWT decoder serve DevOps. Rich viewers are blocked on the 64 KB plugin
content cap (#449).
Cross-cutting: plugin system hardening
The plugin system is the extension surface for pillars 2, 3, and 5, and a
July 2026 review filed its prerequisites: ship bundled plugins in releases
(#477), remove the
startup cargo build fallback
(#478 — security), plugin
stderr diagnostics (#479),
a registry trust model before the install UI ships
(#480), and protocol v3
(request/response, key consumption, provider priorities —
#481).
Engineering principles
Every change — feature, fix, refactor — is planned before it’s coded, not improvised diff-by-diff. Two questions guide that planning, in order:
- Does this shape already exist somewhere in the app? (an overlay, a picker, a scroll offset, a clamp). If yes, reuse or extend it instead of writing a parallel copy.
- Will this shape recur? If a second consumer is foreseeable (the next overlay, the next picker), design the shared primitive now so the second consumer extends it instead of duplicating it. If not, keep it local — this is not license to abstract on the first use case.
AGENTS.md → “Consistency & performance” is the enforced version of this
for Rust code; this section states the intent behind that rule so it stays
legible as the codebase (and this roadmap) grows.
Sequencing
| Release | Theme | Contents |
|---|---|---|
| 0.13 | Trust & polish | Code-review correctness fixes + quick UX wins — tracking issue #473; plugin packaging/security fixes #477/#478 |
| 0.14 | DevOps reader | Log follow mode + JSONL, --diff base startup, secret masking, CSV tables; code folding for Rust/Go/Python (#483) |
| 0.15 | Agent cockpit | --review dashboard, remote-control socket, session summary, protocol v3 + rename (#481); go-to-symbol picker |
| 0.16 | Machine interface | Headless JSON CLI, then the MCP server on top of it; registry trust model + install UI (#480) |
| 1.0 | AI-native viewer | SSH remote (#359), keymap refactor (#298), comprehensive help (#304), plugin registry maturity |
First two bets: prompt-ready copy (days of work, immediate daily-use hook for the AI crowd) and log follow mode (biggest persona expansion per unit of effort — the machinery is ~80% built).
North-star metrics
- Time-to-first-render stays under 50 ms — every feature is gated on this.
- Weekly installs (Homebrew / install script).
- Share of sessions using git/diff mode — are we becoming the review tool?
- MCP tool-call volume, once shipped.