jjf
A fuzzy revision picker for jujutsu (jj).
Wraps any jj subcommand with an interactive picker over jj log. Type to
filter, hit Enter, and jjf runs your command with -r '<id>' filled in
from the revision you picked. The right pane shows a live jj show --summary of whatever's under the cursor; the bottom row shows the exact
command that's about to run.
❯ vx ┌────────────────────────────────
○ tovs chore(release): prepare 0.1.0 │ - Add LICENSE (MIT)
@ xnkx feat(ux): more refinements │ - Add README with install,
○ ssnu feat(ux): batch UX improvements ●──▶│ usage, keybindings…
○ lyxo fix(history): drop jjf entry │
○ tqvr feat: two-column layout │ M Cargo.toml
○ yksz feat: default to 'show' │ A LICENSE
○ lsqz feat: broaden picker revset │ A README.md
○ nzwx feat: initial implementation
▶ jj describe -m 'fix typo' -r 'ss'
[8/8] type filter · ↑↓/^N^P nav · tab select · enter run · ^U clear · esc quit
Install
Requires Rust 1.85+ (edition 2024) and jj 0.40+ on PATH.
Usage
Some examples:
jjf runs jj log, lets you fuzzy-pick a revision (or several), then
invokes the subcommand with -r '<id>' appended. Subcommands that don't
accept revisions (status, log, git fetch, etc.) are detected
upfront and rejected with a friendly message — no wasted picker cycle.
With shell integration installed (see below), jjf with no arguments
defaults to jjf show so it doubles as a quick rev browser.
Smart prefix matching
Type the bold portion of any change ID shown in jj log and that
revision is highlighted instantly. Falls back to fuzzy-matching the
description text:
| Query matches | Score |
|---|---|
Prefix of change_id.shortest() |
1,000,000 |
| Prefix of full short change ID (12) | 100,000 |
Prefix of commit_id.shortest() |
10,000 |
| Prefix of full short commit ID (12) | 1,000 |
| Fuzzy match of description text | nucleo score |
Empty filter shows all revisions in original jj log order.
Keybindings
| Key | Action |
|---|---|
| Any printable char | Append to filter |
↑ / ↓, Ctrl-N / Ctrl-P, Ctrl-J / Ctrl-K |
Move cursor |
PageUp / PageDown |
Page up / down |
Home / End, Ctrl-A / Ctrl-E |
Jump to first / last |
Tab |
Toggle selection on cursor row |
Enter |
Confirm: selected rows, else cursor |
Backspace, Ctrl-H |
Delete char from filter |
Ctrl-U |
Clear filter |
Ctrl-W |
Delete word from filter |
Esc, Ctrl-C |
Cancel |
Multi-select with Tab joins picks into a -r 'A|B|C' revset union.
Subcommands that require exactly one revision (describe, edit) will
surface jj's own error if multi-selected.
Shell integration
Add to your shell rc:
# bash
# zsh
# fish
|
# nushell — save once, source from your config.nu
|
# then add: source ~/.config/nushell/jjf.nu
With this in place:
- History rewriting —
jjf describe -m 'foo'records the resolvedjj describe -m 'foo' -r '<id>'in your shell history rather than thejjf …invocation. Up-arrow recalls the resolved command, ready to re-run or edit. jjfalone runsjjf showso it's a quick rev browser.jjf init …,jjf -h,jjf --versionbypass the picker and pass through to the binary directly.
| Shell | History append | Original jjf … entry |
|---|---|---|
| bash | history -s |
deleted via history -d |
| zsh | print -s |
suppressed via zshaddhistory hook |
| fish | builtin history append |
deleted via fish_postexec hook |
| nushell | history import |
remains (sqlite history; no clean delete API) |
Without shell integration, jjf still works — it just execs jj
directly and your shell history shows the original jjf … invocation.
How it works
jjf invokes jj log --ignore-working-copy --color=always -r 'all()'
with a custom template that emits per-revision metadata (short change
ID, shortest unique change-ID prefix, short commit ID, shortest unique
commit-ID prefix) plus a custom oneline payload, separated by \x1f
sentinels.
The picker is a hand-rolled TUI rendered via direct ANSI to /dev/tty
(no ratatui, no ansi-to-tui). Two-column layout when the terminal
is at least 80 cols wide: picker on the left, jj show --summary of
the cursor row on the right (cached per revision). On Enter, the
resolved jj <sub> [args] -r '<id>' is either exec'd directly or
printed to stdout (in --emit mode, used by the shell wrappers).
The --ignore-working-copy flag avoids snapshotting during the picker
phase, which prevents races with prompt/editor integrations that
auto-snapshot (e.g. starship's jj module).
Dependencies
Runtime crates: crossterm, nucleo-matcher, anyhow, libc. Total
transitive dep count is around 50; release binary is ~700 KB stripped.
Acknowledgments
The idea for jjf came from a bash + fzf script of the same
concept (also discussed on
lobste.rs) by the author at oppi.li.
jjf is an independent Rust implementation — none of the original
script's code was viewed or used by the author — but the core insight (wrap any jj subcommand
with an fzf-style picker over jj log) is theirs and worth crediting.
License
MIT — see LICENSE.