16 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
emmett1 7c2f21d8ec updated man page 2026-04-11 09:05:45 +08:00
emmett1 938955120b fix page down, page up and del key, added del key to delete function 2026-04-11 08:57:44 +08:00
emmett1 f374506215 update 2026-04-11 08:51:01 +08:00
emmett1 fe57971b91 removed extension colouring, and changed some colours 2026-04-11 02:11:23 +08:00
emmett1 2f8d3e8d9e man page updated 2026-04-10 07:35:11 +08:00
6 changed files with 2988 additions and 210 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 MANDIR ?= $(PREFIX)/share/man/man1
DOCDIR ?= $(PREFIX)/share/doc/sfm 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 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 $@ $< $(LDLIBS)
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."
+27 -17
View File
@@ -4,16 +4,24 @@ 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++ binary that works with
standard Unix tools (ls, awk, tput, stty, mv, cp, rm). Designed to be both ncurses and netbsd-curses. A portable POSIX sh version (sfm.sh) is
fast, flicker-free, and keyboard-driven with a vim-inspired key layout. also included, requiring only standard Unix tools (ls, awk, tput, stty,
mv, cp, rm).
REQUIREMENTS 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.) - 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 +31,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
@@ -142,8 +152,8 @@ MULTI-SELECT
BOOKMARKS BOOKMARKS
--------- ---------
b Bookmark current directory (press again to remove) b Open bookmark picker (j/k navigate, enter jump, del remove)
B Open bookmark picker (j/k navigate, enter jump) B Bookmark current directory (press again to remove)
Bookmarks are saved to: ~/.config/sfm/bookmarks Bookmarks are saved to: ~/.config/sfm/bookmarks
+39 -8
View File
@@ -43,6 +43,16 @@ Go to home directory.
.TP .TP
.B \` .B \`
Jump to previous directory (toggle). 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 .SS Search and Filter
.TP .TP
@@ -58,7 +68,9 @@ Exit search mode while keeping the current filter active.
.SS Display Toggles .SS Display Toggles
.TP .TP
.B . .B .
Toggle hidden files (dotfiles). Toggle hidden files (dotfiles). Hidden entries appear before regular
entries (hidden dirs, then regular dirs, then hidden files, then
regular files).
.TP .TP
.B T .B T
Toggle size/date detail column. Toggle size/date detail column.
@@ -86,7 +98,7 @@ Create a new directory. Press esc to cancel.
.B n .B n
Create a new empty file. Press esc to cancel. Create a new empty file. Press esc to cancel.
.TP .TP
.B d .B d " or " del
Delete selected entry or all multi-selected entries. Delete selected entry or all multi-selected entries.
Non-empty directories require a second confirmation. Non-empty directories require a second confirmation.
.TP .TP
@@ -98,6 +110,14 @@ Open the trash directory.
.TP .TP
.B o .B o
Open selected file with a custom program. Press esc to cancel. 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 .SS Clipboard
.TP .TP
@@ -109,9 +129,11 @@ Cut selected entry or all multi-selected entries.
.TP .TP
.B p .B p
Paste clipboard contents into the current directory. Paste clipboard contents into the current directory.
Auto-renames on conflict by appending
.IR _copy .
.TP .TP
.B c .B c
Copy the full path of selected entry to the system clipboard. Copy the full path of the selected entry to the system clipboard.
Requires Requires
.BR wl-copy ", " xclip ", " xsel ", or " pbcopy . .BR wl-copy ", " xclip ", " xsel ", or " pbcopy .
@@ -122,15 +144,18 @@ Toggle multi-select on the current entry (cursor advances).
.TP .TP
.B a .B a
Select all entries / deselect all. Select all entries / deselect all.
.PP
Multi-select works with
.BR y ", " x ", and " d .
.SS Bookmarks .SS Bookmarks
.TP .TP
.B b .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 .TP
.B B .B B
Open the bookmark picker. Bookmark the current directory. Press again to remove it.
Use j/k to navigate, enter to jump, esc to close.
.SS Other .SS Other
.TP .TP
@@ -139,7 +164,8 @@ Drop into
.B $SHELL .B $SHELL
in the current directory. Type in the current directory. Type
.I exit .I exit
to return to sfm. to return to sfm. The working directory is updated if you cd elsewhere
in the shell.
.TP .TP
.B ? .B ?
Show the keyboard shortcuts help overlay. Show the keyboard shortcuts help overlay.
@@ -186,6 +212,9 @@ case "$1" in
esac esac
.fi .fi
.PP .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 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. smart opener which detects file types by extension and MIME type.
@@ -335,11 +364,13 @@ s() { cd "$(sfm)"; }
.fi .fi
.SH AUTHORS .SH AUTHORS
Built interactively with Claude (Anthropic). Emmett1 <me@emmett1.my> with help of AI.
.SH SEE ALSO .SH SEE ALSO
.BR find (1),
.BR ls (1), .BR ls (1),
.BR mv (1), .BR mv (1),
.BR cp (1), .BR cp (1),
.BR rm (1), .BR rm (1),
.BR chmod (1),
.BR vi (1) .BR vi (1)
+2560
View File
File diff suppressed because it is too large Load Diff
+342 -179
View File
@@ -16,44 +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' "${RED}${BOLD}"; return; }
case "$_fc_ext" in
# images
jpg|jpeg|png|gif|bmp|tiff|tif|webp|svg|ico|heic|heif|avif)
printf '%s' "${MAGENTA}" ;;
# video
mp4|mkv|avi|mov|wmv|flv|webm|m4v|mpeg|mpg|3gp|ogv)
printf '%s' "${MAGENTA}${BOLD}" ;;
# audio
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"; }
hide_cursor() { printf '\033[?25l'; } hide_cursor() { printf '\033[?25l'; }
show_cursor() { printf '\033[?25h'; } show_cursor() { printf '\033[?25h'; }
@@ -195,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=""
@@ -211,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,50 +227,47 @@ 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 -nFx "$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?
is_selected() { is_selected() {
case " printf '%s\n' "$SELECTED" | grep -Fxq "$1"
${SELECTED}
" in *"
$1
"*) return 0 ;; esac
return 1
} }
count_selected() { count_selected() {
@@ -362,16 +321,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
@@ -403,17 +368,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
@@ -471,7 +440,7 @@ draw_preview() {
_dl=" ${_dn}/" _dl=" ${_dn}/"
[ "${#_dl}" -gt "$((_pw - 1))" ] && _dl="$(printf '%s' "$_dl" | cut -c1-$((_pw-2)))~" [ "${#_dl}" -gt "$((_pw - 1))" ] && _dl="$(printf '%s' "$_dl" | cut -c1-$((_pw-2)))~"
goto "$_pr" "$_px" goto "$_pr" "$_px"
printf '%s%s%s' "${GREEN}${BOLD}" "$_dl" "${RESET}" printf '%s%s%s' "${BLUE}${BOLD}" "$_dl" "${RESET}"
_pr=$((_pr + 1)) _pr=$((_pr + 1))
done done
for _fe in "$_path"/*; do for _fe in "$_path"/*; do
@@ -684,15 +653,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"
} }
@@ -802,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
@@ -858,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
@@ -935,8 +912,8 @@ do_help() {
help_row 30 " + chmod +x (make executable)" help_row 30 " + chmod +x (make executable)"
help_row 31 " - chmod -x (remove executable)" help_row 31 " - chmod -x (remove executable)"
help_sep 32 help_sep 32
help_row 33 " b bookmark current dir" help_row 33 " b open bookmark picker"
help_row 34 " B open bookmark picker" help_row 34 " B bookmark current dir"
help_row 35 " c copy path to clipboard" help_row 35 " c copy path to clipboard"
help_row 36 " ~ go to home directory" help_row 36 " ~ go to home directory"
help_row 37 " \` jump to previous directory" help_row 37 " \` jump to previous directory"
@@ -1036,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:]')
@@ -1239,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
@@ -1323,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
@@ -1361,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
} }
@@ -1399,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))
@@ -1424,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
@@ -1482,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"
@@ -1491,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" '')
@@ -1509,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
@@ -1529,6 +1674,19 @@ do_bookmark_jump() {
fi fi
return ;; return ;;
'[D') NEED_FULL_REDRAW=1; return ;; # left — close '[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 *) NEED_FULL_REDRAW=1; return ;; # bare esc — close
esac ;; esac ;;
"$(printf '\n')"|\ "$(printf '\n')"|\
@@ -1592,7 +1750,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"
@@ -1600,7 +1759,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
@@ -1612,7 +1772,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"
@@ -1620,7 +1781,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
@@ -1633,7 +1795,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##*/}"
@@ -1644,8 +1805,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=""
@@ -1654,7 +1815,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 QUIT TERM EXIT
setup_term setup_term
hide_cursor hide_cursor
@@ -1674,14 +1835,15 @@ 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')"|\
l) do_open ;; l) do_open ;;
"$(printf '\033[D')"|\ "$(printf '\033[D')"|\
h) do_go_back ;; h) do_go_back ;;
b) do_bookmark_add ;; b) do_bookmark_jump ;;
B) do_bookmark_jump ;; B) do_bookmark_add ;;
'?') do_help ;; '?') do_help ;;
R) load_entries; INFO_MSG="refreshed" ;; R) load_entries; INFO_MSG="refreshed" ;;
/) do_search ;; /) do_search ;;
@@ -1711,6 +1873,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