aboutsummaryrefslogtreecommitdiffhomepage
path: root/zsh/deer
diff options
context:
space:
mode:
authorHsieh Chin Fan <typebrook@gmail.com>2021-10-12 10:55:02 +0800
committerHsieh Chin Fan <typebrook@gmail.com>2021-10-12 10:55:02 +0800
commit8b1a586da07488b4ca67204a33cb5349666ca9f6 (patch)
tree8abd6ac345d79e3392348540874326af8751a559 /zsh/deer
parent6bdea812676fa5c9f6425b24c663ca2aeec522cc (diff)
update
Diffstat (limited to 'zsh/deer')
-rw-r--r--zsh/deer489
1 files changed, 489 insertions, 0 deletions
diff --git a/zsh/deer b/zsh/deer
new file mode 100644
index 0000000..4b0d883
--- /dev/null
+++ b/zsh/deer
@@ -0,0 +1,489 @@
1# -*- mode: shell-script -*-
2# vim: set ft=zsh :
3#########################################################################
4# Copyright (C) 2014-2015 Wojciech Siewierski #
5# #
6# This program is free software: you can redistribute it and/or modify #
7# it under the terms of the GNU General Public License as published by #
8# the Free Software Foundation, either version 3 of the License, or #
9# (at your option) any later version. #
10# #
11# This program is distributed in the hope that it will be useful, #
12# but WITHOUT ANY WARRANTY; without even the implied warranty of #
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14# GNU General Public License for more details. #
15# #
16# You should have received a copy of the GNU General Public License #
17# along with this program. If not, see <http://www.gnu.org/licenses/>. #
18#########################################################################
19
20zstyle -s ":deer:" height DEER_HEIGHT || DEER_HEIGHT=22
21zstyle -b ":deer:" show_hidden DEER_SHOW_HIDDEN
22
23
24typeset -Ag DEER_KEYS
25function ()
26{
27 while [ -n "$2" ]; do
28 DEER_KEYS[$1]=${DEER_KEYS[$1]:-$2}
29 shift 2
30 done
31} down j \
32 page_down J \
33 up k \
34 page_up K \
35 enter l \
36 leave h \
37 next_parent ']' \
38 prev_parent '[' \
39 search / \
40 filter f \
41 toggle_hidden H \
42 quit q \
43 append_path a \
44 append_abs_path A \
45 insert_path i \
46 insert_abs_path I \
47 multi_insert_dwim s \
48 multi_insert_abs S \
49 chdir c \
50 chdir_selected C \
51 rifle r \
52 edit e \
53
54
55# Select the Nth next file. Pass a negative argument for the previous file.
56deer-move()
57{
58 local FILES MOVEMENT INDEX
59 MOVEMENT=$1
60
61 FILES=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t)
62 $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t))
63
64 INDEX=${(k)FILES[(re)$DEER_BASENAME[$DEER_DIRNAME]]}
65
66 if (( INDEX+MOVEMENT <= 0 )); then
67 DEER_BASENAME[$DEER_DIRNAME]=$FILES[1]
68 elif (( INDEX+MOVEMENT > $#FILES )); then
69 DEER_BASENAME[$DEER_DIRNAME]=$FILES[$#FILES]
70 else
71 DEER_BASENAME[$DEER_DIRNAME]=$FILES[$INDEX+$MOVEMENT]
72 fi
73}
74
75# Select the first visible directory (or file if there are no
76# directories) in the current directory. Useful when changing the file
77# filter.
78deer-refocus()
79{
80 local TMP
81 TMP=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t)
82 $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t))
83 DEER_BASENAME[$DEER_DIRNAME]=$TMP[1]
84
85 [ -n "$DEER_BASENAME[$DEER_DIRNAME]" ] # Return if there were any files at all.
86}
87
88# Enter the selected directory
89deer-enter()
90{
91 # Abort if there is no file focused at all or if it is not a
92 # directory.
93 [ -n "$DEER_BASENAME[$DEER_DIRNAME]" -a \
94 -d "$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]" ] || return
95
96 DEER_DIRNAME=${DEER_DIRNAME%/}/$DEER_BASENAME[$DEER_DIRNAME]
97
98 if [ -z $DEER_BASENAME[$DEER_DIRNAME] ]; then
99 deer-refocus
100 fi
101}
102
103# Move to the parent directory
104deer-leave()
105{
106 [ $DEER_DIRNAME = / ] && return
107 DEER_BASENAME[$DEER_DIRNAME:h]=$DEER_DIRNAME:t
108 DEER_DIRNAME=$DEER_DIRNAME:h
109}
110
111# Display a given prompt, read a string and save it into $BUFFER.
112deer-prompt()
113{
114 BUFFER=""
115 PREDISPLAY="$1/ "
116 POSTDISPLAY=""
117
118 local region_highlight
119 region_highlight=("P0 $#1 fg=green")
120 zle recursive-edit
121}
122
123# Read a pattern and select the first matching file.
124deer-search()
125{
126 deer-prompt "search"
127
128 local TMP
129 TMP=($DEER_DIRNAME/${~BUFFER}${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-:t))
130 [ -n "$TMP[1]" ] && DEER_BASENAME[$DEER_DIRNAME]=$TMP[1]
131}
132
133# Read a pattern and use it as a new filter.
134deer-filter()
135{
136 deer-prompt "filter"
137
138 if [ -n "$BUFFER" ] && [[ ! $BUFFER == *\** ]]; then
139 BUFFER=*$BUFFER*
140 fi
141
142 deer-apply-filter $BUFFER || deer-apply-filter
143}
144
145deer-apply-filter()
146{
147 DEER_FILTER[$DEER_DIRNAME]=$1
148 deer-refocus
149}
150
151# Draw an arrow pointing to the selected file.
152deer-mark-file-list()
153{
154 local MARKED=$1
155 shift
156
157 print -l -- "$@" \
158 | grep -Fx -B5 -A$DEER_HEIGHT -- "$MARKED" \
159 | perl -pe 'BEGIN{$name = shift}
160 if ($name."\n" eq $_) {
161 $_="-> $_"
162 } else {
163 $_=" $_"
164 }' -- "$MARKED"
165}
166
167# Draw the file lists in the form of Miller columns.
168deer-refresh()
169{
170 local FILES PREVIEW PARENTFILES OUTPUT REL_DIRNAME
171 local SEPARATOR="------"
172
173 PREDISPLAY=$OLD_LBUFFER
174 REL_DIRNAME=${${DEER_DIRNAME%/}#$DEER_STARTDIR}/
175 [ -n "$DEER_STARTDIR" ] && REL_DIRNAME=${REL_DIRNAME#/}
176 LBUFFER=$REL_DIRNAME$DEER_BASENAME[$DEER_DIRNAME]
177 RBUFFER=""
178 local TMP_FILTER
179 TMP_FILTER=${DEER_FILTER[$DEER_DIRNAME]}
180 POSTDISPLAY=${TMP_FILTER:+ filt:$TMP_FILTER}
181 region_highlight=("P0 $#PREDISPLAY fg=black,bold"
182 "0 $#REL_DIRNAME fg=blue,bold"
183 "$#BUFFER $[$#BUFFER+$#POSTDISPLAY] fg=yellow,bold")
184
185
186 FILES=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t)
187 $SEPARATOR
188 $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t))
189 PARENTFILES=($DEER_DIRNAME:h/${~DEER_FILTER[$DEER_DIRNAME:h]:-'*'}(N$DEER_GLOBFLAGS-/:t))
190
191 local IFS=$'\n'
192 FILES=($(deer-mark-file-list "$DEER_BASENAME[$DEER_DIRNAME]" $FILES))
193 PARENTFILES=($(deer-mark-file-list "$DEER_DIRNAME:t" $PARENTFILES))
194 unset IFS
195
196 FILES=(${(F)FILES[1,$DEER_HEIGHT]})
197 PARENTFILES=(${(F)PARENTFILES[1,$DEER_HEIGHT]})
198
199
200 if [ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]; then
201 if file $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] | grep -Fq text; then
202 PREVIEW="--- Preview: ---"$'\n'$(head -n$DEER_HEIGHT $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME])
203
204 # Replace '/' with '∕' (division slash, U+2215) to allow using it as a
205 # paste(1)/column(1) separator.
206 PREVIEW=${PREVIEW//\//∕}
207 else
208 PREVIEW="--- Binary file, preview unavailable ---"
209 fi
210 else
211 # I'm really sorry about what you see below.
212 # It basically means: PREVIEW=(directories separator files)
213 PREVIEW=($DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]/${~DEER_FILTER[$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]]:-'*'}(N$DEER_GLOBFLAGS-/:t)
214 $SEPARATOR
215 $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]/${~DEER_FILTER[$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]]:-'*'}(N$DEER_GLOBFLAGS-^/:t))
216 PREVIEW=${(F)PREVIEW[1,$DEER_HEIGHT]}
217 fi
218
219 OUTPUT="$(paste -d/ <(<<< $PARENTFILES \
220 | awk '{print substr($0,1,16)}') \
221 <(<<< $FILES) \
222 <(<<< $PREVIEW) \
223 | sed 's,/, / ,g' \
224 | column -t -s/ 2> /dev/null \
225 | awk -v width=$COLUMNS '{print substr($0,1,width-1)}')"
226 zle -M -- $OUTPUT
227 zle -R
228}
229
230# Run `deer-add' with the same arguments, restore the shell state and
231# then exit.
232deer-restore()
233{
234 deer-add "$@"
235 PREDISPLAY=""
236 POSTDISPLAY=""
237 region_highlight=()
238 LBUFFER=$OLD_LBUFFER
239 RBUFFER=$OLD_RBUFFER
240 zle reset-prompt
241 zle -M ""
242}
243
244# Add the given string before or after the cursor.
245deer-add()
246{
247 case $1 in
248 --append)
249 OLD_LBUFFER+=$2
250 shift 2
251 ;;
252 --insert)
253 OLD_RBUFFER=$2$OLD_RBUFFER
254 shift 2
255 ;;
256 esac
257}
258
259# Get the quoted relative path from the absolute unquoted path.
260deer-get-relative()
261{
262 local TMP
263 TMP=${1:-${DEER_DIRNAME%/}/$DEER_BASENAME[$DEER_DIRNAME]}
264 TMP="`python -c '
265import sys, os
266print(os.path.relpath(sys.argv[1], sys.argv[2]))
267' $TMP ${DEER_STARTDIR:-$PWD}`"
268 print -R $TMP:q
269}
270
271# Tries to guess a directory to start in from the current argument.
272deer-set-initial-directory()
273{
274 autoload -U split-shell-arguments modify-current-argument
275 local REPLY REPLY2 reply
276 local DIRECTORY
277
278 ((--CURSOR))
279 split-shell-arguments
280 ((++CURSOR))
281
282 # Find the longest existing directory path in the current argument.
283 DEER_STARTDIR=${(Q)${${reply[$REPLY]%%[[:space:]]#}:a}%/}
284 while [ -n "$DEER_STARTDIR" -a \
285 ! -d "$DEER_STARTDIR" ]; do
286 DEER_STARTDIR=${DEER_STARTDIR%/*}
287 done
288
289 DEER_DIRNAME=${DEER_STARTDIR:-$PWD}
290}
291
292# The main entry function.
293deer-launch()
294{
295 emulate -L zsh
296 setopt extended_glob
297 local DEER_DIRNAME DEER_STARTDIR DEER_GLOBFLAGS
298 local -A DEER_FILTER DEER_BASENAME
299 local REPLY OLD_LBUFFER OLD_RBUFFER
300
301 local GREP_OPTIONS
302 GREP_OPTIONS=""
303
304 OLD_LBUFFER=$LBUFFER
305 OLD_RBUFFER=$RBUFFER
306
307 deer-set-initial-directory
308
309 if [ "$DEER_SHOW_HIDDEN" = yes ]; then
310 DEER_GLOBFLAGS=D
311 else
312 DEER_GLOBFLAGS=""
313 fi
314
315 if [ -n "$NUMERIC" ]; then
316 for i in {1..$NUMERIC}; do
317 deer-leave
318 done
319 else
320 # Don't change cwd but initialize the variables.
321 deer-leave
322 deer-enter
323 fi
324
325 deer-refresh
326 while read -k; do
327 case $REPLY in
328 # Movement
329 $DEER_KEYS[up])
330 deer-move -1
331 deer-refresh
332 ;;
333 $DEER_KEYS[page_up])
334 deer-move -5
335 deer-refresh
336 ;;
337 $DEER_KEYS[down])
338 deer-move 1
339 deer-refresh
340 ;;
341 $DEER_KEYS[page_down])
342 deer-move 5
343 deer-refresh
344 ;;
345 $DEER_KEYS[enter])
346 deer-enter
347 deer-refresh
348 ;;
349 $DEER_KEYS[leave])
350 deer-leave
351 deer-refresh
352 ;;
353 $DEER_KEYS[next_parent])
354 deer-leave
355 deer-move 1
356 deer-enter
357 deer-refresh
358 ;;
359 $DEER_KEYS[prev_parent])
360 deer-leave
361 deer-move -1
362 deer-enter
363 deer-refresh
364 ;;
365 # Search
366 $DEER_KEYS[search])
367 deer-search
368 deer-refresh
369 ;;
370 # Filter
371 $DEER_KEYS[filter])
372 deer-filter
373 deer-refresh
374 ;;
375 $DEER_KEYS[toggle_hidden])
376 if [ -z $DEER_GLOBFLAGS ]; then
377 DEER_GLOBFLAGS="D" # show hidden files
378 else
379 DEER_GLOBFLAGS=""
380 fi
381 # make sure the focus is on a visible file
382 DEER_BASENAME[$DEER_DIRNAME]=
383 deer-leave
384 deer-enter
385 deer-refresh
386 ;;
387 # Quit
388 $DEER_KEYS[quit])
389 deer-restore
390 break
391 ;;
392 # Insert the path and quit.
393 $DEER_KEYS[append_path])
394 deer-restore --append "`deer-get-relative` "
395 break
396 ;;
397 $DEER_KEYS[append_abs_path])
398 deer-restore --append "${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q} "
399 break
400 ;;
401 $DEER_KEYS[insert_path])
402 deer-restore --insert " `deer-get-relative`"
403 break
404 ;;
405 $DEER_KEYS[insert_abs_path])
406 deer-restore --insert " ${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q}"
407 break
408 ;;
409 # Insert the path and don't quit yet.
410 $DEER_KEYS[multi_insert_dwim])
411 if [ "$OLD_LBUFFER[-1]" = "/" ]; then
412 OLD_LBUFFER+="{"
413 fi
414 # replacement used to insert ',' instead of '{' as a separator in {foo,bar,...} lists
415 deer-add --append "`deer-get-relative`"${${OLD_LBUFFER[-1]/\{/,}:- }
416 deer-move 1
417 deer-refresh
418 ;;
419 # Insert the absolute path and don't quit yet.
420 $DEER_KEYS[multi_insert_abs])
421 deer-add --append " ${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q}"
422 deer-move 1
423 deer-refresh
424 ;;
425 # Quit and change the shell's current directory to the selected one.
426 $DEER_KEYS[chdir])
427 deer-leave
428 ;&
429 $DEER_KEYS[chdir_selected])
430 if [[ -d $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] && \
431 -x $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then
432 cd -- $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]
433 deer-restore
434 break
435 fi
436 ;;
437 $DEER_KEYS[edit])
438 if [[ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then
439 "${EDITOR:-vim}" $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]
440 fi
441 ;;
442 # See rifle(1) manpage (included with ranger(1)).
443 $DEER_KEYS[rifle])
444 if [[ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then
445 rifle $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]
446 fi
447 ;;
448 # Arrow keys
449 $'\e')
450 read -k
451 case $REPLY in
452 '[')
453 read -k
454 case $REPLY in
455 'A')
456 deer-move -1
457 deer-refresh
458 ;;
459 'B')
460 deer-move 1
461 deer-refresh
462 ;;
463 'C')
464 deer-enter
465 deer-refresh
466 ;;
467 'D')
468 deer-leave
469 deer-refresh
470 ;;
471 esac
472 ;;
473 esac
474 ;;
475 esac
476 done
477}
478
479if zle; then
480 deer-launch
481else
482 deer()
483 {
484 deer-launch "$@"
485 }
486fi
487
488zle -N deer
489bindkey '\ek' deer