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:
vim "$(ite)" # pick a file, edit it
cd "$(ite ~/src)" # pick a directory (ctrl+enter), go there
ite response.json # explore a JSON document
curl -s api.example.com/users | ite # pipe JSON straight inbrew install jrpat/ite-tap/ite # Homebrew (macOS/Linux)
cargo install ite-cli # from crates.io, installs the `ite` binaryOr grab a prebuilt binary:
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/jrpat/ite/releases/latest/download/ite-cli-installer.sh | shPrebuilt archives for macOS and Linux (x86_64/aarch64) are also available on the releases page.
ite [OPTIONS] [PATH]| 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 |
-s, --show <ADDRESS> |
Reveal a filesystem path or canonical JSON Pointer; repeatable |
-c, --config <FILE> |
Use this config instead of the user config; repeatable, later files win |
--show is a repeatable, best-effort startup hint: ite opens every matching
target's ancestors, then focuses the last match and places it near the center
of the first viewport. It composes with --expand; blank, invalid, ignored,
and unmatched addresses are silently ignored.
By default, ite explores PATH (or .). It shows dotfiles while respecting
.gitignore and friends. -I also shows ignored files.
A filesystem show address may be absolute or relative to the explored directory. Matching is lexical and does not follow symlinks:
ite ~/src/ite --show src/app.rs -s README.mdPass 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.
curl -s api.example.com/users | ite
kubectl get pod mypod -o json | ite --json - --expand 1
ite users.json --show /users/0/nameJSON show addresses must exactly match ite's canonical, document-global JSON
Pointers. For JSONL, that includes the record index, such as
/3/user/name for the fourth record.
To explore a directory while something is piped in, name it: producer | ite .
ignores the pipe and explores the filesystem.
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.
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 |
z |
Center the focused node in the viewport |
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.
ite reads $XDG_CONFIG_HOME/ite/config.toml (usually
~/.config/ite/config.toml). Each table is a keybinding; the table name is
the key:
[ctrl+e]
sh = "vim $path" # run a shell command on the focused node
help = "Edit" # optional keybinding-panel description
exit = true # then leave ite (default: false)
[alt+s]
sh = "attach-to-review $relpath"
help = "Attach to review"
bg = true # run detached, without leaving the TUI (default: false)
[ctrl+l]
cmd = "expand-recursively" # or run an ite command instead
help = "Expand project" # overrides the built-in command descriptionsh 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.
You need a Rust toolchain; everything else is cargo:
cargo build # compile
cargo run -- ~/src # run against a directory
cargo test # the test suite (fast, no terminal needed)
cargo clippy --all-targets # lints; the build is kept warning-free
cargo profile-tui # headless perf profile: real pty, simulated keys
cargo local-bin # release-build and install to $XDG_BIN_HOME/itecargo 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:
cargo watch -x test -x clippy # cargo install cargo-watch
# or: bacon test # cargo install baconTesting 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).
To play with a JSON sample directly from a checkout:
./examples/json-demo.sh
./examples/json-demo.sh --expand all # start with everything openThe script quietly builds the current working copy and opens
examples/sample.json. Edit that file to try your own
shapes.