about summary refs log tree commit diff
path: root/Functions/Chpwd/cdr
blob: 43745e5aa867a115d63faa901c47116b3ac5e23d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# Description
# ===========
#
# Change to a recently used directory recorded in a file so that the
# recent file list persists across sessions.
#
# To use this system,
#
#   autoload -Uz chpwd_recent_dirs cdr add-zsh-hook
#   add-zsh-hook chpwd chpwd_recent_dirs
#
# (add-zsh-hook appeared in zsh version 4.3.4.)  This ensures that all
# directories you change to interactively are registered.  The
# chpwd_recent_dirs function does some guesswork to see if you are "really"
# changing directory permanently, see below.
#
# The argument to cdr is a number corresponding to the Nth most recently
# changed-to directory starting at 1 for the immediately preceding
# directory (the current directory is remembered but is not offered as a
# destination).  You can use directory arguments if you set the
# recent-dirs-default style, see below; however, it should be noted
# that if you do you gain nothing over using cd directly (the recent
# directory list is updated in either case).
#
# If the argument is omitted, 1 is assumed.
#
# Completion is available if compinit has been run; menu selection is
# recommended, using
#
#   zstyle ':completion:*:*:cdr:*:*' menu selection
#
# and also the verbose style to ensure the directory is shown (this
# is on by default).
#
# Options
# =======
#
# "cdr -l" lists the numbers and the corresponding directories in
# abbreviated form (i.e. with "~" substitution reapplied), one per line.
# The directories here are not quoted (this would only be an issue if a
# directory name contained a newline).  This is used by the completion
# system.
#
# "cdr -r" sets the parameter "reply" to the current set of directories.
#
# "cdr -e" allows you to edit the list of directories, one per line.  The
# list can be edited to any extent you like; no sanity checking is
# performed.  Completion is available.  No quoting is necessary (except for
# newlines, where I have in any case no sympathy); directories are in
# unabbreviated from and contain an absolute path, i.e. they start with /
# (and only /).  Usually the first entry should be left as the current
# directory.
#
# "cdr -p 'pattern'" prunes anything matching the given extended glob
# pattern from the directory list.  The match is against the fully
# expanded directory path and the full string must match (use wildcards
# at the ends if needed).  If output is going to a terminal, the
# function will print the new list for the user to confirm; this can be
# skipped by giving -P instead of -p.
#
# Details of directory handling
# =============================
#
# Recent directories are saved to a file immediately and hence are
# preserved across sessions.  Note currently no file locking is applied:
# the list is updated immediately on interactive commands and nowhere else
# (unlike history), and it is assumed you are only going to change
# directory in one window at once.  This is not safe on shared accounts,
# but in any case the system has limited utility when someone else is
# changing to a different set of directories behind your back.
#
# To make this a little safer, only directory changes instituted from the
# command line, either directly or indirectly through shell function calls
# (but not through subshells, evals, traps, completion functions and the
# like) are saved.  This works best in versions of the shell from 4.3.11
# which has facilities to check the evaluation context.  Shell functions
# should use cd -q or pushd -q to avoid side effects if the change to the
# directory is to be invisible at the command line.  See the function
# chpwd_recent_dirs for more details.
#
# Styles
# ======
#
# Various styles are available.  The context for setting styles should be
# ':chpwd:*' in case the meaning of the context is extended in future, for
# example:
#
#   zstyle ':chpwd:*' recent-dirs-max 0
#
# although the style name is specific enough that a context of '*' should
# be fine in practice.  The only exception is recent-dirs-insert, which is
# used exclusively by the completion system and so has the usual completion
# system context (':completion:*' if nothing more specific is needed,
# though again '*' should be fine in practice).
#
#   recent-dirs-default
#     If true, and the command is expecting a recent directory index, and
#     either there is more than one argument or the argument is not an
#     integer, then fall through to "cd".  This allows the lazy to use only
#     one command for directory changing.  Completion recognises this, too;
#     see recent-dirs-insert for how to control completion when this option
#     is in use.
#
#   recent-dirs-file
#     The file where the list of directories is saved.  The default
#     is ${ZDOTDIR:-$HOME}/.chpwd-recent-dirs, i.e. this is in your
#     home directory unless you have set ZDOTDIR to point somewhere else.
#     Directory names are saved in $'...' quoted form, so each line
#     in the file can be supplied directly to the shell as an argument.
#
#     The value of this style may be an array.  In this case, the first
#     file in the list will always be used for saving directories while any
#     other files are left untouched.  When reading the recent directory
#     list, if there are fewer than the maximum number of entries in the
#     first file, the contents of later files in the array will be appended
#     with duplicates removed from the list shown.  The contents of the two
#     files are not sorted together, i.e. all the entries in the first file
#     are shown first.  The special value "+" can appear in the list to
#     indicate the default file should be read at that point.  This allows
#     effects like the following:
#
#       zstyle recent-dirs-file ':chpwd:*' ~/.chpwd-recent-dirs-${TTY##*/} +
#
#     Recent directories are read from a file numbered according to
#     the terminal.  If there are insufficient entries the list
#     is supplemented from the default file.
#
#   recent-dirs-insert
#     Used by completion.  If recent-dirs-default is true, then setting
#     this to true causes the actual directory, rather than its index, to
#     be inserted on the command line; this has the same effect as using
#     the corresponding index, but makes the history clearer and the line
#     easier to edit.  With this setting, if part of an argument was
#     already typed, normal directory completion rather than recent
#     directory completion is done; this is because recent directory
#     completion is expected to be done by cycling through entries menu
#     fashion.  However, if the value of the style is "always", then only
#     recent directories will be completed; in that case, use the cd
#     command when you want to complete other directories.  If the value is
#     "fallback", recent directories will be tried first, then normal
#     directory completion is performed if recent directory completion
#     failed to find a match.  Finally, if the value is "both" then both
#     sets of completions are presented; the usual tag mechanism can be
#     used to distinguish results, with recent directories tagged as
#     "recent-dirs".  Note that the recent directories inserted are
#     abbreviated with directory names where appropriate.
#
#   recent-dirs-max
#     The maximum number of directories to save to the file.  If
#     this is zero or negative there is no maximum.  The default is 20.
#     Note this includes the current directory, which isn't offered,
#     so the highest number of directories you will be offered
#     is one less than the maximum.
#
#   recent-dirs-prune
#     This style is an array determining what directories should (or should
#     not) be added to the recent list.  Elements of the array can include:
#       parent
#         Prune parents (more accurately, ancestors) from the recent list.
#         If present, changing directly down by any number of directories
#         causes the current directory to be overwritten.  For example,
#         changing from ~pws to ~pws/some/other/dir causes ~pws not to be
#         left on the recent directory stack.  This only applies to direct
#         changes to descendant directories; earlier directories on the
#         list are not pruned.  For example, changing from ~pws/yet/another
#         to ~pws/some/other/dir does not cause ~pws to be pruned.
#       pattern:<pattern>
#         Gives a zsh pattern for directories that should not be
#         added to the recent list (if not already there).  This element
#         can be repeated to add different patterns.  For example,
#         'pattern:/tmp(|/*)' stops /tmp or its descendants from being
#         added.  The EXTENDED_GLOB option is always turned on for
#         these patterns.
#
#   recent-dirs-pushd
#     If set to true, cdr will use pushd instead of cd to change the
#     directory, so the directory is saved on the directory stack.  As the
#     directory stack is completely separate from the list of files saved
#     by the mechanism used in this file there is no obvious reason to do
#     this.
#
# Use with dynamic directory naming
# =================================
#
# It is possible to refer to recent directories using the dynamic directory
# name syntax that appeared in zsh version 4.3.7.  If you create and
# autoload a function zsh_directory_name containing the following code,
# ~[1] will refer to the most recent directory other than $PWD, and so on.
# This also includes completion (version 4.3.11 is required for this to
# work; previous versions needed the file _dynamic_directory_name to
# be overloaded).
#
#   if [[ $1 = n ]]; then
#     if [[ $2 = <-> ]]; then
#       # Recent directory
#       typeset -ga reply
#       autoload -Uz cdr
#       cdr -r
#       if [[ -n ${reply[$2]} ]]; then
#         reply=(${reply[$2]})
#         return 0
#       else
#         reply=()
#         return 1
#       fi
#     fi
#   elif [[ $1 = c ]]; then
#     if [[ $PREFIX = <-> || -z $PREFIX ]]; then
#       typeset -a keys values
#   
#       values=(${${(f)"$(cdr -l)"}/ ##/:})
#       keys=(${values%%:*})
#   
#       _describe -t dir-index 'recent directory index' \
#         values keys -V unsorted -S']'
#       return
#     fi
#   fi
#   return 1


