chore(release): 0.1.0

- Add LICENSE (MIT, copyright 2026 Stephen Waits)
- Add README.md reflecting the current TUI: two-column preview pane
  with jj show --summary, live command preview row, default-to-show
  behavior, full keybinding table, shell integration matrix, internals
  overview, attribution to oppi.li's original bash+fzf concept
- Add CHANGELOG.md (keep-a-changelog format) with 0.1.0 release notes
- Add .github/workflows/ci.yml — fmt + clippy + check on every push/PR;
  build matrix on stable + 1.85 (MSRV)
- Update Cargo.toml: refined description, repository/homepage/docs
  URLs, keywords + categories, rust-version=1.85, exclude .github and
  ux.txt from the published tarball
- Update .gitignore to also exclude ux.txt (scratch UX mockup)

Verified clean:
  cargo fmt --all -- --check
  cargo clippy --all-targets -- -D warnings
  cargo publish --dry-run --allow-dirty  (16 files, 20.6 KiB compressed)
This commit is contained in:
swaits committed 2026-04-29 19:49:11 -06:00
1 parent d2740008e9
commit 3b63d67737
7 files changed
+287 -2

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings"
jobs:
check:
name: check / clippy / fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets -- -D warnings
- run: cargo check --all-targets
build:
name: build (${{ matrix.toolchain }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: [stable, "1.85"]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
- uses: Swatinem/rust-cache@v2
- run: cargo build --release
+2
View File
@@ -1 +1,3 @@
/target
/Cargo.lock.bak
ux.txt
+44
View File
@@ -0,0 +1,44 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] — 2026-04-29
Initial release.
### Added
- Fuzzy picker over `jj log` for any jj subcommand that accepts `-r`.
- Smart prefix matching: typing the bold-highlighted portion of a change
ID jumps to that revision instantly. Falls back to nucleo fuzzy
matching against description text.
- Multi-select via `Tab`; selections joined into `-r 'A|B|C'` revset
union.
- Two-column layout (when terminal ≥ 80 cols): picker on left, live
`jj show --summary` preview of the cursor row on right, cached per
revision.
- Live command preview row showing the exact `jj …` invocation that
Enter will run.
- Preflight check via `jj <sub> --help`: subcommands that don't accept
revisions (`status`, `log`, etc.) are rejected upfront with a
friendly message.
- `--ignore-working-copy` on the internal `jj log` call so the picker
doesn't race with prompt/editor auto-snapshot integrations.
- Shell integration via `jjf init <bash|zsh|fish|nu>`:
- rewrites shell history so up-arrow recalls the resolved `jj …`
command rather than the `jjf …` invocation,
- drops the `jjf …` entry on cancel/error so cancelled invocations
don't pollute history,
- defaults `jjf` (no args) to `jjf show`,
- passes meta-commands and bare flags through to the binary directly.
- Vim-style and emacs-style navigation: `↑↓`, `Ctrl-N/P`, `Ctrl-J/K`,
`PageUp/PageDown`, `Home/End`, `Ctrl-A/E`.
- Light-terminal detection via `COLORFGBG`: cursor-row highlight uses
reverse video on light themes (where the dark gray bg is invisible).
- Synchronized output (DEC mode 2026) around each redraw to eliminate
tearing on Kitty / WezTerm / foot / recent iTerm.
[0.1.0]: https://github.com/swaits/jjf/releases/tag/v0.1.0
+14
View File
@@ -2,6 +2,20 @@
name = "jjf"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
authors = ["Stephen Waits <steve@waits.net>"]
description = "Fuzzy revision picker for jujutsu (jj). Wraps any jj subcommand with an interactive picker over jj log, with a live jj show preview pane."
license = "MIT"
repository = "https://github.com/swaits/jjf"
homepage = "https://github.com/swaits/jjf"
documentation = "https://docs.rs/jjf"
readme = "README.md"
keywords = ["jj", "jujutsu", "vcs", "picker", "fuzzy-finder"]
categories = ["command-line-utilities", "development-tools"]
exclude = [
"/.github/",
"/ux.txt",
]
[dependencies]
crossterm = "0.29"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Stephen Waits <steve@waits.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+166
View File
@@ -0,0 +1,166 @@
# jjf
A fuzzy revision picker for [jujutsu (jj)](https://github.com/jj-vcs/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
```sh
cargo install jjf
```
Requires Rust 1.85+ (edition 2024) and `jj` 0.40+ on `PATH`.
## Usage
```sh
jjf <jj-subcommand> [args...]
```
Some examples:
```sh
jjf describe -m 'fix: validate input'
jjf abandon
jjf edit
jjf squash
jjf rebase -d main
```
`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:
```sh
# bash
eval "$(jjf init bash)"
# zsh
eval "$(jjf init zsh)"
# fish
jjf init fish | source
# nushell — save once, source from your config.nu
jjf init nu | save -f ~/.config/nushell/jjf.nu
# then add: source ~/.config/nushell/jjf.nu
```
With this in place:
- **History rewriting** — `jjf describe -m 'foo'` records the resolved
`jj describe -m 'foo' -r '<id>'` in your shell history rather than the
`jjf …` invocation. Up-arrow recalls the resolved command, ready to
re-run or edit.
- **`jjf` alone** runs `jjf show` so it's a quick rev browser.
- **`jjf init …`, `jjf -h`, `jjf --version`** bypass 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](https://oppi.li/posts/jjj/) (also discussed on
[lobste.rs](https://lobste.rs/s/exlogg/jjj)) 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](LICENSE).
+1 -2
View File
@@ -279,8 +279,7 @@ fn run_loop(
passthrough: &[String],
) -> Result<Option<Vec<String>>> {
let mut app = App::new(rows);
let (mut term_cols, mut term_rows) =
crossterm::terminal::size().unwrap_or((80, 24));
let (mut term_cols, mut term_rows) = crossterm::terminal::size().unwrap_or((80, 24));
// Chrome rows: input(1) + cmd_preview(1) + hint(1) = 3.
app.last_height = (term_rows as usize).saturating_sub(3);
loop {