Skip to content

Instantly share code, notes, and snippets.

@jongalloway
Last active March 20, 2026 21:17
Show Gist options
  • Select an option

  • Save jongalloway/a214f146d1cad383d3e2b17a5142defb to your computer and use it in GitHub Desktop.

Select an option

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)
# 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