11 Commits
Author SHA1 Message Date
emmett1 53567c1f20 bookmark updated 2026-08-16 14:25:34 +08:00
emmett1 c6f4023768 updated 2026-08-16 11:52:05 +08:00
emmett1 cc0dbb0cad fix 2026-07-23 12:08:05 +08:00
emmett1 cd2a4dffb8 added permission error messsage, fix pane line, add root message if running as root 2026-06-20 16:07:40 +08:00
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
6 changed files with 2896 additions and 147 deletions
+2
View File
@@ -0,0 +1,2 @@
CLAUDE.md
sfm
+18 -6
View File
@@ -5,33 +5,45 @@ BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man/man1
DOCDIR ?= $(PREFIX)/share/doc/sfm
CXX ?= g++
CXXFLAGS ?= -std=c++17 -O2 -Wall -Wextra
# Pick a curses library: prefer ncursesw, fall back to ncurses, then plain
# curses (netbsd-curses and BSD base systems provide only libcurses).
CURSES_LIBS ?= $(shell pkg-config --libs ncursesw 2>/dev/null || pkg-config --libs ncurses 2>/dev/null || echo -lcurses)
LDLIBS ?= $(CURSES_LIBS)
INSTALL ?= install
RM ?= rm -f
.PHONY: all install uninstall clean
all:
@echo "sfm is a shell script — nothing to build."
@echo "Run 'make install' to install."
all: sfm
install:
sfm: sfm.cpp
$(CXX) $(CXXFLAGS) -o $@ $< $(LDLIBS)
install: all
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
$(INSTALL) -d $(DESTDIR)$(BINDIR)
$(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 ..."
$(INSTALL) -d $(DESTDIR)$(MANDIR)
$(INSTALL) -m 644 sfm.1 $(DESTDIR)$(MANDIR)/sfm.1
@echo "Installing README to $(DESTDIR)$(DOCDIR)/README ..."
$(INSTALL) -d $(DESTDIR)$(DOCDIR)
$(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README
@echo "Done. Run 'sfm' to start, or 'man sfm' for help."
@echo "Done."
uninstall:
@echo "Removing sfm ..."
$(RM) $(DESTDIR)$(BINDIR)/sfm
$(RM) $(DESTDIR)$(BINDIR)/sfm.sh
$(RM) $(DESTDIR)$(MANDIR)/sfm.1
$(RM) -r $(DESTDIR)$(DOCDIR)
@echo "Done."
clean:
@echo "Nothing to clean."
$(RM) sfm
@echo "Cleaned."
+20 -10
View File
@@ -4,16 +4,24 @@ sfm - Simple File Manager
DESCRIPTION
-----------
sfm is a lightweight, terminal-based file manager written entirely in
POSIX sh. It runs in your terminal with no external dependencies beyond
standard Unix tools (ls, awk, tput, stty, mv, cp, rm). Designed to be
fast, flicker-free, and keyboard-driven with a vim-inspired key layout.
sfm is a lightweight, terminal-based file manager with a vim-inspired
key layout. The primary version is a fast C++ binary that works with
both ncurses and netbsd-curses. A portable POSIX sh version (sfm.sh) is
also included, requiring only standard Unix tools (ls, awk, tput, stty,
mv, cp, rm).
REQUIREMENTS
------------
C++ version (sfm):
- A curses library: ncursesw, ncurses, or netbsd-curses (libcurses)
- A C++17 compiler (g++ or compatible)
Shell version (sfm.sh):
- A POSIX-compatible shell (sh, dash, bash, etc.)
- Standard Unix tools: ls, awk, tput, stty, cp, mv, rm, mkdir, touch
Both versions:
- Optional: wl-copy / xclip / xsel / pbcopy (for clipboard support)
- Optional: mpv, vlc, feh, zathura, etc. (for smart file opening)
- Optional: readlink (for symlink display)
@@ -26,7 +34,9 @@ Using make (recommended):
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
@@ -34,10 +44,10 @@ To uninstall:
make uninstall
Manual installation:
Manual installation (shell version only):
cp sfm /usr/local/bin/sfm
chmod +x /usr/local/bin/sfm
cp sfm.sh /usr/local/bin/sfm.sh
chmod +x /usr/local/bin/sfm.sh
USAGE
@@ -142,8 +152,8 @@ MULTI-SELECT
BOOKMARKS
---------
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, del remove)
B Bookmark current directory (press again to remove)
Bookmarks are saved to: ~/.config/sfm/bookmarks
+3 -3
View File
@@ -151,11 +151,11 @@ Multi-select works with
.SS Bookmarks
.TP
.B b
Bookmark the current directory. Press again to remove it.
Open the bookmark picker.
Use j/k to navigate, enter to jump, del to remove, esc to close.
.TP
.B B
Open the bookmark picker.
Use j/k to navigate, enter to jump, esc to close.
Bookmark the current directory. Press again to remove it.
.SS Other
.TP
+2560
View File
File diff suppressed because it is too large Load Diff
+265 -100
View File
@@ -16,23 +16,6 @@ MAGENTA=$(tput_cmd setaf 5)
BLUE=$(tput_cmd setaf 4)
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"; }
hide_cursor() { printf '\033[?25l'; }
show_cursor() { printf '\033[?25h'; }
@@ -174,8 +157,8 @@ $_line"; fi ;;
# sort files using ls into a tmp file
if [ -n "$_files" ]; then
case "$SORT_MODE" in
size) ls -1Sp "$CWD" 2>/dev/null | grep -v '/' > /tmp/_fm_sorted ;;
date) ls -1tp "$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_$$ ;;
esac
# only keep files that were already in our list (respects hidden filter)
_sorted=""
@@ -190,7 +173,7 @@ ${_n}
else _sorted="$_sorted
$_n"; fi ;;
esac
done < /tmp/_fm_sorted
done < /tmp/_fm_sorted_$$
_files="$_sorted"
fi
@@ -265,16 +248,16 @@ $_line"; fi
done
fi
# 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() {
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_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
printf '%d' $((_fe_n - 1))
else
@@ -284,12 +267,7 @@ find_entry() {
# is entry name in SELECTED list?
is_selected() {
case "
${SELECTED}
" in *"
$1
"*) return 0 ;; esac
return 1
printf '%s\n' "$SELECTED" | grep -Fxq "$1"
}
count_selected() {
@@ -799,8 +777,10 @@ open_file() {
# read a line of input in raw mode, returns result in READ_LINE
# returns 1 if user pressed Esc (cancel), 0 on Enter
read_line() {
READ_LINE=""
READ_LINE="${_init:-}"
_init=""
show_cursor
printf '%s' "$READ_LINE"
while true; do
IFS= read -r -n1 _ch 2>/dev/null || IFS= read -r _ch
case "$_ch" in
@@ -855,7 +835,7 @@ do_open_with() {
do_newfile() {
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
touch "${CWD}/${READ_LINE}"
load_entries
@@ -932,8 +912,8 @@ do_help() {
help_row 30 " + chmod +x (make executable)"
help_row 31 " - chmod -x (remove executable)"
help_sep 32
help_row 33 " b bookmark current dir"
help_row 34 " B open bookmark picker"
help_row 33 " b open bookmark picker"
help_row 34 " B bookmark current dir"
help_row 35 " c copy path to clipboard"
help_row 36 " ~ go to home directory"
help_row 37 " \` jump to previous directory"
@@ -1033,7 +1013,7 @@ do_find() {
_query="$READ_LINE"
# 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"
_total=$(wc -l < "$_tmp" | tr -d '[:space:]')
@@ -1236,19 +1216,93 @@ do_open() {
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() {
INFO_MSG=""
_c=$(count_selected)
if [ "$_c" -gt 0 ]; then
# --- bulk delete selected ---
restore_term; show_cursor
goto "$(term_rows)" 1
printf '%s%s Delete %d selected items? [y/N] %s' \
"${ERASE_LINE}" "${RED}${BOLD}" "$_c" "${RESET}"
IFS= read -r ans
setup_term; hide_cursor
case "$ans" in
y|Y)
_prompt="Delete ${_c} selected items?"
confirm_overlay; _yes=$?
if [ "$_yes" -eq 0 ]; then
_rest="$SELECTED"
while [ -n "$_rest" ]; do
_line="${_rest%%
@@ -1258,60 +1312,53 @@ do_delete() {
[ "$_next" = "$_rest" ] && _next=""
_rest="$_next"
[ -z "$_line" ] && continue
_t="${CWD}/${_line%/}"
_name="${_line%@}"; _name="${_name%/}"
_t="${CWD}/${_name}"
if [ -d "$_t" ]; then rm -rf "$_t"; else rm -f "$_t"; fi
done
SELECTED=""
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
[ "$SEL" -lt 0 ] && SEL=0
load_entries
;;
*) NEED_FULL_REDRAW=1 ;;
esac
else
NEED_FULL_REDRAW=1
fi
else
# --- single delete ---
entry=$(get_entry "$SEL")
target="${CWD}/${entry%/}"
restore_term; show_cursor
goto "$(term_rows)" 1
printf '%s%s Delete "%s"? [y/N] %s' "${ERASE_LINE}" "${RED}${BOLD}" "$entry" "${RESET}"
IFS= read -r ans
setup_term; hide_cursor
case "$ans" in
y|Y)
_name="${entry%@}"; _name="${_name%/}"
target="${CWD}/${_name}"
_prompt="Delete \"${entry}\"?"
confirm_overlay; _yes=$?
if [ "$_yes" -ne 0 ]; then
NEED_FULL_REDRAW=1; return
fi
if [ -d "$target" ]; then
if [ -n "$(ls -A "$target" 2>/dev/null)" ]; then
restore_term; show_cursor
goto "$(term_rows)" 1
printf '%s%s "%s" not empty. Delete ALL? [y/N] %s' \
"${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"
_prompt="\"${entry}\" not empty. Delete ALL?"
confirm_overlay; _yes2=$?
if [ "$_yes2" -ne 0 ]; then
NEED_FULL_REDRAW=1; return
fi
fi
rm -rf "$target"
else
rm -f "$target"
fi
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
[ "$SEL" -lt 0 ] && SEL=0
load_entries
;;
*) NEED_FULL_REDRAW=1 ;;
esac
fi
}
do_rename() {
entry=$(get_entry "$SEL")
_name="${entry%@}"; _name="${_name%/}"
goto "$(term_rows)" 1
printf '%s%s Rename "%s" to (esc=cancel): %s' "${ERASE_LINE}" "${YELLOW}${BOLD}" "$entry" "${RESET}"
if read_line && [ -n "$READ_LINE" ] && [ "$READ_LINE" != "$entry" ]; then
mv "${CWD}/${entry%/}" "${CWD}/${READ_LINE}"
printf '%s%s Rename: %s' "${ERASE_LINE}" "${YELLOW}${BOLD}" "${RESET}"
_init="$_name"
if read_line && [ -n "$READ_LINE" ] && [ "$READ_LINE" != "$_name" ]; then
mv "${CWD}/${_name}" "${CWD}/${READ_LINE}"
load_entries
else
NEED_FULL_REDRAW=1; draw
@@ -1320,7 +1367,7 @@ do_rename() {
do_mkdir() {
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
mkdir -p "${CWD}/${READ_LINE}"
load_entries
@@ -1358,15 +1405,96 @@ do_chmod_nox() {
do_info() {
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
target="${CWD}/${entry%/}"
# permissions + type
_perm=$(ls -ld "$target" 2>/dev/null | awk '{print $1}')
# size (human readable via du, fallback to ls)
_size=$(du -sh "$target" 2>/dev/null | cut -f1)
[ -z "$_size" ] && _size=$(ls -lh "$target" 2>/dev/null | awk '{print $5}')
# modification date
_date=$(ls -ld "$target" 2>/dev/null | awk '{print $6, $7, $8}')
INFO_MSG="${_perm} ${_size} ${_date}"
_name="${entry%@}"; _name="${_name%/}"
target="${CWD}/${_name}"
# gather info
_info_perm=$(ls -ld "$target" 2>/dev/null | awk '{print $1}')
_info_owner=$(ls -ld "$target" 2>/dev/null | awk '{print $3}')
_info_group=$(ls -ld "$target" 2>/dev/null | awk '{print $4}')
_info_size=$(du -sh "$target" 2>/dev/null | cut -f1)
[ -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
}
@@ -1396,9 +1524,10 @@ do_sort() {
do_trash() {
entry=$(get_entry "$SEL")
[ -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')
_dest="${TRASH_DIR}/${_ts}_${entry%/}"
_dest="${TRASH_DIR}/${_ts}_${_name}"
if mv "$target" "$_dest"; then
INFO_MSG="trashed: ${entry}"
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
@@ -1421,7 +1550,8 @@ do_open_trash() {
do_copy_path() {
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
_path="${CWD}/${entry%/}"
_name="${entry%@}"; _name="${_name%/}"
_path="${CWD}/${_name}"
if command -v wl-copy >/dev/null 2>&1; then
printf '%s' "$_path" | wl-copy
elif command -v xclip >/dev/null 2>&1; then
@@ -1479,6 +1609,8 @@ do_bookmark_jump() {
_hl=$(printf '%*s' "$_iw" '' | tr ' ' '-')
_bsel=1
_bvis=$((_ph - 3))
_boff=1
while true; do
# draw picker inline
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}"
goto "$((_py+2))" "$_px"
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
while IFS= read -r _bm; do
[ -z "$_bm" ] && continue
_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}"
if [ "${#_bt}" -gt "$_iw" ]; then _bt=$(printf '%s' "$_bt" | cut -c1-$((_iw-1))); _bt="${_bt}~"; fi
_bpl=$((_iw - ${#_bt})); _bp=$(printf '%*s' "$_bpl" '')
@@ -1506,14 +1650,18 @@ do_bookmark_jump() {
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
IFS= read -r -n1 _bk 2>/dev/null || IFS= read -r _bk
case "$_bk" in
j) _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc ;;
k) _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1 ;;
j) _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc
[ "$_bsel" -ge $((_boff + _bvis)) ] && _boff=$((_bsel - _bvis + 1)) ;;
k) _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1
[ "$_bsel" -lt "$_boff" ] && _boff=$_bsel ;;
"$(printf '\033')")
# use timeout to distinguish bare esc from arrow sequences
IFS= read -r -n2 -t 0.1 _seq 2>/dev/null || _seq=""
case "$_seq" in
'[A') _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1 ;;
'[B') _bsel=$((_bsel+1)); [ "$_bsel" -gt "$_bc" ] && _bsel=$_bc ;;
'[A') _bsel=$((_bsel-1)); [ "$_bsel" -lt 1 ] && _bsel=1
[ "$_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
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then
@@ -1526,6 +1674,19 @@ do_bookmark_jump() {
fi
return ;;
'[D') NEED_FULL_REDRAW=1; return ;; # left — close
'[3') # delete — remove selected bookmark
IFS= read -r -n1 -t 0.05 _rest 2>/dev/null # swallow trailing ~
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
if [ -n "$_chosen" ]; then
_tmp=$(grep -vxF "$_chosen" "$BOOKMARK_FILE")
printf '%s\n' "$_tmp" > "$BOOKMARK_FILE"
INFO_MSG="bookmark removed: ${_chosen}"
_bc=$((_bc-1))
if [ "$_bc" -eq 0 ]; then NEED_FULL_REDRAW=1; return; fi
[ "$_bsel" -gt "$_bc" ] && _bsel=$_bc
[ "$_bsel" -lt "$_boff" ] && _boff=$_bsel
[ "$_bsel" -ge $((_boff + _bvis)) ] && _boff=$((_bsel - _bvis + 1))
fi ;;
*) NEED_FULL_REDRAW=1; return ;; # bare esc — close
esac ;;
"$(printf '\n')"|\
@@ -1589,7 +1750,8 @@ do_yank() {
if [ "$_c" -gt 0 ]; then
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
[ -z "$_e" ] && continue
printf '%s\n' "${CWD}/${_e%/}"
_yname="${_e%@}"; _yname="${_yname%/}"
printf '%s\n' "${CWD}/${_yname}"
done)
CLIP_MODE="copy"
INFO_MSG="yanked ${_c} items"
@@ -1597,7 +1759,8 @@ do_yank() {
else
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
CLIPBOARD="${CWD}/${entry%/}"
_yname="${entry%@}"; _yname="${_yname%/}"
CLIPBOARD="${CWD}/${_yname}"
CLIP_MODE="copy"
INFO_MSG="yanked: ${entry}"
fi
@@ -1609,7 +1772,8 @@ do_cut() {
if [ "$_c" -gt 0 ]; then
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
[ -z "$_e" ] && continue
printf '%s\n' "${CWD}/${_e%/}"
_cname="${_e%@}"; _cname="${_cname%/}"
printf '%s\n' "${CWD}/${_cname}"
done)
CLIP_MODE="cut"
INFO_MSG="cut ${_c} items"
@@ -1617,7 +1781,8 @@ do_cut() {
else
entry=$(get_entry "$SEL")
[ -z "$entry" ] && return
CLIPBOARD="${CWD}/${entry%/}"
_cname="${entry%@}"; _cname="${_cname%/}"
CLIPBOARD="${CWD}/${_cname}"
CLIP_MODE="cut"
INFO_MSG="cut: ${entry}"
fi
@@ -1630,7 +1795,6 @@ do_paste() {
NEED_FULL_REDRAW=1
return
fi
_ok=0; _fail=0
printf '%s\n' "$CLIPBOARD" | while IFS= read -r _src; do
[ -z "$_src" ] && continue
_name="${_src##*/}"
@@ -1641,8 +1805,8 @@ do_paste() {
_dest="${CWD}/${_base}_copy${_ext}"
fi
case "$CLIP_MODE" in
copy) cp -r "$_src" "$_dest" && _ok=$((_ok+1)) || _fail=$((_fail+1)) ;;
cut) mv "$_src" "$_dest" && _ok=$((_ok+1)) || _fail=$((_fail+1)) ;;
copy) cp -r "$_src" "$_dest" ;;
cut) mv "$_src" "$_dest" ;;
esac
done
CLIPBOARD=""; CLIP_MODE=""
@@ -1651,7 +1815,7 @@ do_paste() {
}
# --- 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
hide_cursor
@@ -1678,8 +1842,8 @@ while true; do
l) do_open ;;
"$(printf '\033[D')"|\
h) do_go_back ;;
b) do_bookmark_add ;;
B) do_bookmark_jump ;;
b) do_bookmark_jump ;;
B) do_bookmark_add ;;
'?') do_help ;;
R) load_entries; INFO_MSG="refreshed" ;;
/) do_search ;;
@@ -1709,6 +1873,7 @@ while true; do
m) do_mkdir ;;
n) do_newfile ;;
q|Q) break ;;
'!') do_shell ;;
esac
done