feat: add `z --` to jump to last rested directory

Add a new `z --` command that jumps back to the last directory the user
stayed in for at least 5 seconds. The threshold is configurable via the
`_ZO_REST_THRESHOLD` environment variable (default: 5).

This is implemented entirely in the shell templates with no Rust binary
changes. Each shell hook now tracks dwell time per directory and records
the last "rested" directory in a session-local variable.

Supported across all 9 shells: bash, zsh, fish, posix, powershell,
elvish, nushell, tcsh, and xonsh.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Douglas Sellers 2026-02-18 14:19:16 -08:00
parent ce46915901
commit 6d0ba18003
10 changed files with 285 additions and 12 deletions

View File

@ -55,6 +55,7 @@ z ~/foo # z also works like a regular cd command
z foo/ # cd into relative path
z .. # cd one level up
z - # cd into previous directory
z -- # cd into last directory you stayed in for 5+ seconds
zi foo # cd with interactive selection (using fzf)
@ -452,6 +453,11 @@ Environment variables[^2] can be used for configuration. They must be set before
- `_ZO_RESOLVE_SYMLINKS`
- When set to 1, `z` will resolve symlinks before adding directories to the
database.
- `_ZO_REST_THRESHOLD`
- Sets the minimum number of seconds you must stay in a directory before it is
saved as a "rested" directory. Running `z --` will jump back to the last
rested directory.
- By default, this is set to 5.
## Third-party integrations

View File

