Last active
March 20, 2026 21:17
-
-
Save jongalloway/a214f146d1cad383d3e2b17a5142defb to your computer and use it in GitHub Desktop.
jump-utils.zsh - Lightweight directory jump utilities for Zsh (macOS port of jump-utils.ps1)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # jump-utils.zsh - Lightweight directory jump utilities for Zsh | |
| # Ported from: https://gist.github.com/jongalloway/332c3b15d2557dfa71575b79713ffbf1 | |
| # | |
| # Quick install - add to your ~/.zshrc: | |
| # [[ -f ~/.jump-utils.zsh ]] && source ~/.jump-utils.zsh | |
| # | |
| # Usage: | |
| # repo <name> - pushd to a repo (supports partial match + tab completion) | |
| # repo list - list all repos | |
| # repo - pushd to repo root | |
| # jump <target> - pushd to a named directory (supports partial match) | |
| # jump add <n> [p] - add a custom jump target (defaults to current dir) | |
| # jump remove <name>- remove a custom jump target | |
| # jump list - list all jump targets | |
| # jump - popd (go back) | |
| # Aliases: jr = repo, j = jump | |
| # --- Config --- | |
| _JUMP_CONFIG="$HOME/.jump-utils.conf" | |
| _JUMP_TARGETS_FILE="$HOME/.jump-targets" | |
| # --- Repo root (lazy prompt on first use) --- | |
| _jump_ensure_repo_root() { | |
| if [[ -n "$REPO_ROOT" && -d "$REPO_ROOT" ]]; then return; fi | |
| # Try loading from config file | |
| if [[ -f "$_JUMP_CONFIG" ]]; then | |
| source "$_JUMP_CONFIG" | |
| if [[ -n "$REPO_ROOT" && -d "$REPO_ROOT" ]]; then return; fi | |
| fi | |
| # Clear any invalid value | |
| unset REPO_ROOT | |
| local default_root="$(pwd)" | |
| echo -n "Enter your repositories root directory [$default_root]: " | |
| read -r REPO_ROOT | |
| # Default to current directory if empty | |
| if [[ -z "$REPO_ROOT" ]]; then | |
| REPO_ROOT="$default_root" | |
| fi | |
| if [[ ! -d "$REPO_ROOT" ]]; then | |
| echo "\033[31mDirectory '$REPO_ROOT' does not exist. REPO_ROOT not set.\033[0m" >&2 | |
| unset REPO_ROOT | |
| return 1 | |
| fi | |
| export REPO_ROOT | |
| # Persist it | |
| echo "export REPO_ROOT=\"$REPO_ROOT\"" > "$_JUMP_CONFIG" | |
| echo "\033[32mREPO_ROOT saved to $_JUMP_CONFIG.\033[0m" | |
| } | |
| # --- repo command --- | |
| repo() { | |
| local name="$1" | |
| if [[ "${name:l}" == "root" ]]; then | |
| # Force re-prompt for REPO_ROOT | |
| unset REPO_ROOT | |
| rm -f "$_JUMP_CONFIG" | |
| _jump_ensure_repo_root | |
| return | |
| fi | |
| _jump_ensure_repo_root || return | |
| if [[ "${name:l}" == "list" ]]; then | |
| for d in "$REPO_ROOT"/*/; do | |
| [[ -d "$d" ]] && echo " ${d:t}" | |
| done | |
| return | |
| fi | |
| if [[ -n "$name" ]]; then | |
| local target="$REPO_ROOT/$name" | |
| if [[ -d "$target" ]]; then | |
| pushd "$target" > /dev/null | |
| else | |
| # Partial match (case-insensitive) | |
| local -a matches | |
| matches=("$REPO_ROOT"/${~name}*(N/i)) | |
| if [[ ${#matches} -eq 0 ]]; then | |
| # Try prefix glob | |
| matches=("$REPO_ROOT"/${~name}*(N/Di)) | |
| fi | |
| # Case-insensitive prefix matching via loop | |
| matches=() | |
| for d in "$REPO_ROOT"/*/; do | |
| local dname="${d:t}" | |
| if [[ "${dname:l}" == "${name:l}"* ]]; then | |
| matches+=("$d") | |
| fi | |
| done | |
| if [[ ${#matches} -eq 1 ]]; then | |
| pushd "${matches[1]}" > /dev/null | |
| elif [[ ${#matches} -gt 1 ]]; then | |
| echo "\033[31mAmbiguous match for '$name':\033[0m" >&2 | |
| for m in "${matches[@]}"; do echo " ${m:t}" >&2; done | |
| else | |
| echo "\033[31mRepository '$name' not found in $REPO_ROOT\033[0m" >&2 | |
| fi | |
| fi | |
| else | |
| pushd "$REPO_ROOT" > /dev/null | |
| fi | |
| } | |
| # --- Tab completion for repo --- | |
| _repo_complete() { | |
| if [[ -z "$REPO_ROOT" && -f "$_JUMP_CONFIG" ]]; then | |
| source "$_JUMP_CONFIG" | |
| fi | |
| [[ -z "$REPO_ROOT" ]] && return | |
| local -a repos | |
| repos=(list root) | |
| for d in "$REPO_ROOT"/*/; do | |
| [[ -d "$d" ]] && repos+=("${d:t}") | |
| done | |
| _describe 'repo' repos | |
| } | |
| compdef _repo_complete repo | |
| compdef _repo_complete jr | |
| # --- Built-in jump targets --- | |
| typeset -A _JUMP_BUILTINS | |
| _JUMP_BUILTINS=( | |
| Documents "$HOME/Documents" | |
| Downloads "$HOME/Downloads" | |
| Desktop "$HOME/Desktop" | |
| Pictures "$HOME/Pictures" | |
| Movies "$HOME/Movies" | |
| Music "$HOME/Music" | |
| ) | |
| # --- Custom targets (simple key=path file) --- | |
| _jump_load_custom() { | |
| typeset -gA _JUMP_CUSTOM | |
| _JUMP_CUSTOM=() | |
| if [[ -f "$_JUMP_TARGETS_FILE" ]]; then | |
| while IFS='=' read -r key val; do | |
| [[ -n "$key" && "$key" != \#* ]] && _JUMP_CUSTOM[$key]="$val" | |
| done < "$_JUMP_TARGETS_FILE" | |
| fi | |
| } | |
| _jump_save_custom() { | |
| : > "$_JUMP_TARGETS_FILE" | |
| for key in "${(@k)_JUMP_CUSTOM}"; do | |
| echo "${key}=${_JUMP_CUSTOM[$key]}" >> "$_JUMP_TARGETS_FILE" | |
| done | |
| } | |
| _jump_all_targets() { | |
| typeset -gA _JUMP_ALL | |
| _JUMP_ALL=() | |
| for key in "${(@k)_JUMP_BUILTINS}"; do | |
| _JUMP_ALL[$key]="${_JUMP_BUILTINS[$key]}" | |
| done | |
| _jump_load_custom | |
| for key in "${(@k)_JUMP_CUSTOM}"; do | |
| _JUMP_ALL[$key]="${_JUMP_CUSTOM[$key]}" | |
| done | |
| } | |
| # --- jump command --- | |
| jump() { | |
| local name="$1" | |
| shift 2>/dev/null | |
| local rest=("$@") | |
| if [[ -z "$name" ]]; then | |
| popd > /dev/null | |
| return | |
| fi | |
| if [[ "${name:l}" == "add" ]]; then | |
| local tname="${rest[1]}" | |
| if [[ -z "$tname" ]]; then | |
| echo "\033[31mUsage: jump add <name> [path] (defaults to current directory)\033[0m" >&2 | |
| return 1 | |
| fi | |
| local tpath="${rest[2]:-$(pwd)}" | |
| if [[ -n "${_JUMP_BUILTINS[$tname]}" ]]; then | |
| echo "\033[31m'$tname' is a built-in target and cannot be overridden.\033[0m" >&2 | |
| return 1 | |
| fi | |
| _jump_load_custom | |
| _JUMP_CUSTOM[$tname]="$tpath" | |
| _jump_save_custom | |
| echo "\033[32mAdded jump target '$tname' -> $tpath\033[0m" | |
| return | |
| fi | |
| if [[ "${name:l}" == "remove" ]]; then | |
| local tname="${rest[1]}" | |
| if [[ -z "$tname" ]]; then | |
| echo "\033[31mUsage: jump remove <name>\033[0m" >&2 | |
| return 1 | |
| fi | |
| _jump_load_custom | |
| if [[ -n "${_JUMP_CUSTOM[$tname]}" ]]; then | |
| unset "_JUMP_CUSTOM[$tname]" | |
| _jump_save_custom | |
| echo "\033[33mRemoved jump target '$tname'\033[0m" | |
| else | |
| echo "\033[31m'$tname' is not a custom jump target. (Built-in targets cannot be removed.)\033[0m" >&2 | |
| return 1 | |
| fi | |
| return | |
| fi | |
| if [[ "${name:l}" == "list" ]]; then | |
| _jump_all_targets | |
| _jump_load_custom | |
| printf "%-3s %-15s %s\n" " " "NAME" "PATH" | |
| printf "%-3s %-15s %s\n" "---" "---------------" "----" | |
| for key in "${(@ok)_JUMP_ALL}"; do | |
| local marker=" " | |
| [[ -n "${_JUMP_CUSTOM[$key]}" ]] && marker="*" | |
| printf "%-3s %-15s %s\n" "$marker" "$key" "${_JUMP_ALL[$key]}" | |
| done | |
| return | |
| fi | |
| # Direct or partial match | |
| _jump_all_targets | |
| if [[ -n "${_JUMP_ALL[$name]}" ]]; then | |
| pushd "${_JUMP_ALL[$name]}" > /dev/null | |
| return | |
| fi | |
| # Case-insensitive prefix match | |
| local -a matches matched_keys | |
| for key in "${(@k)_JUMP_ALL}"; do | |
| if [[ "${key:l}" == "${name:l}"* ]]; then | |
| matches+=("${_JUMP_ALL[$key]}") | |
| matched_keys+=("$key") | |
| fi | |
| done | |
| if [[ ${#matches} -eq 1 ]]; then | |
| pushd "${matches[1]}" > /dev/null | |
| elif [[ ${#matches} -gt 1 ]]; then | |
| echo "\033[31mAmbiguous match for '$name': ${(j:, :)matched_keys}\033[0m" >&2 | |
| else | |
| echo "\033[31mUnknown jump target '$name'. Run 'jump list' to see available targets.\033[0m" >&2 | |
| fi | |
| } | |
| # --- Tab completion for jump --- | |
| _jump_complete() { | |
| _jump_all_targets | |
| local -a targets | |
| targets=(add remove list) | |
| for key in "${(@k)_JUMP_ALL}"; do | |
| targets+=("$key") | |
| done | |
| _describe 'jump target' targets | |
| } | |
| compdef _jump_complete jump | |
| compdef _jump_complete j | |
| # --- Aliases --- | |
| alias j='jump' | |
| alias jr='repo' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment