Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

45 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

riffnav

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.

riffnav demo

Requirements

  • delta on your PATH — riffnav renders diffs with it.
  • A Nerd Font for filetype icons (optional). No Nerd Font? Press i to cycle to unicode or ascii icons, or set icon_style in the config.

Install

With the Rust toolchain (cargo):

# From a local checkout:
cargo install --path .

# Or straight from the repository:
cargo install --git https://github.com/ollipa/riffnav

This puts the riffnav binary in ~/.cargo/bin (make sure that's on your PATH).

Usage

Pipe any unified diff into it:

git diff | riffnav
git diff HEAD~3 | riffnav
git show <commit> | riffnav

Or run it bare inside a repo to diff the current branch automatically — see Run without a piped diff.

Use it as git's pager

git config --global pager.diff riffnav
git config --global pager.show riffnav

Now 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).

Keybindings

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

Configuration

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.

Reviewing changes

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.

Run without a piped diff

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 branch

Press d to cycle what's shown:

  • all uncommitted — staged + unstaged + untracked
  • stagedgit diff --staged
  • unstagedgit diff
  • branch vs basegit 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.

Watch mode

-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 second

In 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.

herdr integration

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.

How it works

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.

License

MIT

About

TUI diffing tool with file tree

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages