A git diff pager with a GitHub-style file tree, powered by delta.
🤖 Built with AI. riffnav — its code, tests, and docs — was written with AI assistance
riffnav reads a unified diff on stdin, renders each file with delta, and wraps
it in a terminal UI: a navigable file tree on the left, the rendered diff on the
right. It's a Rust take on diffnav.
- delta on your
PATH— riffnav renders diffs with it. - A Nerd Font for filetype icons (optional). No Nerd Font? Press
ito cycle tounicodeorasciiicons, or seticon_stylein the config.
With the Rust toolchain (cargo):
# From a local checkout:
cargo install --path .
# Or straight from the repository:
cargo install --git https://github.com/ollipa/riffnavThis puts the riffnav binary in ~/.cargo/bin (make sure that's on your PATH).
Pipe any unified diff into it:
git diff | riffnav
git diff HEAD~3 | riffnav
git show <commit> | riffnavOr run it bare inside a repo to diff the current branch automatically — see Run without a piped diff.
git config --global pager.diff riffnav
git config --global pager.show riffnavNow git diff and git show open in riffnav. (Setting core.pager also works,
but scoping to diff/show avoids sending git log through it.)
By default riffnav follows your delta.side-by-side git setting; force a layout
for one run with -s (side-by-side) or -u (unified).
| Key | Action |
|---|---|
j / k (or ↑ / ↓) |
Move selection (tree) / scroll (diff), per focus |
n / p (or N) |
Next / previous file |
Ctrl-d / Ctrl-u |
Scroll diff half a page |
PgDn / PgUp |
Page down / up (scroll diff or move tree, per focus) |
g / G |
Top / bottom of the diff |
Enter / Space |
Expand / collapse the selected folder |
Tab |
Switch focus between tree and diff |
t / / |
Fuzzy-find a file |
s |
Toggle side-by-side / unified |
e |
Toggle the file tree |
i |
Cycle icon style (nerd → unicode → ascii) |
T |
Cycle diff theme (delta → github-dark → github-light) |
y |
Copy the selected file's path |
v / V |
Mark the file viewed / jump to the next unviewed file |
d |
Cycle the diff source — uncommitted → staged → unstaged → branch-vs-base (only on a bare launch) |
o |
Open the selected file in $EDITOR |
z |
Toggle zoom on riffnav's pane (only inside herdr) |
? |
Toggle the help overlay |
q / Esc / Ctrl-c |
Quit |
riffnav reads $XDG_CONFIG_HOME/riffnav/config.toml (or
~/.config/riffnav/config.toml); override with --config <FILE>. Every key is
optional. Settings resolve as defaults < config file < CLI flags.
# ~/.config/riffnav/config.toml
# side_by_side = false # omit to follow your delta.side-by-side default
icon_style = "nerd" # nerd | unicode | ascii
diff_theme = "github-dark" # github-dark | github-light | delta (inherit gitconfig)
tree_width = 32 # columns for the file-tree pane
show_tree = true
start_focus = "diff" # "diff": open in the first file (n/p between files) | "tree"
show_header = true
show_footer = true
open_depth = 64 # expand folders shallower than this on launch
review_retention_days = 90 # days to keep "viewed" marks before GC
review_auto_advance = true # jump to next unviewed file after marking viewed
review_sync_github = false # push "viewed" marks to the matching GitHub PR (needs `gh`)
# base_branch = "main" # base for "branch vs base"; omit to auto-detect
# diff_source = "all" # bare-launch view: all|committed|staged|unstaged (omit = adaptive)See config.example.toml for the annotated version.
Press v to mark the selected file viewed — it gets a green ✓ and dims in
the tree — and V to jump to the next unviewed file. Marking viewed also
advances to the next unviewed file by default (review_auto_advance), so review
flows file-to-file. The header shows your progress (✓ 3/8 viewed).
Viewed marks persist across runs, scoped per repository and branch (like
GitHub's per-PR "Viewed" checkbox), and are keyed on the content of each
change: edit a file you'd marked viewed and it reverts to unviewed automatically,
just as GitHub un-ticks a file the author pushes to. State lives under
$XDG_STATE_HOME/riffnav/viewed/ and is garbage-collected by age
(review_retention_days, default 90). Outside a git repo (e.g. an arbitrary diff
piped in) marking still works for the session but isn't persisted.
Launch riffnav bare — no diff on stdin, not watch mode — inside a git repo and
it diffs the repo for you. By default it shows your uncommitted changes
(staged, unstaged, and untracked files); when the working tree is clean it falls
back to what your branch adds over its base (git diff <base>...HEAD, like a
PR diff).
riffnav # in a repo: uncommitted changes, or branch-vs-base if clean
riffnav --diff committed # force the branch-vs-base (PR) view
riffnav --base develop # compare against a specific base branchPress d to cycle what's shown:
- all uncommitted — staged + unstaged + untracked
- staged —
git diff --staged - unstaged —
git diff - branch vs base —
git diff <base>...HEAD
The base branch is detected from origin/HEAD (falling back to a local
main/master); set it with --base <ref> or the base_branch config key.
Choose the starting view with --diff <all|committed|staged|unstaged> or the
diff_source config key. Piping a diff in (or --watch) behaves exactly as
before — the bare launch is just an extra entry point.
-w / --watch keeps riffnav open and refreshes when your working tree changes —
handy on a second monitor while you edit.
riffnav --watch # re-runs `git diff` on change
riffnav --watch --watch-cmd "git diff --staged"
riffnav --watch --watch-interval 1 # also poll every secondIn watch mode the diff is produced by --watch-cmd (default git diff), not
stdin. Changes are detected by a filesystem watcher (debounced) plus the polling
interval as a safety net; the view only rebuilds when the diff actually changes,
and your selected file is preserved across refreshes.
When riffnav runs inside herdr (detected via HERDR_ENV=1),
the z key toggles zoom on riffnav's pane — maximizing it to fill the window,
or restoring it. riffnav talks to herdr's socket API over its Unix
control socket (found via HERDR_SOCKET_PATH / HERDR_SESSION, or the default
session socket). Outside herdr the key does nothing and isn't shown in the footer
or help.
stdin → split per file (diff --git) → build the tree → on selection, run the
file's hunk through delta (cached per file/width/layout) and convert its ANSI
output to styled text with ansi-to-tui, drawn with
ratatui. Because stdin is the diff, key input is read from /dev/tty.
MIT
