Skip to content
bitbrew-devPublic

About

A smarter cd, built in Rust: directory history, Git worktree jumps, named shortcuts, and optional zoxide/fzf integration.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

cj

cj is a small shell companion for jumping between useful directories. It adds named aliases and mounts, Git-aware shortcuts, optional zoxide resolution, and an fzf worktree jump while leaving the final directory change to your shell's real cd builtin.

Starting with 1.0.0, documented commands, configuration, shell behavior, and JSON output follow the v1 compatibility contract. That contract also describes how to upgrade generated shell integration and consume output in scripts.

Install

Homebrew

The Homebrew formula installs a prebuilt binary on macOS or Linux for ARM64 or x86_64 (Intel/AMD). The release repository is private, so provide a GitHub token that can read it:

export HOMEBREW_GITHUB_TEAM_BITBREW_DEV_API_TOKEN="$(gh auth token)"
brew install benbenbang/forge/cj

Cargo

cargo install cj-rs

Building from source requires Rust 1.96.0 or newer. The same minimum version is pinned in rust-toolchain.toml and exercised by CI.

Windows

Download cj-rs-<version>-x86_64-pc-windows-msvc.zip from GitHub Releases, extract it, and put cj.exe on PATH.

cj supports Bash, Zsh, Nushell, and PowerShell. Generate integration and completion files, then load them from the matching shell configuration. cj prints source text by default; -o/--output writes generated shell source to a file and creates missing parent directories. It never edits a shell configuration or profile.

For Bash:

cj init bash -o ~/.config/cj/init.bash
cj completions bash -o ~/.config/cj/completions.bash

# Add to ~/.bashrc:
source ~/.config/cj/init.bash
source ~/.config/cj/completions.bash

For Zsh:

cj init zsh -o ~/.config/cj/init.zsh
cj completions zsh -o ~/.config/cj/completions.zsh

# Add to ~/.zshrc:
autoload -Uz compinit && compinit
source ~/.config/cj/init.zsh
source ~/.config/cj/completions.zsh

For Nushell:

cj init nu -o ~/.config/nushell/cj.nu
cj completions nu -o ~/.config/nushell/cj-completions.nu

# Add to config.nu:
use ~/.config/nushell/cj.nu *
use ~/.config/nushell/cj-completions.nu *

For PowerShell 7:

$CjConfig = Join-Path (Split-Path -Parent $PROFILE) 'cj'
cj init powershell -o (Join-Path $CjConfig 'init.ps1')
cj completions powershell -o (Join-Path $CjConfig 'completions.ps1')

# Add these lines to $PROFILE:
$CjConfig = Join-Path (Split-Path -Parent $PROFILE) 'cj'
. (Join-Path $CjConfig 'init.ps1')
. (Join-Path $CjConfig 'completions.ps1')

Restart the shell or source its configuration after making the change. pwsh is accepted as an input alias for powershell; generated source and completion candidates use the canonical powershell name. cj init installs the configured directory completion binding by default on Bash, Zsh, Nushell, and PowerShell. The Bash widget requires Bash 4 or newer; macOS's system Bash 3 still supports the cd wrapper and Tab completion. PowerShell installs the widget when PSReadLine is available. Use cj init <shell> --no-setup-key-binding to generate the wrapper and completions without installing the widget.

Jumping

Once the shell integration is loaded, use cd normally:

cd src          # existing directories always use the builtin directly
cd project      # otherwise use the configured resolver (zoxide by default)
cd code         # a configured alias
cd external-ssd # a configured mount
cd top          # top level of the current Git repository
cd origin       # main worktree; "og" is also enabled by default
cd ^^^          # three directories up
cd ^3           # the same three parent moves
cd vvv          # go back three directory-history entries
cd v3           # the same three history entries

The down ticker uses browser-style directory history: every successful cd change remembers the directory you left, including unrelated paths, aliases, mounts, raw paths, and zoxide jumps. cd v goes back one entry; cd vvv goes back three. Moving up with cd ^^^ records each traversed parent so cd vvv retraces the move. History keeps the latest 100 departures in the current shell session and resets when you reload the integration. Existing directories always win over tickers.

Repeated tickers and counted tickers are equivalent: ^^ means ^2, and vvv means v3. Counts are decimal integers from 1 to 2147483647; leading zeros are accepted as decimal (^08 means eight levels). Zero, signed, fractional, and overflowing counts report an error without changing the directory or history. The same syntax uses your configured tickers, for example u5 or k3.

Upward navigation stops at the filesystem root even if the count exceeds the available parents. It records only the actual parent moves, subject to the history limit; already at root is a successful no-op. For example, cd ^5 from /a/b reaches /, and cd v2 returns to /a/b. Backward navigation remains strict: cd v5 requires five history entries and fails without consuming any if fewer are available. It goes back through history, not down into child directories.

