ite
interactive tree explorer — a terminal UI for walking a tree, poking at it, and doing something useful with whatever you land on.
The elevator pitch: tree shows you everything and scrolls off the screen;
ite shows you a collapsed tree and lets you open exactly the doors you care
about. Press enter on a leaf and its selection lands on stdout. For a
filesystem tree that is the absolute path; for a JSON document it is the
selected node's JSON Pointer. That's the whole trick, and it composes
beautifully:
|
The interface draws on stderr, so stdout stays clean for the value. If you
have ever piped fzf, you already know this dance.
Usage
| Flag | Meaning |
|---|---|
PATH |
Directory to explore (default: .) |
-j, --json <PATH> |
Explore a JSON file instead of a directory (- 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 --json PATH (or -j PATH) to explore one JSON document instead.
Objects become tree branches, arrays keep their input order under indexed
children, and scalar values are leaves:
$ ite --json 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, fzf-style. When stdin is not a terminal and
no directory is given, ite reads a JSON document from stdin; --json -
requests the same thing explicitly. Keyboard input then comes from
/dev/tty, so the interface works as usual:
|
|
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, with arrows for the unconverted:
| Key | Action |
|---|---|
j / ↓, k / ↑ |
Move focus down / up, one visible line |
l / → |
Expand a collapsed container; on an expanded container, focus its first child |
h / ← |
Collapse an expanded container; otherwise focus its parent |
L / shift+→ |
Expand recursively |
H / shift+← |
Collapse 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 |
Descend into a container (expanding it if needed) |
J, K |
Next / previous sibling, hurdling expanded subtrees |
ctrl+f / ctrl+b |
Page down / up |
ctrl+d / ctrl+u |
Half-page down / up |
gg, G |
First line, last visible line |
q, esc, ctrl+c |
Quit |
A note for the fine print: ctrl+enter and shift+arrow require a terminal
that speaks the kitty keyboard protocol (kitty, WezTerm, foot, recent
iTerm2...). Elsewhere, the synonyms — tab, L, H — have you covered.
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
= true # then leave ite (default: false)
[]
= "attach-to-review $relpath"
= true # run detached, without leaving the TUI (default: false)
[]
= "expand-recursively" # or run an ite command instead
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, select, accept,
accept-alternate, descend, next-sibling, prev-sibling, page-down,
page-up, half-page-down, half-page-up, first, last, quit.
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.