emulate -L zsh
setopt extendedglob

autoload -Uz chpwd_recent_filehandler chpwd_recent_add

integer list set_reply i bad edit force_prune
local opt dir prune
local -aU dirs

while getopts "elp:P:r" opt; do
  case $opt in
    (e)
    edit=1
    ;;

    (l)
    list=1
    ;;

    ([pP])
    prune=$OPTARG
    edit=1
    [[ $opt = P ]] && force_prune=1
    ;;

    (r)
    set_reply=1
    ;;

    (*)
    return 1
    ;;
  esac
done
shift $(( OPTIND - 1 ))

if (( set_reply )); then
  typeset -ga reply
else
  local -a reply
fi

if (( list || set_reply || edit )); then
  (( $# )) && bad=1
else
  if [[ $#1 -eq 0 ]]; then
    1=1
  elif [[ $# -ne 1 || $1 != <-> ]]; then
    if zstyle -t ':chpwd:' recent-dirs-default; then
      cd "$@"
      return
    else
      bad=1
    fi
  fi
fi

if (( bad )); then
  print "Usage: $0 [-l | -r | <dir-num> ]
Use $0 -l or completion to see possible directories."
  return 1
fi

chpwd_recent_filehandler

if [[ $PWD != $reply[1] ]]; then
  # When we first start we don't have the current directory.
  # Add it now for consistency.
  chpwd_recent_add $PWD && chpwd_recent_filehandler $reply
fi

if (( edit )); then
  if [[ -n $prune ]]; then
    reply=(${reply:#$~prune})
    if [[ force_prune -eq 0 && -t 1 ]]; then
      print -nrl "New list:" $reply 'Accept? '
      if ! read -q; then
	print
	return 1
      fi
      print
    fi
  else
    local compcontext='directories:directory:_path_files -/'
    IFS='
' vared reply || return 1
  fi
  chpwd_recent_filehandler $reply
fi

# Skip current directory if present (may have been pruned).
[[ $reply[1] = $PWD ]] && reply=($reply[2,-1])

if (( list )); then
  dirs=($reply)
  for (( i = 1; i <= ${#dirs}; i++ )); do
    print -n ${(r.5.)i}
    print -r ${(D)dirs[i]}
  done
  return
fi

(( set_reply || edit )) && return

if (( $1 > ${#reply} )); then
  print "Not enough directories ($(( ${#dirs} - 1)) possibilities)" >&2
  return 1
fi
dir=${reply[$1]}

if zstyle -t ':chpwd:' recent-dirs-pushd; then
  pushd -- $dir
else
  cd -- $dir
fi