A real directory named ^2 or even ^-1 still takes precedence. Otherwise, numeric-looking targets beginning with a configured ticker are reserved for counted navigation before aliases, mounts, or Git keywords. Names such as venv remain ordinary targets. Use -r to treat a destination as a literal path.

Going back consumes entries without adding the current directory. An empty or insufficient history reports cj: directory history exhausted; a missing history destination reports the shell's directory error. Both leave history and the current directory unchanged. Failed commands and moves to the current directory also preserve history. Native escapes such as builtin cd or Set-Location bypass history tracking. Regenerate and reload your integration after upgrading to use this behavior.

Resolver flags make the choice explicit:

cd -z project   # use only zoxide; missing/failing zoxide is an error
cd -Z code      # disable zoxide; retain aliases, mounts, tickers, and Git shortcuts
cd -r code      # treat "code" literally; bypass all cj resolution and zoxide

When zoxide is the configured default but is unavailable or cannot find a match, cj falls back to normal literal-path behavior. cj invokes the configured zoxide executable directly; it does not run commands through a shell.

For ordinary resolution, the first match wins: an existing directory, navigation ticker, alias, mount, Git keyword, then zoxide or the literal fallback. A configured alias or mount whose destination is unavailable is an error; it does not fall through to a later resolver.

Worktrees

List the current repository's worktrees in a table:

cj -w
cj --worktree

JSON output and paths relative to the current directory are also available:

cj -w --format json
cj -w --relative

Jump to the main worktree with Enter, or choose a worktree with Tab completion:

cd -jw<Enter>          # jump directly to the main worktree
cd -jw<Tab>            # complete a worktree path, then press Enter
cd -jw <Tab>           # a space before Tab also works
cd --jump-worktree<Tab> # long form of the same completion trigger

cj -w/--worktree lists worktrees. The cd completion requires the generated shell integration so the parent shell can perform the directory change. In Bash, Zsh, Nushell, and PowerShell, pressing Tab after -jw or --jump-worktree, with or without trailing whitespace, offers only this repository's worktree paths through the shell's native completion system. A completion frontend such as fzf-tab may render those candidates with fzf, but cj does not launch a nested picker during completion and works without fzf. Choosing a candidate only inserts its path; press Enter to change directory. After whitespace, Bash, Zsh, and Nushell keep the flag and complete its literal destination argument; PowerShell replaces the flag and whitespace together.

Pressing Enter on bare cd -jw or cd --jump-worktree jumps directly to the primary worktree, whichever branch it currently has checked out. This shortcut is independent of configured keyword names and local directories named origin or og; it does not switch branches or launch fzf. A successful move records one history entry, so cd v returns to the previous directory. Already at the primary root is a no-op; outside a Git repository, the command reports an error and leaves the directory and history unchanged. Trailing spaces before Enter behave the same.

Zsh completion menus, including fzf-tab, display paths under $HOME as ~/…; the inserted destination and preview path remain absolute.

The standalone cj -jw / cj --jump-worktree command remains available to print a worktree path selected through fzf.

The key installed by cj init completes a directory from history. Its default chord is Ctrl+O on macOS and Windows, and Alt+O on Linux. Each OS's key-bindings entry has an ordered behaviors list, defaulting to ["zoxide", "cj"]. zoxide opens its interactive directory picker using fzf. cj cycles through cj's browser-style directory history without external dependencies. Both append a safely quoted directory to the current command line; press Enter to execute it. The key itself never changes the current directory.

Behaviors run from left to right until one supplies a directory. A missing dependency or empty candidate source falls through to the next behavior. Cancelling a picker stops the chain and preserves the command line, cursor, and current directory; other errors stop with a diagnostic. Reorder the list to change which behavior is tried first, or use a single entry to select one source.

After a cj history completion, repeated presses cycle from newest to oldest, then restore the original line and cursor. Editing the line starts a new cycle. Completion itself does not consume history. Dependency availability is checked on each invocation, so installing or removing zoxide or fzf needs no regeneration. The binding preserves zoxide settings such as _ZO_FZF_OPTS.

Regenerate shell integration after upgrading to receive the default binding:

cj init zsh -o ~/.config/cj/init.zsh
source ~/.config/cj/init.zsh

To keep the cd wrapper and Tab completion without the directory widget, add --no-setup-key-binding. The old --setup-key-binding flag is still accepted as a compatibility no-op; existing initialization commands can keep it or remove it. The two flags cannot be combined. An empty behaviors list in the current OS's key-bindings entry disables the widget in configuration.

Configuration