@ -37,8 +37,19 @@ function __zoxide_cd() {
{%- if hook == InitHook::Prompt %}
function __zoxide_hook() {
\builtin local -r retval="$?"
\builtin local __zoxide_pwd_tmp
__zoxide_pwd_tmp="$(__zoxide_pwd)"
if [[ ${__zoxide_current_dir:-} != "${__zoxide_pwd_tmp}" ]]; then
\builtin local __zoxide_now="${SECONDS}"
\builtin local __zoxide_elapsed=$(( __zoxide_now - ${__zoxide_entered_at:-0} ))
if [[ ${__zoxide_elapsed} -ge ${__zoxide_rest_threshold:-5} && -n ${__zoxide_current_dir:-} ]]; then
__zoxide_last_rested="${__zoxide_current_dir}"
fi
__zoxide_entered_at="${__zoxide_now}"
__zoxide_current_dir="${__zoxide_pwd_tmp}"
fi
# shellcheck disable=SC2312
\command zoxide add -- "$(__zoxide_pwd)"
\command zoxide add -- "${__zoxide_pwd_tmp}"
return "${retval}"
}
@ -50,13 +61,26 @@ function __zoxide_hook() {
\builtin local pwd_tmp
pwd_tmp="$(__zoxide_pwd)"
if [[ ${__zoxide_oldpwd} != "${pwd_tmp}" ]]; then
\builtin local __zoxide_now="${SECONDS}"
\builtin local __zoxide_elapsed=$(( __zoxide_now - ${__zoxide_entered_at:-0} ))
if [[ ${__zoxide_elapsed} -ge ${__zoxide_rest_threshold:-5} && -n ${__zoxide_current_dir:-} ]]; then
__zoxide_last_rested="${__zoxide_current_dir}"
fi
__zoxide_entered_at="${__zoxide_now}"
__zoxide_current_dir="${pwd_tmp}"
__zoxide_oldpwd="${pwd_tmp}"
\command zoxide add -- "${__zoxide_oldpwd}"
\command zoxide add -- "${pwd_tmp}"
fi
return "${retval}"
}
{%- endif %}
# Rest tracking for z --.
__zoxide_rest_threshold="${_ZO_REST_THRESHOLD:-5}"
__zoxide_entered_at="${SECONDS}"
__zoxide_current_dir="$(__zoxide_pwd)"
__zoxide_last_rested=""
# Initialize hook.
if [[ ${PROMPT_COMMAND:=} != *'__zoxide_hook'* ]]; then
if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then
@ -113,6 +137,13 @@ function __zoxide_z() {
__zoxide_cd "${OLDPWD}"
elif [[ $# -eq 1 && -d $1 ]]; then
__zoxide_cd "$1"
elif [[ $# -eq 1 && $1 == '--' ]]; then
if [[ -n ${__zoxide_last_rested:-} ]]; then
__zoxide_cd "${__zoxide_last_rested}"
else
\builtin printf 'zoxide: no rested directory found\n' >&2
return 1
fi
elif [[ $# -eq 2 && $1 == '--' ]]; then
__zoxide_cd "$2"
elif [[ ${@: -1} == "${__zoxide_z_prefix}"?* ]]; then

View File

@ -3,6 +3,7 @@
use builtin
use path
use time
{{ section }}
# Utility functions for zoxide.
@ -32,12 +33,38 @@ set builtin:before-chdir = [$@builtin:before-chdir {|_| set oldpwd = $builtin:pw
if (builtin:not (builtin:eq $E:__zoxide_shlvl $E:SHLVL)) {
set E:__zoxide_shlvl = $E:SHLVL
{%- if hook == InitHook::Prompt %}
set edit:before-readline = [$@edit:before-readline {|| zoxide add -- $pwd }]
set edit:before-readline = [$@edit:before-readline {||
if (builtin:not-eq $__zoxide_current_dir $pwd) {
var now = (num (time:now)[unix])
var elapsed = (- $now $__zoxide_entered_at)
if (and (>= $elapsed $__zoxide_rest_threshold) (builtin:not-eq $__zoxide_current_dir '')) {
set __zoxide_last_rested = $__zoxide_current_dir
}
set __zoxide_entered_at = $now
set __zoxide_current_dir = $pwd
}
zoxide add -- $pwd
}]
{%- else if hook == InitHook::Pwd %}
set builtin:after-chdir = [$@builtin:after-chdir {|_| zoxide add -- $pwd }]
set builtin:after-chdir = [$@builtin:after-chdir {|_|
var now = (num (time:now)[unix])
var elapsed = (- $now $__zoxide_entered_at)
if (and (>= $elapsed $__zoxide_rest_threshold) (builtin:not-eq $__zoxide_current_dir '')) {
set __zoxide_last_rested = $__zoxide_current_dir
}
set __zoxide_entered_at = $now
set __zoxide_current_dir = $pwd
zoxide add -- $pwd
}]
{%- endif %}
}
# Rest tracking for z --.
var __zoxide_rest_threshold = (if (builtin:not-eq $E:_ZO_REST_THRESHOLD '') { num $E:_ZO_REST_THRESHOLD } else { num 5 })
var __zoxide_entered_at = (num (time:now)[unix])
var __zoxide_current_dir = $builtin:pwd
var __zoxide_last_rested = ''
{%- endif %}
{{ section }}
@ -52,6 +79,12 @@ fn __zoxide_z {|@rest|
__zoxide_cd $oldpwd
} elif (and ('builtin:==' (builtin:count $rest) 1) (path:is-dir &follow-symlink=$true $rest[0])) {
__zoxide_cd $rest[0]
} elif (builtin:eq [--] $rest) {
if (builtin:not-eq $__zoxide_last_rested '') {
__zoxide_cd $__zoxide_last_rested
} else {
fail "zoxide: no rested directory found"
}
} else {
var path
try {

View File

@ -59,12 +59,43 @@ end
# Initialize hook to add new entries to the database.
{%- if hook == InitHook::Prompt %}
function __zoxide_hook --on-event fish_prompt
set -l __zoxide_pwd_tmp (__zoxide_pwd)
if test "$__zoxide_current_dir" != "$__zoxide_pwd_tmp"
set -l __zoxide_now (command date +%s)
set -l __zoxide_elapsed (math $__zoxide_now - $__zoxide_entered_at)
if test $__zoxide_elapsed -ge $__zoxide_rest_threshold -a -n "$__zoxide_current_dir"
set -g __zoxide_last_rested $__zoxide_current_dir
end
set -g __zoxide_entered_at $__zoxide_now
set -g __zoxide_current_dir $__zoxide_pwd_tmp
end
test -z "$fish_private_mode"
and command zoxide add -- $__zoxide_pwd_tmp
end
{%- else if hook == InitHook::Pwd %}
function __zoxide_hook --on-variable PWD
{%- endif %}
set -l __zoxide_now (command date +%s)
set -l __zoxide_elapsed (math $__zoxide_now - $__zoxide_entered_at)
set -l __zoxide_pwd_tmp (__zoxide_pwd)
if test $__zoxide_elapsed -ge $__zoxide_rest_threshold -a -n "$__zoxide_current_dir"
set -g __zoxide_last_rested $__zoxide_current_dir
end
set -g __zoxide_entered_at $__zoxide_now
set -g __zoxide_current_dir $__zoxide_pwd_tmp
test -z "$fish_private_mode"
and command zoxide add -- (__zoxide_pwd)
and command zoxide add -- $__zoxide_pwd_tmp
end
{%- endif %}
# Rest tracking for z --.
if set -q _ZO_REST_THRESHOLD
set -g __zoxide_rest_threshold $_ZO_REST_THRESHOLD
else
set -g __zoxide_rest_threshold 5
end
set -g __zoxide_entered_at (command date +%s)
set -g __zoxide_current_dir (__zoxide_pwd)
set -g __zoxide_last_rested ""
{%- endif %}
@ -81,6 +112,13 @@ function __zoxide_z
__zoxide_cd -
else if test $argc -eq 1 -a -d $argv[1]
__zoxide_cd $argv[1]
else if test $argc -eq 1 -a "$argv[1]" = --
if test -n "$__zoxide_last_rested"
__zoxide_cd $__zoxide_last_rested
else
builtin echo "zoxide: no rested directory found" >&2
return 1
end
else if test $argc -eq 2 -a $argv[1] = --
__zoxide_cd -- $argv[2]
else

View File

@ -26,7 +26,18 @@ export-env {
if not $__zoxide_hooked {
$env.config.hooks.pre_prompt = ($env.config.hooks.pre_prompt | append {
__zoxide_hook: true,
code: {|| ^zoxide add -- $env.PWD}
code: {||
if $env.__zoxide_current_dir != $env.PWD {
let now = (date now | format date '%s' | into int)
let elapsed = $now - $env.__zoxide_entered_at
if $elapsed >= $env.__zoxide_rest_threshold and ($env.__zoxide_current_dir | is-not-empty) {
$env.__zoxide_last_rested = $env.__zoxide_current_dir
}
$env.__zoxide_entered_at = $now
$env.__zoxide_current_dir = $env.PWD
}
^zoxide add -- $env.PWD
}
})
}
{%- else if hook == InitHook::Pwd %}
@ -43,10 +54,25 @@ export-env {
if not $__zoxide_hooked {
$env.config.hooks.env_change.PWD = ($env.config.hooks.env_change.PWD | append {
__zoxide_hook: true,
code: {|_, dir| ^zoxide add -- $dir}
code: {|_, dir|
let now = (date now | format date '%s' | into int)
let elapsed = $now - $env.__zoxide_entered_at
if $elapsed >= $env.__zoxide_rest_threshold and ($env.__zoxide_current_dir | is-not-empty) {
$env.__zoxide_last_rested = $env.__zoxide_current_dir
}
$env.__zoxide_entered_at = $now
$env.__zoxide_current_dir = $dir
^zoxide add -- $dir
}
})
}
{%- endif %}
# Rest tracking for z --.
$env.__zoxide_rest_threshold = (try { $env._ZO_REST_THRESHOLD | into int } catch { 5 })
$env.__zoxide_entered_at = (date now | format date '%s' | into int)
$env.__zoxide_current_dir = $env.PWD
$env.__zoxide_last_rested = ''
}
{%- endif %}
@ -61,6 +87,13 @@ def --env --wrapped __zoxide_z [...rest: string] {
[] => {'~'},
[ '-' ] => {'-'},
[ $arg ] if ($arg | path expand | path type) == 'dir' => {$arg}
[ '--' ] => {
if $env.__zoxide_last_rested != '' {
$env.__zoxide_last_rested
} else {
error make {msg: "zoxide: no rested directory found"}
}
}
_ => {
^zoxide query --exclude $env.PWD -- ...$rest | str trim -r -c "\n"
}

View File

@ -48,9 +48,25 @@ __zoxide_cd() {
{%- when InitHook::Prompt -%}
# Hook to add new entries to the database.
__zoxide_hook() {
\command zoxide add -- "$(__zoxide_pwd || \command true)"
__zoxide_hook_pwd="$(__zoxide_pwd || \command true)"
if [ "${__zoxide_current_dir}" != "${__zoxide_hook_pwd}" ]; then
__zoxide_hook_now="$(\command date +%s)"
__zoxide_hook_elapsed=$(( __zoxide_hook_now - __zoxide_entered_at ))
if [ "${__zoxide_hook_elapsed}" -ge "${__zoxide_rest_threshold}" ] && [ -n "${__zoxide_current_dir}" ]; then
__zoxide_last_rested="${__zoxide_current_dir}"
fi
__zoxide_entered_at="${__zoxide_hook_now}"
__zoxide_current_dir="${__zoxide_hook_pwd}"
fi
\command zoxide add -- "${__zoxide_hook_pwd}"
}
# Rest tracking for z --.
__zoxide_rest_threshold="${_ZO_REST_THRESHOLD:-5}"
__zoxide_entered_at="$(\command date +%s)"
__zoxide_current_dir="$(__zoxide_pwd || \command true)"
__zoxide_last_rested=""
# Initialize hook.
if [ "${PS1:=}" = "${PS1#*\$(__zoxide_hook)}" ]; then
PS1="${PS1}\$(__zoxide_hook)"
@ -107,6 +123,13 @@ __zoxide_z() {
fi
elif [ "$#" -eq 1 ] && [ -d "$1" ]; then
__zoxide_cd "$1"
elif [ "$#" -eq 1 ] && [ "$1" = '--' ]; then
if [ -n "${__zoxide_last_rested}" ]; then
__zoxide_cd "${__zoxide_last_rested}"
else
\command printf 'zoxide: no rested directory found\n' >&2
return 1
fi
else
__zoxide_result="$(\command zoxide query --exclude "$(__zoxide_pwd || \command true)" -- "$@")" &&
__zoxide_cd "${__zoxide_result}"

View File

@ -62,6 +62,15 @@ function global:__zoxide_cd($dir, $literal) {
function global:__zoxide_hook {
$result = __zoxide_pwd
if ($null -ne $result) {
if ($global:__zoxide_current_dir -ne $result) {
$now = [DateTimeOffset]::Now.ToUnixTimeSeconds()
$elapsed = $now - $global:__zoxide_entered_at
if ($elapsed -ge $global:__zoxide_rest_threshold -and $null -ne $global:__zoxide_current_dir -and $global:__zoxide_current_dir -ne '') {
$global:__zoxide_last_rested = $global:__zoxide_current_dir
}
$global:__zoxide_entered_at = $now
$global:__zoxide_current_dir = $result
}
zoxide add "--" $result
}
}
@ -71,6 +80,13 @@ $global:__zoxide_oldpwd = __zoxide_pwd
function global:__zoxide_hook {
$result = __zoxide_pwd
if ($result -ne $global:__zoxide_oldpwd) {
$now = [DateTimeOffset]::Now.ToUnixTimeSeconds()
$elapsed = $now - $global:__zoxide_entered_at
if ($elapsed -ge $global:__zoxide_rest_threshold -and $null -ne $global:__zoxide_current_dir -and $global:__zoxide_current_dir -ne '') {
$global:__zoxide_last_rested = $global:__zoxide_current_dir
}
$global:__zoxide_entered_at = $now
$global:__zoxide_current_dir = $result
if ($null -ne $result) {
zoxide add "--" $result
}
@ -79,6 +95,12 @@ function global:__zoxide_hook {
}
{%- endif %}
# Rest tracking for z --.
$global:__zoxide_rest_threshold = if ($null -ne $env:_ZO_REST_THRESHOLD) { [int]$env:_ZO_REST_THRESHOLD } else { 5 }
$global:__zoxide_entered_at = [DateTimeOffset]::Now.ToUnixTimeSeconds()
$global:__zoxide_current_dir = __zoxide_pwd
$global:__zoxide_last_rested = ''
# Initialize hook.
$global:__zoxide_hooked = (Get-Variable __zoxide_hooked -ErrorAction Ignore -ValueOnly)
if ($global:__zoxide_hooked -ne 1) {
@ -112,6 +134,14 @@ function global:__zoxide_z {
elseif ($args.Length -eq 1 -and (Test-Path -PathType Container -Path $args[0] )) {
__zoxide_cd $args[0] $false
}
elseif ($args.Length -eq 1 -and $args[0] -eq '--') {
if ($null -ne $global:__zoxide_last_rested -and $global:__zoxide_last_rested -ne '') {
__zoxide_cd $global:__zoxide_last_rested $true
}
else {
Write-Error "zoxide: no rested directory found"
}
}
else {
$result = __zoxide_pwd
if ($null -ne $result) {

View File

@ -15,13 +15,40 @@
# Hook to add new entries to the database.
{%- if hook == InitHook::Prompt %}
alias __zoxide_hook 'zoxide add -- "`{{ pwd_cmd }}`"'
alias __zoxide_hook 'set __zoxide_hook_pwd = "`{{ pwd_cmd }}`"\
; if ("$__zoxide_hook_pwd" != "$__zoxide_current_dir") then\
; set __zoxide_hook_now = `date +%s`\
; @ __zoxide_hook_elapsed = $__zoxide_hook_now - $__zoxide_entered_at\
; if ($__zoxide_hook_elapsed >= $__zoxide_rest_threshold && "$__zoxide_current_dir" != "") set __zoxide_last_rested = "$__zoxide_current_dir"\
; set __zoxide_entered_at = "$__zoxide_hook_now"\
; set __zoxide_current_dir = "$__zoxide_hook_pwd"\
; endif\
; zoxide add -- "$__zoxide_hook_pwd"'
{%- else if hook == InitHook::Pwd %}
set __zoxide_pwd_old = `{{ pwd_cmd }}`
alias __zoxide_hook 'set __zoxide_pwd_tmp = "`{{ pwd_cmd }}`"; test "$__zoxide_pwd_tmp" != "$__zoxide_pwd_old" && zoxide add -- "$__zoxide_pwd_tmp"; set __zoxide_pwd_old = "$__zoxide_pwd_tmp"'
alias __zoxide_hook 'set __zoxide_pwd_tmp = "`{{ pwd_cmd }}`"\
; if ("$__zoxide_pwd_tmp" != "$__zoxide_pwd_old") then\
; set __zoxide_hook_now = `date +%s`\
; @ __zoxide_hook_elapsed = $__zoxide_hook_now - $__zoxide_entered_at\
; if ($__zoxide_hook_elapsed >= $__zoxide_rest_threshold && "$__zoxide_current_dir" != "") set __zoxide_last_rested = "$__zoxide_current_dir"\
; set __zoxide_entered_at = "$__zoxide_hook_now"\
; set __zoxide_current_dir = "$__zoxide_pwd_tmp"\
; zoxide add -- "$__zoxide_pwd_tmp"\
; endif\
; set __zoxide_pwd_old = "$__zoxide_pwd_tmp"'
{%- endif %}
# Rest tracking for z --.
if ($?_ZO_REST_THRESHOLD) then
set __zoxide_rest_threshold = "$_ZO_REST_THRESHOLD"
else
set __zoxide_rest_threshold = 5
endif
set __zoxide_entered_at = `date +%s`
set __zoxide_current_dir = `{{ pwd_cmd }}`
set __zoxide_last_rested = ""
# Initialize hook.
alias precmd ';__zoxide_hook'
@ -38,6 +65,12 @@ if ("$#__zoxide_args" == 0) then\
else\
if ("$#__zoxide_args" == 1 && "$__zoxide_args[1]" == "-") then\
cd -\
else if ("$#__zoxide_args" == 1 && "$__zoxide_args[1]" == "--") then\
if ("$__zoxide_last_rested" != "") then\
cd "$__zoxide_last_rested"\
else\
echo "zoxide: no rested directory found"\
endif\
else if ("$#__zoxide_args" == 1 && -d "$__zoxide_args[1]") then\
cd "$__zoxide_args[1]"\
else\

View File

@ -8,6 +8,7 @@ import os
import os.path
import subprocess
import sys
import time
import typing
import xonsh.dirstack # type: ignore # pylint: disable=import-error
@ -98,13 +99,29 @@ if "__zoxide_hook" not in globals():
{%- endif %}
def __zoxide_hook(**_kwargs: typing.Any) -> None:
"""Hook to add new entries to the database."""
global __zoxide_entered_at, __zoxide_current_dir, __zoxide_last_rested # pylint: disable=global-statement
pwd = __zoxide_pwd()
if __zoxide_current_dir != pwd:
now = time.time()
elapsed = now - __zoxide_entered_at
if elapsed >= __zoxide_rest_threshold and __zoxide_current_dir:
__zoxide_last_rested = __zoxide_current_dir
__zoxide_entered_at = now
__zoxide_current_dir = pwd
zoxide = __zoxide_bin()
subprocess.run(
[zoxide, "add", "--", pwd],
check=False,
env=__zoxide_env(),
)
# Rest tracking for z --.
if "__zoxide_rest_threshold" not in globals():
__zoxide_rest_threshold = int(os.environ.get("_ZO_REST_THRESHOLD", "5"))
__zoxide_entered_at: float = time.time()
__zoxide_current_dir: typing.Optional[str] = __zoxide_pwd()
__zoxide_last_rested: typing.Optional[str] = None
{% endif %}
{{ section }}
@ -121,6 +138,11 @@ def __zoxide_z(args: list[str]) -> None:
__zoxide_cd("-")
elif len(args) == 1 and os.path.isdir(args[0]):
__zoxide_cd(args[0])
elif args == ["--"]:
if __zoxide_last_rested is not None:
__zoxide_cd(__zoxide_last_rested)
else:
raise RuntimeError("no rested directory found")
else:
try:
zoxide = __zoxide_bin()

View File

@ -34,10 +34,27 @@ function __zoxide_cd() {
# Hook to add new entries to the database.
function __zoxide_hook() {
\builtin local __zoxide_pwd_tmp
__zoxide_pwd_tmp="$(__zoxide_pwd)"
if [[ "${__zoxide_current_dir:-}" != "${__zoxide_pwd_tmp}" ]]; then
\builtin local __zoxide_now="${SECONDS%.*}"
\builtin local __zoxide_elapsed=$(( __zoxide_now - ${__zoxide_entered_at:-0} ))
if [[ ${__zoxide_elapsed} -ge ${__zoxide_rest_threshold:-5} ]] && [[ -n "${__zoxide_current_dir:-}" ]]; then
__zoxide_last_rested="${__zoxide_current_dir}"
fi
__zoxide_entered_at="${__zoxide_now}"
__zoxide_current_dir="${__zoxide_pwd_tmp}"
fi
# shellcheck disable=SC2312
\command zoxide add -- "$(__zoxide_pwd)"
\command zoxide add -- "${__zoxide_pwd_tmp}"
}
# Rest tracking for z --.
__zoxide_rest_threshold="${_ZO_REST_THRESHOLD:-5}"
__zoxide_entered_at="${SECONDS%.*}"
__zoxide_current_dir="$(__zoxide_pwd)"
__zoxide_last_rested=""
# Initialize hook.
\builtin typeset -ga precmd_functions
\builtin typeset -ga chpwd_functions
@ -90,6 +107,13 @@ function __zoxide_z() {
__zoxide_cd ~
elif [[ "$#" -eq 1 ]] && { [[ -d "$1" ]] || [[ "$1" = '-' ]] || [[ "$1" =~ ^[-+][0-9]+$ ]]; }; then
__zoxide_cd "$1"
elif [[ "$#" -eq 1 ]] && [[ "$1" = "--" ]]; then
if [[ -n "${__zoxide_last_rested:-}" ]]; then
__zoxide_cd "${__zoxide_last_rested}"
else
\builtin printf 'zoxide: no rested directory found\n' >&2
return 1
fi
elif [[ "$#" -eq 2 ]] && [[ "$1" = "--" ]]; then
__zoxide_cd "$2"
else