Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0504dec9c7 | ||
|
|
f85d1af121 | ||
|
|
308637d874 | ||
|
|
aaf7fd829d | ||
|
|
8db9a070b7 | ||
|
|
bed888c3c3 | ||
|
|
7c2f21d8ec | ||
|
|
938955120b | ||
|
|
f374506215 | ||
|
|
fe57971b91 | ||
|
|
2f8d3e8d9e | ||
|
|
2a1f8fc0a5 | ||
|
|
346ef7b7cb | ||
|
|
09f60a85ef | ||
|
|
32337e4cca | ||
|
|
03df88a150 | ||
|
|
54352d036c |
@@ -15,17 +15,21 @@ all:
|
||||
@echo "Run 'make install' to install."
|
||||
|
||||
install:
|
||||
@echo "Installing sfm to $(BINDIR)/sfm ..."
|
||||
@echo "Installing sfm to $(DESTDIR)$(BINDIR)/sfm ..."
|
||||
$(INSTALL) -d $(DESTDIR)$(BINDIR)
|
||||
$(INSTALL) -m 755 sfm $(DESTDIR)$(BINDIR)/sfm
|
||||
@echo "Installing README to $(DOCDIR) ..."
|
||||
@echo "Installing man page to $(DESTDIR)$(MANDIR)/sfm.1 ..."
|
||||
$(INSTALL) -d $(DESTDIR)$(MANDIR)
|
||||
$(INSTALL) -m 644 sfm.1 $(DESTDIR)$(MANDIR)/sfm.1
|
||||
@echo "Installing README to $(DESTDIR)$(DOCDIR)/README ..."
|
||||
$(INSTALL) -d $(DESTDIR)$(DOCDIR)
|
||||
$(INSTALL) -m 644 README.txt $(DESTDIR)$(DOCDIR)/README.txt
|
||||
@echo "Done. Run 'sfm' to start."
|
||||
$(INSTALL) -m 644 README $(DESTDIR)$(DOCDIR)/README
|
||||
@echo "Done. Run 'sfm' to start, or 'man sfm' for help."
|
||||
|
||||
uninstall:
|
||||
@echo "Removing sfm ..."
|
||||
$(RM) $(DESTDIR)$(BINDIR)/sfm
|
||||
$(RM) $(DESTDIR)$(MANDIR)/sfm.1
|
||||
$(RM) -r $(DESTDIR)$(DOCDIR)
|
||||
@echo "Done."
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/bin/sh
|
||||
# sfm - Simple File Manager in POSIX sh (flicker-free)
|
||||
# version 0.4
|
||||
# sfm - Simple File Manager in POSIX sh
|
||||
|
||||
# --- terminal control ---
|
||||
tput_cmd() { command -v tput >/dev/null 2>&1 && tput "$@"; }
|
||||
@@ -17,44 +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' "${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"; }
|
||||
hide_cursor() { printf '\033[?25l'; }
|
||||
show_cursor() { printf '\033[?25h'; }
|
||||
@@ -107,22 +68,11 @@ load_entries() {
|
||||
ENTRIES=""
|
||||
COUNT=0
|
||||
|
||||
# build list of dir names then file names, including dotfiles if enabled
|
||||
for d in "$CWD"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
[ -L "${d%/}" ] && continue # symlinks handled separately
|
||||
name="${d%/}"; name="${name##*/}"
|
||||
[ "$name" = "*" ] && continue
|
||||
case "$name" in .*) continue ;; esac
|
||||
ENTRIES="$ENTRIES
|
||||
${name}/"
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
# hidden dirs first
|
||||
if [ "$SHOW_HIDDEN" = "1" ]; then
|
||||
for d in "$CWD"/./; do : ; done
|
||||
for d in "$CWD"/.*/; do
|
||||
[ -d "$d" ] || continue
|
||||
[ -L "${d%/}" ] && continue # symlinks handled separately
|
||||
[ -L "${d%/}" ] && continue
|
||||
name="${d%/}"; name="${name##*/}"
|
||||
[ "$name" = "." ] || [ "$name" = ".." ] && continue
|
||||
[ "$name" = ".*" ] && continue
|
||||
@@ -132,21 +82,19 @@ ${name}/"
|
||||
done
|
||||
fi
|
||||
|
||||
for f in "$CWD"/*; do
|
||||
[ -e "$f" ] || [ -L "$f" ] || continue
|
||||
[ -d "$f" ] && ! [ -L "$f" ] && continue
|
||||
name="${f##*/}"
|
||||
# regular dirs
|
||||
for d in "$CWD"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
[ -L "${d%/}" ] && continue
|
||||
name="${d%/}"; name="${name##*/}"
|
||||
[ "$name" = "*" ] && continue
|
||||
case "$name" in .*) continue ;; esac
|
||||
if [ -L "$f" ]; then
|
||||
ENTRIES="$ENTRIES
|
||||
${name}@"
|
||||
else
|
||||
ENTRIES="$ENTRIES
|
||||
${name}"
|
||||
fi
|
||||
${name}/"
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
|
||||
# hidden files first
|
||||
if [ "$SHOW_HIDDEN" = "1" ]; then
|
||||
for f in "$CWD"/.*; do
|
||||
[ -e "$f" ] || [ -L "$f" ] || continue
|
||||
@@ -164,6 +112,23 @@ ${name}"
|
||||
done
|
||||
fi
|
||||
|
||||
# regular files
|
||||
for f in "$CWD"/*; do
|
||||
[ -e "$f" ] || [ -L "$f" ] || continue
|
||||
[ -d "$f" ] && ! [ -L "$f" ] && continue
|
||||
name="${f##*/}"
|
||||
[ "$name" = "*" ] && continue
|
||||
case "$name" in .*) continue ;; esac
|
||||
if [ -L "$f" ]; then
|
||||
ENTRIES="$ENTRIES
|
||||
${name}@"
|
||||
else
|
||||
ENTRIES="$ENTRIES
|
||||
${name}"
|
||||
fi
|
||||
COUNT=$((COUNT + 1))
|
||||
done
|
||||
|
||||
ENTRIES="${ENTRIES#
|
||||
}"
|
||||
|
||||
@@ -192,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=""
|
||||
@@ -208,7 +173,7 @@ ${_n}
|
||||
else _sorted="$_sorted
|
||||
$_n"; fi ;;
|
||||
esac
|
||||
done < /tmp/_fm_sorted
|
||||
done < /tmp/_fm_sorted_$$
|
||||
_files="$_sorted"
|
||||
fi
|
||||
|
||||
@@ -262,50 +227,47 @@ apply_filter() {
|
||||
if [ -z "$FILTER" ]; then
|
||||
ENTRIES="$ALL_ENTRIES"
|
||||
COUNT="$ALL_COUNT"
|
||||
return
|
||||
fi
|
||||
else
|
||||
ENTRIES=""
|
||||
COUNT=0
|
||||
printf '%s\n' "$ALL_ENTRIES" | while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*"$FILTER"*) printf '%s\n' "$line" ;;
|
||||
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#
|
||||
_rest="$ALL_ENTRIES"
|
||||
while [ -n "$_rest" ]; do
|
||||
_line="${_rest%%
|
||||
*}"; _next="${_rest#*
|
||||
}"
|
||||
[ "$_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() {
|
||||
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_entry() {
|
||||
printf '%s\n' "$ENTRIES" | awk -v name="$1" '
|
||||
{ if ($0 == name) { print NR-1; found=1; exit } }
|
||||
END { if (!found) print -1 }
|
||||
'
|
||||
_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
|
||||
printf '%d' -1
|
||||
fi
|
||||
}
|
||||
|
||||
# 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() {
|
||||
@@ -359,16 +321,22 @@ render_row() {
|
||||
entry=$(get_entry "$_idx")
|
||||
|
||||
case "$entry" in
|
||||
*/) colour="${GREEN}${BOLD}" ;;
|
||||
*/) colour="${BLUE}${BOLD}" ;;
|
||||
*@)
|
||||
_name="${entry%@}"
|
||||
# broken symlink if target doesn't exist
|
||||
if [ -e "${CWD}/${_name}" ]; then
|
||||
colour="${MAGENTA}${BOLD}"
|
||||
colour="${CYAN}${BOLD}"
|
||||
else
|
||||
colour="${RED}${BOLD}"
|
||||
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
|
||||
|
||||
# multi-select marker
|
||||
@@ -400,17 +368,21 @@ render_row() {
|
||||
if [ "$SHOW_DETAILS" = "1" ]; then
|
||||
_path="${CWD}/${entry%/}"; _path="${_path%@}"
|
||||
_info=$(ls -ldh "$_path" 2>/dev/null)
|
||||
_sz=$(printf '%s' "$_info" | awk '{print $5}')
|
||||
_dt=$(printf '%s' "$_info" | awk '{print $6, $7}')
|
||||
# fixed width: size=6, date=6 → total detail = 14 chars
|
||||
# parse ls output with parameter expansion — no awk fork
|
||||
set -- $_info
|
||||
_sz=$5; _dt="$6 $7"
|
||||
_detail=$(printf ' %6s %-6s' "$_sz" "$_dt")
|
||||
set --
|
||||
fi
|
||||
|
||||
# layout: marker(1) + name + spaces + detail
|
||||
_dlen=${#_detail}
|
||||
maxw=$((_cols - 1 - _dlen - 1))
|
||||
maxw=$((_cols - 2 - _dlen))
|
||||
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}..."
|
||||
fi
|
||||
_namew=$((${#display} + 1)) # 1 for marker
|
||||
@@ -468,7 +440,7 @@ draw_preview() {
|
||||
_dl=" ${_dn}/"
|
||||
[ "${#_dl}" -gt "$((_pw - 1))" ] && _dl="$(printf '%s' "$_dl" | cut -c1-$((_pw-2)))~"
|
||||
goto "$_pr" "$_px"
|
||||
printf '%s%s%s' "${GREEN}${BOLD}" "$_dl" "${RESET}"
|
||||
printf '%s%s%s' "${BLUE}${BOLD}" "$_dl" "${RESET}"
|
||||
_pr=$((_pr + 1))
|
||||
done
|
||||
for _fe in "$_path"/*; do
|
||||
@@ -681,15 +653,21 @@ draw() {
|
||||
read_key() {
|
||||
IFS= read -r -n1 key 2>/dev/null || IFS= read -r key
|
||||
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=""
|
||||
if [ -z "$_k2" ]; then
|
||||
# nothing followed — bare esc
|
||||
printf '\033'; return
|
||||
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=""
|
||||
# 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
|
||||
printf '%s' "$key"
|
||||
}
|
||||
@@ -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
|
||||
@@ -891,7 +871,7 @@ do_help() {
|
||||
bw=$((COLS - 4))
|
||||
[ "$bw" -gt 52 ] && bw=52
|
||||
[ "$bw" -lt 36 ] && bw=36
|
||||
bh=42
|
||||
bh=46
|
||||
_help_bx=$(( (COLS - bw) / 2 )); [ "$_help_bx" -lt 1 ] && _help_bx=1
|
||||
_help_by=$(( (ROWS - bh) / 2 )); [ "$_help_by" -lt 1 ] && _help_by=1
|
||||
_help_iw=$((bw - 2))
|
||||
@@ -929,17 +909,21 @@ do_help() {
|
||||
help_row 27 " U open trash directory"
|
||||
help_row 28 " ! drop to shell in CWD"
|
||||
help_row 29 " o open with custom program"
|
||||
help_sep 30
|
||||
help_row 31 " b bookmark current dir"
|
||||
help_row 32 " B open bookmark picker"
|
||||
help_row 33 " c copy path to clipboard"
|
||||
help_row 34 " ~ go to home directory"
|
||||
help_row 35 " \` jump to previous directory"
|
||||
help_sep 36
|
||||
help_row 37 " q quit"
|
||||
help_row 38 " ? this help"
|
||||
help_row 39 ""
|
||||
help_row 40 " press any key to close..."
|
||||
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 35 " c copy path to clipboard"
|
||||
help_row 36 " ~ go to home directory"
|
||||
help_row 37 " \` jump to previous directory"
|
||||
help_row 38 " : jump to path"
|
||||
help_row 39 " f find files recursively"
|
||||
help_sep 40
|
||||
help_row 41 " q quit"
|
||||
help_row 42 " ? this help"
|
||||
help_row 43 ""
|
||||
help_row 44 " press any key to close..."
|
||||
|
||||
goto "$(( _help_by + bh ))" "$_help_bx"
|
||||
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}"
|
||||
@@ -1020,6 +1004,141 @@ do_toggle_preview() {
|
||||
NEED_FULL_REDRAW=1
|
||||
}
|
||||
|
||||
do_find() {
|
||||
goto "$(term_rows)" 1
|
||||
printf '%s%s find (recursive): %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}"
|
||||
if ! read_line || [ -z "$READ_LINE" ]; then
|
||||
NEED_FULL_REDRAW=1; draw; return
|
||||
fi
|
||||
_query="$READ_LINE"
|
||||
|
||||
# run find and collect results into a tmp file
|
||||
_tmp=/tmp/_sfm_find_$$
|
||||
find "$CWD" -name "*${_query}*" 2>/dev/null | sort > "$_tmp"
|
||||
_total=$(wc -l < "$_tmp" | tr -d '[:space:]')
|
||||
|
||||
if [ "$_total" -eq 0 ]; then
|
||||
INFO_MSG="no results for: ${_query}"
|
||||
NEED_FULL_REDRAW=1; return
|
||||
fi
|
||||
|
||||
# show results in an interactive picker
|
||||
ROWS=$(term_rows); COLS=$(term_cols)
|
||||
_pw=$(( COLS - 4 )); [ "$_pw" -gt 80 ] && _pw=80; [ "$_pw" -lt 30 ] && _pw=30
|
||||
_ph=$(( ROWS - 4 )); [ "$_ph" -lt 5 ] && _ph=5
|
||||
_px=$(( (COLS - _pw) / 2 )); [ "$_px" -lt 1 ] && _px=1
|
||||
_py=$(( (ROWS - _ph) / 2 )); [ "$_py" -lt 1 ] && _py=1
|
||||
_iw=$((_pw - 2))
|
||||
_hl=$(printf '%*s' "$_iw" '' | tr ' ' '-')
|
||||
_vis=$((_ph - 3)) # visible result rows
|
||||
|
||||
_fsel=1
|
||||
_foff=1 # scroll offset (1-based)
|
||||
|
||||
while true; do
|
||||
# draw box
|
||||
goto "$_py" "$_px"
|
||||
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
|
||||
|
||||
# title
|
||||
goto "$((_py+1))" "$_px"
|
||||
_title=" find: ${_query} (${_total} results)"
|
||||
[ "${#_title}" -gt "$_iw" ] && _title=$(printf '%s' "$_title" | cut -c1-"$_iw")
|
||||
_tpl=$((_iw - ${#_title})); _tp=$(printf '%*s' "$_tpl" '')
|
||||
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_title" "$_tp" "${BOLD}${CYAN}" "${RESET}"
|
||||
|
||||
goto "$((_py+2))" "$_px"
|
||||
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
|
||||
|
||||
# results
|
||||
_ri=0
|
||||
while [ "$_ri" -lt "$_vis" ]; do
|
||||
_ridx=$((_foff + _ri))
|
||||
goto "$((_py+3+_ri))" "$_px"
|
||||
if [ "$_ridx" -le "$_total" ]; then
|
||||
_rline=$(awk -v n="$_ridx" 'NR==n{print;exit}' "$_tmp")
|
||||
# strip CWD prefix for display
|
||||
_rdisplay="${_rline#$CWD/}"
|
||||
[ "${#_rdisplay}" -gt "$_iw" ] && \
|
||||
_rdisplay="...$(printf '%s' "$_rdisplay" | rev | cut -c1-$((_iw-4)) | rev)"
|
||||
_rpl=$((_iw - ${#_rdisplay})); _rp=$(printf '%*s' "$_rpl" '')
|
||||
if [ "$_ridx" -eq "$_fsel" ]; then
|
||||
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${REV}${YELLOW}" "$_rdisplay" "$_rp" "${RESET}${BOLD}${CYAN}" "${RESET}"
|
||||
else
|
||||
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_rdisplay" "$_rp" "${BOLD}${CYAN}" "${RESET}"
|
||||
fi
|
||||
else
|
||||
_ep=$(printf '%*s' "$_iw" '')
|
||||
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_ep" "${RESET}"
|
||||
fi
|
||||
_ri=$((_ri+1))
|
||||
done
|
||||
|
||||
goto "$((_py+_ph))" "$_px"
|
||||
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
|
||||
|
||||
# read key
|
||||
IFS= read -r -n1 _fk 2>/dev/null || IFS= read -r _fk
|
||||
case "$_fk" in
|
||||
j) _fsel=$((_fsel+1)); [ "$_fsel" -gt "$_total" ] && _fsel=$_total
|
||||
[ "$_fsel" -ge $((_foff+_vis)) ] && _foff=$((_foff+1)) ;;
|
||||
k) _fsel=$((_fsel-1)); [ "$_fsel" -lt 1 ] && _fsel=1
|
||||
[ "$_fsel" -lt "$_foff" ] && _foff=$((_foff-1)); [ "$_foff" -lt 1 ] && _foff=1 ;;
|
||||
"$(printf '\033')")
|
||||
IFS= read -r -n2 -t 0.1 _fseq 2>/dev/null || _fseq=""
|
||||
case "$_fseq" in
|
||||
'[A') _fsel=$((_fsel-1)); [ "$_fsel" -lt 1 ] && _fsel=1
|
||||
[ "$_fsel" -lt "$_foff" ] && _foff=$((_foff-1)); [ "$_foff" -lt 1 ] && _foff=1 ;;
|
||||
'[B') _fsel=$((_fsel+1)); [ "$_fsel" -gt "$_total" ] && _fsel=$_total
|
||||
[ "$_fsel" -ge $((_foff+_vis)) ] && _foff=$((_foff+1)) ;;
|
||||
*) NEED_FULL_REDRAW=1; return ;; # esc — close
|
||||
esac ;;
|
||||
"$(printf '\n')"|\
|
||||
"$(printf '\r')"|\
|
||||
l)
|
||||
_chosen=$(awk -v n="$_fsel" 'NR==n{print;exit}' "$_tmp")
|
||||
if [ -d "$_chosen" ]; then
|
||||
PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
|
||||
elif [ -e "$_chosen" ]; then
|
||||
PREV_CWD="$CWD"; CWD=$(dirname "$_chosen"); norm_cwd
|
||||
fi
|
||||
FILTER=""; SELECTED=""
|
||||
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
||||
load_entries
|
||||
# try to highlight the found file
|
||||
_fname="${_chosen##*/}"
|
||||
[ -d "$_chosen" ] && _fname="${_fname}/"
|
||||
_fidx=$(find_entry "$_fname")
|
||||
[ "$_fidx" -ge 0 ] 2>/dev/null && SEL=$_fidx
|
||||
return ;;
|
||||
q|Q|h) NEED_FULL_REDRAW=1; return ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
do_jump_path() {
|
||||
goto "$(term_rows)" 1
|
||||
printf '%s%s jump to: %s' "${ERASE_LINE}" "${CYAN}${BOLD}" "${RESET}"
|
||||
if read_line && [ -n "$READ_LINE" ]; then
|
||||
# expand ~ manually
|
||||
case "$READ_LINE" in
|
||||
"~"*) READ_LINE="${HOME}${READ_LINE#\~}" ;;
|
||||
esac
|
||||
if [ -d "$READ_LINE" ]; then
|
||||
PREV_CWD="$CWD"
|
||||
CWD="$READ_LINE"; norm_cwd
|
||||
FILTER=""; SELECTED=""
|
||||
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
||||
load_entries
|
||||
else
|
||||
INFO_MSG="not found: ${READ_LINE}"
|
||||
NEED_FULL_REDRAW=1; draw
|
||||
fi
|
||||
else
|
||||
NEED_FULL_REDRAW=1; draw
|
||||
fi
|
||||
}
|
||||
|
||||
do_go_back() {
|
||||
[ "$CWD" = "/" ] && return
|
||||
FILTER=""
|
||||
@@ -1097,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%%
|
||||
@@ -1119,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
|
||||
@@ -1181,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
|
||||
@@ -1190,18 +1376,125 @@ do_mkdir() {
|
||||
fi
|
||||
}
|
||||
|
||||
do_chmod_x() {
|
||||
entry=$(get_entry "$SEL")
|
||||
[ -z "$entry" ] && return
|
||||
case "$entry" in */) INFO_MSG="cannot chmod a directory"; NEED_FULL_REDRAW=1; return ;; esac
|
||||
_target=$(joinpath "${entry%@}")
|
||||
if chmod +x "$_target" 2>/dev/null; then
|
||||
INFO_MSG="chmod +x: ${entry}"
|
||||
else
|
||||
INFO_MSG="chmod failed: ${entry}"
|
||||
fi
|
||||
NEED_FULL_REDRAW=1
|
||||
}
|
||||
|
||||
do_chmod_nox() {
|
||||
entry=$(get_entry "$SEL")
|
||||
[ -z "$entry" ] && return
|
||||
case "$entry" in */) INFO_MSG="cannot chmod a directory"; NEED_FULL_REDRAW=1; return ;; esac
|
||||
_target=$(joinpath "${entry%@}")
|
||||
if chmod -x "$_target" 2>/dev/null; then
|
||||
INFO_MSG="chmod -x: ${entry}"
|
||||
else
|
||||
INFO_MSG="chmod failed: ${entry}"
|
||||
fi
|
||||
NEED_FULL_REDRAW=1
|
||||
}
|
||||
|
||||
do_info() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1231,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))
|
||||
@@ -1256,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
|
||||
@@ -1314,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"
|
||||
@@ -1323,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" '')
|
||||
@@ -1341,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
|
||||
@@ -1424,7 +1737,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"
|
||||
@@ -1432,7 +1746,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
|
||||
@@ -1444,7 +1759,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"
|
||||
@@ -1452,7 +1768,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
|
||||
@@ -1465,7 +1782,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##*/}"
|
||||
@@ -1476,8 +1792,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=""
|
||||
@@ -1486,7 +1802,7 @@ do_paste() {
|
||||
}
|
||||
|
||||
# --- 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
|
||||
hide_cursor
|
||||
@@ -1506,6 +1822,7 @@ while true; do
|
||||
G) SEL=$((COUNT - 1)) ;;
|
||||
"$(printf '\033[6~')") SEL=$((SEL + $(term_rows) / 2)) ;;
|
||||
"$(printf '\033[5~')") SEL=$((SEL - $(term_rows) / 2)) ;;
|
||||
"$(printf '\033[3~')") do_delete ;;
|
||||
"$(printf '\n')"|\
|
||||
"$(printf '\r')"|\
|
||||
"$(printf '\033[C')"|\
|
||||
@@ -1521,12 +1838,14 @@ while true; do
|
||||
T) do_toggle_details ;;
|
||||
P) do_toggle_preview ;;
|
||||
i) do_info ;;
|
||||
'!') do_shell ;;
|
||||
'+') do_chmod_x ;;
|
||||
'-') do_chmod_nox ;;
|
||||
o) do_open_with ;;
|
||||
s) do_sort ;;
|
||||
u) do_trash ;;
|
||||
U) do_open_trash ;;
|
||||
'`') do_jump_back ;;
|
||||
f) do_find ;;
|
||||
':') do_jump_path ;;
|
||||
'~') PREV_CWD="$CWD"; CWD=$(cd "$HOME" && pwd); FILTER=""; SELECTED=""
|
||||
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0; load_entries ;;
|
||||
c) do_copy_path ;;
|
||||
@@ -1541,6 +1860,7 @@ while true; do
|
||||
m) do_mkdir ;;
|
||||
n) do_newfile ;;
|
||||
q|Q) break ;;
|
||||
'!') do_shell ;;
|
||||
esac
|
||||
done
|
||||
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
.TH SFM 1 "2026-03-16" "0.4" "Simple File Manager"
|
||||
|
||||
.SH NAME
|
||||
sfm \- Simple File Manager
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B sfm
|
||||
.RI [ path ]
|
||||
|
||||
.SH DESCRIPTION
|
||||
.B sfm
|
||||
is a lightweight, terminal-based file manager written entirely in POSIX sh.
|
||||
It runs in your terminal with no external dependencies beyond standard Unix
|
||||
tools. Designed to be fast, flicker-free, and keyboard-driven with a
|
||||
vim-inspired key layout.
|
||||
|
||||
If
|
||||
.I path
|
||||
is given, sfm opens that directory. Otherwise it opens in the current
|
||||
working directory.
|
||||
|
||||
.SH KEY BINDINGS
|
||||
|
||||
.SS Navigation
|
||||
.TP
|
||||
.BR j ", " k " or " "up/down arrows"
|
||||
Move cursor up or down.
|
||||
.TP
|
||||
.BR h " or " "left arrow"
|
||||
Go to parent directory.
|
||||
.TP
|
||||
.BR l ", " "right arrow" ", or " enter
|
||||
Open selected file or enter directory.
|
||||
.TP
|
||||
.B g
|
||||
Jump to top of list.
|
||||
.TP
|
||||
.B G
|
||||
Jump to bottom of list.
|
||||
.TP
|
||||
.B ~
|
||||
Go to home directory.
|
||||
.TP
|
||||
.B \`
|
||||
Jump to previous directory (toggle).
|
||||
.TP
|
||||
.B :
|
||||
Prompt for a path to jump to directly. Supports
|
||||
.B ~
|
||||
expansion. Shows an error if the path does not exist. Press esc to cancel.
|
||||
.TP
|
||||
.B f
|
||||
Find files recursively under the current directory. Opens an interactive
|
||||
picker. Press j/k or arrows to navigate results, enter or l to jump to the
|
||||
selected result, esc/q/h to close.
|
||||
|
||||
.SS Search and Filter
|
||||
.TP
|
||||
.B /
|
||||
Enter search mode. The listing is filtered as you type.
|
||||
.TP
|
||||
.B esc
|
||||
Clear filter and exit search mode.
|
||||
.TP
|
||||
.B enter
|
||||
Exit search mode while keeping the current filter active.
|
||||
|
||||
.SS Display Toggles
|
||||
.TP
|
||||
.B .
|
||||
Toggle hidden files (dotfiles). Hidden entries appear before regular
|
||||
entries (hidden dirs, then regular dirs, then hidden files, then
|
||||
regular files).
|
||||
.TP
|
||||
.B T
|
||||
Toggle size/date detail column.
|
||||
.TP
|
||||
.B P
|
||||
Toggle preview pane on the right side.
|
||||
.TP
|
||||
.B s
|
||||
Cycle sort mode: name \-> size \-> date.
|
||||
.TP
|
||||
.B i
|
||||
Show file info (permissions, size, date) in the status bar.
|
||||
.TP
|
||||
.B R
|
||||
Refresh the current directory listing.
|
||||
|
||||
.SS File Operations
|
||||
.TP
|
||||
.B r
|
||||
Rename selected entry. Press esc to cancel.
|
||||
.TP
|
||||
.B m
|
||||
Create a new directory. Press esc to cancel.
|
||||
.TP
|
||||
.B n
|
||||
Create a new empty file. Press esc to cancel.
|
||||
.TP
|
||||
.B d " or " del
|
||||
Delete selected entry or all multi-selected entries.
|
||||
Non-empty directories require a second confirmation.
|
||||
.TP
|
||||
.B u
|
||||
Move selected entry to trash (safe delete).
|
||||
.TP
|
||||
.B U
|
||||
Open the trash directory.
|
||||
.TP
|
||||
.B o
|
||||
Open selected file with a custom program. Press esc to cancel.
|
||||
.TP
|
||||
.B +
|
||||
Make the selected file executable
|
||||
.RB ( "chmod +x" ).
|
||||
.TP
|
||||
.B \-
|
||||
Remove execute permission from the selected file
|
||||
.RB ( "chmod -x" ).
|
||||
|
||||
.SS Clipboard
|
||||
.TP
|
||||
.B y
|
||||
Yank (copy) selected entry or all multi-selected entries.
|
||||
.TP
|
||||
.B x
|
||||
Cut selected entry or all multi-selected entries.
|
||||
.TP
|
||||
.B p
|
||||
Paste clipboard contents into the current directory.
|
||||
Auto-renames on conflict by appending
|
||||
.IR _copy .
|
||||
.TP
|
||||
.B c
|
||||
Copy the full path of the selected entry to the system clipboard.
|
||||
Requires
|
||||
.BR wl-copy ", " xclip ", " xsel ", or " pbcopy .
|
||||
|
||||
.SS Multi-Select
|
||||
.TP
|
||||
.B space
|
||||
Toggle multi-select on the current entry (cursor advances).
|
||||
.TP
|
||||
.B a
|
||||
Select all entries / deselect all.
|
||||
.PP
|
||||
Multi-select works with
|
||||
.BR y ", " x ", and " d .
|
||||
|
||||
.SS Bookmarks
|
||||
.TP
|
||||
.B b
|
||||
Bookmark the current directory. Press again to remove it.
|
||||
.TP
|
||||
.B B
|
||||
Open the bookmark picker.
|
||||
Use j/k to navigate, enter to jump, esc to close.
|
||||
|
||||
.SS Other
|
||||
.TP
|
||||
.B !
|
||||
Drop into
|
||||
.B $SHELL
|
||||
in the current directory. Type
|
||||
.I exit
|
||||
to return to sfm. The working directory is updated if you cd elsewhere
|
||||
in the shell.
|
||||
.TP
|
||||
.B ?
|
||||
Show the keyboard shortcuts help overlay.
|
||||
.TP
|
||||
.B q
|
||||
Quit sfm.
|
||||
|
||||
.SH PREVIEW PANE
|
||||
When the preview pane is enabled with
|
||||
.BR P ,
|
||||
the terminal is split into a left pane (file list) and a right pane
|
||||
(preview). The preview shows:
|
||||
.IP \(bu 2
|
||||
.B Text files \-
|
||||
file contents displayed line by line.
|
||||
.IP \(bu 2
|
||||
.B Directories \-
|
||||
directory contents (subdirectories first, then files).
|
||||
.IP \(bu 2
|
||||
.B Symlinks \-
|
||||
the link target path.
|
||||
.IP \(bu 2
|
||||
.B Binary files \-
|
||||
file size.
|
||||
|
||||
.SH CUSTOM OPENER
|
||||
.B sfm
|
||||
checks for a user-defined opener script at:
|
||||
.PP
|
||||
.I ~/.config/sfm/opener
|
||||
.PP
|
||||
If the file exists and is executable, sfm passes the selected file path
|
||||
to it as the first argument instead of using the built-in smart opener.
|
||||
.PP
|
||||
Example opener script:
|
||||
.PP
|
||||
.nf
|
||||
#!/bin/sh
|
||||
case "$1" in
|
||||
*.jpg|*.jpeg) imv "$1" ;;
|
||||
*.mp4) mpv "$1" ;;
|
||||
*.html|*.pdf) firefox "$1" ;;
|
||||
*) echo "no program set for this file type" ;;
|
||||
esac
|
||||
.fi
|
||||
.PP
|
||||
Make it executable with:
|
||||
.B chmod +x ~/.config/sfm/opener
|
||||
.PP
|
||||
If the opener script does not exist, sfm falls back to its built-in
|
||||
smart opener which detects file types by extension and MIME type.
|
||||
|
||||
.SH SMART FILE OPENER
|
||||
When no custom opener is configured, sfm opens files using the first
|
||||
available program for each file type:
|
||||
.TP
|
||||
.B Text / code
|
||||
$EDITOR or vi.
|
||||
.TP
|
||||
.B Images
|
||||
imv, feh, sxiv, eog, gimp.
|
||||
.TP
|
||||
.B Video
|
||||
mpv, vlc, mplayer, totem.
|
||||
.TP
|
||||
.B Audio
|
||||
mpv, vlc, cmus, mocp.
|
||||
.TP
|
||||
.B PDF
|
||||
zathura, evince, okular, mupdf.
|
||||
.TP
|
||||
.B Office documents
|
||||
libreoffice.
|
||||
.TP
|
||||
.B Archives
|
||||
atool or bsdtar (lists contents in pager).
|
||||
.PP
|
||||
Falls back to xdg-open, open (macOS), or $EDITOR.
|
||||
|
||||
.SH FILE COLOURS
|
||||
.TP
|
||||
.B Green bold
|
||||
Directories.
|
||||
.TP
|
||||
.B Red bold
|
||||
Executables and broken symlinks.
|
||||
.TP
|
||||
.B Magenta
|
||||
Images.
|
||||
.TP
|
||||
.B Magenta bold
|
||||
Video files and valid symlinks.
|
||||
.TP
|
||||
.B Cyan bold
|
||||
Audio files.
|
||||
.TP
|
||||
.B Red
|
||||
Archives (zip, tar, gz, etc.).
|
||||
.TP
|
||||
.B Yellow
|
||||
Documents (pdf, docx, etc.).
|
||||
.TP
|
||||
.B Green
|
||||
Code and scripts (py, sh, rs, etc.).
|
||||
.TP
|
||||
.B Cyan
|
||||
Config and data files (json, yaml, etc.).
|
||||
.TP
|
||||
.B White
|
||||
Text, markdown, and all other files.
|
||||
|
||||
.SH STATUS BAR INDICATORS
|
||||
.TP
|
||||
.B [hidden]
|
||||
Hidden (dot) files are currently visible.
|
||||
.TP
|
||||
.B [sort:size] or [sort:date]
|
||||
Active sort mode. Not shown when sorting by name (default).
|
||||
.TP
|
||||
.B [copy] or [cut]
|
||||
A file is stored in the clipboard.
|
||||
.TP
|
||||
.B [sel:N]
|
||||
N items are currently multi-selected.
|
||||
.TP
|
||||
.B [details]
|
||||
Size/date detail column is visible.
|
||||
|
||||
.SH TRASH
|
||||
Files deleted with
|
||||
.B u
|
||||
are moved to:
|
||||
.PP
|
||||
.I ~/.local/share/sfm-trash/
|
||||
.PP
|
||||
Files are prefixed with a timestamp in the format
|
||||
.IR YYYYMMDD_HHMMSS_filename .
|
||||
Use
|
||||
.B U
|
||||
to browse the trash directory. To restore a file, rename it with
|
||||
.B r
|
||||
to remove the timestamp prefix, then cut with
|
||||
.B x
|
||||
and paste with
|
||||
.B p
|
||||
to the desired location.
|
||||
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.config/sfm/bookmarks
|
||||
Saved bookmarks, one directory path per line.
|
||||
.TP
|
||||
.I ~/.config/sfm/opener
|
||||
Custom file opener script (optional, must be executable).
|
||||
.TP
|
||||
.I ~/.local/share/sfm-trash/
|
||||
Directory where trashed files are stored.
|
||||
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B EDITOR
|
||||
Editor used to open text files when no custom opener is set.
|
||||
.TP
|
||||
.B SHELL
|
||||
Shell used when dropping to the terminal with
|
||||
.BR ! .
|
||||
.TP
|
||||
.B PAGER
|
||||
Pager used when listing archive contents.
|
||||
.TP
|
||||
.B XDG_CONFIG_HOME
|
||||
Base directory for sfm config files. Defaults to
|
||||
.IR ~/.config .
|
||||
.TP
|
||||
.B XDG_DATA_HOME
|
||||
Base directory for sfm data files (trash). Defaults to
|
||||
.IR ~/.local/share .
|
||||
|
||||
.SH EXAMPLES
|
||||
Open sfm in the current directory:
|
||||
.PP
|
||||
.nf
|
||||
sfm
|
||||
.fi
|
||||
.PP
|
||||
Open sfm at a specific path:
|
||||
.PP
|
||||
.nf
|
||||
sfm /mnt/data
|
||||
.fi
|
||||
.PP
|
||||
Use sfm as a cd wrapper (add to shell rc):
|
||||
.PP
|
||||
.nf
|
||||
s() { cd "$(sfm)"; }
|
||||
.fi
|
||||
|
||||
.SH AUTHORS
|
||||
Emmett1 <me@emmett1.my> with help of AI.
|
||||
|
||||
.SH SEE ALSO
|
||||
.BR find (1),
|
||||
.BR ls (1),
|
||||
.BR mv (1),
|
||||
.BR cp (1),
|
||||
.BR rm (1),
|
||||
.BR chmod (1),
|
||||
.BR vi (1)
|
||||
Reference in New Issue
Block a user