#!/usr/bin/env bash
# bin/z.sh -- a tiny frecency-based directory jumper.
#
# I used rupa/z for years and finally wanted something I could audit on
# my own machine. This is roughly the same idea: track cd targets in a
# flat file, score them by frecency, jump with `z <substring>`.
#
# Source this file from .zshrc (or .bashrc):
# source "$HOME/.dotfiles/bin/z.sh"
: "${_Z_DATA:=${XDG_DATA_HOME:-$HOME/.local/share}/z/history}"
: "${_Z_CMD:=z}"
: "${_Z_MAX_SCORE:=9000}"
mkdir -p -- "$(dirname -- "$_Z_DATA")"
touch -- "$_Z_DATA"
_z_add() {
# Record cwd on every prompt. Skip $HOME and /tmp to keep the list signal-y.
local pwd=$PWD
case $pwd in
$HOME|/|/tmp|/tmp/*) return ;;
esac
local tmp
tmp=$(mktemp "${_Z_DATA}.XXXXXX") || return
local now rank time
now=$(date +%s)
awk -v target="$pwd" -v now="$now" -v maxscore="$_Z_MAX_SCORE" '
BEGIN { FS = "|"; OFS = "|"; seen = 0; total = 0 }
NF == 3 {
if ($1 == target) {
$2 += 1
$3 = now
seen = 1
}
print
total += $2
}
END {
if (!seen) print target, 1, now
# Age the table down proportionally if it is getting heavy.
if (total > maxscore) {
# signal back via exit status; caller will trigger an age pass.
exit 77
}
}
' "$_Z_DATA" > "$tmp"
local rc=$?
if (( rc == 77 )); then
awk -F'|' -v OFS='|' '{ $2 = $2 * 0.9; print }' "$tmp" > "$_Z_DATA"
rm -f -- "$tmp"
else
mv -f -- "$tmp" "$_Z_DATA"
fi
}
_z_frecency() {
# Args: rank, last-visit timestamp, now.
# Stolen-by-reference from rupa/z's formula.
local rank=$1 ts=$2 now=$3
local dx=$(( now - ts ))
if (( dx < 3600 )); then awk -v r="$rank" 'BEGIN { print r * 4 }'
elif (( dx < 86400 )); then awk -v r="$rank" 'BEGIN { print r * 2 }'
elif (( dx < 604800 )); then awk -v r="$rank" 'BEGIN { print r / 2 }'
else awk -v r="$rank" 'BEGIN { print r / 4 }'
fi
}
_z_search() {
local pattern=$*
local now; now=$(date +%s)
awk -v pat="$pattern" -v now="$now" '
BEGIN { FS = "|"; OFS = "|" }
NF == 3 && index($1, pat) {
dx = now - $3
if (dx < 3600) s = $2 * 4
else if (dx < 86400) s = $2 * 2
else if (dx < 604800) s = $2 / 2
else s = $2 / 4
print s, $1
}
' "$_Z_DATA" | sort -nr | awk '{ $1 = ""; sub(/^ /, ""); print }'
}
"$_Z_CMD"() {
if [[ $# -eq 0 ]]; then
_z_search '' | head -n 20
return
fi
case $1 in
-l) shift; _z_search "$*"; return ;;
-c) : > "$_Z_DATA"; return ;;
-h|--help)
cat <<HELP
usage: z <substring> jump to best match
z -l <substring> list matches, best first
z -c clear history
HELP
return ;;
esac
local match
match=$(_z_search "$*" | head -n 1)
if [[ -z $match ]]; then
print -r -- "z: no match for '$*'" >&2
return 1
fi
if [[ ! -d $match ]]; then
# Clean up stale entries so the next lookup finds something real.
local tmp; tmp=$(mktemp "${_Z_DATA}.XXXXXX") || return
awk -v bad="$match" 'BEGIN{FS=OFS="|"} $1 != bad' "$_Z_DATA" > "$tmp"
mv -f -- "$tmp" "$_Z_DATA"
print -r -- "z: pruned missing dir '$match'" >&2
return 1
fi
cd -- "$match"
}
# Hook the recorder into the shell's per-prompt callbacks.
if [[ -n $ZSH_VERSION ]]; then
autoload -Uz add-zsh-hook
add-zsh-hook chpwd _z_add
elif [[ -n $BASH_VERSION ]]; then
case "$PROMPT_COMMAND" in
*_z_add*) : ;;
*) PROMPT_COMMAND="_z_add;${PROMPT_COMMAND:-}" ;;
esac
fi