master ohmu/dihfiles / bin / shfm
ohmu  ·  2026-09-26
  1#!/bin/sh
  2
  3esc() {
  4    case $1 in
  5        # vt100 (IL is vt102) (DECTCEM is vt520)
  6        CUD)     printf '%s[%sB'    "$esc_c" "$2"      ;; # cursor down
  7        CUP)     printf '%s[%s;%sH' "$esc_c" "$2" "$3" ;; # cursor home
  8        CUU)     printf '%s[%sA'    "$esc_c" "$2"      ;; # cursor up
  9        DECAWM)  printf '%s[?7%s'   "$esc_c" "$2"      ;; # line wrap
 10        DECRC)   printf '%s8'       "$esc_c"           ;; # cursor restore
 11        DECSC)   printf '%s7'       "$esc_c"           ;; # cursor save
 12        DECSTBM) printf '%s[%s;%sr' "$esc_c" "$2" "$3" ;; # scroll region
 13        DECTCEM) printf '%s[?25%s'  "$esc_c" "$2"      ;; # cursor visible
 14        ED[0-2]) printf '%s[%sJ'    "$esc_c" "${1#ED}" ;; # clear screen
 15        EL[0-2]) printf '%s[%sK'    "$esc_c" "${1#EL}" ;; # clear line
 16        IL)      printf '%s[%sL'    "$esc_c" "$2"      ;; # insert line
 17        SGR)     printf '%s[%s;%sm' "$esc_c" "$2" "$3" ;; # colors
 18
 19        # xterm (since 1988, supported widely)
 20        screen_alt) printf '%s[?1049%s' "$esc_c" "$2" ;; # alternate buffer
 21    esac
 22}
 23
 24term_setup() {
 25    stty=$(stty -g)
 26    stty -icanon -echo
 27    esc screen_alt h
 28    esc DECAWM l
 29    esc DECTCEM l
 30    esc ED2
 31    esc DECSTBM 1 "$((LINES - 2))"
 32}
 33
 34term_reset() {
 35    esc DECAWM h     >&2
 36    esc DECTCEM h    >&2
 37    esc ED2          >&2
 38    esc DECSTBM      >&2
 39    esc screen_alt l >&2
 40    stty "$stty"
 41
 42    # needed for cd-on-exit
 43    printf '%s\n' "$PWD" >&1
 44}
 45
 46term_resize() {
 47    # false-positive, behavior intentional, globbing is disabled.
 48    # shellcheck disable=2046
 49    {
 50        set -f
 51        set +f -- $(stty size)
 52    }
 53
 54    LINES=$1 COLUMNS=$2
 55
 56    # space for status_line
 57    bottom=$((LINES - 2))
 58}
 59
 60term_scroll_down() {
 61    case $((y - $#)) in
 62        [0-9]*) return
 63    esac
 64
 65    y=$((y + 1))
 66    y2=$((y2 + 1 < bottom ? y2 + 1 : bottom))
 67
 68    line_print "$((y - 1))" "$@"
 69    printf '\n'
 70    line_print "$y" "$@"
 71    status_line "$#"
 72}
 73
 74term_scroll_up() {
 75    case $y in
 76        -*|0|1) return
 77    esac
 78
 79    y=$((y - 1))
 80
 81    line_print "$((y + 1))" "$@"
 82
 83    case $y2 in
 84        1) esc IL ;;
 85        *) esc CUU; y2=$((y2 > 1 ? y2 - 1 : 1))
 86    esac
 87
 88    line_print "$y" "$@"
 89    status_line "$#"
 90}
 91
 92cmd_run() {
 93    stty "$stty"
 94    esc DECTCEM h
 95    esc DECSTBM
 96    esc ED2
 97    "$@" ||:
 98    esc DECSTBM 1 "$((LINES - 2))"
 99    esc DECTCEM l
100    stty -icanon -echo
101    hist=2
102}
103
104file_escape() {
105    tmp=$1 safe=
106
107    # loop over string char by char
108    while c=${tmp%"${tmp#?}"}; do
109        case $c in
110            '')          return ;;
111            [[:cntrl:]]) safe=$safe\? ;;
112            *)           safe=$safe$c ;;
113        esac
114
115        tmp=${tmp#?}
116    done
117}
118
119hist_search() {
120    hist=0 j=1
121
122    for file do
123        case ${PWD%%/}/$file in
124            "$old_pwd") y=$j y2=$((j >= bottom ? mid : j)) cur=$file
125        esac
126
127        j=$((j + 1))
128    done
129}
130
131list_print() {
132    esc ED2
133    esc CUP
134
135    i=1
136    end=$((bottom + 1))
137    mid=$((bottom / 4 < 5 ? 1 : bottom / 4))
138
139    case $# in
140        1) [ -e "$1" ] || [ "$1" = 'no results' ] || set -- empty
141    esac
142
143    case $hist in
144        2) # redraw after cmd run
145            shift "$((y > y2 ? y - y2 : 0))"
146        ;;
147
148        1) # redraw after go-to-parent
149            hist_search "$@"
150            shift "$((y >= bottom ? y - mid : 0))"
151        ;;
152
153        *) # everything else
154            shift "$((y >= bottom ? y - bottom : 0))"
155        ;;
156    esac
157
158    for file do
159        case $i in
160            "$y2") esc SGR 0 7
161        esac
162
163        case $((i - end)) in
164            -*)
165                line_format "$file"
166                esc CUD
167            ;;
168        esac
169
170        i=$((i + 1))
171    done
172
173    esc CUP "$((y > y2 ? y2 : y))"
174}
175
176redraw() {
177    list_print "$@"
178    status_line "$#"
179}
180
181status_line() {
182    esc DECSC
183    esc CUP "$LINES"
184
185    case $USER in
186        root) esc SGR 31 7 ;;
187           *) esc SGR 34 7 ;;
188    esac
189
190    printf '%*s\r%s ' "$COLUMNS" "" "($y/$1)"
191
192    case $ltype in
193        '') printf %s "$PWD" ;;
194         *) printf %s "$ltype"
195    esac
196
197    esc SGR 0 0
198    esc DECRC
199}
200
201prompt() {
202    esc DECSC
203    esc CUP "$LINES"
204    printf %s "$1"
205    esc DECTCEM h
206    esc EL0
207
208    case $2 in
209        r)
210            stty icanon echo
211            read -r ans ||:
212            stty -icanon -echo
213        ;;
214    esac
215
216    esc DECRC
217    esc DECTCEM l
218    status_line "($y/$#) $PWD"
219}
220
221line_print() {
222    offset=$1
223
224    case $offset in
225        "$y") esc SGR 0 7
226    esac
227
228    shift "$offset"
229
230    case $offset in
231        "$y") cur=$1
232    esac
233
234    line_format "$1"
235}
236
237line_format() {
238    file_escape "$1"
239    [ -d "$1" ] && esc SGR 1 31
240    printf %s "$safe"
241    [ -d "$1" ] && printf /
242    esc SGR 0 0
243    esc EL0
244    printf '\r'
245}
246
247main() {
248    set -e
249
250    case $1 in
251        -h|--help)
252            printf 'shfm -[hv] <starting dir>\n'
253            exit 0
254        ;;
255
256        -v|--version)
257            printf 'shfm 0.4.2\n'
258            exit 0
259        ;;
260
261        *)
262            cd -- "${1:-"$PWD"}"
263        ;;
264    esac
265
266    esc_c=$(printf '\033')
267    bs_char=$(printf '\177')
268
269    set -- *
270    cur=$1
271
272    term_resize
273    term_setup
274
275    trap 'term_reset'  EXIT INT
276    trap 'term_resize; term_setup; y=1 y2=1; redraw "$@"' WINCH
277
278    y=1 y2=1
279    redraw "$@"
280
281    while key=$(dd ibs=1 count=1 2>/dev/null); do
282        case $key${esc:=0} in
283            k?|A2)
284                term_scroll_up "$@"
285            ;;
286
287            j?|B2)
288                term_scroll_down "$@"
289            ;;
290
291            l?|C2|"$esc") # ARROW RIGHT
292                if [ -d "$cur" ] && cd -- "$cur" >/dev/null 2>&1; then
293                    set -- *
294                    y=1 y2=1 cur=$1 ltype=
295                    redraw "$@"
296
297                elif [ -e "$cur" ]; then
298                    cmd_run "${SHFM_OPENER:="${EDITOR:=vi}"}" "$cur"
299                    redraw "$@"
300                fi
301            ;;
302
303            h?|D2|"$bs_char"?) # ARROW LEFT
304                old_pwd=$PWD
305
306                case $ltype in
307                    '') cd .. || continue ;;
308                     *) ltype= ;;
309                esac
310
311                set -- *
312                y=1 y2=1 cur=$1 hist=1
313                redraw "$@"
314            ;;
315
316            g?)
317                case $y in
318                    1) continue
319                esac
320
321                y=1 y2=1 cur=$1
322                redraw "$@"
323            ;;
324
325            G?)
326                y=$#
327                y2=$(($# < bottom ? $# : bottom))
328                line_print "$y" "$@"
329                redraw "$@"
330            ;;
331
332            .?)
333                case ${hidden:=1} in
334                    1) hidden=0; set -- .* ;;
335                    0) hidden=1; set -- *
336                esac
337
338                y=1 y2=1 cur=$1
339                redraw "$@"
340            ;;
341
342            :?)
343                prompt "cd: " r
344
345                # false positive, behavior intentional
346                # shellcheck disable=2088
347                case $ans in
348                    '~')   ans=$HOME ;;
349                    '~/'*) ans=$HOME/${ans#"~/"}
350                esac
351
352                cd -- "${ans:="$0"}" >/dev/null 2>&1|| continue
353                set -- *
354                y=1 y2=1 cur=$1
355                redraw "$@"
356            ;;
357
358            /?)
359                prompt / r
360
361                IFS=
362                # globbing intentional, word splitting is disabled.
363                # shellcheck disable=2086
364                set -- $ans*
365                unset IFS
366
367                case $1$# in
368                    "$ans*1") set -- 'no results'
369                esac
370
371                y=1 y2=1 cur=$1 ltype="search $PWD/$ans*"
372                redraw "$@"
373                status_line "$#"
374            ;;
375
376            -?)
377                cd -- "$OLDPWD" >/dev/null 2>&1|| continue
378                set -- *
379                y=1 y2=1 cur=$1
380                redraw "$@"
381            ;;
382
383            \~?)
384                cd || continue
385                set -- *
386                y=1 y2=1 cur=$1
387                redraw "$@"
388            ;;
389
390            \!?)
391                export SHFM_LEVEL
392                SHFM_LEVEL=$((SHFM_LEVEL + 1))
393                cmd_run "${SHELL:=/bin/sh}"
394                redraw "$@"
395            ;;
396
397            \??)
398                set -- 'j - down' \
399                       'k - up' \
400                       'l - open file or directory' \
401                       'h - go up level' \
402                       'g - go to top' \
403                       'G - go to bottom' \
404                       'q - quit' \
405                       ': - cd to <input>' \
406                       '/ - search current directory <input>*' \
407                       '- - go to last directory' \
408                       '~ - go home' \
409                       '! - spawn shell' \
410                       '. - toggle hidden files' \
411                       '? - show keybinds'
412
413                y=1 y2=1 cur=$1 ltype=keybinds
414                redraw "$@"
415                status_line "$#"
416            ;;
417
418            q?) exit 0 ;;
419
420            # handle keys which emit escape sequences
421            "$esc_c"*) esc=1 ;;
422                 '[1') esc=2 ;;
423                    *) esc=0 ;;
424        esac
425    done
426}
427
428main "$@" >/dev/tty