#!/bin/sh esc() { case $1 in # vt100 (IL is vt102) (DECTCEM is vt520) CUD) printf '%s[%sB' "$esc_c" "$2" ;; # cursor down CUP) printf '%s[%s;%sH' "$esc_c" "$2" "$3" ;; # cursor home CUU) printf '%s[%sA' "$esc_c" "$2" ;; # cursor up DECAWM) printf '%s[?7%s' "$esc_c" "$2" ;; # line wrap DECRC) printf '%s8' "$esc_c" ;; # cursor restore DECSC) printf '%s7' "$esc_c" ;; # cursor save DECSTBM) printf '%s[%s;%sr' "$esc_c" "$2" "$3" ;; # scroll region DECTCEM) printf '%s[?25%s' "$esc_c" "$2" ;; # cursor visible ED[0-2]) printf '%s[%sJ' "$esc_c" "${1#ED}" ;; # clear screen EL[0-2]) printf '%s[%sK' "$esc_c" "${1#EL}" ;; # clear line IL) printf '%s[%sL' "$esc_c" "$2" ;; # insert line SGR) printf '%s[%s;%sm' "$esc_c" "$2" "$3" ;; # colors # xterm (since 1988, supported widely) screen_alt) printf '%s[?1049%s' "$esc_c" "$2" ;; # alternate buffer esac } term_setup() { stty=$(stty -g) stty -icanon -echo esc screen_alt h esc DECAWM l esc DECTCEM l esc ED2 esc DECSTBM 1 "$((LINES - 2))" } term_reset() { esc DECAWM h >&2 esc DECTCEM h >&2 esc ED2 >&2 esc DECSTBM >&2 esc screen_alt l >&2 stty "$stty" # needed for cd-on-exit printf '%s\n' "$PWD" >&1 } term_resize() { # false-positive, behavior intentional, globbing is disabled. # shellcheck disable=2046 { set -f set +f -- $(stty size) } LINES=$1 COLUMNS=$2 # space for status_line bottom=$((LINES - 2)) } term_scroll_down() { case $((y - $#)) in [0-9]*) return esac y=$((y + 1)) y2=$((y2 + 1 < bottom ? y2 + 1 : bottom)) line_print "$((y - 1))" "$@" printf '\n' line_print "$y" "$@" status_line "$#" } term_scroll_up() { case $y in -*|0|1) return esac y=$((y - 1)) line_print "$((y + 1))" "$@" case $y2 in 1) esc IL ;; *) esc CUU; y2=$((y2 > 1 ? y2 - 1 : 1)) esac line_print "$y" "$@" status_line "$#" } cmd_run() { stty "$stty" esc DECTCEM h esc DECSTBM esc ED2 "$@" ||: esc DECSTBM 1 "$((LINES - 2))" esc DECTCEM l stty -icanon -echo hist=2 } file_escape() { tmp=$1 safe= # loop over string char by char while c=${tmp%"${tmp#?}"}; do case $c in '') return ;; [[:cntrl:]]) safe=$safe\? ;; *) safe=$safe$c ;; esac tmp=${tmp#?} done } hist_search() { hist=0 j=1 for file do case ${PWD%%/}/$file in "$old_pwd") y=$j y2=$((j >= bottom ? mid : j)) cur=$file esac j=$((j + 1)) done } list_print() { esc ED2 esc CUP i=1 end=$((bottom + 1)) mid=$((bottom / 4 < 5 ? 1 : bottom / 4)) case $# in 1) [ -e "$1" ] || [ "$1" = 'no results' ] || set -- empty esac case $hist in 2) # redraw after cmd run shift "$((y > y2 ? y - y2 : 0))" ;; 1) # redraw after go-to-parent hist_search "$@" shift "$((y >= bottom ? y - mid : 0))" ;; *) # everything else shift "$((y >= bottom ? y - bottom : 0))" ;; esac for file do case $i in "$y2") esc SGR 0 7 esac case $((i - end)) in -*) line_format "$file" esc CUD ;; esac i=$((i + 1)) done esc CUP "$((y > y2 ? y2 : y))" } redraw() { list_print "$@" status_line "$#" } status_line() { esc DECSC esc CUP "$LINES" case $USER in root) esc SGR 31 7 ;; *) esc SGR 34 7 ;; esac printf '%*s\r%s ' "$COLUMNS" "" "($y/$1)" case $ltype in '') printf %s "$PWD" ;; *) printf %s "$ltype" esac esc SGR 0 0 esc DECRC } prompt() { esc DECSC esc CUP "$LINES" printf %s "$1" esc DECTCEM h esc EL0 case $2 in r) stty icanon echo read -r ans ||: stty -icanon -echo ;; esac esc DECRC esc DECTCEM l status_line "($y/$#) $PWD" } line_print() { offset=$1 case $offset in "$y") esc SGR 0 7 esac shift "$offset" case $offset in "$y") cur=$1 esac line_format "$1" } line_format() { file_escape "$1" [ -d "$1" ] && esc SGR 1 31 printf %s "$safe" [ -d "$1" ] && printf / esc SGR 0 0 esc EL0 printf '\r' } main() { set -e case $1 in -h|--help) printf 'shfm -[hv] \n' exit 0 ;; -v|--version) printf 'shfm 0.4.2\n' exit 0 ;; *) cd -- "${1:-"$PWD"}" ;; esac esc_c=$(printf '\033') bs_char=$(printf '\177') set -- * cur=$1 term_resize term_setup trap 'term_reset' EXIT INT trap 'term_resize; term_setup; y=1 y2=1; redraw "$@"' WINCH y=1 y2=1 redraw "$@" while key=$(dd ibs=1 count=1 2>/dev/null); do case $key${esc:=0} in k?|A2) term_scroll_up "$@" ;; j?|B2) term_scroll_down "$@" ;; l?|C2|"$esc") # ARROW RIGHT if [ -d "$cur" ] && cd -- "$cur" >/dev/null 2>&1; then set -- * y=1 y2=1 cur=$1 ltype= redraw "$@" elif [ -e "$cur" ]; then cmd_run "${SHFM_OPENER:="${EDITOR:=vi}"}" "$cur" redraw "$@" fi ;; h?|D2|"$bs_char"?) # ARROW LEFT old_pwd=$PWD case $ltype in '') cd .. || continue ;; *) ltype= ;; esac set -- * y=1 y2=1 cur=$1 hist=1 redraw "$@" ;; g?) case $y in 1) continue esac y=1 y2=1 cur=$1 redraw "$@" ;; G?) y=$# y2=$(($# < bottom ? $# : bottom)) line_print "$y" "$@" redraw "$@" ;; .?) case ${hidden:=1} in 1) hidden=0; set -- .* ;; 0) hidden=1; set -- * esac y=1 y2=1 cur=$1 redraw "$@" ;; :?) prompt "cd: " r # false positive, behavior intentional # shellcheck disable=2088 case $ans in '~') ans=$HOME ;; '~/'*) ans=$HOME/${ans#"~/"} esac cd -- "${ans:="$0"}" >/dev/null 2>&1|| continue set -- * y=1 y2=1 cur=$1 redraw "$@" ;; /?) prompt / r IFS= # globbing intentional, word splitting is disabled. # shellcheck disable=2086 set -- $ans* unset IFS case $1$# in "$ans*1") set -- 'no results' esac y=1 y2=1 cur=$1 ltype="search $PWD/$ans*" redraw "$@" status_line "$#" ;; -?) cd -- "$OLDPWD" >/dev/null 2>&1|| continue set -- * y=1 y2=1 cur=$1 redraw "$@" ;; \~?) cd || continue set -- * y=1 y2=1 cur=$1 redraw "$@" ;; \!?) export SHFM_LEVEL SHFM_LEVEL=$((SHFM_LEVEL + 1)) cmd_run "${SHELL:=/bin/sh}" redraw "$@" ;; \??) set -- 'j - down' \ 'k - up' \ 'l - open file or directory' \ 'h - go up level' \ 'g - go to top' \ 'G - go to bottom' \ 'q - quit' \ ': - cd to ' \ '/ - search current directory *' \ '- - go to last directory' \ '~ - go home' \ '! - spawn shell' \ '. - toggle hidden files' \ '? - show keybinds' y=1 y2=1 cur=$1 ltype=keybinds redraw "$@" status_line "$#" ;; q?) exit 0 ;; # handle keys which emit escape sequences "$esc_c"*) esc=1 ;; '[1') esc=2 ;; *) esc=0 ;; esac done } main "$@" >/dev/tty