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