5 Commits
Author SHA1 Message Date
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
2 changed files with 105 additions and 76 deletions
+66 -68
View File
@@ -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"; }
@@ -265,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
printf '%s\n' "$ALL_ENTRIES" | while IFS= read -r line; do _rest="$ALL_ENTRIES"
case "$line" in while [ -n "$_rest" ]; do
*"$FILTER"*) printf '%s\n' "$line" ;; _line="${_rest%%
esac *}"; _next="${_rest#*
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?
@@ -362,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
@@ -403,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
@@ -471,7 +462,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 +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=""
# 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}" key="${key}${_k2}${_k3}"
;;
esac
fi fi
printf '%s' "$key" printf '%s' "$key"
} }
@@ -1654,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
@@ -1674,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')"|\
+36 -5
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,6 +144,9 @@ 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
@@ -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)