From 3b63d677370eac591ffc8ab4dce8d73f470b6ff7 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Wed, 29 Apr 2026 19:35:30 -0600 Subject: [PATCH] chore(release): 0.1.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .github/workflows/ci.yml | 39 +++++++++ .gitignore | 2 + CHANGELOG.md | 44 +++++++++++ Cargo.toml | 14 ++++ LICENSE | 21 +++++ README.md | 166 +++++++++++++++++++++++++++++++++++++++ src/tui.rs | 3 +- 7 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..42b360b --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index ea8c4bf..c5ba117 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/Cargo.lock.bak +ux.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6bceb38 --- /dev/null +++ b/CHANGELOG.md @@ -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 --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 `: + - 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 diff --git a/Cargo.toml b/Cargo.toml index 15bd4c1..582261e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,20 @@ name = "jjf" version = "0.1.0" edition = "2024" +rust-version = "1.85" +authors = ["Stephen Waits "] +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" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..694cb77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Stephen Waits + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c4b541 --- /dev/null +++ b/README.md @@ -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 ''` 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 [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 ''` 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 ''` 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 [args] -r ''` 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). diff --git a/src/tui.rs b/src/tui.rs index 2ba7256..07f129e 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -279,8 +279,7 @@ fn run_loop( passthrough: &[String], ) -> Result>> { 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 {