7 Commits
Author SHA1 Message Date
emmett1 40e5d2815c added cpp version, sh version as backup 2026-06-16 07:43:51 +08:00
emmett1 0504dec9c7 info mode now shows info box 2026-05-31 00:08:51 +08:00
emmett1 f85d1af121 fix new directory mode text 2026-05-31 00:00:48 +08:00
emmett1 308637d874 fix new file mode text 2026-05-30 23:59:28 +08:00
emmett1 aaf7fd829d improve rename mode 2026-05-30 23:57:36 +08:00
emmett1 8db9a070b7 yes no box for file deletion confirmation 2026-05-30 23:51:26 +08:00
emmett1 bed888c3c3 fix and updates 2026-05-29 11:32:58 +08:00
5 changed files with 2684 additions and 138 deletions
+2
View File
@@ -0,0 +1,2 @@
CLAUDE.md
sfm
+15 -6
View File
@@ -5,33 +5,42 @@ BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man/man1 MANDIR ?= $(PREFIX)/share/man/man1
DOCDIR ?= $(PREFIX)/share/doc/sfm DOCDIR ?= $(PREFIX)/share/doc/sfm
CXX ?= g++
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra
LDFLAGS ?= -lncursesw
INSTALL ?= install INSTALL ?= install
RM ?= rm -f RM ?= rm -f
.PHONY: all install uninstall clean .PHONY: all install uninstall clean
all: all: sfm
@echo "sfm is a shell script — nothing to build."
@echo "Run 'make install' to install."
install: sfm: sfm.cpp
$(CXX) $(CXXFLAGS) -o $@ $< $(LDFLAGS)
install: all
@echo "Installing sfm to $(DESTDIR)$(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 sfm.sh to $(DESTDIR)$(BINDIR)/sfm.sh ..."
$(INSTALL) -m 755 sfm.sh $(DESTDIR)$(BINDIR)/sfm.sh
@echo "Installing man page to $(DESTDIR)$(MANDIR)/sfm.1 ..." @echo "Installing man page to $(DESTDIR)$(MANDIR)/sfm.1 ..."
$(INSTALL) -d $(DESTDIR)$(MANDIR) $(INSTALL) -d $(DESTDIR)$(MANDIR)
$(INSTALL) -m 644 sfm.1 $(DESTDIR)$(MANDIR)/sfm.1 $(INSTALL) -m 644 sfm.1 $(DESTDIR)$(MANDIR)/sfm.1
@echo "Installing README to $(DESTDIR)$(DOCDIR)/README ..." @echo "Installing README to $(DESTDIR)$(DOCDIR)/README ..."
$(INSTALL) -d $(DESTDIR)$(DOCDIR) $(INSTALL) -d $(DESTDIR)$(DOCDIR)
$(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README $(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README
@echo "Done. Run 'sfm' to start, or 'man sfm' for help." @echo "Done."
uninstall: uninstall:
@echo "Removing sfm ..." @echo "Removing sfm ..."
$(RM) $(DESTDIR)$(BINDIR)/sfm $(RM) $(DESTDIR)$(BINDIR)/sfm
$(RM) $(DESTDIR)$(BINDIR)/sfm.sh
$(RM) $(DESTDIR)$(MANDIR)/sfm.1 $(RM) $(DESTDIR)$(MANDIR)/sfm.1
$(RM) -r $(DESTDIR)$(DOCDIR) $(RM) -r $(DESTDIR)$(DOCDIR)
@echo "Done." @echo "Done."
clean: clean:
@echo "Nothing to clean." $(RM) sfm
@echo "Cleaned."
+24 -15
View File
@@ -4,16 +4,23 @@ 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 with a vim-inspired
POSIX sh. It runs in your terminal with no external dependencies beyond key layout. The primary version is a fast C++/ncurses binary. A portable
standard Unix tools (ls, awk, tput, stty, mv, cp, rm). Designed to be POSIX sh version (sfm.sh) is also included, requiring only standard Unix
fast, flicker-free, and keyboard-driven with a vim-inspired key layout. tools (ls, awk, tput, stty, mv, cp, rm).
REQUIREMENTS REQUIREMENTS
------------ ------------
C++ version (sfm):
- ncursesw (libncursesw)
- A C++17 compiler (g++ or compatible)
Shell version (sfm.sh):
- A POSIX-compatible shell (sh, dash, bash, etc.) - A POSIX-compatible shell (sh, dash, bash, etc.)
- Standard Unix tools: ls, awk, tput, stty, cp, mv, rm, mkdir, touch - Standard Unix tools: ls, awk, tput, stty, cp, mv, rm, mkdir, touch
Both versions:
- 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)
@@ -23,21 +30,23 @@ REQUIREMENTS
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 builds the C++ binary and installs both sfm (C++ binary)
and sfm.sh (shell script) 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 (shell version only):
cp sfm /usr/local/bin/sfm cp sfm.sh /usr/local/bin/sfm.sh
chmod +x /usr/local/bin/sfm chmod +x /usr/local/bin/sfm.sh
USAGE USAGE
+2374
View File
File diff suppressed because it is too large Load Diff
+269 -117
View File
@@ -16,23 +16,6 @@ MAGENTA=$(tput_cmd setaf 5)
BLUE=$(tput_cmd setaf 4) BLUE=$(tput_cmd setaf 4)
ERASE_LINE=$(tput_cmd el || printf '\033[K') ERASE_LINE=$(tput_cmd el || printf '\033[K')
# map a filename to a display colour based on extension
file_colour() {
_fc_name="$1"
_fc_ext="${_fc_name##*.}"
_fc_ext=$(printf '%s' "$_fc_ext" | tr '[:upper:]' '[:lower:]')
# executable?
[ -x "${CWD}/${_fc_name}" ] && { printf '%s' "${GREEN}${BOLD}" ; return; }
# node device?
[ -c "${CWD}/${_fc_name}" ] && { printf '%s' "${MAGENTA}${BOLD}"; return; }
# block device?
[ -b "${CWD}/${_fc_name}" ] && { printf '%s' "${MAGENTA}${BOLD}"; return; }
# readable?
[ -r "${CWD}/${_fc_name}" ] || { printf '%s' "${RED}${BOLD}" ; return; }
# writable?
[ -w "${CWD}/${_fc_name}" ] || { printf '%s' "${RED}${BOLD}" ; return; }
}
goto() { printf '\033[%d;%dH' "$1" "$2"; } goto() { printf '\033[%d;%dH' "$1" "$2"; }
hide_cursor() { printf '\033[?25l'; } hide_cursor() { printf '\033[?25l'; }
show_cursor() { printf '\033[?25h'; } show_cursor() { printf '\033[?25h'; }
@@ -174,8 +157,8 @@ $_line"; fi ;;
# sort files using ls into a tmp file # sort files using ls into a tmp file
if [ -n "$_files" ]; then if [ -n "$_files" ]; then
case "$SORT_MODE" in case "$SORT_MODE" in
size) ls -1Sp "$CWD" 2>/dev/null | grep -v '/' > /tmp/_fm_sorted ;; size) ls -1Sp "$CWD" 2>/dev/null | grep -v '/' > /tmp/_fm_sorted_$$ ;;
date) ls -1tp "$CWD" 2>/dev/null | grep -v '/' > /tmp/_fm_sorted ;; date) ls -1tp "$CWD" 2>/dev/null | grep -v '/' > /tmp/_fm_sorted_$$ ;;
esac esac
# only keep files that were already in our list (respects hidden filter) # only keep files that were already in our list (respects hidden filter)
_sorted="" _sorted=""
@@ -190,7 +173,7 @@ ${_n}
else _sorted="$_sorted else _sorted="$_sorted
$_n"; fi ;; $_n"; fi ;;
esac esac
done < /tmp/_fm_sorted done < /tmp/_fm_sorted_$$
_files="$_sorted" _files="$_sorted"
fi fi
@@ -265,16 +248,16 @@ $_line"; fi
done done
fi fi
# write to tmp file for O(1) line access in get_entry # write to tmp file for O(1) line access in get_entry
printf '%s\n' "$ENTRIES" > /tmp/_sfm_list printf '%s\n' "$ENTRIES" > /tmp/_sfm_list_$$
} }
get_entry() { get_entry() {
sed -n "$(($1 + 1))p" /tmp/_sfm_list 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() {
_fe_n=$(grep -n "^${1}$" /tmp/_sfm_list 2>/dev/null | head -1 | cut -d: -f1) _fe_n=$(grep -nFx "$1" /tmp/_sfm_list_$$ 2>/dev/null | head -1 | cut -d: -f1)
if [ -n "$_fe_n" ]; then if [ -n "$_fe_n" ]; then
printf '%d' $((_fe_n - 1)) printf '%d' $((_fe_n - 1))
else else
@@ -284,12 +267,7 @@ find_entry() {
# is entry name in SELECTED list? # is entry name in SELECTED list?
is_selected() { is_selected() {
case " printf '%s\n' "$SELECTED" | grep -Fxq "$1"
${SELECTED}
" in *"
$1
"*) return 0 ;; esac
return 1
} }
count_selected() { count_selected() {
@@ -799,8 +777,10 @@ open_file() {
# read a line of input in raw mode, returns result in READ_LINE # read a line of input in raw mode, returns result in READ_LINE
# returns 1 if user pressed Esc (cancel), 0 on Enter # returns 1 if user pressed Esc (cancel), 0 on Enter
read_line() { read_line() {
READ_LINE="" READ_LINE="${_init:-}"
_init=""
show_cursor show_cursor
printf '%s' "$READ_LINE"
while true; do while true; do
IFS= read -r -n1 _ch 2>/dev/null || IFS= read -r _ch IFS= read -r -n1 _ch 2>/dev/null || IFS= read -r _ch
case "$_ch" in case "$_ch" in
@@ -855,7 +835,7 @@ do_open_with() {
do_newfile() { do_newfile() {
goto "$(term_rows)" 1 goto "$(term_rows)" 1
printf '%s%s New file name (esc=cancel): %s' "${ERASE_LINE}" "${WHITE}${BOLD}" "${RESET}" printf '%s%s File name: %s' "${ERASE_LINE}" "${WHITE}${BOLD}" "${RESET}"
if read_line && [ -n "$READ_LINE" ]; then if read_line && [ -n "$READ_LINE" ]; then
touch "${CWD}/${READ_LINE}" touch "${CWD}/${READ_LINE}"
load_entries load_entries
@@ -1033,7 +1013,7 @@ do_find() {
_query="$READ_LINE" _query="$READ_LINE"
# run find and collect results into a tmp file # run find and collect results into a tmp file
_tmp=/tmp/_sfm_find _tmp=/tmp/_sfm_find_$$
find "$CWD" -name "*${_query}*" 2>/dev/null | sort > "$_tmp" find "$CWD" -name "*${_query}*" 2>/dev/null | sort > "$_tmp"
_total=$(wc -l < "$_tmp" | tr -d '[:space:]') _total=$(wc -l < "$_tmp" | tr -d '[:space:]')
@@ -1236,82 +1216,149 @@ do_open() {
esac esac
} }
# --- confirm overlay (yes/no popup) ---
confirm_overlay() {
_co_rows=$(term_rows); _co_cols=$(term_cols)
_co_yes=" Yes "
_co_no=" No "
_co_gap=" "
_co_content="${_co_yes}${_co_gap}${_co_no}"
# size box to fit prompt or buttons, whichever is wider
_co_w=$(( ${#_prompt} + 4 ))
[ "$_co_w" -lt $(( ${#_co_content} + 4 )) ] && _co_w=$(( ${#_co_content} + 4 ))
[ "$_co_w" -gt "$_co_cols" ] && _co_w=$((_co_cols - 2))
_co_iw=$((_co_w - 2))
_co_hl=$(printf '%*s' "$_co_iw" '' | tr ' ' '-')
_co_x=$(( (_co_cols - _co_w) / 2 )); [ "$_co_x" -lt 0 ] && _co_x=0
_co_y=$(( (_co_rows - 5) / 2 )); [ "$_co_y" -lt 1 ] && _co_y=1
_co_sel=2 # 1=Yes, 2=No (default No for safety)
while true; do
# draw box top
goto "$_co_y" "$_co_x"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_co_hl" "${RESET}"
# prompt line
goto "$((_co_y+1))" "$_co_x"
_co_pl=$((_co_iw - ${#_prompt}))
[ "$_co_pl" -lt 0 ] && _co_pl=0
_co_pad=$(printf '%*s' "$_co_pl" '')
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_prompt" "$_co_pad" "${BOLD}${CYAN}" "${RESET}"
# separator
goto "$((_co_y+2))" "$_co_x"
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_co_hl" "${RESET}"
# yes/no buttons
goto "$((_co_y+3))" "$_co_x"
_co_cpad=$(( (_co_iw - ${#_co_content}) / 2 ))
[ "$_co_cpad" -lt 0 ] && _co_cpad=0
_co_left=$(printf '%*s' "$_co_cpad" '')
_co_cpad=$(( _co_iw - ${#_co_content} - _co_cpad ))
[ "$_co_cpad" -lt 0 ] && _co_cpad=0
_co_right=$(printf '%*s' "$_co_cpad" '')
printf '%s|%s%s' "${BOLD}${CYAN}" "${RESET}" "$_co_left"
if [ "$_co_sel" -eq 1 ]; then
printf '%s%s%s' "${REV}${GREEN}${BOLD}" "$_co_yes" "${RESET}"
else
printf '%s' "$_co_yes"
fi
printf '%s' "$_co_gap"
if [ "$_co_sel" -eq 2 ]; then
printf '%s%s%s' "${REV}${RED}${BOLD}" "$_co_no" "${RESET}"
else
printf '%s' "$_co_no"
fi
printf '%s%s|%s' "$_co_right" "${BOLD}${CYAN}" "${RESET}"
# box bottom
goto "$((_co_y+4))" "$_co_x"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_co_hl" "${RESET}"
# read key
IFS= read -r -n1 _co_ch 2>/dev/null || IFS= read -r _co_ch
case "$_co_ch" in
"$(printf '\033')")
IFS= read -r -n2 -t 0.1 _co_seq 2>/dev/null || _co_seq=""
case "$_co_seq" in
'[D'|'[C') # left/right arrow
if [ "$_co_sel" -eq 1 ]; then _co_sel=2; else _co_sel=1; fi ;;
*) return 1 ;; # bare esc or unrecognised sequence
esac ;;
h) _co_sel=1 ;;
l) _co_sel=2 ;;
j) _co_sel=2 ;;
k) _co_sel=1 ;;
y|Y) return 0 ;;
n|N) return 1 ;;
"$(printf '\n')"|"$(printf '\r')")
if [ "$_co_sel" -eq 1 ]; then return 0; else return 1; fi ;;
esac
done
}
do_delete() { do_delete() {
INFO_MSG="" INFO_MSG=""
_c=$(count_selected) _c=$(count_selected)
if [ "$_c" -gt 0 ]; then if [ "$_c" -gt 0 ]; then
# --- bulk delete selected --- # --- bulk delete selected ---
restore_term; show_cursor _prompt="Delete ${_c} selected items?"
goto "$(term_rows)" 1 confirm_overlay; _yes=$?
printf '%s%s Delete %d selected items? [y/N] %s' \ if [ "$_yes" -eq 0 ]; then
"${ERASE_LINE}" "${RED}${BOLD}" "$_c" "${RESET}" _rest="$SELECTED"
IFS= read -r ans while [ -n "$_rest" ]; do
setup_term; hide_cursor _line="${_rest%%
case "$ans" in
y|Y)
_rest="$SELECTED"
while [ -n "$_rest" ]; do
_line="${_rest%%
*}" *}"
_next="${_rest#* _next="${_rest#*
}" }"
[ "$_next" = "$_rest" ] && _next="" [ "$_next" = "$_rest" ] && _next=""
_rest="$_next" _rest="$_next"
[ -z "$_line" ] && continue [ -z "$_line" ] && continue
_t="${CWD}/${_line%/}" _name="${_line%@}"; _name="${_name%/}"
if [ -d "$_t" ]; then rm -rf "$_t"; else rm -f "$_t"; fi _t="${CWD}/${_name}"
done if [ -d "$_t" ]; then rm -rf "$_t"; else rm -f "$_t"; fi
SELECTED="" done
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2)) SELECTED=""
[ "$SEL" -lt 0 ] && SEL=0 [ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
load_entries [ "$SEL" -lt 0 ] && SEL=0
;; load_entries
*) NEED_FULL_REDRAW=1 ;; else
esac NEED_FULL_REDRAW=1
fi
else else
# --- single delete --- # --- single delete ---
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
target="${CWD}/${entry%/}" _name="${entry%@}"; _name="${_name%/}"
restore_term; show_cursor target="${CWD}/${_name}"
goto "$(term_rows)" 1 _prompt="Delete \"${entry}\"?"
printf '%s%s Delete "%s"? [y/N] %s' "${ERASE_LINE}" "${RED}${BOLD}" "$entry" "${RESET}" confirm_overlay; _yes=$?
IFS= read -r ans if [ "$_yes" -ne 0 ]; then
setup_term; hide_cursor NEED_FULL_REDRAW=1; return
case "$ans" in fi
y|Y) if [ -d "$target" ]; then
if [ -d "$target" ]; then if [ -n "$(ls -A "$target" 2>/dev/null)" ]; then
if [ -n "$(ls -A "$target" 2>/dev/null)" ]; then _prompt="\"${entry}\" not empty. Delete ALL?"
restore_term; show_cursor confirm_overlay; _yes2=$?
goto "$(term_rows)" 1 if [ "$_yes2" -ne 0 ]; then
printf '%s%s "%s" not empty. Delete ALL? [y/N] %s' \ NEED_FULL_REDRAW=1; return
"${ERASE_LINE}" "${RED}${BOLD}" "$entry" "${RESET}"
IFS= read -r ans2
setup_term; hide_cursor
case "$ans2" in
y|Y) rm -rf "$target" ;;
*) NEED_FULL_REDRAW=1; return ;;
esac
else
rm -rf "$target"
fi
else
rm -f "$target"
fi fi
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2)) fi
[ "$SEL" -lt 0 ] && SEL=0 rm -rf "$target"
load_entries else
;; rm -f "$target"
*) NEED_FULL_REDRAW=1 ;; fi
esac [ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
[ "$SEL" -lt 0 ] && SEL=0
load_entries
fi fi
} }
do_rename() { do_rename() {
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
_name="${entry%@}"; _name="${_name%/}"
goto "$(term_rows)" 1 goto "$(term_rows)" 1
printf '%s%s Rename "%s" to (esc=cancel): %s' "${ERASE_LINE}" "${YELLOW}${BOLD}" "$entry" "${RESET}" printf '%s%s Rename: %s' "${ERASE_LINE}" "${YELLOW}${BOLD}" "${RESET}"
if read_line && [ -n "$READ_LINE" ] && [ "$READ_LINE" != "$entry" ]; then _init="$_name"
mv "${CWD}/${entry%/}" "${CWD}/${READ_LINE}" if read_line && [ -n "$READ_LINE" ] && [ "$READ_LINE" != "$_name" ]; then
mv "${CWD}/${_name}" "${CWD}/${READ_LINE}"
load_entries load_entries
else else
NEED_FULL_REDRAW=1; draw NEED_FULL_REDRAW=1; draw
@@ -1320,7 +1367,7 @@ do_rename() {
do_mkdir() { do_mkdir() {
goto "$(term_rows)" 1 goto "$(term_rows)" 1
printf '%s%s New dir name (esc=cancel): %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}" printf '%s%s Directory name: %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}"
if read_line && [ -n "$READ_LINE" ]; then if read_line && [ -n "$READ_LINE" ]; then
mkdir -p "${CWD}/${READ_LINE}" mkdir -p "${CWD}/${READ_LINE}"
load_entries load_entries
@@ -1358,15 +1405,96 @@ do_chmod_nox() {
do_info() { do_info() {
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
[ -z "$entry" ] && return [ -z "$entry" ] && return
target="${CWD}/${entry%/}" _name="${entry%@}"; _name="${_name%/}"
# permissions + type target="${CWD}/${_name}"
_perm=$(ls -ld "$target" 2>/dev/null | awk '{print $1}')
# size (human readable via du, fallback to ls) # gather info
_size=$(du -sh "$target" 2>/dev/null | cut -f1) _info_perm=$(ls -ld "$target" 2>/dev/null | awk '{print $1}')
[ -z "$_size" ] && _size=$(ls -lh "$target" 2>/dev/null | awk '{print $5}') _info_owner=$(ls -ld "$target" 2>/dev/null | awk '{print $3}')
# modification date _info_group=$(ls -ld "$target" 2>/dev/null | awk '{print $4}')
_date=$(ls -ld "$target" 2>/dev/null | awk '{print $6, $7, $8}') _info_size=$(du -sh "$target" 2>/dev/null | cut -f1)
INFO_MSG="${_perm} ${_size} ${_date}" [ -z "$_info_size" ] && _info_size=$(ls -lh "$target" 2>/dev/null | awk '{print $5}')
_info_date=$(ls -ld "$target" 2>/dev/null | awk '{print $6, $7, $8}')
case "$entry" in
*/) _info_type="directory" ;;
*@) _info_type="symlink -> $(ls -ld "$target" 2>/dev/null | awk '{print $NF}')" ;;
*) _info_type="file"
if [ -x "$target" ]; then _info_type="executable"; fi ;;
esac
_info_lines=""
_info_lines="${_info_lines}Name: ${_name}
"
_info_lines="${_info_lines}Type: ${_info_type}
"
_info_lines="${_info_lines}Perm: ${_info_perm}
"
_info_lines="${_info_lines}Owner: ${_info_owner}:${_info_group}
"
_info_lines="${_info_lines}Size: ${_info_size}
"
_info_lines="${_info_lines}Mod: ${_info_date}
"
# measure longest line
_maxw=0
_info_rest="$_info_lines"
while [ -n "$_info_rest" ]; do
_info_l="${_info_rest%%
*}"
[ "${#_info_l}" -gt "$_maxw" ] && _maxw=${#_info_l}
_info_next="${_info_rest#*
}"
[ "$_info_next" = "$_info_rest" ] && break
_info_rest="$_info_next"
done
_info_h=8 # 6 data rows + top + bottom
ROWS=$(term_rows); COLS=$(term_cols)
_iw=$((_maxw + 2)); [ "$_iw" -lt 20 ] && _iw=20
_bw=$((_iw + 2)); [ "$_bw" -gt "$COLS" ] && _bw=$COLS && _iw=$((_bw - 2))
_ix=$(( (COLS - _bw) / 2 )); [ "$_ix" -lt 0 ] && _ix=0
_iy=$(( (ROWS - _info_h) / 2 )); [ "$_iy" -lt 1 ] && _iy=1
_hl=$(printf '%*s' "$_iw" '' | tr ' ' '-')
# draw
goto "$_iy" "$_ix"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
goto "$((_iy+1))" "$_ix"
_title=" FILE INFO"
_tpad=$((_iw - ${#_title}))
[ "$_tpad" -lt 0 ] && _tpad=0
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_title" "$(printf '%*s' "$_tpad" '')" "${BOLD}${CYAN}" "${RESET}"
goto "$((_iy+2))" "$_ix"
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
_ri=3
_info_rest="$_info_lines"
while [ -n "$_info_rest" ]; do
_info_l="${_info_rest%%
*}"
_info_next="${_info_rest#*
}"
[ "$_info_next" = "$_info_rest" ] && break
_info_rest="$_info_next"
[ -z "$_info_l" ] && continue
goto "$((_iy+_ri))" "$_ix"
_lpad=$((_iw - ${#_info_l}))
[ "$_lpad" -lt 0 ] && _lpad=0
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_info_l" "$(printf '%*s' "$_lpad" '')" "${BOLD}${CYAN}" "${RESET}"
_ri=$((_ri + 1))
done
goto "$((_iy+_ri))" "$_ix"
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
# wait for key
IFS= read -r -n1 _infok 2>/dev/null || IFS= read -r _infok
case "$_infok" in
"$(printf '\033')")
IFS= read -r -n5 -t 0.1 _infoseq 2>/dev/null || _infoseq="" ;;
esac
NEED_FULL_REDRAW=1 NEED_FULL_REDRAW=1
} }
@@ -1396,9 +1524,10 @@ do_sort() {
do_trash() { do_trash() {
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
[ -z "$entry" ] && return [ -z "$entry" ] && return
target="${CWD}/${entry%/}" _name="${entry%@}"; _name="${_name%/}"
target="${CWD}/${_name}"
_ts=$(date '+%Y%m%d_%H%M%S' 2>/dev/null || date '+%s') _ts=$(date '+%Y%m%d_%H%M%S' 2>/dev/null || date '+%s')
_dest="${TRASH_DIR}/${_ts}_${entry%/}" _dest="${TRASH_DIR}/${_ts}_${_name}"
if mv "$target" "$_dest"; then if mv "$target" "$_dest"; then
INFO_MSG="trashed: ${entry}" INFO_MSG="trashed: ${entry}"
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2)) [ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
@@ -1421,7 +1550,8 @@ do_open_trash() {
do_copy_path() { do_copy_path() {
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
[ -z "$entry" ] && return [ -z "$entry" ] && return
_path="${CWD}/${entry%/}" _name="${entry%@}"; _name="${_name%/}"
_path="${CWD}/${_name}"
if command -v wl-copy >/dev/null 2>&1; then if command -v wl-copy >/dev/null 2>&1; then
printf '%s' "$_path" | wl-copy printf '%s' "$_path" | wl-copy
elif command -v xclip >/dev/null 2>&1; then elif command -v xclip >/dev/null 2>&1; then
@@ -1479,6 +1609,8 @@ do_bookmark_jump() {
_hl=$(printf '%*s' "$_iw" '' | tr ' ' '-') _hl=$(printf '%*s' "$_iw" '' | tr ' ' '-')
_bsel=1 _bsel=1
_bvis=$((_ph - 3))
_boff=1
while true; do while true; do
# draw picker inline # draw picker inline
goto "$_py" "$_px" goto "$_py" "$_px"
@@ -1488,11 +1620,23 @@ do_bookmark_jump() {
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_t" "$_p" "${BOLD}${CYAN}" "${RESET}" printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_t" "$_p" "${BOLD}${CYAN}" "${RESET}"
goto "$((_py+2))" "$_px" goto "$((_py+2))" "$_px"
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_hl" "${RESET}" printf '%s|%s|%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
# clear visible rows first
_bri=0
while [ "$_bri" -lt "$_bvis" ]; do
goto "$((_py + 3 + _bri))" "$_px"
_bep=$(printf '%*s' "$_iw" '')
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_bep" "${RESET}"
_bri=$((_bri + 1))
done
# draw visible bookmarks
_bi=0 _bi=0
while IFS= read -r _bm; do while IFS= read -r _bm; do
[ -z "$_bm" ] && continue [ -z "$_bm" ] && continue
_bi=$((_bi+1)) _bi=$((_bi+1))
goto "$((_py+2+_bi))" "$_px" [ "$_bi" -lt "$_boff" ] && continue
[ "$_bi" -ge $((_boff + _bvis)) ] && continue
_drow=$((_py + 2 + _bi - _boff + 1))
goto "$_drow" "$_px"
_bt="${_bi} ${_bm}" _bt="${_bi} ${_bm}"
if [ "${#_bt}" -gt "$_iw" ]; then _bt=$(printf '%s' "$_bt" | cut -c1-$((_iw-1))); _bt="${_bt}~"; fi if [ "${#_bt}" -gt "$_iw" ]; then _bt=$(printf '%s' "$_bt" | cut -c1-$((_iw-1))); _bt="${_bt}~"; fi
_bpl=$((_iw - ${#_bt})); _bp=$(printf '%*s' "$_bpl" '') _bpl=$((_iw - ${#_bt})); _bp=$(printf '%*s' "$_bpl" '')
@@ -1506,14 +1650,18 @@ do_bookmark_jump() {
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}" printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
IFS= read -r -n1 _bk 2>/dev/null || IFS= read -r _bk IFS= read -r -n1 _bk 2>/dev/null || IFS= read -r _bk
case "$_bk" in case "$_bk" in
j) _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc ;; j) _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc
k) _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1 ;; [ "$_bsel" -ge $((_boff + _bvis)) ] && _boff=$((_bsel - _bvis + 1)) ;;
k) _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1
[ "$_bsel" -lt "$_boff" ] && _boff=$_bsel ;;
"$(printf '\033')") "$(printf '\033')")
# use timeout to distinguish bare esc from arrow sequences # use timeout to distinguish bare esc from arrow sequences
IFS= read -r -n2 -t 0.1 _seq 2>/dev/null || _seq="" IFS= read -r -n2 -t 0.1 _seq 2>/dev/null || _seq=""
case "$_seq" in case "$_seq" in
'[A') _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1 ;; '[A') _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1
'[B') _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc ;; [ "$_bsel" -lt "$_boff" ] && _boff=$_bsel ;;
'[B') _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc
[ "$_bsel" -ge $((_boff + _bvis)) ] && _boff=$((_bsel - _bvis + 1)) ;;
'[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
@@ -1589,7 +1737,8 @@ do_yank() {
if [ "$_c" -gt 0 ]; then if [ "$_c" -gt 0 ]; then
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
[ -z "$_e" ] && continue [ -z "$_e" ] && continue
printf '%s\n' "${CWD}/${_e%/}" _yname="${_e%@}"; _yname="${_yname%/}"
printf '%s\n' "${CWD}/${_yname}"
done) done)
CLIP_MODE="copy" CLIP_MODE="copy"
INFO_MSG="yanked ${_c} items" INFO_MSG="yanked ${_c} items"
@@ -1597,7 +1746,8 @@ do_yank() {
else else
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
[ -z "$entry" ] && return [ -z "$entry" ] && return
CLIPBOARD="${CWD}/${entry%/}" _yname="${entry%@}"; _yname="${_yname%/}"
CLIPBOARD="${CWD}/${_yname}"
CLIP_MODE="copy" CLIP_MODE="copy"
INFO_MSG="yanked: ${entry}" INFO_MSG="yanked: ${entry}"
fi fi
@@ -1609,7 +1759,8 @@ do_cut() {
if [ "$_c" -gt 0 ]; then if [ "$_c" -gt 0 ]; then
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
[ -z "$_e" ] && continue [ -z "$_e" ] && continue
printf '%s\n' "${CWD}/${_e%/}" _cname="${_e%@}"; _cname="${_cname%/}"
printf '%s\n' "${CWD}/${_cname}"
done) done)
CLIP_MODE="cut" CLIP_MODE="cut"
INFO_MSG="cut ${_c} items" INFO_MSG="cut ${_c} items"
@@ -1617,7 +1768,8 @@ do_cut() {
else else
entry=$(get_entry "$SEL") entry=$(get_entry "$SEL")
[ -z "$entry" ] && return [ -z "$entry" ] && return
CLIPBOARD="${CWD}/${entry%/}" _cname="${entry%@}"; _cname="${_cname%/}"
CLIPBOARD="${CWD}/${_cname}"
CLIP_MODE="cut" CLIP_MODE="cut"
INFO_MSG="cut: ${entry}" INFO_MSG="cut: ${entry}"
fi fi
@@ -1630,7 +1782,6 @@ do_paste() {
NEED_FULL_REDRAW=1 NEED_FULL_REDRAW=1
return return
fi fi
_ok=0; _fail=0
printf '%s\n' "$CLIPBOARD" | while IFS= read -r _src; do printf '%s\n' "$CLIPBOARD" | while IFS= read -r _src; do
[ -z "$_src" ] && continue [ -z "$_src" ] && continue
_name="${_src##*/}" _name="${_src##*/}"
@@ -1641,8 +1792,8 @@ do_paste() {
_dest="${CWD}/${_base}_copy${_ext}" _dest="${CWD}/${_base}_copy${_ext}"
fi fi
case "$CLIP_MODE" in case "$CLIP_MODE" in
copy) cp -r "$_src" "$_dest" && _ok=$((_ok+1)) || _fail=$((_fail+1)) ;; copy) cp -r "$_src" "$_dest" ;;
cut) mv "$_src" "$_dest" && _ok=$((_ok+1)) || _fail=$((_fail+1)) ;; cut) mv "$_src" "$_dest" ;;
esac esac
done done
CLIPBOARD=""; CLIP_MODE="" CLIPBOARD=""; CLIP_MODE=""
@@ -1651,7 +1802,7 @@ do_paste() {
} }
# --- main --- # --- main ---
trap 'restore_term; show_cursor; printf "\033[2J\033[H"; rm -f /tmp/_sfm_list /tmp/_sfm_find; 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 QUIT TERM EXIT
setup_term setup_term
hide_cursor hide_cursor
@@ -1709,6 +1860,7 @@ while true; do
m) do_mkdir ;; m) do_mkdir ;;
n) do_newfile ;; n) do_newfile ;;
q|Q) break ;; q|Q) break ;;
'!') do_shell ;;
esac esac
done done