8 Commits
Author SHA1 Message Date
emmett1 2a1f8fc0a5 add find function 2026-04-05 20:00:02 +08:00
emmett1 346ef7b7cb add jump to path function 2026-04-05 19:39:24 +08:00
emmett1 09f60a85ef add chmod +x and -x function 2026-04-05 19:33:56 +08:00
emmett1 32337e4cca show hidden files first when enabled hidden 2026-04-05 01:35:44 +08:00
emmett1 03df88a150 added man page 2026-04-01 00:35:31 +08:00
emmett1 54352d036c renamed README.txt to README 2026-04-01 00:35:04 +08:00
emmett1 6b729ffe8a updated panel mode 2026-03-28 07:54:42 +08:00
emmett1 25a329aba9 added opener script support 2026-03-28 00:08:08 +08:00
4 changed files with 678 additions and 99 deletions
+8 -4
View File
@@ -15,17 +15,21 @@ all:
@echo "Run 'make install' to install."
install:
@echo "Installing sfm to $(BINDIR)/sfm ..."
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(INSTALL) -m 755 sfm $(DESTDIR)$(BINDIR)/sfm
@echo "Installing README to $(DOCDIR) ..."
@echo "Installing man page to $(DESTDIR)$(MANDIR)/sfm.1 ..."
$(INSTALL) -d $(DESTDIR)$(MANDIR)
$(INSTALL) -m 644 sfm.1 $(DESTDIR)$(MANDIR)/sfm.1
@echo "Installing README to $(DESTDIR)$(DOCDIR)/README ..."
$(INSTALL) -d $(DESTDIR)$(DOCDIR)
$(INSTALL) -m 644 README.txt $(DESTDIR)$(DOCDIR)/README.txt
@echo "Done. Run 'sfm' to start."
$(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README
@echo "Done. Run 'sfm' to start, or 'man sfm' for help."
uninstall:
@echo "Removing sfm ..."
$(RM) $(DESTDIR)$(BINDIR)/sfm
$(RM) $(DESTDIR)$(MANDIR)/sfm.1
$(RM) -r $(DESTDIR)$(DOCDIR)
@echo "Done."
+78 -53
View File
@@ -1,15 +1,15 @@
sfm - Simple File Manager
==========================
DESCRIPTION
-----------
sfm is a lightweight, terminal-based file manager written entirely in
POSIX sh. It runs in your terminal with no external dependencies beyond
standard Unix tools (ls, awk, tput, stty, mv, cp, rm). Designed to be
fast, flicker-free, and keyboard-driven with a vim-inspired key layout.
REQUIREMENTS
------------
- A POSIX-compatible shell (sh, dash, bash, etc.)
@@ -18,41 +18,41 @@ REQUIREMENTS
- Optional: mpv, vlc, feh, zathura, etc. (for smart file opening)
- Optional: readlink (for symlink display)
- Optional: file (for MIME type detection)
INSTALLATION
------------
Using make (recommended):
make install
This installs sfm to /usr/local/bin by default. To change the prefix:
make install PREFIX=/usr
To uninstall:
make uninstall
Manual installation:
cp sfm /usr/local/bin/sfm
chmod +x /usr/local/bin/sfm
USAGE
-----
sfm [path]
If no path is given, sfm opens in the current directory.
Examples:
sfm # open in current directory
sfm /mnt/data # open at a specific path
sfm ~ # open home directory
sfm .. # open parent directory
NAVIGATION
----------
j / k or up/down arrows Move up / down
@@ -62,15 +62,15 @@ NAVIGATION
G Jump to bottom of list
~ Go to home directory
` (backtick) Jump to previous directory
SEARCH & FILTER
---------------
/ Enter search mode (filters listing as you type)
esc Clear filter and exit search mode
enter Exit search mode but keep filter active
DISPLAY TOGGLES
---------------
. Toggle hidden files (dotfiles)
@@ -79,19 +79,42 @@ DISPLAY TOGGLES
s Cycle sort mode: name -> size -> date
i Show file info (permissions, size, date) in status bar
R Refresh current directory listing
PREVIEW PANE
------------
Press P to toggle the preview pane on the right side of the screen.
The list pane takes the left half, preview takes the right half.
Text files Shows file contents line by line
Directories Shows entry count
Directories Shows directory contents
Symlinks Shows link target
Binary files Shows file size
CUSTOM OPENER
-------------
sfm checks for a user-defined opener script at:
~/.config/sfm/opener
If the file exists and is executable, sfm passes the selected file
path to it instead of using the built-in smart opener. Example:
#!/bin/sh
case "$1" in
*.jpg|*.jpeg) imv "$1" ;;
*.mp4) mpv "$1" ;;
*.html|*.pdf) firefox "$1" ;;
*) echo "no program set for this file type" ;;
esac
Make it executable: chmod +x ~/.config/sfm/opener
If the opener script does not exist, sfm falls back to its built-in
smart opener automatically.
FILE OPERATIONS
---------------
r Rename selected entry
@@ -101,45 +124,45 @@ FILE OPERATIONS
u Move to trash (safe delete)
U Open trash directory
o Open with custom program
CLIPBOARD
---------
y Yank / copy (works on multi-selection)
x Cut (works on multi-selection)
p Paste into current directory
c Copy full path of selected entry to system clipboard
MULTI-SELECT
------------
space Toggle multi-select on current entry (cursor advances)
a Select all / deselect all
BOOKMARKS
---------
b Bookmark current directory (press again to remove)
B Open bookmark picker (j/k navigate, enter jump)
Bookmarks are saved to: ~/.config/sfm/bookmarks
SHELL & UTILITIES
-----------------
! Drop into $SHELL in current directory (type "exit" to return)
HELP
----
? Show keyboard shortcuts overlay
QUIT
----
q Quit sfm
STATUS BAR INDICATORS
---------------------
[hidden] Hidden files are visible
@@ -148,13 +171,13 @@ STATUS BAR INDICATORS
[cut] A file is in the clipboard (cut mode)
[sel:N] N items are currently selected
[details] Size/date column is visible
SMART FILE OPENER
-----------------
When you open a file, sfm detects the file type by extension and
MIME type, then picks an appropriate program automatically:
Text / code $EDITOR (or vi)
Images imv, feh, sxiv, eog, gimp
Video mpv, vlc, mplayer, totem
@@ -162,24 +185,26 @@ MIME type, then picks an appropriate program automatically:
PDF zathura, evince, okular, mupdf
Office docs libreoffice
Archives atool / bsdtar (lists contents in pager)
Falls back to xdg-open, open (macOS), or $EDITOR if nothing matches.
Use 'o' to manually specify any program.
TRASH
-----
Files deleted with 'u' are moved to:
~/.local/share/sfm-trash/
Files are prefixed with a timestamp: YYYYMMDD_HHMMSS_filename
Use U to browse the trash directory. To restore a file, rename it
(press r) to remove the timestamp prefix, then cut (x) and paste (p)
it back to the desired location.
DATA FILES
----------
~/.config/sfm/bookmarks Saved bookmarks (one path per line)
~/.config/sfm/opener Custom file opener script (optional)
~/.local/share/sfm-trash/ Trashed files
+247 -42
View File
@@ -106,22 +106,11 @@ load_entries() {
ENTRIES=""
COUNT=0
# build list of dir names then file names, including dotfiles if enabled
for d in "$CWD"/*/; do
[ -d "$d" ] || continue
[ -L "${d%/}" ] && continue # symlinks handled separately
name="${d%/}"; name="${name##*/}"
[ "$name" = "*" ] && continue
case "$name" in .*) continue ;; esac
ENTRIES="$ENTRIES
${name}/"
COUNT=$((COUNT + 1))
done
# hidden dirs first
if [ "$SHOW_HIDDEN" = "1" ]; then
for d in "$CWD"/./; do : ; done
for d in "$CWD"/.*/; do
[ -d "$d" ] || continue
[ -L "${d%/}" ] && continue # symlinks handled separately
[ -L "${d%/}" ] && continue
name="${d%/}"; name="${name##*/}"
[ "$name" = "." ] || [ "$name" = ".." ] && continue
[ "$name" = ".*" ] && continue
@@ -131,21 +120,19 @@ ${name}/"
done
fi
for f in "$CWD"/*; do
[ -e "$f" ] || [ -L "$f" ] || continue
[ -d "$f" ] && ! [ -L "$f" ] && continue
name="${f##*/}"
# regular dirs
for d in "$CWD"/*/; do
[ -d "$d" ] || continue
[ -L "${d%/}" ] && continue
name="${d%/}"; name="${name##*/}"
[ "$name" = "*" ] && continue
case "$name" in .*) continue ;; esac
if [ -L "$f" ]; then
ENTRIES="$ENTRIES
${name}@"
else
ENTRIES="$ENTRIES
${name}"
fi
ENTRIES="$ENTRIES
${name}/"
COUNT=$((COUNT + 1))
done
# hidden files first
if [ "$SHOW_HIDDEN" = "1" ]; then
for f in "$CWD"/.*; do
[ -e "$f" ] || [ -L "$f" ] || continue
@@ -163,6 +150,23 @@ ${name}"
done
fi
# regular files
for f in "$CWD"/*; do
[ -e "$f" ] || [ -L "$f" ] || continue
[ -d "$f" ] && ! [ -L "$f" ] && continue
name="${f##*/}"
[ "$name" = "*" ] && continue
case "$name" in .*) continue ;; esac
if [ -L "$f" ]; then
ENTRIES="$ENTRIES
${name}@"
else
ENTRIES="$ENTRIES
${name}"
fi
COUNT=$((COUNT + 1))
done
ENTRIES="${ENTRIES#
}"
@@ -456,10 +460,36 @@ draw_preview() {
case "$entry" in
*/)
# directory: show entry count
_dc=$(ls -1 "$_path" 2>/dev/null | wc -l | tr -d '[:space:]')
goto 2 "$_px"
printf '%s[dir] %s items%s' "${CYAN}" "${_dc}" "${RESET}"
# directory: list contents
_max_lines=$((_rows - 3))
_pr=2
for _de in "$_path"/*/; do
[ "$_pr" -gt $((_rows - 1)) ] && break
[ -d "$_de" ] || continue
_dn="${_de%/}"; _dn="${_dn##*/}"
[ "$_dn" = "*" ] && continue
_dl=" ${_dn}/"
[ "${#_dl}" -gt "$((_pw - 1))" ] && _dl="$(printf '%s' "$_dl" | cut -c1-$((_pw-2)))~"
goto "$_pr" "$_px"
printf '%s%s%s' "${GREEN}${BOLD}" "$_dl" "${RESET}"
_pr=$((_pr + 1))
done
for _fe in "$_path"/*; do
[ "$_pr" -gt $((_rows - 1)) ] && break
[ -e "$_fe" ] || continue
[ -d "$_fe" ] && continue
_fn="${_fe##*/}"
[ "$_fn" = "*" ] && continue
_fl=" ${_fn}"
[ "${#_fl}" -gt "$((_pw - 1))" ] && _fl="$(printf '%s' "$_fl" | cut -c1-$((_pw-2)))~"
goto "$_pr" "$_px"
printf '%s%s%s' "${WHITE}" "$_fl" "${RESET}"
_pr=$((_pr + 1))
done
if [ "$_pr" -eq 2 ]; then
goto 2 "$_px"
printf '%s(empty)%s' "${WHITE}" "${RESET}"
fi
;;
*@)
# symlink
@@ -679,6 +709,14 @@ _run_gui() { "$@" >/dev/null 2>&1 & }
open_file() {
_f="$1"
# use user opener script if it exists and is executable
_opener="${XDG_CONFIG_HOME:-$HOME/.config}/sfm/opener"
if [ -x "$_opener" ]; then
"$_opener" "$_f"
return
fi
_ext="${_f##*.}"
_ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')
@@ -856,7 +894,7 @@ do_help() {
bw=$((COLS - 4))
[ "$bw" -gt 52 ] && bw=52
[ "$bw" -lt 36 ] && bw=36
bh=42
bh=46
_help_bx=$(( (COLS - bw) / 2 )); [ "$_help_bx" -lt 1 ] && _help_bx=1
_help_by=$(( (ROWS - bh) / 2 )); [ "$_help_by" -lt 1 ] && _help_by=1
_help_iw=$((bw - 2))
@@ -894,17 +932,21 @@ do_help() {
help_row 27 " U open trash directory"
help_row 28 " ! drop to shell in CWD"
help_row 29 " o open with custom program"
help_sep 30
help_row 31 " b bookmark current dir"
help_row 32 " B open bookmark picker"
help_row 33 " c copy path to clipboard"
help_row 34 " ~ go to home directory"
help_row 35 " \` jump to previous directory"
help_sep 36
help_row 37 " q quit"
help_row 38 " ? this help"
help_row 39 ""
help_row 40 " press any key to close..."
help_row 30 " + chmod +x (make executable)"
help_row 31 " - chmod -x (remove executable)"
help_sep 32
help_row 33 " b bookmark current dir"
help_row 34 " B open bookmark picker"
help_row 35 " c copy path to clipboard"
help_row 36 " ~ go to home directory"
help_row 37 " \` jump to previous directory"
help_row 38 " : jump to path"
help_row 39 " f find files recursively"
help_sep 40
help_row 41 " q quit"
help_row 42 " ? this help"
help_row 43 ""
help_row 44 " press any key to close..."
goto "$(( _help_by + bh ))" "$_help_bx"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}"
@@ -985,6 +1027,141 @@ do_toggle_preview() {
NEED_FULL_REDRAW=1
}
do_find() {
goto "$(term_rows)" 1
printf '%s%s find (recursive): %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}"
if ! read_line || [ -z "$READ_LINE" ]; then
NEED_FULL_REDRAW=1; draw; return
fi
_query="$READ_LINE"
# run find and collect results into a tmp file
_tmp=/tmp/_sfm_find
find "$CWD" -name "*${_query}*" 2>/dev/null | sort > "$_tmp"
_total=$(wc -l < "$_tmp" | tr -d '[:space:]')
if [ "$_total" -eq 0 ]; then
INFO_MSG="no results for: ${_query}"
NEED_FULL_REDRAW=1; return
fi
# show results in an interactive picker
ROWS=$(term_rows); COLS=$(term_cols)
_pw=$(( COLS - 4 )); [ "$_pw" -gt 80 ] && _pw=80; [ "$_pw" -lt 30 ] && _pw=30
_ph=$(( ROWS - 4 )); [ "$_ph" -lt 5 ] && _ph=5
_px=$(( (COLS - _pw) / 2 )); [ "$_px" -lt 1 ] && _px=1
_py=$(( (ROWS - _ph) / 2 )); [ "$_py" -lt 1 ] && _py=1
_iw=$((_pw - 2))
_hl=$(printf '%*s' "$_iw" '' | tr ' ' '-')
_vis=$((_ph - 3)) # visible result rows
_fsel=1
_foff=1 # scroll offset (1-based)
while true; do
# draw box
goto "$_py" "$_px"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
# title
goto "$((_py+1))" "$_px"
_title=" find: ${_query} (${_total} results)"
[ "${#_title}" -gt "$_iw" ] && _title=$(printf '%s' "$_title" | cut -c1-"$_iw")
_tpl=$((_iw - ${#_title})); _tp=$(printf '%*s' "$_tpl" '')
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_title" "$_tp" "${BOLD}${CYAN}" "${RESET}"
goto "$((_py+2))" "$_px"
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
# results
_ri=0
while [ "$_ri" -lt "$_vis" ]; do
_ridx=$((_foff + _ri))
goto "$((_py+3+_ri))" "$_px"
if [ "$_ridx" -le "$_total" ]; then
_rline=$(awk -v n="$_ridx" 'NR==n{print;exit}' "$_tmp")
# strip CWD prefix for display
_rdisplay="${_rline#$CWD/}"
[ "${#_rdisplay}" -gt "$_iw" ] && \
_rdisplay="...$(printf '%s' "$_rdisplay" | rev | cut -c1-$((_iw-4)) | rev)"
_rpl=$((_iw - ${#_rdisplay})); _rp=$(printf '%*s' "$_rpl" '')
if [ "$_ridx" -eq "$_fsel" ]; then
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${REV}${YELLOW}" "$_rdisplay" "$_rp" "${RESET}${BOLD}${CYAN}" "${RESET}"
else
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_rdisplay" "$_rp" "${BOLD}${CYAN}" "${RESET}"
fi
else
_ep=$(printf '%*s' "$_iw" '')
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_ep" "${RESET}"
fi
_ri=$((_ri+1))
done
goto "$((_py+_ph))" "$_px"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
# read key
IFS= read -r -n1 _fk 2>/dev/null || IFS= read -r _fk
case "$_fk" in
j) _fsel=$((_fsel+1)); [ "$_fsel" -gt "$_total" ] && _fsel=$_total
[ "$_fsel" -ge $((_foff+_vis)) ] && _foff=$((_foff+1)) ;;
k) _fsel=$((_fsel-1)); [ "$_fsel" -lt 1 ] && _fsel=1
[ "$_fsel" -lt "$_foff" ] && _foff=$((_foff-1)); [ "$_foff" -lt 1 ] && _foff=1 ;;
"$(printf '\033')")
IFS= read -r -n2 -t 0.1 _fseq 2>/dev/null || _fseq=""
case "$_fseq" in
'[A') _fsel=$((_fsel-1)); [ "$_fsel" -lt 1 ] && _fsel=1
[ "$_fsel" -lt "$_foff" ] && _foff=$((_foff-1)); [ "$_foff" -lt 1 ] && _foff=1 ;;
'[B') _fsel=$((_fsel+1)); [ "$_fsel" -gt "$_total" ] && _fsel=$_total
[ "$_fsel" -ge $((_foff+_vis)) ] && _foff=$((_foff+1)) ;;
*) NEED_FULL_REDRAW=1; return ;; # esc — close
esac ;;
"$(printf '\n')"|\
"$(printf '\r')"|\
l)
_chosen=$(awk -v n="$_fsel" 'NR==n{print;exit}' "$_tmp")
if [ -d "$_chosen" ]; then
PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
elif [ -e "$_chosen" ]; then
PREV_CWD="$CWD"; CWD=$(dirname "$_chosen"); norm_cwd
fi
FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries
# try to highlight the found file
_fname="${_chosen##*/}"
[ -d "$_chosen" ] && _fname="${_fname}/"
_fidx=$(find_entry "$_fname")
[ "$_fidx" -ge 0 ] 2>/dev/null && SEL=$_fidx
return ;;
q|Q|h) NEED_FULL_REDRAW=1; return ;;
esac
done
}
do_jump_path() {
goto "$(term_rows)" 1
printf '%s%s jump to: %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}"
if read_line && [ -n "$READ_LINE" ]; then
# expand ~ manually
case "$READ_LINE" in
"~"*) READ_LINE="${HOME}${READ_LINE#\~}" ;;
esac
if [ -d "$READ_LINE" ]; then
PREV_CWD="$CWD"
CWD="$READ_LINE"; norm_cwd
FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries
else
INFO_MSG="not found: ${READ_LINE}"
NEED_FULL_REDRAW=1; draw
fi
else
NEED_FULL_REDRAW=1; draw
fi
}
do_go_back() {
[ "$CWD" = "/" ] && return
FILTER=""
@@ -1155,6 +1332,32 @@ do_mkdir() {
fi
}
do_chmod_x() {
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
case "$entry" in */) INFO_MSG="cannot chmod a directory"; NEED_FULL_REDRAW=1; return ;; esac
_target=$(joinpath "${entry%@}")
if chmod +x "$_target" 2>/dev/null; then
INFO_MSG="chmod +x: ${entry}"
else
INFO_MSG="chmod failed: ${entry}"
fi
NEED_FULL_REDRAW=1
}
do_chmod_nox() {
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
case "$entry" in */) INFO_MSG="cannot chmod a directory"; NEED_FULL_REDRAW=1; return ;; esac
_target=$(joinpath "${entry%@}")
if chmod -x "$_target" 2>/dev/null; then
INFO_MSG="chmod -x: ${entry}"
else
INFO_MSG="chmod failed: ${entry}"
fi
NEED_FULL_REDRAW=1
}
do_info() {
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
@@ -1486,12 +1689,14 @@ while true; do
T) do_toggle_details ;;
P) do_toggle_preview ;;
i) do_info ;;
'!') do_shell ;;
'+') do_chmod_x ;;
'-') do_chmod_nox ;;
o) do_open_with ;;
s) do_sort ;;
u) do_trash ;;
U) do_open_trash ;;
'`') do_jump_back ;;
f) do_find ;;
':') do_jump_path ;;
'~') PREV_CWD="$CWD"; CWD=$(cd "$HOME" && pwd); FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0; load_entries ;;
c) do_copy_path ;;
+345
View File
@@ -0,0 +1,345 @@
.TH SFM 1 "2026-03-16" "0.4" "Simple File Manager"
.SH NAME
sfm \- Simple File Manager
.SH SYNOPSIS
.B sfm
.RI [ path ]
.SH DESCRIPTION
.B sfm
is a lightweight, terminal-based file manager written entirely in POSIX sh.
It runs in your terminal with no external dependencies beyond standard Unix
tools. Designed to be fast, flicker-free, and keyboard-driven with a
vim-inspired key layout.
If
.I path
is given, sfm opens that directory. Otherwise it opens in the current
working directory.
.SH KEY BINDINGS
.SS Navigation
.TP
.BR j ", " k " or " "up/down arrows"
Move cursor up or down.
.TP
.BR h " or " "left arrow"
Go to parent directory.
.TP
.BR l ", " "right arrow" ", or " enter
Open selected file or enter directory.
.TP
.B g
Jump to top of list.
.TP
.B G
Jump to bottom of list.
.TP
.B ~
Go to home directory.
.TP
.B \`
Jump to previous directory (toggle).
.SS Search and Filter
.TP
.B /
Enter search mode. The listing is filtered as you type.
.TP
.B esc
Clear filter and exit search mode.
.TP
.B enter
Exit search mode while keeping the current filter active.
.SS Display Toggles
.TP
.B .
Toggle hidden files (dotfiles).
.TP
.B T
Toggle size/date detail column.
.TP
.B P
Toggle preview pane on the right side.
.TP
.B s
Cycle sort mode: name \-> size \-> date.
.TP
.B i
Show file info (permissions, size, date) in the status bar.
.TP
.B R
Refresh the current directory listing.
.SS File Operations
.TP
.B r
Rename selected entry. Press esc to cancel.
.TP
.B m
Create a new directory. Press esc to cancel.
.TP
.B n
Create a new empty file. Press esc to cancel.
.TP
.B d
Delete selected entry or all multi-selected entries.
Non-empty directories require a second confirmation.
.TP
.B u
Move selected entry to trash (safe delete).
.TP
.B U
Open the trash directory.
.TP
.B o
Open selected file with a custom program. Press esc to cancel.
.SS Clipboard
.TP
.B y
Yank (copy) selected entry or all multi-selected entries.
.TP
.B x
Cut selected entry or all multi-selected entries.
.TP
.B p
Paste clipboard contents into the current directory.
.TP
.B c
Copy the full path of selected entry to the system clipboard.
Requires
.BR wl-copy ", " xclip ", " xsel ", or " pbcopy .
.SS Multi-Select
.TP
.B space
Toggle multi-select on the current entry (cursor advances).
.TP
.B a
Select all entries / deselect all.
.SS Bookmarks
.TP
.B b
Bookmark the current directory. Press again to remove it.
.TP
.B B
Open the bookmark picker.
Use j/k to navigate, enter to jump, esc to close.
.SS Other
.TP
.B !
Drop into
.B $SHELL
in the current directory. Type
.I exit
to return to sfm.
.TP
.B ?
Show the keyboard shortcuts help overlay.
.TP
.B q
Quit sfm.
.SH PREVIEW PANE
When the preview pane is enabled with
.BR P ,
the terminal is split into a left pane (file list) and a right pane
(preview). The preview shows:
.IP \(bu 2
.B Text files \-
file contents displayed line by line.
.IP \(bu 2
.B Directories \-
directory contents (subdirectories first, then files).
.IP \(bu 2
.B Symlinks \-
the link target path.
.IP \(bu 2
.B Binary files \-
file size.
.SH CUSTOM OPENER
.B sfm
checks for a user-defined opener script at:
.PP
.I ~/.config/sfm/opener
.PP
If the file exists and is executable, sfm passes the selected file path
to it as the first argument instead of using the built-in smart opener.
.PP
Example opener script:
.PP
.nf
#!/bin/sh
case "$1" in
*.jpg|*.jpeg) imv "$1" ;;
*.mp4) mpv "$1" ;;
*.html|*.pdf) firefox "$1" ;;
*) echo "no program set for this file type" ;;
esac
.fi
.PP
If the opener script does not exist, sfm falls back to its built-in
smart opener which detects file types by extension and MIME type.
.SH SMART FILE OPENER
When no custom opener is configured, sfm opens files using the first
available program for each file type:
.TP
.B Text / code
$EDITOR or vi.
.TP
.B Images
imv, feh, sxiv, eog, gimp.
.TP
.B Video
mpv, vlc, mplayer, totem.
.TP
.B Audio
mpv, vlc, cmus, mocp.
.TP
.B PDF
zathura, evince, okular, mupdf.
.TP
.B Office documents
libreoffice.
.TP
.B Archives
atool or bsdtar (lists contents in pager).
.PP
Falls back to xdg-open, open (macOS), or $EDITOR.
.SH FILE COLOURS
.TP
.B Green bold
Directories.
.TP
.B Red bold
Executables and broken symlinks.
.TP
.B Magenta
Images.
.TP
.B Magenta bold
Video files and valid symlinks.
.TP
.B Cyan bold
Audio files.
.TP
.B Red
Archives (zip, tar, gz, etc.).
.TP
.B Yellow
Documents (pdf, docx, etc.).
.TP
.B Green
Code and scripts (py, sh, rs, etc.).
.TP
.B Cyan
Config and data files (json, yaml, etc.).
.TP
.B White
Text, markdown, and all other files.
.SH STATUS BAR INDICATORS
.TP
.B [hidden]
Hidden (dot) files are currently visible.
.TP
.B [sort:size] or [sort:date]
Active sort mode. Not shown when sorting by name (default).
.TP
.B [copy] or [cut]
A file is stored in the clipboard.
.TP
.B [sel:N]
N items are currently multi-selected.
.TP
.B [details]
Size/date detail column is visible.
.SH TRASH
Files deleted with
.B u
are moved to:
.PP
.I ~/.local/share/sfm-trash/
.PP
Files are prefixed with a timestamp in the format
.IR YYYYMMDD_HHMMSS_filename .
Use
.B U
to browse the trash directory. To restore a file, rename it with
.B r
to remove the timestamp prefix, then cut with
.B x
and paste with
.B p
to the desired location.
.SH FILES
.TP
.I ~/.config/sfm/bookmarks
Saved bookmarks, one directory path per line.
.TP
.I ~/.config/sfm/opener
Custom file opener script (optional, must be executable).
.TP
.I ~/.local/share/sfm-trash/
Directory where trashed files are stored.
.SH ENVIRONMENT
.TP
.B EDITOR
Editor used to open text files when no custom opener is set.
.TP
.B SHELL
Shell used when dropping to the terminal with
.BR ! .
.TP
.B PAGER
Pager used when listing archive contents.
.TP
.B XDG_CONFIG_HOME
Base directory for sfm config files. Defaults to
.IR ~/.config .
.TP
.B XDG_DATA_HOME
Base directory for sfm data files (trash). Defaults to
.IR ~/.local/share .
.SH EXAMPLES
Open sfm in the current directory:
.PP
.nf
sfm
.fi
.PP
Open sfm at a specific path:
.PP
.nf
sfm /mnt/data
.fi
.PP
Use sfm as a cd wrapper (add to shell rc):
.PP
.nf
s() { cd "$(sfm)"; }
.fi
.SH AUTHORS
Built interactively with Claude (Anthropic).
.SH SEE ALSO
.BR ls (1),
.BR mv (1),
.BR cp (1),
.BR rm (1),
.BR vi (1)