hop
A fast, fuzzy-finder TUI to jump between your git repositories and folders - a Rust port of git-repo-jumper, built on ratatui.
Pick an entry and hop writes its path to a handoff file your shell reads to cd into it; for git repos it also launches your git tool (lazygit by default). On top of the original it adds in-app management, two tabs each with its own archive, sections, slugs for a one-word jump, sort modes and repair for paths that have moved.
Screenshots


Features
- Fuzzy finder over all visible columns (name, branch, status, GitHub name, path).
- Live git status (branch, uncommitted changes, ahead/behind, GitHub repo name), gathered in the background and cached so the list shows instantly.
- Two tabs, each with its own archive: Git Repos and Files and Folders. Press a tab's key (
1/2) again to toggle into that kind's archive;Tab/Shift+Tabcycle the two active tabs. Each kind remembers its own sort, columns, grouping and fav-float across runs. - Sections on both kinds: group entries under named, reorderable section headers (Git and Files each keep their own section list; an archived entry keeps its group).
.toggles the grouped view (off = a flat, globally sorted list with a Section column),sjumps to a section,Mmanages them, and inside a group entries follow the active sort mode. - In-app management: add, edit, delete, favourite, archive/restore, set a slug - the config is written back preserving its comments. The add/edit form picks a section from a fuzzy-searchable list (type a new name to create it). Selecting several entries and pressing
ebulk-edits their Section/Favourite/Backup/Kind at once, changing only the fields you touch and marking differing values as mixed. - Content-aware open: a file/folder entry auto-detects its target – a folder
cds, a text file (by extension; configurable viaeditor_extensions) opens in the editor, and any other file (image, PDF, …) opens with the system's default app. - Slugs:
hop <slug>jumps straight to an entry from the shell;hop add [PATH]registers one without opening the TUI. - Bulk import:
hop scan [DIR]finds git repos recursively and offers them in a multi-select picker (--dry-runto preview,--nestedfor repos inside repos). - Sort modes: by name, most recently used, frecency (frequency weighted by recency), or a custom drag order; favourites float to the top by default (toggle with
,) and are sorted among themselves. In the grouped view the sort applies within each section. - ZIP backups:
zzips the selected/cursor entry (git repo or folder) andZzips every entry opted into the backup intozip_backup_folder, excluding build artefacts (zip_exclude_dirs, matched as a name prefix sotargetalso coverstarget.nosync) but keeping.git. The backup membership is theBackuptoggle in the add/edit form: git repos are included by default, file/folder entries excluded by default;zignores it (an explicit target is always backed up). Excluded entries show a⊘marker in theZIP Backupcolumn. The archive is named after the entry (the slugified name, e.g.(rs) mdtask→rs-mdtask.zip); two entries with the same name each get a short path-hash suffix so neither is overwritten (hop doctorreports such name clashes). A repo is only (re)written when its content changed since the last backup (name + size + CRC32 comparison), so unchanged archives are left untouched and not re-uploaded by cloud sync. Progress shows in the header bar; every tab shows aZIP Backupcolumn with each entry's last-backup date. - Detail panel (
v): an optional pane (right or bottom) with the entry's details and a recentgit log. - Missing-path marker (a red
!) with a picker that opens at the closest existing ancestor to repair the path, plus an error list (!) to repair / edit / delete all broken entries. - Status bar: a local status line (entry count, sort, last status time) and a remote line showing the last
git fetch(warns when over a day old); a progress bar replaces it while a refresh runs. - Two icon tiers (Unicode by default, or ASCII), selectable in the config.
Install
hop requires Rust 1.88 or newer (edition 2024).
From crates.io (the crate is rs-hop; the installed binary is hop):
cargo install rs-hop
Or from the project directory (puts hop in ~/.cargo/bin, which is on your PATH):
cargo install --path .
To update after code changes, re-run the install with --force (plain cargo install refuses to overwrite an existing binary):
cargo install --path . --force
cargo uninstall rs-hop removes it. (For a local build without installing, use cargo build --release; the binary is then at target.nosync/release/hop.)
Shell integration (zsh)
The binary alone writes the chosen path and exits - it cannot change your shell's directory. Wrap it in a shell function so your shell cds into the selected entry after hop exits. hop writes that path to $XDG_STATE_HOME/hop/selected-repo.txt (default ~/.local/state/hop/selected-repo.txt).
Add this to a sourced zsh file (e.g. ~/.zshrc, or a functions file it sources):
# Run hop, then cd into the selected entry after it exits.
# Optional short alias.
Clearing the file right after reading matters: commands that do not pick an entry (hop scan/add/list/doctor) leave it untouched, so without the reset a stale path from an earlier jump would cd you around unexpectedly.
The function is named hop and shadows the binary; it runs the real binary via command hop, so there is no conflict. Reload with source ~/.zshrc (or open a new shell).
Now hop (or hp) opens the TUI and drops you into the selected directory, and hop <slug> jumps directly.
Windows (PowerShell)
The handoff file is shell-agnostic; on Windows it lives under %LOCALAPPDATA%\hop\selected-repo.txt. Add this to your PowerShell profile ($PROFILE):
function hop {
& hop.exe @args
$stateHome = if ($env:LOCALAPPDATA) { $env:LOCALAPPDATA } else { $env:XDG_STATE_HOME }
$f = Join-Path $stateHome 'hop\selected-repo.txt'
if (Test-Path -LiteralPath $f -PathType Leaf) {
$d = (Get-Content -LiteralPath $f -Raw).Trim()
Clear-Content -LiteralPath $f # clear so a stale path can't cd on the next non-jump run
if ($d -and (Test-Path -LiteralPath $d -PathType Container)) {
Set-Location -LiteralPath $d
}
}
}
Set-Alias hp hop
Configuration
hop reads $XDG_CONFIG_HOME/hop/config.toml (default ~/.config/hop/config.toml). It is created on first use; add entries from the TUI (n) or with hop add <path>.
= "lazygit" # tool launched for git repos; omit to disable
= "you" # stripped from displayed remote names
= false # show example_git_info instead of real status
= false # git fetch in the background when hop starts
= false # ask before quitting with `q` (Ctrl+Q never asks)
# editor = "nvim" # for opening text files; else $VISUAL / $EDITOR
# editor_extensions = ["rs", "md", "txt"] # override the built-in text list
# zip_backup_folder = "~/Backups/repos" # where z / Z write ZIP backups
# zip_exclude_dirs = ["target", "node_modules"] # omit for the built-in list
[]
= "default" # default | monochrome | <your [themes.*] name>
= "unicode" # unicode | ascii
# colors = { accent = "#f7a3bd" } # override individual palette colours
# [themes.midnight] # a custom theme, selected via [appearance].theme
# accent = "#8899ff"
# background = "#0b0b14"
# [keys] # rebind an action; a value may be a list
# add = "n"
# delete = ["d", "backspace"]
[]
= 30
= 6
= 20
[]
= 10
= 14
[[]]
= "(rs) hop" # optional; defaults to the path's basename
= "/Users/you/Code/hop"
= "git" # git | path (path = file or folder, auto-detected)
= "hop" # optional; enables `hop hop`
= true # favourites sort first
# section = "Work" # groups the entry (per-kind section list)
# archived = false # archived entries move to their kind's archive
See examples/config.toml for a fuller sample. Try it without touching your real config:
HOP_CONFIG=examples/config.toml cargo run
Commands
hop open the TUI (the default when no command is given)
hop <slug> jump to a slug: write the path (cd only; no tool/editor)
hop <slug> -s deprecated no-op (jumping only cd's anyway)
hop add [PATH] add an entry (default: the current dir; --slug/--section/--name/--kind)
hop scan [DIR] find git repos under DIR and import the chosen ones
(--depth N / --nested / --dry-run / --yes)
hop doctor report problems (missing paths, bad/duplicate slugs)
hop list list entries (a table on a terminal, plain lines when piped)
hop list --json list entries as JSON, for scripts
hop show-config print the resolved config file path (was: config-path)
hop completions SH print a shell completion script (bash/zsh/fish/…)
hop man print the manpage in roff format
-C / --config PATH use a specific config file (also via HOP_CONFIG)
--fetch git fetch first (TUI: on start; hop <slug>: before the jump)
--no-fetch skip the fetch even when fetch_on_start is enabled
--cached TUI: show only cached status, run no git
--demo open the TUI with built-in demo data (for screenshots)
-v / --verbose more detail in the log file; repeat for more
-q / --quiet log errors only
--no-color disable colored output (as does setting NO_COLOR)
-h / --help show the help (on every command) and exit
-V / --version show the version and exit
hop scan needs a terminal to show its picker. Without one it exits 2 and names the way out: pass --yes to import everything it found, or --dry-run to only list it.
Streams and exit codes
stdout carries only the payload a caller would pipe onwards; every hint, confirmation, warning and error goes to stderr. So hop list > entries.txt writes entries and nothing else, and hop list --json | jq . never chokes on a message.
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | runtime error (including hop doctor finding problems) |
| 2 | usage error: unknown option or command, missing value, or a prompt with no terminal to show it on |
| 130 | interrupted with Ctrl+C |
Errors are reported on stderr as hop: error: <what happened>, one greppable line.
Environment overrides
Each takes precedence over config.toml:
HOP_CONFIG the config file to use (same as -C / --config)
HOP_GIT_PROGRAM the tool launched for git repos
HOP_EDITOR the editor for text files
HOP_THEME the active theme name
HOP_GLYPHS unicode | ascii
HOP_CONFIRM_QUIT true | false: ask before quitting with `q`
Keyboard shortcuts
| Key | Action |
|---|---|
1 / 2 |
Git Repos / Files and Folders (press again for that kind's archive) |
Tab / Shift+Tab |
cycle the two active tabs |
↑ / ↓ |
move cursor (wraps) |
Home / End · g / G |
top / bottom (both spellings work) |
PgUp/PgDn · Ctrl+u/Ctrl+d |
page · half page |
Space |
toggle selection · Esc: clear |
Shift+↑/↓ · Shift+PgUp/PgDn |
extend the selection by a row · by a page |
Enter |
jump only: write path and exit (folder → cd, file → its parent) |
L |
open: git → tool · folder → cd · text file → editor · other file → default app |
l |
git repo: open the git tool (lazygit) as an overlay, then return to the list |
o |
jump only: write path and exit (folder → cd, file → its parent) |
O |
force open with the default app (regardless of kind) |
b |
open the selected repo on GitHub in the browser |
v |
show or hide the detail panel |
V |
move the detail panel: right ↔ bottom |
Ctrl+↑/↓ · Ctrl+PgUp/PgDn |
scroll the detail panel by a row · by a page |
Ctrl+←/→ |
make the detail panel smaller / bigger |
c |
cycle the table's columns (Standard → Code → Activity) |
t |
pick the column to sort by (picking the active one flips the direction) |
, |
toggle floating favourites to the top |
. |
toggle grouping into sections (off = flat, globally sorted, with a Section column) |
f |
live fuzzy filter (Esc clears; matched characters are highlighted) |
F |
toggle showing only git repos with a status change |
s |
jump to a section (in the grouped view) |
M |
manage sections (add / rename / delete / reorder) |
Alt+↑/↓ |
reorder the entry within its group (custom sort; favourites stay on top) |
n |
add an entry (fill the form; ^O opens the path picker) |
e |
edit the entry (bulk-edit Section/Favourite/Backup/Kind when several are selected) |
d / Del / Backspace |
delete (acts on the selection, else the cursor; confirm) |
u |
undo the last change |
* |
toggle favourite (selection or cursor) |
z |
zip the selected/cursor entry (repo or folder) to the backup folder |
Z |
zip every entry opted into backup (the form's Backup toggle) |
A |
archive / restore (selection or cursor) |
S |
set or change the slug |
i |
toggle the slug column (dim, italic) after the name |
y |
copy the selected entry's path to the clipboard |
p |
repair a missing path |
! |
list entries with path errors, then repair / edit / delete |
r |
git tabs: reload status (R: + git fetch) · Files tab: check that paths still exist |
x |
refresh selection/cursor · X: with git fetch |
? |
toggle the help overlay |
F1 |
show / hide the shortcut-hint footer (remembered across runs) |
q |
quit (asks first when confirm_quit = true) |
Ctrl+Q |
force quit, no questions (from any state) |
Development
cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmt --check
See CLAUDE.md for the architecture and coding rules, and CONTRIBUTING.md for the contribution workflow.
License
Licensed under the MIT License.