ite
interactive tree explorer — a terminal UI for walking a tree, poking at it, and doing something useful with whatever you land on.
It is designed for two distict workflows:
- In a pipeline it behaves like
fzf: you open it, pick one thing, and it hands that thing to whatever comes next. - It can also behave like a sidebar, and you can easily bind keys to run shell commands against the focused node.
Used in a pipeline, simply press enter on a leaf and its value goes to stdout — the absolute path for a filesystem tree, the node's JSON Pointer for a JSON document.
Use it like you do fzf:
|
Installation
Or grab a prebuilt binary:
|
Prebuilt archives for macOS and Linux (x86_64/aarch64) are also available on the releases page.
Usage
| Flag | Meaning |
|---|---|
PATH |
Directory or .json/.jsonl file to explore (default: .) |
-j, --json <PATH> |
Explore a JSON file with any extension (- reads stdin) |
-l, --jsonl <PATH> |
Explore a JSON Lines file with any extension, forcing JSONL parsing (- reads stdin) |
-I, --no-ignore |
Show ignored files by disabling ignore-file rules |
-e, --expand <N|all> |
Start with N levels expanded (-e 1 opens top-level containers), or all of them |
-c, --config <FILE> |
Use this config instead of the user config; repeatable, later files win |
Filesystem
By default, ite explores PATH (or .). It shows dotfiles while respecting
.gitignore and friends. -I also shows ignored files.
JSON
Pass a .json or .jsonl path to explore it directly. For files with other
extensions, use --json PATH (or -j PATH) or --jsonl PATH (or -l PATH).
Objects become tree branches, arrays keep their input order under indexed
children, and scalar values are leaves:
$ ite users.json
▼ users [2]
├ ▼ [0] {2} id: 12 · name: "Ada"
│ ├ • id: 12
│ └ • name: "Ada"
└ • [1]: null
Accepting a JSON node writes its canonical JSON Pointer, such as /users/0/name; the
root pointer is empty. $path and $relpath in shell bindings contain the same value.
JSON can also arrive on a pipe. When stdin is not a terminal and no directory is given,
ite reads a JSON document from stdin; --json - requests the same thing explicitly.
|
|
To explore a directory while something is piped in, name it: producer | ite .
ignores the pipe and explores the filesystem.
Exit Codes
Exit codes are honest: 0 means a value was printed, 130 means you quit
without choosing, and a keybinding configured with exit = true passes its
command's status through.
Keys
Navigation is vim-flavored:
| Key | Action |
|---|---|
j / ↓, k / ↑ |
Move focus down / up, one visible line |
l / → |
Expand a collapsed container; on an expanded container, focus its first child; on a leaf, focus the next sibling |
h / ← |
Collapse an expanded container; otherwise focus its parent |
L / shift+→ |
Expand recursively; on a leaf, focus the next sibling |
H / shift+← |
Collapse an expanded container recursively; otherwise collapse its parent recursively and focus it |
space / ctrl+space |
Toggle a container open or shut, plainly / recursively |
enter |
Expand a collapsed container; on a leaf, print its path or JSON Pointer and exit |
ctrl+enter |
Print the focused path or JSON Pointer and exit, container or not |
alt+enter |
Print the focused basename or compact JSON value and exit |
tab / shift+tab |
Make the focused node the root / restore the previous root |
J, K |
Next / previous sibling, hurdling expanded subtrees |
ctrl+f / ctrl+b |
Page down / up |
ctrl+d / ctrl+u |
Half-page down / up |
g, G |
First line, last visible line |
o |
Open the focused leaf with your desktop's default application |
? |
Toggle the keybinding panel |
esc |
Restore the previous root; quit from the original tree |
q, ctrl+c |
Quit |
o hands the focused leaf's path to the platform's default opener — open on
macOS, xdg-open on Linux and the BSDs, start on Windows — and leaves it
running detached while you keep exploring. Containers expand instead of opening,
and JSON nodes are addressed by pointer rather than by path, so o does nothing
there.
The keybinding panel stays open while its listed bindings run; ? or esc
closes it. On short terminals, point at the panel and use the mouse wheel to
scroll its complete effective keymap.
Configuration
ite reads $XDG_CONFIG_HOME/ite/config.toml (usually
~/.config/ite/config.toml). Each table is a keybinding; the table name is
the key:
[]
= "vim $path" # run a shell command on the focused node
= "Edit" # optional keybinding-panel description
= true # then leave ite (default: false)
[]
= "attach-to-review $relpath"
= "Attach to review"
= true # run detached, without leaving the TUI (default: false)
[]
= "expand-recursively" # or run an ite command instead
= "Expand project" # overrides the built-in command description
sh commands run via sh -c with two environment variables set. For a
filesystem tree, $path is absolute and $relpath is relative to the
explored root. For JSON, both are the selected node's JSON Pointer. No string
splicing, no quoting accidents — the shell expands them the way shells do.
Without bg, the TUI steps aside while your command runs and returns when it
finishes; editors work exactly as you'd hope.
cmd accepts any built-in command: down, up, expand, collapse,
expand-recursively, collapse-recursively, toggle, toggle-recursively,
select, accept, accept-alternate, descend, root, pop-root, back,
next-sibling, prev-sibling, page-down, page-up, half-page-down,
half-page-up, first, last, jump, open, quit.
help is optional. Its trimmed first line is displayed verbatim in the
keybinding panel (control characters become spaces). Without help, cmd
bindings use the built-in command description; sh bindings show the
command's first line in backticks, truncated to 12 terminal cells. The ? key
is reserved for the panel: a configured [?] table is accepted but silently
ignored.
User bindings override the defaults, so if you bind j to something exotic,
ite assumes you meant it.
Development
You need a Rust toolchain; everything else is cargo:
cargo profile-tui (a cargo alias — cargo's answer to npm scripts) spawns
the release binary in a genuine PTY, drives it with keypresses, and prints
per-key latency plus an internal span table. If a keystroke ever feels
sluggish, run it before theorizing; it has already caught one absurd
regression.
The codebase separates decisions from I/O: app.rs turns keys into Effect
values (print this, run that, quit) and is fully unit-tested without a
terminal; main.rs owns the actual terminal and executes effects. If you're
adding behavior, this project is developed test-first — write the failing
test, then the code. AGENTS.md has the module-by-module map.
For a tight loop, run the tests on save with your watcher of choice:
# or: bacon test # cargo install bacon
Testing the TUI itself by hand is best done in a real terminal. If you must
script it, use expect and be prepared to answer the terminal's
cursor-position query (ESC[6n) yourself — ratatui asks at startup and will
wait politely, then give up. See AGENTS.md for the incantation.
This repository uses Jujutsu (jj commit,
not git commit).
JSON
To play with a JSON sample directly from a checkout:
The script quietly builds the current working copy and opens
examples/sample.json. Edit that file to try your own
shapes.