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:
7 files changed
+287
-2
No files matched your search
@@ -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
|
||||||
@@ -1 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
|
/Cargo.lock.bak
|
||||||
|
ux.txt
|
||||||
@@ -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
@@ -2,6 +2,20 @@
|
|||||||
name = "jjf"
|
name = "jjf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
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]
|
[dependencies]
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -279,8 +279,7 @@ fn run_loop(
|
|||||||
passthrough: &[String],
|
passthrough: &[String],
|
||||||
) -> Result<Option<Vec<String>>> {
|
) -> Result<Option<Vec<String>>> {
|
||||||
let mut app = App::new(rows);
|
let mut app = App::new(rows);
|
||||||
let (mut term_cols, mut term_rows) =
|
let (mut term_cols, mut term_rows) = crossterm::terminal::size().unwrap_or((80, 24));
|
||||||
crossterm::terminal::size().unwrap_or((80, 24));
|
|
||||||
// Chrome rows: input(1) + cmd_preview(1) + hint(1) = 3.
|
// Chrome rows: input(1) + cmd_preview(1) + hint(1) = 3.
|
||||||
app.last_height = (term_rows as usize).saturating_sub(3);
|
app.last_height = (term_rows as usize).saturating_sub(3);
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
Reference in new issue
Block a user