The default config path is ${XDG_CONFIG_HOME:-$HOME/.config}/cj/config.toml on macOS and Linux. Windows uses %APPDATA%\cj\config.toml (while still honoring XDG_CONFIG_HOME when set). Every section and field is optional. A complete example is:

[behavior]
default = "zoxide" # or "builtin"

[programs]
zoxide = "zoxide"
fzf = "fzf"

[key-bindings]
macos = { key = "ctrl-o", behaviors = ["zoxide", "cj"] }
linux = { key = "alt-o", behaviors = ["zoxide", "cj"] }
windows = { key = "ctrl-o", behaviors = ["zoxide", "cj"] }

[keywords]
top = ["top"]
main-worktree = ["origin", "og"]

[tickers]
navigate_up = "^"
navigate_down = "v"

[aliases]
code = "~/Git"
downloads = "~/Downloads"

[mounts]
icloud = { provider = "icloud" }
google-work = { path = "~/Library/CloudStorage/GoogleDrive-work@example.com" }
external-ssd = { path = "/Volumes/External SSD" }
onedrive-work = { provider = "onedrive", account = "business" }

Program values may be executable names found on PATH or explicit executable paths. Key binding keys accept ctrl-o, alt-o, or none. Behaviors run from left to right: zoxide uses its interactive picker and cj cycles through browser-style directory history. Missing dependencies or an empty candidate source fall through; cancellation preserves the command line, cursor, and directory, and other errors stop the chain. Use ["cj", "zoxide"] to prefer cj, ["cj"] or ["zoxide"] for one source, or [] to disable the binding. Unknown or duplicate behaviors are errors. The legacy strings (for example, linux = "alt-o") remain valid and use ["zoxide", "cj"]; "none" disables the binding. Omitted keys retain the platform default, and omitted behaviors retain the default chain.

Alias and explicit mount paths must be absolute or start with ~/; Windows also accepts ~\. Drive-letter paths and UNC paths are supported. cj expands only that leading home marker and does not evaluate shell variables, globs, or command substitutions. Paths containing spaces or single quotes are preserved. A mount must specify exactly one path or provider; supported providers are icloud, google-drive, and onedrive. OneDrive alone accepts the optional account selector personal or business; resolution succeeds only when exactly one matching root is registered. Alias and mount names may not collide.

Navigation tickers are configurable single characters. Allowed values are ^, v, u, d, j, and k; the intentionally small allowlist excludes shell operators and other characters that are unsafe to type unquoted. Up and down must use different values. After changing tickers or key bindings, rerun the matching cj init command and reload the generated file. The cd -jw Tab hook is also part of this generated integration, so regenerate it after upgrading cj. Completions contain a deterministic snapshot of configured keywords, aliases, mounts, and tickers, so regenerate the completion file after changing them.

When migrating from 0.1.0, replace keywords.tickers = ["^"] with the new top-level [tickers] section shown above.

Use -C or --config to select a different file. When generating shell setup, the selected config path is embedded safely in the generated wrapper:

cj -C "$HOME/.config/cj/work.toml" init zsh \
  -o "$HOME/.config/cj/init.zsh"

There is no configurable cd executable: cd is a shell builtin. cj only prints the destination, and the generated integration invokes the parent shell's native location command (cd or Set-Location).

Config initialization and mount discovery

Create a new config at the default path, or at the path selected with -C:

cj config init
cj -C ~/.config/cj/work.toml config init

Initialization never overwrites an existing file. On macOS and Windows, --preamp scans for reachable mounts and adds only unambiguous results as explicit absolute path entries:

cj config init --preamp

Discovery checks the standard iCloud Drive directory, Google Drive directories under ~/Library/CloudStorage/GoogleDrive-* and /Volumes/GoogleDrive*, and mounted directories under /Volumes. It uses the fixed names icloud and google-drive; other volume labels are converted to names such as External SSD → external-ssd. Ambiguous Google accounts and name collisions are reported and not written.

On Windows, discovery reads the current user's registered cloud sync roots through the Windows sync-root API and enumerates non-system drive roots. OneDrive personal and business roots are named separately. Multiple matching accounts, duplicate volume labels, mapped network drives, and cloud drives with changeable drive letters are reported but never persisted automatically. Verbose output shows explicit path snippets so the user can choose one. Discovery does not inspect OneDrive settings, credentials, account tokens, or Microsoft Graph.

Inspect discoverable mounts without changing the config:

cj mounts scan
cj mounts scan -f json
cj -v mounts scan

The default format is a table. JSON is available for tooling, and the global -v/--verbose flag includes discovery details such as skipped candidates and ready-to-copy mount entries.

License

MIT

About

A smarter cd, built in Rust: directory history, Git worktree jumps, named shortcuts, and optional zoxide/fzf integration.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages