I wrote a ~1500-line bash file manager. fzf for the picker, eza for
listing, Kitty's graphics protocol for inline image/video/PDF previews.
Everything else is stock coreutils.

Repo: https://github.com/eHab-codeX/fm
Why post it here
r/commandline got the "here's a tool" version last week. The bash bits
are probably more interesting to this crowd, so here they are.
The bash-specific parts
Double-click detection without EPOCHREALTIME getting in the way.
EPOCHREALTIME returns a locale-formatted decimal — . in en_US,
, in de_DE. Strip the separator before comparing:
now=${EPOCHREALTIME/[.,]/}
(( idx == last_idx && now - last_t < 400000 )) # 400ms
A hand-rolled CSI/SS3 parser for the gallery grid. The full-screen
grid needs arrow keys, PgUp/PgDn, Home/End, and SGR mouse events. Rather
than depend on a helper, it reads bytes into a buffer and dispatches on
the final byte:
case "$final" in
A) KEY=up ;; B) KEY=down ;;
C) KEY=right ;; D) KEY=left ;;
'~') case "${seq%%;*}" in
5) KEY=pgup ;; 6) KEY=pgdn ;;
esac ;;
esac
The tricky part is not swallowing stray bytes: an unknown CSI sequence
(\e[1;5H for Ctrl+Home, for example) has to be fully consumed or it
leaks into the next read as literal characters.
**stty -g around the alternate screen.** The grid switches to cbreak
mode (stty -icanon -echo min 1 time 0). Saving the previous state and
restoring it in the EXIT trap is what keeps Ctrl-C from leaving the
terminal in a broken state:
STTY_SAVED=$(stty -g </dev/tty) || STTY_SAVED=""
trap '[[ -n "$STTY_SAVED" ]] && stty "$STTY_SAVED" </dev/tty' EXIT
Two-phase rename so swaps work. A naive mv old new loop breaks
a→b, b→a. First move every source to a temp name, then move temps to
final names:
for k in "${!ro[@]}"; do
mv -T -- "${ro[k]}" "${ro[k]}.fm-tmp.$$"
done
for k in "${!ro[@]}"; do
mv -T -- "${ro[k]}.fm-tmp.$$" "${rn[k]}"
done
Parallel thumbnails by self-invocation. The thumbnail builder is
the same script with --thumb-any:
printf '%s\0' "${slice[@]}" | \
xargs -0 -r -n1 -P"$(nproc)" bash "$SELF" --thumb-any
Bash dispatches; ImageMagick/ffmpeg/pdftoppm do the work.
GNU vs BSD head. head -z is GNU-only. A contributor on macOS
sent a PR that falls back to ghead, then to a pure-bash reader:
while ((count < $1)) && IFS= read -r -d '' record; do
printf '%s\0' "$record"
((++count))
done
What I'd want reviewed
- The escape parser in
_g_readkey — there are terminals that emit
sequences it might mis-handle (Konsole? older tmux?).
- The
_fm_list sort pipeline — it does one batched stat and one
awk pass, then sort -V. I'm not 100% sure the tie-breaking
behavior for identical timestamps is stable.
- Anything in
_trash_and_record — I'm handling the Freedesktop
.trashinfo format's Path= percent-encoding with a bash-only
encoder, which is probably slower than it should be.
Requirements
Bash 5+ (uses EPOCHREALTIME and ${var,,}). Linux is the primary
target; macOS works with GNU coreutils installed (the _head0 helper
falls back to bash-only if ghead isn't present).
git clone https://github.com/eHab-codeX/fm.git ~/.local/share/fm
ln -sf ~/.local/share/fm/fm ~/.local/bin/fm
MIT.