8 Commits
Author SHA1 Message Date
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
emmett1 6f9feaf9b4 updated README.txt 2026-03-16 00:44:07 +08:00
emmett1 fbc3227515 added preview function 2026-03-16 00:36:44 +08:00
emmett1 02a8adadc4 fix when browse into empty directory 2026-03-16 00:21:22 +08:00
emmett1 1e6fdd9e29 add function to run from target directory 2026-03-16 00:14:29 +08:00
emmett1 12c85202f9 fix double slash at directory name 2026-03-16 00:11:24 +08:00
emmett1 7f41df91f1 fix symlink show twice, one as symlink, one as directory 2026-03-15 23:58:18 +08:00
2 changed files with 308 additions and 109 deletions
+94 -51
View File
@@ -1,15 +1,15 @@
sfm - Simple File Manager sfm - Simple File Manager
========================== ==========================
DESCRIPTION DESCRIPTION
----------- -----------
sfm is a lightweight, terminal-based file manager written entirely in sfm is a lightweight, terminal-based file manager written entirely in
POSIX sh. It runs in your terminal with no external dependencies beyond 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 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. fast, flicker-free, and keyboard-driven with a vim-inspired key layout.
REQUIREMENTS REQUIREMENTS
------------ ------------
- A POSIX-compatible shell (sh, dash, bash, etc.) - A POSIX-compatible shell (sh, dash, bash, etc.)
@@ -17,35 +17,42 @@ REQUIREMENTS
- Optional: wl-copy / xclip / xsel / pbcopy (for clipboard support) - Optional: wl-copy / xclip / xsel / pbcopy (for clipboard support)
- Optional: mpv, vlc, feh, zathura, etc. (for smart file opening) - Optional: mpv, vlc, feh, zathura, etc. (for smart file opening)
- Optional: readlink (for symlink display) - Optional: readlink (for symlink display)
- Optional: file (for MIME type detection)
INSTALLATION INSTALLATION
------------ ------------
Using make (recommended): Using make (recommended):
make install make install
This installs sfm to /usr/local/bin by default. To change the prefix: This installs sfm to /usr/local/bin by default. To change the prefix:
make install PREFIX=/usr make install PREFIX=/usr
To uninstall: To uninstall:
make uninstall make uninstall
Manual installation: Manual installation:
cp sfm /usr/local/bin/sfm cp sfm /usr/local/bin/sfm
chmod +x /usr/local/bin/sfm chmod +x /usr/local/bin/sfm
USAGE USAGE
----- -----
sfm sfm [path]
sfm will opens in the current directory. 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 NAVIGATION
---------- ----------
j / k or up/down arrows Move up / down j / k or up/down arrows Move up / down
@@ -55,24 +62,59 @@ NAVIGATION
G Jump to bottom of list G Jump to bottom of list
~ Go to home directory ~ Go to home directory
` (backtick) Jump to previous directory ` (backtick) Jump to previous directory
SEARCH & FILTER SEARCH & FILTER
--------------- ---------------
/ Enter search mode (filters listing as you type) / Enter search mode (filters listing as you type)
esc Clear filter and exit search mode esc Clear filter and exit search mode
enter Exit search mode but keep filter active enter Exit search mode but keep filter active
DISPLAY TOGGLES DISPLAY TOGGLES
--------------- ---------------
. Toggle hidden files (dotfiles) . Toggle hidden files (dotfiles)
T Toggle size/date detail column T Toggle size/date detail column
P Toggle preview pane (right side)
s Cycle sort mode: name -> size -> date s Cycle sort mode: name -> size -> date
i Show file info (permissions, size, date) in status bar i Show file info (permissions, size, date) in status bar
R Refresh current directory listing 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 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 FILE OPERATIONS
--------------- ---------------
r Rename selected entry r Rename selected entry
@@ -82,45 +124,45 @@ FILE OPERATIONS
u Move to trash (safe delete) u Move to trash (safe delete)
U Open trash directory U Open trash directory
o Open with custom program o Open with custom program
CLIPBOARD CLIPBOARD
--------- ---------
y Yank / copy (works on multi-selection) y Yank / copy (works on multi-selection)
x Cut (works on multi-selection) x Cut (works on multi-selection)
p Paste into current directory p Paste into current directory
c Copy full path of selected entry to system clipboard c Copy full path of selected entry to system clipboard
MULTI-SELECT MULTI-SELECT
------------ ------------
space Toggle multi-select on current entry (cursor advances) space Toggle multi-select on current entry (cursor advances)
a Select all / deselect all a Select all / deselect all
BOOKMARKS BOOKMARKS
--------- ---------
b Bookmark current directory (press again to remove) b Bookmark current directory (press again to remove)
B Open bookmark picker (j/k navigate, enter jump) B Open bookmark picker (j/k navigate, enter jump)
Bookmarks are saved to: ~/.config/sfm/bookmarks Bookmarks are saved to: ~/.config/sfm/bookmarks
SHELL & UTILITIES SHELL & UTILITIES
----------------- -----------------
! Drop into $SHELL in current directory (type "exit" to return) ! Drop into $SHELL in current directory (type "exit" to return)
HELP HELP
---- ----
? Show keyboard shortcuts overlay ? Show keyboard shortcuts overlay
QUIT QUIT
---- ----
q Quit sfm q Quit sfm
STATUS BAR INDICATORS STATUS BAR INDICATORS
--------------------- ---------------------
[hidden] Hidden files are visible [hidden] Hidden files are visible
@@ -129,13 +171,13 @@ STATUS BAR INDICATORS
[cut] A file is in the clipboard (cut mode) [cut] A file is in the clipboard (cut mode)
[sel:N] N items are currently selected [sel:N] N items are currently selected
[details] Size/date column is visible [details] Size/date column is visible
SMART FILE OPENER SMART FILE OPENER
----------------- -----------------
When you open a file, sfm detects the file type by extension and When you open a file, sfm detects the file type by extension and
MIME type, then picks an appropriate program automatically: MIME type, then picks an appropriate program automatically:
Text / code $EDITOR (or vi) Text / code $EDITOR (or vi)
Images imv, feh, sxiv, eog, gimp Images imv, feh, sxiv, eog, gimp
Video mpv, vlc, mplayer, totem Video mpv, vlc, mplayer, totem
@@ -143,25 +185,26 @@ MIME type, then picks an appropriate program automatically:
PDF zathura, evince, okular, mupdf PDF zathura, evince, okular, mupdf
Office docs libreoffice Office docs libreoffice
Archives atool / bsdtar (lists contents in pager) Archives atool / bsdtar (lists contents in pager)
Falls back to xdg-open, open (macOS), or $EDITOR if nothing matches. Falls back to xdg-open, open (macOS), or $EDITOR if nothing matches.
Use 'o' to manually specify any program. Use 'o' to manually specify any program.
TRASH TRASH
----- -----
Files deleted with 'u' are moved to: Files deleted with 'u' are moved to:
~/.local/share/sfm-trash/ ~/.local/share/sfm-trash/
Files are prefixed with a timestamp: YYYYMMDD_HHMMSS_filename Files are prefixed with a timestamp: YYYYMMDD_HHMMSS_filename
Use U to browse the trash directory. To restore a file, rename it 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) (press r) to remove the timestamp prefix, then cut (x) and paste (p)
it back to the desired location. it back to the desired location.
DATA FILES DATA FILES
---------- ----------
~/.config/sfm/bookmarks Saved bookmarks (one path per line) ~/.config/sfm/bookmarks Saved bookmarks (one path per line)
~/.config/sfm/opener Custom file opener script (optional)
~/.local/share/sfm-trash/ Trashed files ~/.local/share/sfm-trash/ Trashed files
+214 -58
View File
@@ -1,5 +1,6 @@
#!/bin/sh #!/bin/sh
# sfm - Simple File Manager in POSIX sh # sfm - Simple File Manager in POSIX sh (flicker-free)
# version 0.4
# --- terminal control --- # --- terminal control ---
tput_cmd() { command -v tput >/dev/null 2>&1 && tput "$@"; } tput_cmd() { command -v tput >/dev/null 2>&1 && tput "$@"; }
@@ -61,7 +62,11 @@ term_rows() { tput_cmd lines || echo 24; }
term_cols() { tput_cmd cols || echo 80; } term_cols() { tput_cmd cols || echo 80; }
# --- state --- # --- state ---
CWD="$PWD" if [ -n "$1" ]; then
CWD=$(cd "$1" 2>/dev/null && pwd) || { printf 'sfm: %s: no such directory\n' "$1" >&2; exit 1; }
else
CWD=$(cd "$PWD" 2>/dev/null && pwd) || CWD="/"
fi
SEL=0 SEL=0
OFFSET=0 OFFSET=0
PREV_SEL=0 PREV_SEL=0
@@ -79,9 +84,20 @@ INFO_MSG="" # ephemeral message shown in botbar
SHOW_HIDDEN=0 # 1 = show dotfiles SHOW_HIDDEN=0 # 1 = show dotfiles
SORT_MODE="name" # name | size | date SORT_MODE="name" # name | size | date
SHOW_DETAILS=0 # 1 = show size+date column SHOW_DETAILS=0 # 1 = show size+date column
SHOW_PREVIEW=0 # 1 = show preview pane on right
TRASH_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/sfm-trash" TRASH_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/sfm-trash"
mkdir -p "$TRASH_DIR" mkdir -p "$TRASH_DIR"
PREV_CWD="" # last visited directory, for jumping back PREV_CWD="" # last visited directory, for jumping back
# normalise CWD: resolve to absolute canonical path, no double slashes
norm_cwd() {
CWD=$(cd "$CWD" 2>/dev/null && pwd) || CWD="/"
# squeeze any double slashes (some systems return // for //<path>)
CWD=$(printf '%s' "$CWD" | tr -s '/')
}
# safely join CWD with a name, avoiding double slash at root
joinpath() { [ "$CWD" = "/" ] && printf '/%s' "$1" || printf '%s/%s' "$CWD" "$1"; }
BOOKMARK_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/sfm/bookmarks" BOOKMARK_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/sfm/bookmarks"
mkdir -p "$(dirname "$BOOKMARK_FILE")" mkdir -p "$(dirname "$BOOKMARK_FILE")"
[ -f "$BOOKMARK_FILE" ] || touch "$BOOKMARK_FILE" [ -f "$BOOKMARK_FILE" ] || touch "$BOOKMARK_FILE"
@@ -94,17 +110,19 @@ load_entries() {
# build list of dir names then file names, including dotfiles if enabled # build list of dir names then file names, including dotfiles if enabled
for d in "$CWD"/*/; do for d in "$CWD"/*/; do
[ -d "$d" ] || continue [ -d "$d" ] || continue
[ -L "${d%/}" ] && continue # symlinks handled separately
name="${d%/}"; name="${name##*/}" name="${d%/}"; name="${name##*/}"
[ "$name" = "*" ] && continue [ "$name" = "*" ] && continue
case "$name" in .*) continue ;; esac # skip dotdirs here, handle below case "$name" in .*) continue ;; esac
ENTRIES="$ENTRIES ENTRIES="$ENTRIES
${name}/" ${name}/"
COUNT=$((COUNT + 1)) COUNT=$((COUNT + 1))
done done
if [ "$SHOW_HIDDEN" = "1" ]; then if [ "$SHOW_HIDDEN" = "1" ]; then
for d in "$CWD"/./; do : ; done # no-op, just to avoid error for d in "$CWD"/./; do : ; done
for d in "$CWD"/.*/; do for d in "$CWD"/.*/; do
[ -d "$d" ] || continue [ -d "$d" ] || continue
[ -L "${d%/}" ] && continue # symlinks handled separately
name="${d%/}"; name="${name##*/}" name="${d%/}"; name="${name##*/}"
[ "$name" = "." ] || [ "$name" = ".." ] && continue [ "$name" = "." ] || [ "$name" = ".." ] && continue
[ "$name" = ".*" ] && continue [ "$name" = ".*" ] && continue
@@ -416,6 +434,111 @@ render_row() {
fi fi
} }
draw_preview() {
_rows=$1; _cols=$2; _px=$3; _pw=$4
entry=$(get_entry "$SEL")
# clear preview area first
_r=2
while [ "$_r" -le $((_rows - 1)) ]; do
goto "$_r" "$_px"
printf '\033[K'
_r=$((_r + 1))
done
[ -z "$entry" ] && return
_path=$(joinpath "${entry%/}"); _path="${_path%@}"
# draw vertical divider
_r=2
while [ "$_r" -le $((_rows - 1)) ]; do
goto "$_r" $((_px - 1))
printf '%s|%s' "${CYAN}" "${RESET}"
_r=$((_r + 1))
done
case "$entry" in
*/)
# 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
_tgt=$(readlink "$_path" 2>/dev/null || echo "?")
goto 2 "$_px"
printf '%s[symlink]%s' "${MAGENTA}${BOLD}" "${RESET}"
goto 3 "$_px"
printf '%s-> %s%s' "${WHITE}" "${_tgt}" "${RESET}"
;;
*)
# detect if text via extension or mime
_ext="${entry##*.}"
_ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')
_is_text=0
case "$_ext" in
txt|md|markdown|rst|log|conf|cfg|ini|toml|yaml|yml|\
sh|bash|zsh|py|rb|pl|lua|js|ts|json|xml|html|htm|\
css|c|h|cpp|rs|go|java|php|r|sql|vim|env|gitignore|\
diff|patch|csv|tsv|lock|mod|sum) _is_text=1 ;;
esac
if [ "$_is_text" = "0" ] && command -v file >/dev/null 2>&1; then
case "$(file --mime-type -b "$_path" 2>/dev/null)" in
text/*) _is_text=1 ;;
esac
fi
if [ "$_is_text" = "1" ]; then
_max_lines=$((_rows - 3))
_line_n=0
while IFS= read -r _line && [ "$_line_n" -lt "$_max_lines" ]; do
# truncate to preview width
if [ "${#_line}" -gt "$((_pw - 1))" ]; then
_line=$(printf '%s' "$_line" | cut -c1-$((_pw - 2)))
_line="${_line}~"
fi
goto "$((_line_n + 2))" "$_px"
printf '%s%s%s' "${WHITE}" "$_line" "${RESET}"
_line_n=$((_line_n + 1))
done < "$_path"
if [ "$_line_n" -eq 0 ]; then
goto 2 "$_px"
printf '%s(empty file)%s' "${WHITE}" "${RESET}"
fi
else
# binary / unknown
goto 2 "$_px"
_sz=$(ls -lh "$_path" 2>/dev/null | awk '{print $5}')
printf '%s[binary] %s%s' "${YELLOW}" "${_sz}" "${RESET}"
fi
;;
esac
}
draw_topbar() { draw_topbar() {
_cols=$1 _cols=$1
spaces=$(printf '%*s' "$_cols" '') spaces=$(printf '%*s' "$_cols" '')
@@ -426,7 +549,7 @@ draw_topbar() {
draw_botbar() { draw_botbar() {
_rows=$1; _cols=$2 _rows=$1; _cols=$2
if [ "$SEARCHING" = "1" ]; then if [ "$SEARCHING" = "1" ]; then
info=" $((SEL + 1))/${COUNT} ${CWD}" info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}"
keys=" search: ${FILTER} " keys=" search: ${FILTER} "
maxk=$((_cols - ${#info} - 1)) maxk=$((_cols - ${#info} - 1))
[ "${#keys}" -gt "$maxk" ] && keys=$(printf '%s' "$keys" | cut -c1-"$maxk") [ "${#keys}" -gt "$maxk" ] && keys=$(printf '%s' "$keys" | cut -c1-"$maxk")
@@ -440,9 +563,8 @@ draw_botbar() {
"${YELLOW}${keys}" \ "${YELLOW}${keys}" \
"${RESET}" "${RESET}"
elif [ -n "$INFO_MSG" ]; then elif [ -n "$INFO_MSG" ]; then
info=" $((SEL + 1))/${COUNT} ${CWD}" info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}"
msg=" ${INFO_MSG} " msg=" ${INFO_MSG} " padlen=$((_cols - ${#info} - ${#msg}))
padlen=$((_cols - ${#info} - ${#msg}))
[ "$padlen" -lt 0 ] && padlen=0 [ "$padlen" -lt 0 ] && padlen=0
pad=$(printf '%*s' "$padlen" '') pad=$(printf '%*s' "$padlen" '')
goto "$_rows" 1 goto "$_rows" 1
@@ -461,7 +583,7 @@ draw_botbar() {
[ "$SORT_MODE" != "name" ] && _ind="${_ind} [sort:${SORT_MODE}]" [ "$SORT_MODE" != "name" ] && _ind="${_ind} [sort:${SORT_MODE}]"
[ "$SHOW_DETAILS" = "1" ] && _ind="${_ind} [details]" [ "$SHOW_DETAILS" = "1" ] && _ind="${_ind} [details]"
hint=" press ? for help " hint=" press ? for help "
info=" $((SEL + 1))/${COUNT} ${CWD}${_ind}" info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}${_ind}"
padlen=$((_cols - ${#info} - ${#hint})) padlen=$((_cols - ${#info} - ${#hint}))
[ "$padlen" -lt 0 ] && padlen=0 [ "$padlen" -lt 0 ] && padlen=0
pad=$(printf '%*s' "$padlen" '') pad=$(printf '%*s' "$padlen" '')
@@ -479,6 +601,15 @@ draw() {
COLS=$(term_cols) COLS=$(term_cols)
LIST_ROWS=$((ROWS - 2)) # row 1 = topbar, row ROWS = botbar LIST_ROWS=$((ROWS - 2)) # row 1 = topbar, row ROWS = botbar
# split layout when preview enabled
if [ "$SHOW_PREVIEW" = "1" ]; then
LIST_COLS=$((COLS / 2))
PREV_COL=$((LIST_COLS + 2))
PREV_WIDTH=$((COLS - LIST_COLS - 2))
else
LIST_COLS=$COLS
fi
# clamp selection # clamp selection
[ "$SEL" -lt 0 ] && SEL=0 [ "$SEL" -lt 0 ] && SEL=0
[ "$COUNT" -gt 0 ] && [ "$SEL" -ge "$COUNT" ] && SEL=$((COUNT - 1)) [ "$COUNT" -gt 0 ] && [ "$SEL" -ge "$COUNT" ] && SEL=$((COUNT - 1))
@@ -494,7 +625,6 @@ draw() {
[ "$OFFSET" != "$PREV_OFFSET" ] && NEED_FULL_REDRAW=1 [ "$OFFSET" != "$PREV_OFFSET" ] && NEED_FULL_REDRAW=1
if [ "$NEED_FULL_REDRAW" = "1" ]; then if [ "$NEED_FULL_REDRAW" = "1" ]; then
# --- full repaint: move to top without erasing, overwrite every row ---
printf '\033[H' printf '\033[H'
draw_topbar "$COLS" draw_topbar "$COLS"
@@ -502,22 +632,29 @@ draw() {
idx=$OFFSET idx=$OFFSET
while [ "$row" -le $((LIST_ROWS + 1)) ] && [ "$idx" -lt "$COUNT" ]; do while [ "$row" -le $((LIST_ROWS + 1)) ] && [ "$idx" -lt "$COUNT" ]; do
sel=0; [ "$idx" -eq "$SEL" ] && sel=1 sel=0; [ "$idx" -eq "$SEL" ] && sel=1
render_row "$row" "$idx" "$sel" "$COLS" render_row "$row" "$idx" "$sel" "$LIST_COLS"
row=$((row + 1)) row=$((row + 1))
idx=$((idx + 1)) idx=$((idx + 1))
done done
# blank leftover rows (e.g. after entering a smaller directory) # show (empty) if directory has no entries
if [ "$COUNT" -eq 0 ]; then
goto 2 1
printf '%s (empty)%s%s' "${WHITE}" "${ERASE_LINE}" "${RESET}"
row=3
fi
# blank leftover rows
while [ "$row" -le $((LIST_ROWS + 1)) ]; do while [ "$row" -le $((LIST_ROWS + 1)) ]; do
goto "$row" 1 goto "$row" 1
printf '%s' "${ERASE_LINE}" printf '%s' "${ERASE_LINE}"
row=$((row + 1)) row=$((row + 1))
done done
[ "$SHOW_PREVIEW" = "1" ] && draw_preview "$ROWS" "$COLS" "$PREV_COL" "$PREV_WIDTH"
draw_botbar "$ROWS" "$COLS" draw_botbar "$ROWS" "$COLS"
NEED_FULL_REDRAW=0 NEED_FULL_REDRAW=0
else else
# --- fast path: only touch the two rows whose highlight state changed --- # --- fast path ---
# if nothing moved, skip entirely — no flicker at boundaries
if [ "$SEL" = "$PREV_SEL" ] && [ "$OFFSET" = "$PREV_OFFSET" ]; then if [ "$SEL" = "$PREV_SEL" ] && [ "$OFFSET" = "$PREV_OFFSET" ]; then
return 2>/dev/null || true return 2>/dev/null || true
fi fi
@@ -525,12 +662,14 @@ draw() {
new_row=$(( (SEL - OFFSET) + 2 )) new_row=$(( (SEL - OFFSET) + 2 ))
if [ "$prev_row" -ge 2 ] && [ "$prev_row" -le $((LIST_ROWS + 1)) ]; then if [ "$prev_row" -ge 2 ] && [ "$prev_row" -le $((LIST_ROWS + 1)) ]; then
render_row "$prev_row" "$PREV_SEL" 0 "$COLS" render_row "$prev_row" "$PREV_SEL" 0 "$LIST_COLS"
fi fi
if [ "$new_row" -ge 2 ] && [ "$new_row" -le $((LIST_ROWS + 1)) ]; then if [ "$new_row" -ge 2 ] && [ "$new_row" -le $((LIST_ROWS + 1)) ]; then
render_row "$new_row" "$SEL" 1 "$COLS" render_row "$new_row" "$SEL" 1 "$LIST_COLS"
fi fi
[ "$SHOW_PREVIEW" = "1" ] && draw_preview "$ROWS" "$COLS" "$PREV_COL" "$PREV_WIDTH"
draw_botbar "$ROWS" "$COLS" draw_botbar "$ROWS" "$COLS"
fi fi
@@ -567,6 +706,14 @@ _run_gui() { "$@" >/dev/null 2>&1 & }
open_file() { open_file() {
_f="$1" _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="${_f##*.}"
_ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]') _ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')
@@ -744,7 +891,7 @@ do_help() {
bw=$((COLS - 4)) bw=$((COLS - 4))
[ "$bw" -gt 52 ] && bw=52 [ "$bw" -gt 52 ] && bw=52
[ "$bw" -lt 36 ] && bw=36 [ "$bw" -lt 36 ] && bw=36
bh=41 bh=42
_help_bx=$(( (COLS - bw) / 2 )); [ "$_help_bx" -lt 1 ] && _help_bx=1 _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_by=$(( (ROWS - bh) / 2 )); [ "$_help_by" -lt 1 ] && _help_by=1
_help_iw=$((bw - 2)) _help_iw=$((bw - 2))
@@ -766,32 +913,33 @@ do_help() {
help_row 11 " i file info in status bar" help_row 11 " i file info in status bar"
help_row 12 " s cycle sort: name/size/date" help_row 12 " s cycle sort: name/size/date"
help_row 13 " T toggle size/date details" help_row 13 " T toggle size/date details"
help_sep 14 help_row 14 " P toggle preview pane"
help_row 15 " space toggle multi-select" help_sep 15
help_row 16 " a select all / deselect all" help_row 16 " space toggle multi-select"
help_row 17 " y yank/copy (works on selection)" help_row 17 " a select all / deselect all"
help_row 18 " x cut (works on selection)" help_row 18 " y yank/copy (works on selection)"
help_row 19 " p paste" help_row 19 " x cut (works on selection)"
help_row 20 " d delete (works on selection)" help_row 20 " p paste"
help_sep 21 help_row 21 " d delete (works on selection)"
help_row 22 " r rename R refresh" help_sep 22
help_row 23 " m make directory" help_row 23 " r rename R refresh"
help_row 24 " n new file" help_row 24 " m make directory"
help_row 25 " u trash file (safe delete)" help_row 25 " n new file"
help_row 26 " U open trash directory" help_row 26 " u trash file (safe delete)"
help_row 27 " ! drop to shell in CWD" help_row 27 " U open trash directory"
help_row 28 " o open with custom program" help_row 28 " ! drop to shell in CWD"
help_sep 29 help_row 29 " o open with custom program"
help_row 30 " b bookmark current dir" help_sep 30
help_row 31 " B open bookmark picker" help_row 31 " b bookmark current dir"
help_row 32 " c copy path to clipboard" help_row 32 " B open bookmark picker"
help_row 33 " ~ go to home directory" help_row 33 " c copy path to clipboard"
help_row 34 " \` jump to previous directory" help_row 34 " ~ go to home directory"
help_sep 35 help_row 35 " \` jump to previous directory"
help_row 36 " q quit" help_sep 36
help_row 37 " ? this help" help_row 37 " q quit"
help_row 38 "" help_row 38 " ? this help"
help_row 39 " press any key to close..." help_row 39 ""
help_row 40 " press any key to close..."
goto "$(( _help_by + bh ))" "$_help_bx" goto "$(( _help_by + bh ))" "$_help_bx"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}" printf '%s+%s+%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}"
@@ -856,11 +1004,18 @@ do_toggle_hidden() {
do_toggle_details() { do_toggle_details() {
if [ "$SHOW_DETAILS" = "0" ]; then if [ "$SHOW_DETAILS" = "0" ]; then
SHOW_DETAILS=1 SHOW_DETAILS=1; INFO_MSG="details on"
INFO_MSG="details on"
else else
SHOW_DETAILS=0 SHOW_DETAILS=0; INFO_MSG="details off"
INFO_MSG="details off" fi
NEED_FULL_REDRAW=1
}
do_toggle_preview() {
if [ "$SHOW_PREVIEW" = "0" ]; then
SHOW_PREVIEW=1; INFO_MSG="preview on"
else
SHOW_PREVIEW=0; INFO_MSG="preview off"
fi fi
NEED_FULL_REDRAW=1 NEED_FULL_REDRAW=1
} }
@@ -871,7 +1026,7 @@ do_go_back() {
SELECTED="" SELECTED=""
PREV_CWD="$CWD" PREV_CWD="$CWD"
LAST_CHILD="${CWD##*/}/" # name of current dir with trailing slash LAST_CHILD="${CWD##*/}/" # name of current dir with trailing slash
CWD=$(dirname "$CWD") CWD=$(cd "$(dirname "$CWD")" && pwd)
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
# restore selection to the directory we just came from # restore selection to the directory we just came from
@@ -888,20 +1043,20 @@ do_open() {
*/) */)
FILTER="" FILTER=""
SELECTED="" SELECTED=""
_target="${CWD}/${entry%/}" _target=$(joinpath "${entry%/}")
if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then
INFO_MSG="permission denied: ${entry%/}" INFO_MSG="permission denied: ${entry%/}"
NEED_FULL_REDRAW=1 NEED_FULL_REDRAW=1
return return
fi fi
PREV_CWD="$CWD" PREV_CWD="$CWD"
CWD=$(cd "$_target" && pwd) CWD=$(cd "$_target" && pwd); norm_cwd
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
;; ;;
*@) *@)
_name="${entry%@}" _name="${entry%@}"
_target="${CWD}/${_name}" _target=$(joinpath "$_name")
if [ -d "$_target" ]; then if [ -d "$_target" ]; then
if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then
INFO_MSG="permission denied: ${_name}" INFO_MSG="permission denied: ${_name}"
@@ -910,7 +1065,7 @@ do_open() {
fi fi
FILTER=""; SELECTED="" FILTER=""; SELECTED=""
PREV_CWD="$CWD" PREV_CWD="$CWD"
CWD=$(cd "$_target" && pwd) CWD=$(cd "$_target" && pwd); norm_cwd
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
else else
@@ -1057,7 +1212,7 @@ do_shell() {
printf '%s(type "exit" to return to fm)%s\n' "${YELLOW}" "${RESET}" printf '%s(type "exit" to return to fm)%s\n' "${YELLOW}" "${RESET}"
${SHELL:-sh} ${SHELL:-sh}
# restore CWD in case user cd'd around # restore CWD in case user cd'd around
CWD="${PWD}" CWD="${PWD}"; norm_cwd
setup_term; hide_cursor setup_term; hide_cursor
NEED_FULL_REDRAW=1 NEED_FULL_REDRAW=1
} }
@@ -1093,7 +1248,7 @@ do_trash() {
do_open_trash() { do_open_trash() {
PREV_CWD="$CWD" PREV_CWD="$CWD"
FILTER=""; SELECTED="" FILTER=""; SELECTED=""
CWD="$TRASH_DIR" CWD="$TRASH_DIR"; norm_cwd
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
} }
@@ -1122,7 +1277,7 @@ do_copy_path() {
do_jump_back() { do_jump_back() {
[ -z "$PREV_CWD" ] && { INFO_MSG="no previous directory"; NEED_FULL_REDRAW=1; return; } [ -z "$PREV_CWD" ] && { INFO_MSG="no previous directory"; NEED_FULL_REDRAW=1; return; }
_tmp="$CWD" _tmp="$CWD"
CWD="$PREV_CWD" CWD="$PREV_CWD"; norm_cwd
PREV_CWD="$_tmp" PREV_CWD="$_tmp"
FILTER=""; SELECTED="" FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
@@ -1197,7 +1352,7 @@ do_bookmark_jump() {
'[C') # right — open '[C') # right — open
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE") _chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then
PREV_CWD="$CWD"; CWD="$_chosen" PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
FILTER=""; SELECTED="" FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
@@ -1213,7 +1368,7 @@ do_bookmark_jump() {
l) l)
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE") _chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then
PREV_CWD="$CWD"; CWD="$_chosen" PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
FILTER=""; SELECTED="" FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0 SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
load_entries load_entries
@@ -1364,6 +1519,7 @@ while true; do
/) do_search ;; /) do_search ;;
'.') do_toggle_hidden ;; '.') do_toggle_hidden ;;
T) do_toggle_details ;; T) do_toggle_details ;;
P) do_toggle_preview ;;
i) do_info ;; i) do_info ;;
'!') do_shell ;; '!') do_shell ;;
o) do_open_with ;; o) do_open_with ;;
@@ -1371,7 +1527,7 @@ while true; do
u) do_trash ;; u) do_trash ;;
U) do_open_trash ;; U) do_open_trash ;;
'`') do_jump_back ;; '`') do_jump_back ;;
'~') PREV_CWD="$CWD"; CWD="$HOME"; FILTER=""; SELECTED="" '~') PREV_CWD="$CWD"; CWD=$(cd "$HOME" && pwd); FILTER=""; SELECTED=""
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0; load_entries ;; SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0; load_entries ;;
c) do_copy_path ;; c) do_copy_path ;;
"$(printf '\033')") do_clear_filter ;; "$(printf '\033')") do_clear_filter ;;