# -*- mode: shell-script -*- # vim: set ft=zsh : ######################################################################### # Copyright (C) 2014-2015 Wojciech Siewierski # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ######################################################################### zstyle -s ":deer:" height DEER_HEIGHT || DEER_HEIGHT=22 typeset -Ag DEER_KEYS function () { while [ -n "$2" ]; do DEER_KEYS[$1]=${DEER_KEYS[$1]:-$2} shift 2 done } down j \ page_down J \ up k \ page_up K \ enter l \ leave h \ next_parent ']' \ prev_parent '[' \ search / \ filter f \ toggle_hidden H \ quit q \ append_path a \ append_abs_path A \ insert_path i \ insert_abs_path I \ multi_insert_dwim s \ multi_insert_abs S \ chdir c \ chdir_selected C \ rifle r \ edit e \ # Select the Nth next file. Pass a negative argument for the previous file. deer-move() { local FILES MOVEMENT INDEX MOVEMENT=$1 FILES=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t) $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t)) INDEX=${(k)FILES[(re)$DEER_BASENAME[$DEER_DIRNAME]]} if (( INDEX+MOVEMENT <= 0 )); then DEER_BASENAME[$DEER_DIRNAME]=$FILES[1] elif (( INDEX+MOVEMENT > $#FILES )); then DEER_BASENAME[$DEER_DIRNAME]=$FILES[$#FILES] else DEER_BASENAME[$DEER_DIRNAME]=$FILES[$INDEX+$MOVEMENT] fi } # Select the first visible directory (or file if there are no # directories) in the current directory. Useful when changing the file # filter. deer-refocus() { local TMP TMP=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t) $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t)) DEER_BASENAME[$DEER_DIRNAME]=$TMP[1] [ -n "$DEER_BASENAME[$DEER_DIRNAME]" ] # Return if there were any files at all. } # Enter the selected directory deer-enter() { # Abort if there is no file focused at all or if it is not a # directory. [ -n "$DEER_BASENAME[$DEER_DIRNAME]" -a \ -d "$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]" ] || return DEER_DIRNAME=${DEER_DIRNAME%/}/$DEER_BASENAME[$DEER_DIRNAME] if [ -z $DEER_BASENAME[$DEER_DIRNAME] ]; then deer-refocus fi } # Move to the parent directory deer-leave() { [ $DEER_DIRNAME = / ] && return DEER_BASENAME[$DEER_DIRNAME:h]=$DEER_DIRNAME:t DEER_DIRNAME=$DEER_DIRNAME:h } # Display a given prompt, read a string and save it into $BUFFER. deer-prompt() { BUFFER="" PREDISPLAY="$1/ " POSTDISPLAY="" local region_highlight region_highlight=("P0 $#1 fg=green") zle recursive-edit } # Read a pattern and select the first matching file. deer-search() { deer-prompt "search" local TMP TMP=($DEER_DIRNAME/${~BUFFER}${DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-:t)) [ -n "$TMP[1]" ] && DEER_BASENAME[$DEER_DIRNAME]=$TMP[1] } # Read a pattern and use it as a new filter. deer-filter() { deer-prompt "filter" if [ -n "$BUFFER" ] && [[ ! $BUFFER == *\** ]]; then BUFFER=*$BUFFER* fi deer-apply-filter $BUFFER || deer-apply-filter } deer-apply-filter() { DEER_FILTER[$DEER_DIRNAME]=$1 deer-refocus } # Draw an arrow pointing to the selected file. deer-mark-file-list() { local MARKED=$1 shift print -l -- "$@" \ | grep -Fx -B5 -A$DEER_HEIGHT -- "$MARKED" \ | perl -pe 'BEGIN{$name = shift} if ($name."\n" eq $_) { $_="-> $_" } else { $_=" $_" }' -- "$MARKED" } # Draw the file lists in the form of Miller columns. deer-refresh() { local FILES PREVIEW PARENTFILES OUTPUT REL_DIRNAME local SEPARATOR="------" PREDISPLAY=$OLD_LBUFFER REL_DIRNAME=${${DEER_DIRNAME%/}#$DEER_STARTDIR}/ [ -n "$DEER_STARTDIR" ] && REL_DIRNAME=${REL_DIRNAME#/} LBUFFER=$REL_DIRNAME$DEER_BASENAME[$DEER_DIRNAME] RBUFFER="" local TMP_FILTER TMP_FILTER=${DEER_FILTER[$DEER_DIRNAME]} POSTDISPLAY=${TMP_FILTER:+ filt:$TMP_FILTER} region_highlight=("P0 $#PREDISPLAY fg=black,bold" "0 $#REL_DIRNAME fg=blue,bold" "$#BUFFER $[$#BUFFER+$#POSTDISPLAY] fg=yellow,bold") FILES=($DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-/:t) $SEPARATOR $DEER_DIRNAME/${~DEER_FILTER[$DEER_DIRNAME]:-'*'}(N$DEER_GLOBFLAGS-^/:t)) PARENTFILES=($DEER_DIRNAME:h/${~DEER_FILTER[$DEER_DIRNAME:h]:-'*'}(N$DEER_GLOBFLAGS-/:t)) local IFS=$'\n' FILES=($(deer-mark-file-list "$DEER_BASENAME[$DEER_DIRNAME]" $FILES)) PARENTFILES=($(deer-mark-file-list "$DEER_DIRNAME:t" $PARENTFILES)) unset IFS FILES=(${(F)FILES[1,$DEER_HEIGHT]}) PARENTFILES=(${(F)PARENTFILES[1,$DEER_HEIGHT]}) if [ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]; then if file $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] | grep -Fq text; then PREVIEW="--- Preview: ---"$'\n'$(head -n$DEER_HEIGHT $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]) # Replace '/' with '∕' (division slash, U+2215) to allow using it as a # paste(1)/column(1) separator. PREVIEW=${PREVIEW//\//∕} else PREVIEW="--- Binary file, preview unavailable ---" fi else # I'm really sorry about what you see below. # It basically means: PREVIEW=(directories separator files) PREVIEW=($DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]/${~DEER_FILTER[$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]]:-'*'}(N$DEER_GLOBFLAGS-/:t) $SEPARATOR $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]/${~DEER_FILTER[$DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME]]:-'*'}(N$DEER_GLOBFLAGS-^/:t)) PREVIEW=${(F)PREVIEW[1,$DEER_HEIGHT]} fi OUTPUT="$(paste -d/ <(<<< $PARENTFILES \ | awk '{print substr($0,1,16)}') \ <(<<< $FILES) \ <(<<< $PREVIEW) \ | sed 's,/, / ,g' \ | column -t -s/ 2> /dev/null \ | awk -v width=$COLUMNS '{print substr($0,1,width-1)}')" zle -M -- $OUTPUT zle -R } # Run `deer-add' with the same arguments, restore the shell state and # then exit. deer-restore() { deer-add "$@" PREDISPLAY="" POSTDISPLAY="" region_highlight=() LBUFFER=$OLD_LBUFFER RBUFFER=$OLD_RBUFFER zle reset-prompt zle -M "" } # Add the given string before or after the cursor. deer-add() { case $1 in --append) OLD_LBUFFER+=$2 shift 2 ;; --insert) OLD_RBUFFER=$2$OLD_RBUFFER shift 2 ;; esac } # Get the quoted relative path from the absolute unquoted path. deer-get-relative() { local TMP TMP=${1:-${DEER_DIRNAME%/}/$DEER_BASENAME[$DEER_DIRNAME]} TMP="`python -c ' import sys, os print(os.path.relpath(sys.argv[1], sys.argv[2])) ' $TMP ${DEER_STARTDIR:-$PWD}`" print -R $TMP:q } # Tries to guess a directory to start in from the current argument. deer-set-initial-directory() { autoload -U split-shell-arguments modify-current-argument local REPLY REPLY2 reply local DIRECTORY ((--CURSOR)) split-shell-arguments ((++CURSOR)) # Find the longest existing directory path in the current argument. DEER_STARTDIR=${(Q)${${reply[$REPLY]%%[[:space:]]#}:a}%/} while [ -n "$DEER_STARTDIR" -a \ ! -d "$DEER_STARTDIR" ]; do DEER_STARTDIR=${DEER_STARTDIR%/*} done DEER_DIRNAME=${DEER_STARTDIR:-$PWD} } # The main entry function. deer-launch() { emulate -L zsh setopt extended_glob local DEER_DIRNAME DEER_STARTDIR DEER_GLOBFLAGS local -A DEER_FILTER DEER_BASENAME local REPLY OLD_LBUFFER OLD_RBUFFER local GREP_OPTIONS GREP_OPTIONS="" OLD_LBUFFER=$LBUFFER OLD_RBUFFER=$RBUFFER deer-set-initial-directory DEER_GLOBFLAGS="" if [ -n "$NUMERIC" ]; then for i in {1..$NUMERIC}; do deer-leave done else # Don't change cwd but initialize the variables. deer-leave deer-enter fi deer-refresh while read -k; do case $REPLY in # Movement $DEER_KEYS[up]) deer-move -1 deer-refresh ;; $DEER_KEYS[page_up]) deer-move -5 deer-refresh ;; $DEER_KEYS[down]) deer-move 1 deer-refresh ;; $DEER_KEYS[page_down]) deer-move 5 deer-refresh ;; $DEER_KEYS[enter]) deer-enter deer-refresh ;; $DEER_KEYS[leave]) deer-leave deer-refresh ;; $DEER_KEYS[next_parent]) deer-leave deer-move 1 deer-enter deer-refresh ;; $DEER_KEYS[prev_parent]) deer-leave deer-move -1 deer-enter deer-refresh ;; # Search $DEER_KEYS[search]) deer-search deer-refresh ;; # Filter $DEER_KEYS[filter]) deer-filter deer-refresh ;; $DEER_KEYS[toggle_hidden]) if [ -z $DEER_GLOBFLAGS ]; then DEER_GLOBFLAGS="D" # show hidden files else DEER_GLOBFLAGS="" fi # make sure the focus is on a visible file DEER_BASENAME[$DEER_DIRNAME]= deer-leave deer-enter deer-refresh ;; # Quit $DEER_KEYS[quit]) deer-restore break ;; # Insert the path and quit. $DEER_KEYS[append_path]) deer-restore --append "`deer-get-relative` " break ;; $DEER_KEYS[append_abs_path]) deer-restore --append "${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q} " break ;; $DEER_KEYS[insert_path]) deer-restore --insert " `deer-get-relative`" break ;; $DEER_KEYS[insert_abs_path]) deer-restore --insert " ${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q}" break ;; # Insert the path and don't quit yet. $DEER_KEYS[multi_insert_dwim]) if [ "$OLD_LBUFFER[-1]" = "/" ]; then OLD_LBUFFER+="{" fi # replacement used to insert ',' instead of '{' as a separator in {foo,bar,...} lists deer-add --append "`deer-get-relative`"${${OLD_LBUFFER[-1]/\{/,}:- } deer-move 1 deer-refresh ;; # Insert the absolute path and don't quit yet. $DEER_KEYS[multi_insert_abs]) deer-add --append " ${${DEER_DIRNAME%/}:q}/${DEER_BASENAME[$DEER_DIRNAME]:q}" deer-move 1 deer-refresh ;; # Quit and change the shell's current directory to the selected one. $DEER_KEYS[chdir]) deer-leave ;& $DEER_KEYS[chdir_selected]) if [[ -d $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] && \ -x $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then cd -- $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] deer-restore break fi ;; $DEER_KEYS[edit]) if [[ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then "${EDITOR:-vim}" $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] fi ;; # See rifle(1) manpage (included with ranger(1)). $DEER_KEYS[rifle]) if [[ -f $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] ]]; then rifle $DEER_DIRNAME/$DEER_BASENAME[$DEER_DIRNAME] fi ;; # Arrow keys $'\e') read -k case $REPLY in '[') read -k case $REPLY in 'A') deer-move -1 deer-refresh ;; 'B') deer-move 1 deer-refresh ;; 'C') deer-enter deer-refresh ;; 'D') deer-leave deer-refresh ;; esac ;; esac ;; esac done } if zle; then deer-launch else deer() { deer-launch "$@" } fi ~