Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c2f21d8ec | ||
|
|
938955120b | ||
|
|
f374506215 | ||
|
|
fe57971b91 | ||
|
|
2f8d3e8d9e | ||
|
|
2a1f8fc0a5 | ||
|
|
346ef7b7cb | ||
|
|
09f60a85ef | ||
|
|
32337e4cca | ||
|
|
03df88a150 | ||
|
|
54352d036c | ||
|
|
6b729ffe8a | ||
|
|
25a329aba9 | ||
|
|
6f9feaf9b4 | ||
|
|
fbc3227515 | ||
|
|
02a8adadc4 | ||
|
|
1e6fdd9e29 | ||
|
|
12c85202f9 | ||
|
|
7f41df91f1 |
@@ -15,17 +15,21 @@ all:
|
|||||||
@echo "Run 'make install' to install."
|
@echo "Run 'make install' to install."
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@echo "Installing sfm to $(BINDIR)/sfm ..."
|
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
|
||||||
$(INSTALL) -d $(DESTDIR)$(BINDIR)
|
$(INSTALL) -d $(DESTDIR)$(BINDIR)
|
||||||
$(INSTALL) -m 755 sfm $(DESTDIR)$(BINDIR)/sfm
|
$(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) -d $(DESTDIR)$(DOCDIR)
|
||||||
$(INSTALL) -m 644 README.txt $(DESTDIR)$(DOCDIR)/README.txt
|
$(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README
|
||||||
@echo "Done. Run 'sfm' to start."
|
@echo "Done. Run 'sfm' to start, or 'man sfm' for help."
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@echo "Removing sfm ..."
|
@echo "Removing sfm ..."
|
||||||
$(RM) $(DESTDIR)$(BINDIR)/sfm
|
$(RM) $(DESTDIR)$(BINDIR)/sfm
|
||||||
|
$(RM) $(DESTDIR)$(MANDIR)/sfm.1
|
||||||
$(RM) -r $(DESTDIR)$(DOCDIR)
|
$(RM) -r $(DESTDIR)$(DOCDIR)
|
||||||
@echo "Done."
|
@echo "Done."
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -17,6 +17,7 @@ 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
|
||||||
@@ -41,9 +42,15 @@ Manual installation:
|
|||||||
|
|
||||||
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
|
||||||
@@ -68,11 +75,46 @@ 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
|
||||||
@@ -163,5 +205,6 @@ 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
|
||||||
|
|
||||||
@@ -22,36 +22,15 @@ file_colour() {
|
|||||||
_fc_ext="${_fc_name##*.}"
|
_fc_ext="${_fc_name##*.}"
|
||||||
_fc_ext=$(printf '%s' "$_fc_ext" | tr '[:upper:]' '[:lower:]')
|
_fc_ext=$(printf '%s' "$_fc_ext" | tr '[:upper:]' '[:lower:]')
|
||||||
# executable?
|
# executable?
|
||||||
[ -x "${CWD}/${_fc_name}" ] && { printf '%s' "${RED}${BOLD}"; return; }
|
[ -x "${CWD}/${_fc_name}" ] && { printf '%s' "${GREEN}${BOLD}" ; return; }
|
||||||
case "$_fc_ext" in
|
# node device?
|
||||||
# images
|
[ -c "${CWD}/${_fc_name}" ] && { printf '%s' "${MAGENTA}${BOLD}"; return; }
|
||||||
jpg|jpeg|png|gif|bmp|tiff|tif|webp|svg|ico|heic|heif|avif)
|
# block device?
|
||||||
printf '%s' "${MAGENTA}" ;;
|
[ -b "${CWD}/${_fc_name}" ] && { printf '%s' "${MAGENTA}${BOLD}"; return; }
|
||||||
# video
|
# readable?
|
||||||
mp4|mkv|avi|mov|wmv|flv|webm|m4v|mpeg|mpg|3gp|ogv)
|
[ -r "${CWD}/${_fc_name}" ] || { printf '%s' "${RED}${BOLD}" ; return; }
|
||||||
printf '%s' "${MAGENTA}${BOLD}" ;;
|
# writable?
|
||||||
# audio
|
[ -w "${CWD}/${_fc_name}" ] || { printf '%s' "${RED}${BOLD}" ; return; }
|
||||||
mp3|flac|ogg|wav|aac|m4a|opus|wma|aiff)
|
|
||||||
printf '%s' "${CYAN}${BOLD}" ;;
|
|
||||||
# archives
|
|
||||||
zip|tar|gz|bz2|xz|zst|7z|rar|tgz|tbz2)
|
|
||||||
printf '%s' "${RED}" ;;
|
|
||||||
# documents / text
|
|
||||||
pdf|doc|docx|odt|xls|xlsx|ods|ppt|pptx|odp)
|
|
||||||
printf '%s' "${YELLOW}" ;;
|
|
||||||
# code / scripts
|
|
||||||
sh|bash|zsh|fish|py|rb|pl|lua|js|ts|jsx|tsx|\
|
|
||||||
c|h|cpp|cc|cxx|hpp|rs|go|java|kt|swift|cs|php|r)
|
|
||||||
printf '%s' "${GREEN}" ;;
|
|
||||||
# data / config
|
|
||||||
json|xml|yaml|yml|toml|ini|conf|cfg|env)
|
|
||||||
printf '%s' "${CYAN}" ;;
|
|
||||||
# markdown / text
|
|
||||||
md|markdown|rst|txt|log)
|
|
||||||
printf '%s' "${WHITE}" ;;
|
|
||||||
# default
|
|
||||||
*) printf '%s' "${WHITE}" ;;
|
|
||||||
esac
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goto() { printf '\033[%d;%dH' "$1" "$2"; }
|
goto() { printf '\033[%d;%dH' "$1" "$2"; }
|
||||||
@@ -61,7 +40,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 +62,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"
|
||||||
@@ -91,20 +85,11 @@ load_entries() {
|
|||||||
ENTRIES=""
|
ENTRIES=""
|
||||||
COUNT=0
|
COUNT=0
|
||||||
|
|
||||||
# build list of dir names then file names, including dotfiles if enabled
|
# hidden dirs first
|
||||||
for d in "$CWD"/*/; do
|
|
||||||
[ -d "$d" ] || continue
|
|
||||||
name="${d%/}"; name="${name##*/}"
|
|
||||||
[ "$name" = "*" ] && continue
|
|
||||||
case "$name" in .*) continue ;; esac # skip dotdirs here, handle below
|
|
||||||
ENTRIES="$ENTRIES
|
|
||||||
${name}/"
|
|
||||||
COUNT=$((COUNT + 1))
|
|
||||||
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
|
for d in "$CWD"/.*/; do
|
||||||
[ -d "$d" ] || continue
|
[ -d "$d" ] || continue
|
||||||
|
[ -L "${d%/}" ] && continue
|
||||||
name="${d%/}"; name="${name##*/}"
|
name="${d%/}"; name="${name##*/}"
|
||||||
[ "$name" = "." ] || [ "$name" = ".." ] && continue
|
[ "$name" = "." ] || [ "$name" = ".." ] && continue
|
||||||
[ "$name" = ".*" ] && continue
|
[ "$name" = ".*" ] && continue
|
||||||
@@ -114,21 +99,19 @@ ${name}/"
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for f in "$CWD"/*; do
|
# regular dirs
|
||||||
[ -e "$f" ] || [ -L "$f" ] || continue
|
for d in "$CWD"/*/; do
|
||||||
[ -d "$f" ] && ! [ -L "$f" ] && continue
|
[ -d "$d" ] || continue
|
||||||
name="${f##*/}"
|
[ -L "${d%/}" ] && continue
|
||||||
|
name="${d%/}"; name="${name##*/}"
|
||||||
[ "$name" = "*" ] && continue
|
[ "$name" = "*" ] && continue
|
||||||
case "$name" in .*) continue ;; esac
|
case "$name" in .*) continue ;; esac
|
||||||
if [ -L "$f" ]; then
|
ENTRIES="$ENTRIES
|
||||||
ENTRIES="$ENTRIES
|
${name}/"
|
||||||
${name}@"
|
|
||||||
else
|
|
||||||
ENTRIES="$ENTRIES
|
|
||||||
${name}"
|
|
||||||
fi
|
|
||||||
COUNT=$((COUNT + 1))
|
COUNT=$((COUNT + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# hidden files first
|
||||||
if [ "$SHOW_HIDDEN" = "1" ]; then
|
if [ "$SHOW_HIDDEN" = "1" ]; then
|
||||||
for f in "$CWD"/.*; do
|
for f in "$CWD"/.*; do
|
||||||
[ -e "$f" ] || [ -L "$f" ] || continue
|
[ -e "$f" ] || [ -L "$f" ] || continue
|
||||||
@@ -146,6 +129,23 @@ ${name}"
|
|||||||
done
|
done
|
||||||
fi
|
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#
|
ENTRIES="${ENTRIES#
|
||||||
}"
|
}"
|
||||||
|
|
||||||
@@ -244,40 +244,42 @@ apply_filter() {
|
|||||||
if [ -z "$FILTER" ]; then
|
if [ -z "$FILTER" ]; then
|
||||||
ENTRIES="$ALL_ENTRIES"
|
ENTRIES="$ALL_ENTRIES"
|
||||||
COUNT="$ALL_COUNT"
|
COUNT="$ALL_COUNT"
|
||||||
return
|
else
|
||||||
fi
|
ENTRIES=""
|
||||||
ENTRIES=""
|
COUNT=0
|
||||||
COUNT=0
|
_rest="$ALL_ENTRIES"
|
||||||
printf '%s\n' "$ALL_ENTRIES" | while IFS= read -r line; do
|
while [ -n "$_rest" ]; do
|
||||||
case "$line" in
|
_line="${_rest%%
|
||||||
*"$FILTER"*) printf '%s\n' "$line" ;;
|
*}"; _next="${_rest#*
|
||||||
esac
|
|
||||||
done | {
|
|
||||||
while IFS= read -r line; do
|
|
||||||
ENTRIES="$ENTRIES
|
|
||||||
$line"
|
|
||||||
COUNT=$((COUNT + 1))
|
|
||||||
done
|
|
||||||
# write back via temp — subshell can't modify parent vars directly
|
|
||||||
printf '%s\n' "$COUNT" > /tmp/_fm_count
|
|
||||||
printf '%s' "$ENTRIES" > /tmp/_fm_entries
|
|
||||||
}
|
|
||||||
COUNT=$(cat /tmp/_fm_count 2>/dev/null || echo 0)
|
|
||||||
ENTRIES=$(cat /tmp/_fm_entries 2>/dev/null || echo "")
|
|
||||||
ENTRIES="${ENTRIES#
|
|
||||||
}"
|
}"
|
||||||
|
[ "$_next" = "$_rest" ] && _next=""
|
||||||
|
_rest="$_next"
|
||||||
|
[ -z "$_line" ] && continue
|
||||||
|
case "$_line" in
|
||||||
|
*"$FILTER"*)
|
||||||
|
if [ -z "$ENTRIES" ]; then ENTRIES="$_line"
|
||||||
|
else ENTRIES="$ENTRIES
|
||||||
|
$_line"; fi
|
||||||
|
COUNT=$((COUNT + 1)) ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# write to tmp file for O(1) line access in get_entry
|
||||||
|
printf '%s\n' "$ENTRIES" > /tmp/_sfm_list
|
||||||
}
|
}
|
||||||
|
|
||||||
get_entry() {
|
get_entry() {
|
||||||
printf '%s\n' "$ENTRIES" | awk -v n="$(($1 + 1))" 'NR==n{print; exit}'
|
sed -n "$(($1 + 1))p" /tmp/_sfm_list
|
||||||
}
|
}
|
||||||
|
|
||||||
# find index of entry by name (0-based), returns -1 if not found
|
# find index of entry by name (0-based), returns -1 if not found
|
||||||
find_entry() {
|
find_entry() {
|
||||||
printf '%s\n' "$ENTRIES" | awk -v name="$1" '
|
_fe_n=$(grep -n "^${1}$" /tmp/_sfm_list 2>/dev/null | head -1 | cut -d: -f1)
|
||||||
{ if ($0 == name) { print NR-1; found=1; exit } }
|
if [ -n "$_fe_n" ]; then
|
||||||
END { if (!found) print -1 }
|
printf '%d' $((_fe_n - 1))
|
||||||
'
|
else
|
||||||
|
printf '%d' -1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# is entry name in SELECTED list?
|
# is entry name in SELECTED list?
|
||||||
@@ -341,16 +343,22 @@ render_row() {
|
|||||||
entry=$(get_entry "$_idx")
|
entry=$(get_entry "$_idx")
|
||||||
|
|
||||||
case "$entry" in
|
case "$entry" in
|
||||||
*/) colour="${GREEN}${BOLD}" ;;
|
*/) colour="${BLUE}${BOLD}" ;;
|
||||||
*@)
|
*@)
|
||||||
_name="${entry%@}"
|
_name="${entry%@}"
|
||||||
# broken symlink if target doesn't exist
|
|
||||||
if [ -e "${CWD}/${_name}" ]; then
|
if [ -e "${CWD}/${_name}" ]; then
|
||||||
colour="${MAGENTA}${BOLD}"
|
colour="${CYAN}${BOLD}"
|
||||||
else
|
else
|
||||||
colour="${RED}${BOLD}"
|
colour="${RED}${BOLD}"
|
||||||
fi ;;
|
fi ;;
|
||||||
*) colour=$(file_colour "$entry") ;;
|
*)
|
||||||
|
_fc="${CWD}/${entry}"
|
||||||
|
if [ -x "$_fc" ]; then colour="${GREEN}${BOLD}"
|
||||||
|
elif [ -c "$_fc" ]; then colour="${MAGENTA}${BOLD}"
|
||||||
|
elif [ -b "$_fc" ]; then colour="${MAGENTA}${BOLD}"
|
||||||
|
elif [ ! -r "$_fc" ] || [ ! -w "$_fc" ]; then colour="${RED}${BOLD}"
|
||||||
|
else colour="${WHITE}"
|
||||||
|
fi ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# multi-select marker
|
# multi-select marker
|
||||||
@@ -382,17 +390,21 @@ render_row() {
|
|||||||
if [ "$SHOW_DETAILS" = "1" ]; then
|
if [ "$SHOW_DETAILS" = "1" ]; then
|
||||||
_path="${CWD}/${entry%/}"; _path="${_path%@}"
|
_path="${CWD}/${entry%/}"; _path="${_path%@}"
|
||||||
_info=$(ls -ldh "$_path" 2>/dev/null)
|
_info=$(ls -ldh "$_path" 2>/dev/null)
|
||||||
_sz=$(printf '%s' "$_info" | awk '{print $5}')
|
# parse ls output with parameter expansion — no awk fork
|
||||||
_dt=$(printf '%s' "$_info" | awk '{print $6, $7}')
|
set -- $_info
|
||||||
# fixed width: size=6, date=6 → total detail = 14 chars
|
_sz=$5; _dt="$6 $7"
|
||||||
_detail=$(printf ' %6s %-6s' "$_sz" "$_dt")
|
_detail=$(printf ' %6s %-6s' "$_sz" "$_dt")
|
||||||
|
set --
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# layout: marker(1) + name + spaces + detail
|
# layout: marker(1) + name + spaces + detail
|
||||||
_dlen=${#_detail}
|
_dlen=${#_detail}
|
||||||
maxw=$((_cols - 1 - _dlen - 1))
|
maxw=$((_cols - 2 - _dlen))
|
||||||
if [ "${#display}" -gt "$maxw" ]; then
|
if [ "${#display}" -gt "$maxw" ]; then
|
||||||
display=$(printf '%s' "$display" | cut -c1-$((_cols - _dlen - 4)))
|
_trunc=$((maxw - 3))
|
||||||
|
while [ "${#display}" -gt "$_trunc" ]; do
|
||||||
|
display="${display%?}"
|
||||||
|
done
|
||||||
display="${display}..."
|
display="${display}..."
|
||||||
fi
|
fi
|
||||||
_namew=$((${#display} + 1)) # 1 for marker
|
_namew=$((${#display} + 1)) # 1 for marker
|
||||||
@@ -416,6 +428,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' "${BLUE}${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 +543,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 +557,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 +577,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 +595,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 +619,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 +626,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 +656,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
|
||||||
|
|
||||||
@@ -542,15 +675,21 @@ draw() {
|
|||||||
read_key() {
|
read_key() {
|
||||||
IFS= read -r -n1 key 2>/dev/null || IFS= read -r key
|
IFS= read -r -n1 key 2>/dev/null || IFS= read -r key
|
||||||
if [ "$key" = "$(printf '\033')" ]; then
|
if [ "$key" = "$(printf '\033')" ]; then
|
||||||
# peek at next char with a very short timeout
|
|
||||||
IFS= read -r -n1 -t 0.1 _k2 2>/dev/null || _k2=""
|
IFS= read -r -n1 -t 0.1 _k2 2>/dev/null || _k2=""
|
||||||
if [ -z "$_k2" ]; then
|
if [ -z "$_k2" ]; then
|
||||||
# nothing followed — bare esc
|
|
||||||
printf '\033'; return
|
printf '\033'; return
|
||||||
fi
|
fi
|
||||||
# got a second char — read one more for full sequence (e.g. [A [B [C [D)
|
|
||||||
IFS= read -r -n1 -t 0.1 _k3 2>/dev/null || _k3=""
|
IFS= read -r -n1 -t 0.1 _k3 2>/dev/null || _k3=""
|
||||||
key="${key}${_k2}${_k3}"
|
# if sequence ends with ~, it may have more chars — read one more
|
||||||
|
case "${_k2}${_k3}" in
|
||||||
|
'['[0-9])
|
||||||
|
IFS= read -r -n1 -t 0.1 _k4 2>/dev/null || _k4=""
|
||||||
|
key="${key}${_k2}${_k3}${_k4}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
key="${key}${_k2}${_k3}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
printf '%s' "$key"
|
printf '%s' "$key"
|
||||||
}
|
}
|
||||||
@@ -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=46
|
||||||
_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,37 @@ 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_row 30 " + chmod +x (make executable)"
|
||||||
help_row 31 " B open bookmark picker"
|
help_row 31 " - chmod -x (remove executable)"
|
||||||
help_row 32 " c copy path to clipboard"
|
help_sep 32
|
||||||
help_row 33 " ~ go to home directory"
|
help_row 33 " b bookmark current dir"
|
||||||
help_row 34 " \` jump to previous directory"
|
help_row 34 " B open bookmark picker"
|
||||||
help_sep 35
|
help_row 35 " c copy path to clipboard"
|
||||||
help_row 36 " q quit"
|
help_row 36 " ~ go to home directory"
|
||||||
help_row 37 " ? this help"
|
help_row 37 " \` jump to previous directory"
|
||||||
help_row 38 ""
|
help_row 38 " : jump to path"
|
||||||
help_row 39 " press any key to close..."
|
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"
|
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,22 +1008,164 @@ 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
|
fi
|
||||||
NEED_FULL_REDRAW=1
|
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
|
||||||
|
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() {
|
do_go_back() {
|
||||||
[ "$CWD" = "/" ] && return
|
[ "$CWD" = "/" ] && return
|
||||||
FILTER=""
|
FILTER=""
|
||||||
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 +1182,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 +1204,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
|
||||||
@@ -1035,6 +1329,32 @@ do_mkdir() {
|
|||||||
fi
|
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() {
|
do_info() {
|
||||||
entry=$(get_entry "$SEL")
|
entry=$(get_entry "$SEL")
|
||||||
[ -z "$entry" ] && return
|
[ -z "$entry" ] && return
|
||||||
@@ -1057,7 +1377,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 +1413,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 +1442,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 +1517,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 +1533,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
|
||||||
@@ -1331,7 +1651,7 @@ do_paste() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- main ---
|
# --- main ---
|
||||||
trap 'restore_term; show_cursor; printf "\033[2J\033[H"; exit 0' INT TERM EXIT
|
trap 'restore_term; show_cursor; printf "\033[2J\033[H"; rm -f /tmp/_sfm_list /tmp/_sfm_find; exit 0' INT TERM EXIT
|
||||||
|
|
||||||
setup_term
|
setup_term
|
||||||
hide_cursor
|
hide_cursor
|
||||||
@@ -1351,6 +1671,7 @@ while true; do
|
|||||||
G) SEL=$((COUNT - 1)) ;;
|
G) SEL=$((COUNT - 1)) ;;
|
||||||
"$(printf '\033[6~')") SEL=$((SEL + $(term_rows) / 2)) ;;
|
"$(printf '\033[6~')") SEL=$((SEL + $(term_rows) / 2)) ;;
|
||||||
"$(printf '\033[5~')") SEL=$((SEL - $(term_rows) / 2)) ;;
|
"$(printf '\033[5~')") SEL=$((SEL - $(term_rows) / 2)) ;;
|
||||||
|
"$(printf '\033[3~')") do_delete ;;
|
||||||
"$(printf '\n')"|\
|
"$(printf '\n')"|\
|
||||||
"$(printf '\r')"|\
|
"$(printf '\r')"|\
|
||||||
"$(printf '\033[C')"|\
|
"$(printf '\033[C')"|\
|
||||||
@@ -1364,14 +1685,17 @@ 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_chmod_x ;;
|
||||||
|
'-') do_chmod_nox ;;
|
||||||
o) do_open_with ;;
|
o) do_open_with ;;
|
||||||
s) do_sort ;;
|
s) do_sort ;;
|
||||||
u) do_trash ;;
|
u) do_trash ;;
|
||||||
U) do_open_trash ;;
|
U) do_open_trash ;;
|
||||||
'`') do_jump_back ;;
|
f) do_find ;;
|
||||||
'~') PREV_CWD="$CWD"; CWD="$HOME"; FILTER=""; SELECTED=""
|
':') do_jump_path ;;
|
||||||
|
'~') 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 ;;
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
.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).
|
||||||
|
.TP
|
||||||
|
.B :
|
||||||
|
Prompt for a path to jump to directly. Supports
|
||||||
|
.B ~
|
||||||
|
expansion. Shows an error if the path does not exist. Press esc to cancel.
|
||||||
|
.TP
|
||||||
|
.B f
|
||||||
|
Find files recursively under the current directory. Opens an interactive
|
||||||
|
picker. Press j/k or arrows to navigate results, enter or l to jump to the
|
||||||
|
selected result, esc/q/h to close.
|
||||||
|
|
||||||
|
.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). Hidden entries appear before regular
|
||||||
|
entries (hidden dirs, then regular dirs, then hidden files, then
|
||||||
|
regular files).
|
||||||
|
.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 " or " del
|
||||||
|
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.
|
||||||
|
.TP
|
||||||
|
.B +
|
||||||
|
Make the selected file executable
|
||||||
|
.RB ( "chmod +x" ).
|
||||||
|
.TP
|
||||||
|
.B \-
|
||||||
|
Remove execute permission from the selected file
|
||||||
|
.RB ( "chmod -x" ).
|
||||||
|
|
||||||
|
.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.
|
||||||
|
Auto-renames on conflict by appending
|
||||||
|
.IR _copy .
|
||||||
|
.TP
|
||||||
|
.B c
|
||||||
|
Copy the full path of the 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.
|
||||||
|
.PP
|
||||||
|
Multi-select works with
|
||||||
|
.BR y ", " x ", and " d .
|
||||||
|
|
||||||
|
.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. The working directory is updated if you cd elsewhere
|
||||||
|
in the shell.
|
||||||
|
.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
|
||||||
|
Make it executable with:
|
||||||
|
.B chmod +x ~/.config/sfm/opener
|
||||||
|
.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
|
||||||
|
Emmett1 <me@emmett1.my> with help of AI.
|
||||||
|
|
||||||
|
.SH SEE ALSO
|
||||||
|
.BR find (1),
|
||||||
|
.BR ls (1),
|
||||||
|
.BR mv (1),
|
||||||
|
.BR cp (1),
|
||||||
|
.BR rm (1),
|
||||||
|
.BR chmod (1),
|
||||||
|
.BR vi (1)
|
||||||
Reference in New Issue
Block a user