1871 lines
60 KiB
Bash
Executable File
1871 lines
60 KiB
Bash
Executable File
#!/bin/sh
|
|
# sfm - Simple File Manager in POSIX sh
|
|
|
|
# --- terminal control ---
|
|
tput_cmd() { command -v tput >/dev/null 2>&1 && tput "$@"; }
|
|
|
|
RESET=$(tput_cmd sgr0)
|
|
BOLD=$(tput_cmd bold)
|
|
REV=$(tput_cmd rev)
|
|
RED=$(tput_cmd setaf 1)
|
|
GREEN=$(tput_cmd setaf 2)
|
|
YELLOW=$(tput_cmd setaf 3)
|
|
CYAN=$(tput_cmd setaf 6)
|
|
WHITE=$(tput_cmd setaf 7)
|
|
MAGENTA=$(tput_cmd setaf 5)
|
|
BLUE=$(tput_cmd setaf 4)
|
|
ERASE_LINE=$(tput_cmd el || printf '\033[K')
|
|
|
|
goto() { printf '\033[%d;%dH' "$1" "$2"; }
|
|
hide_cursor() { printf '\033[?25l'; }
|
|
show_cursor() { printf '\033[?25h'; }
|
|
term_rows() { tput_cmd lines || echo 24; }
|
|
term_cols() { tput_cmd cols || echo 80; }
|
|
|
|
# --- state ---
|
|
if [ -n "$1" ]; then
|
|
CWD=$(cd "$1" 2>/dev/null && pwd) || { printf 'sfm: %s: no such directory\n' "$1" >&2; exit 1; }
|
|
else
|
|
CWD=$(cd "$PWD" 2>/dev/null && pwd) || CWD="/"
|
|
fi
|
|
SEL=0
|
|
OFFSET=0
|
|
PREV_SEL=0
|
|
PREV_OFFSET=0
|
|
NEED_FULL_REDRAW=1
|
|
LAST_CHILD="" # name of dir we came from, used to restore selection on go-back
|
|
FILTER="" # current search string; empty = no filter
|
|
ALL_ENTRIES="" # unfiltered entries
|
|
ALL_COUNT=0
|
|
SEARCHING=0 # 1 while in search input mode
|
|
SELECTED="" # newline-separated list of multi-selected entry names
|
|
CLIPBOARD="" # full path of yanked/cut entry
|
|
CLIP_MODE="" # "copy" or "cut"
|
|
INFO_MSG="" # ephemeral message shown in botbar
|
|
SHOW_HIDDEN=0 # 1 = show dotfiles
|
|
SORT_MODE="name" # name | size | date
|
|
SHOW_DETAILS=0 # 1 = show size+date column
|
|
SHOW_PREVIEW=0 # 1 = show preview pane on right
|
|
TRASH_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/sfm-trash"
|
|
mkdir -p "$TRASH_DIR"
|
|
PREV_CWD="" # last visited directory, for jumping back
|
|
|
|
# normalise CWD: resolve to absolute canonical path, no double slashes
|
|
norm_cwd() {
|
|
CWD=$(cd "$CWD" 2>/dev/null && pwd) || CWD="/"
|
|
# squeeze any double slashes (some systems return // for //<path>)
|
|
CWD=$(printf '%s' "$CWD" | tr -s '/')
|
|
}
|
|
|
|
# safely join CWD with a name, avoiding double slash at root
|
|
joinpath() { [ "$CWD" = "/" ] && printf '/%s' "$1" || printf '%s/%s' "$CWD" "$1"; }
|
|
BOOKMARK_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/sfm/bookmarks"
|
|
mkdir -p "$(dirname "$BOOKMARK_FILE")"
|
|
[ -f "$BOOKMARK_FILE" ] || touch "$BOOKMARK_FILE"
|
|
|
|
# --- load directory ---
|
|
load_entries() {
|
|
ENTRIES=""
|
|
COUNT=0
|
|
|
|
# hidden dirs first
|
|
if [ "$SHOW_HIDDEN" = "1" ]; then
|
|
for d in "$CWD"/.*/; do
|
|
[ -d "$d" ] || continue
|
|
[ -L "${d%/}" ] && continue
|
|
name="${d%/}"; name="${name##*/}"
|
|
[ "$name" = "." ] || [ "$name" = ".." ] && continue
|
|
[ "$name" = ".*" ] && continue
|
|
ENTRIES="$ENTRIES
|
|
${name}/"
|
|
COUNT=$((COUNT + 1))
|
|
done
|
|
fi
|
|
|
|
# regular dirs
|
|
for d in "$CWD"/*/; do
|
|
[ -d "$d" ] || continue
|
|
[ -L "${d%/}" ] && continue
|
|
name="${d%/}"; name="${name##*/}"
|
|
[ "$name" = "*" ] && continue
|
|
case "$name" in .*) continue ;; esac
|
|
ENTRIES="$ENTRIES
|
|
${name}/"
|
|
COUNT=$((COUNT + 1))
|
|
done
|
|
|
|
# hidden files first
|
|
if [ "$SHOW_HIDDEN" = "1" ]; then
|
|
for f in "$CWD"/.*; do
|
|
[ -e "$f" ] || [ -L "$f" ] || continue
|
|
[ -d "$f" ] && ! [ -L "$f" ] && continue
|
|
name="${f##*/}"
|
|
[ "$name" = ".*" ] && continue
|
|
if [ -L "$f" ]; then
|
|
ENTRIES="$ENTRIES
|
|
${name}@"
|
|
else
|
|
ENTRIES="$ENTRIES
|
|
${name}"
|
|
fi
|
|
COUNT=$((COUNT + 1))
|
|
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#
|
|
}"
|
|
|
|
# --- sort files only (dirs always stay first) ---
|
|
if [ "$SORT_MODE" != "name" ]; then
|
|
# separate dirs and files
|
|
_dirs=""; _files=""
|
|
_rest="$ENTRIES"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"; _next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -z "$_line" ] && continue
|
|
case "$_line" in
|
|
*/) if [ -z "$_dirs" ]; then _dirs="$_line"
|
|
else _dirs="$_dirs
|
|
$_line"; fi ;;
|
|
*) if [ -z "$_files" ]; then _files="$_line"
|
|
else _files="$_files
|
|
$_line"; fi ;;
|
|
esac
|
|
done
|
|
|
|
# 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_$$ ;;
|
|
esac
|
|
# only keep files that were already in our list (respects hidden filter)
|
|
_sorted=""
|
|
while IFS= read -r _n; do
|
|
[ -z "$_n" ] && continue
|
|
# check if _n is in _files
|
|
case "
|
|
${_files}
|
|
" in *"
|
|
${_n}
|
|
"*) if [ -z "$_sorted" ]; then _sorted="$_n"
|
|
else _sorted="$_sorted
|
|
$_n"; fi ;;
|
|
esac
|
|
done < /tmp/_fm_sorted_$$
|
|
_files="$_sorted"
|
|
fi
|
|
|
|
# reassemble: dirs + sorted files
|
|
ENTRIES=""
|
|
_rest="$_dirs"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"; _next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -z "$_line" ] && continue
|
|
if [ -z "$ENTRIES" ]; then ENTRIES="$_line"
|
|
else ENTRIES="$ENTRIES
|
|
$_line"; fi
|
|
done
|
|
_rest="$_files"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"; _next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -z "$_line" ] && continue
|
|
if [ -z "$ENTRIES" ]; then ENTRIES="$_line"
|
|
else ENTRIES="$ENTRIES
|
|
$_line"; fi
|
|
done
|
|
# recount
|
|
COUNT=0
|
|
_rest="$ENTRIES"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"; _next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -n "$_line" ] && COUNT=$((COUNT + 1))
|
|
done
|
|
fi
|
|
|
|
ALL_ENTRIES="$ENTRIES"
|
|
ALL_COUNT="$COUNT"
|
|
apply_filter
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
# filter ALL_ENTRIES by FILTER string, update ENTRIES/COUNT
|
|
apply_filter() {
|
|
if [ -z "$FILTER" ]; then
|
|
ENTRIES="$ALL_ENTRIES"
|
|
COUNT="$ALL_COUNT"
|
|
else
|
|
ENTRIES=""
|
|
COUNT=0
|
|
_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() {
|
|
sed -n "$(($1 + 1))p" /tmp/_sfm_list_$$
|
|
}
|
|
|
|
# find index of entry by name (0-based), returns -1 if not found
|
|
find_entry() {
|
|
_fe_n=$(grep -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() {
|
|
printf '%s\n' "$SELECTED" | grep -Fxq "$1"
|
|
}
|
|
|
|
count_selected() {
|
|
_cnt=0
|
|
_rest="$SELECTED"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"
|
|
_rest="${_rest#*
|
|
}"
|
|
[ "$_rest" = "$_line" ] && _rest="" # no more newlines
|
|
[ -n "$_line" ] && _cnt=$((_cnt + 1))
|
|
done
|
|
printf '%d' "$_cnt"
|
|
}
|
|
|
|
|
|
toggle_selected() {
|
|
if is_selected "$1"; then
|
|
_new=""
|
|
_rest="$SELECTED"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"
|
|
_next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ "$_line" = "$1" ] && continue
|
|
[ -z "$_line" ] && continue
|
|
if [ -z "$_new" ]; then _new="$_line"
|
|
else _new="$_new
|
|
$_line"
|
|
fi
|
|
done
|
|
SELECTED="$_new"
|
|
else
|
|
if [ -z "$SELECTED" ]; then
|
|
SELECTED="$1"
|
|
else
|
|
SELECTED="$SELECTED
|
|
$1"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# render one entry row in-place (no newline, no cursor movement after)
|
|
# args: row idx is_selected cols
|
|
render_row() {
|
|
_row=$1; _idx=$2; _selected=$3; _cols=$4
|
|
entry=$(get_entry "$_idx")
|
|
|
|
case "$entry" in
|
|
*/) colour="${BLUE}${BOLD}" ;;
|
|
*@)
|
|
_name="${entry%@}"
|
|
if [ -e "${CWD}/${_name}" ]; then
|
|
colour="${CYAN}${BOLD}"
|
|
else
|
|
colour="${RED}${BOLD}"
|
|
fi ;;
|
|
*)
|
|
_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
|
|
if is_selected "$entry"; then
|
|
_marker="${YELLOW}*${colour}"
|
|
else
|
|
_marker=" "
|
|
fi
|
|
|
|
# build display string
|
|
case "$entry" in
|
|
*@)
|
|
_name="${entry%@}"
|
|
_target=$(readlink "${CWD}/${_name}" 2>/dev/null || echo "?")
|
|
# mark broken symlinks clearly
|
|
if [ ! -e "${CWD}/${_name}" ]; then
|
|
display="${_name} -> ${_target} [broken]"
|
|
else
|
|
display="${_name} -> ${_target}"
|
|
fi
|
|
;;
|
|
*)
|
|
display="${entry}"
|
|
;;
|
|
esac
|
|
|
|
# build detail string (size + date) if enabled — fixed width for alignment
|
|
_detail=""
|
|
if [ "$SHOW_DETAILS" = "1" ]; then
|
|
_path="${CWD}/${entry%/}"; _path="${_path%@}"
|
|
_info=$(ls -ldh "$_path" 2>/dev/null)
|
|
# 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 - 2 - _dlen))
|
|
if [ "${#display}" -gt "$maxw" ]; then
|
|
_trunc=$((maxw - 3))
|
|
while [ "${#display}" -gt "$_trunc" ]; do
|
|
display="${display%?}"
|
|
done
|
|
display="${display}..."
|
|
fi
|
|
_namew=$((${#display} + 1)) # 1 for marker
|
|
padlen=$((_cols - _namew - _dlen))
|
|
[ "$padlen" -lt 0 ] && padlen=0
|
|
spaces=$(printf '%*s' "$padlen" '')
|
|
|
|
goto "$_row" 1
|
|
if [ "$_selected" = "1" ]; then
|
|
printf '%s%s%s%s%s%s%s' \
|
|
"${REV}${colour}" "${_marker}" "${display}" \
|
|
"${spaces}" \
|
|
"${colour}${_detail}" \
|
|
"${RESET}" ""
|
|
else
|
|
printf '%s%s%s%s%s%s%s' \
|
|
"${colour}" "${_marker}" "${display}" \
|
|
"${spaces}" \
|
|
"${colour}${_detail}" \
|
|
"${RESET}" ""
|
|
fi
|
|
}
|
|
|
|
draw_preview() {
|
|
_rows=$1; _cols=$2; _px=$3; _pw=$4
|
|
entry=$(get_entry "$SEL")
|
|
# clear preview area first
|
|
_r=2
|
|
while [ "$_r" -le $((_rows - 1)) ]; do
|
|
goto "$_r" "$_px"
|
|
printf '\033[K'
|
|
_r=$((_r + 1))
|
|
done
|
|
[ -z "$entry" ] && return
|
|
_path=$(joinpath "${entry%/}"); _path="${_path%@}"
|
|
|
|
# draw vertical divider
|
|
_r=2
|
|
while [ "$_r" -le $((_rows - 1)) ]; do
|
|
goto "$_r" $((_px - 1))
|
|
printf '%s|%s' "${CYAN}" "${RESET}"
|
|
_r=$((_r + 1))
|
|
done
|
|
|
|
case "$entry" in
|
|
*/)
|
|
# directory: list contents
|
|
_max_lines=$((_rows - 3))
|
|
_pr=2
|
|
for _de in "$_path"/*/; do
|
|
[ "$_pr" -gt $((_rows - 1)) ] && break
|
|
[ -d "$_de" ] || continue
|
|
_dn="${_de%/}"; _dn="${_dn##*/}"
|
|
[ "$_dn" = "*" ] && continue
|
|
_dl=" ${_dn}/"
|
|
[ "${#_dl}" -gt "$((_pw - 1))" ] && _dl="$(printf '%s' "$_dl" | cut -c1-$((_pw-2)))~"
|
|
goto "$_pr" "$_px"
|
|
printf '%s%s%s' "${BLUE}${BOLD}" "$_dl" "${RESET}"
|
|
_pr=$((_pr + 1))
|
|
done
|
|
for _fe in "$_path"/*; do
|
|
[ "$_pr" -gt $((_rows - 1)) ] && break
|
|
[ -e "$_fe" ] || continue
|
|
[ -d "$_fe" ] && continue
|
|
_fn="${_fe##*/}"
|
|
[ "$_fn" = "*" ] && continue
|
|
_fl=" ${_fn}"
|
|
[ "${#_fl}" -gt "$((_pw - 1))" ] && _fl="$(printf '%s' "$_fl" | cut -c1-$((_pw-2)))~"
|
|
goto "$_pr" "$_px"
|
|
printf '%s%s%s' "${WHITE}" "$_fl" "${RESET}"
|
|
_pr=$((_pr + 1))
|
|
done
|
|
if [ "$_pr" -eq 2 ]; then
|
|
goto 2 "$_px"
|
|
printf '%s(empty)%s' "${WHITE}" "${RESET}"
|
|
fi
|
|
;;
|
|
*@)
|
|
# symlink
|
|
_tgt=$(readlink "$_path" 2>/dev/null || echo "?")
|
|
goto 2 "$_px"
|
|
printf '%s[symlink]%s' "${MAGENTA}${BOLD}" "${RESET}"
|
|
goto 3 "$_px"
|
|
printf '%s-> %s%s' "${WHITE}" "${_tgt}" "${RESET}"
|
|
;;
|
|
*)
|
|
# detect if text via extension or mime
|
|
_ext="${entry##*.}"
|
|
_ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')
|
|
_is_text=0
|
|
case "$_ext" in
|
|
txt|md|markdown|rst|log|conf|cfg|ini|toml|yaml|yml|\
|
|
sh|bash|zsh|py|rb|pl|lua|js|ts|json|xml|html|htm|\
|
|
css|c|h|cpp|rs|go|java|php|r|sql|vim|env|gitignore|\
|
|
diff|patch|csv|tsv|lock|mod|sum) _is_text=1 ;;
|
|
esac
|
|
if [ "$_is_text" = "0" ] && command -v file >/dev/null 2>&1; then
|
|
case "$(file --mime-type -b "$_path" 2>/dev/null)" in
|
|
text/*) _is_text=1 ;;
|
|
esac
|
|
fi
|
|
if [ "$_is_text" = "1" ]; then
|
|
_max_lines=$((_rows - 3))
|
|
_line_n=0
|
|
while IFS= read -r _line && [ "$_line_n" -lt "$_max_lines" ]; do
|
|
# truncate to preview width
|
|
if [ "${#_line}" -gt "$((_pw - 1))" ]; then
|
|
_line=$(printf '%s' "$_line" | cut -c1-$((_pw - 2)))
|
|
_line="${_line}~"
|
|
fi
|
|
goto "$((_line_n + 2))" "$_px"
|
|
printf '%s%s%s' "${WHITE}" "$_line" "${RESET}"
|
|
_line_n=$((_line_n + 1))
|
|
done < "$_path"
|
|
if [ "$_line_n" -eq 0 ]; then
|
|
goto 2 "$_px"
|
|
printf '%s(empty file)%s' "${WHITE}" "${RESET}"
|
|
fi
|
|
else
|
|
# binary / unknown
|
|
goto 2 "$_px"
|
|
_sz=$(ls -lh "$_path" 2>/dev/null | awk '{print $5}')
|
|
printf '%s[binary] %s%s' "${YELLOW}" "${_sz}" "${RESET}"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
draw_topbar() {
|
|
_cols=$1
|
|
spaces=$(printf '%*s' "$_cols" '')
|
|
goto 1 1
|
|
printf '%s%s%s' "${CYAN}${BOLD}" "${spaces}" "${RESET}"
|
|
}
|
|
|
|
draw_botbar() {
|
|
_rows=$1; _cols=$2
|
|
if [ "$SEARCHING" = "1" ]; then
|
|
info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}"
|
|
keys=" search: ${FILTER} "
|
|
maxk=$((_cols - ${#info} - 1))
|
|
[ "${#keys}" -gt "$maxk" ] && keys=$(printf '%s' "$keys" | cut -c1-"$maxk")
|
|
padlen=$((_cols - ${#info} - ${#keys}))
|
|
[ "$padlen" -lt 0 ] && padlen=0
|
|
pad=$(printf '%*s' "$padlen" '')
|
|
goto "$_rows" 1
|
|
printf '%s%s%s%s%s%s' \
|
|
"${BOLD}${CYAN}" "${info}" \
|
|
"${RESET}${pad}" \
|
|
"${YELLOW}${keys}" \
|
|
"${RESET}"
|
|
elif [ -n "$INFO_MSG" ]; then
|
|
info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}"
|
|
msg=" ${INFO_MSG} " padlen=$((_cols - ${#info} - ${#msg}))
|
|
[ "$padlen" -lt 0 ] && padlen=0
|
|
pad=$(printf '%*s' "$padlen" '')
|
|
goto "$_rows" 1
|
|
printf '%s%s%s%s%s%s' \
|
|
"${BOLD}${CYAN}" "${info}" \
|
|
"${RESET}${pad}" \
|
|
"${YELLOW}${msg}" \
|
|
"${RESET}"
|
|
INFO_MSG=""
|
|
else
|
|
_ind=""
|
|
[ -n "$CLIPBOARD" ] && _ind=" [${CLIP_MODE}]"
|
|
_sc=$(count_selected)
|
|
[ "$_sc" -gt 0 ] && _ind="${_ind} [sel:${_sc}]"
|
|
[ "$SHOW_HIDDEN" = "1" ] && _ind="${_ind} [hidden]"
|
|
[ "$SORT_MODE" != "name" ] && _ind="${_ind} [sort:${SORT_MODE}]"
|
|
[ "$SHOW_DETAILS" = "1" ] && _ind="${_ind} [details]"
|
|
hint=" press ? for help "
|
|
info=" $([ "$COUNT" -eq 0 ] && echo 0 || echo $((SEL + 1)))/${COUNT} ${CWD}${_ind}"
|
|
padlen=$((_cols - ${#info} - ${#hint}))
|
|
[ "$padlen" -lt 0 ] && padlen=0
|
|
pad=$(printf '%*s' "$padlen" '')
|
|
goto "$_rows" 1
|
|
printf '%s%s%s%s%s%s' \
|
|
"${BOLD}${CYAN}" "${info}" \
|
|
"${RESET}${pad}" \
|
|
"${YELLOW}${hint}" \
|
|
"${RESET}"
|
|
fi
|
|
}
|
|
|
|
draw() {
|
|
ROWS=$(term_rows)
|
|
COLS=$(term_cols)
|
|
LIST_ROWS=$((ROWS - 2)) # row 1 = topbar, row ROWS = botbar
|
|
|
|
# split layout when preview enabled
|
|
if [ "$SHOW_PREVIEW" = "1" ]; then
|
|
LIST_COLS=$((COLS / 2))
|
|
PREV_COL=$((LIST_COLS + 2))
|
|
PREV_WIDTH=$((COLS - LIST_COLS - 2))
|
|
else
|
|
LIST_COLS=$COLS
|
|
fi
|
|
|
|
# clamp selection
|
|
[ "$SEL" -lt 0 ] && SEL=0
|
|
[ "$COUNT" -gt 0 ] && [ "$SEL" -ge "$COUNT" ] && SEL=$((COUNT - 1))
|
|
|
|
# update scroll offset
|
|
if [ "$SEL" -lt "$OFFSET" ]; then
|
|
OFFSET=$SEL
|
|
elif [ "$SEL" -ge $((OFFSET + LIST_ROWS)) ]; then
|
|
OFFSET=$((SEL - LIST_ROWS + 1))
|
|
fi
|
|
|
|
# viewport shifted → must redraw all rows
|
|
[ "$OFFSET" != "$PREV_OFFSET" ] && NEED_FULL_REDRAW=1
|
|
|
|
if [ "$NEED_FULL_REDRAW" = "1" ]; then
|
|
printf '\033[H'
|
|
draw_topbar "$COLS"
|
|
|
|
row=2
|
|
idx=$OFFSET
|
|
while [ "$row" -le $((LIST_ROWS + 1)) ] && [ "$idx" -lt "$COUNT" ]; do
|
|
sel=0; [ "$idx" -eq "$SEL" ] && sel=1
|
|
render_row "$row" "$idx" "$sel" "$LIST_COLS"
|
|
row=$((row + 1))
|
|
idx=$((idx + 1))
|
|
done
|
|
# show (empty) if directory has no entries
|
|
if [ "$COUNT" -eq 0 ]; then
|
|
goto 2 1
|
|
printf '%s (empty)%s%s' "${WHITE}" "${ERASE_LINE}" "${RESET}"
|
|
row=3
|
|
fi
|
|
# blank leftover rows
|
|
while [ "$row" -le $((LIST_ROWS + 1)) ]; do
|
|
goto "$row" 1
|
|
printf '%s' "${ERASE_LINE}"
|
|
row=$((row + 1))
|
|
done
|
|
|
|
[ "$SHOW_PREVIEW" = "1" ] && draw_preview "$ROWS" "$COLS" "$PREV_COL" "$PREV_WIDTH"
|
|
|
|
draw_botbar "$ROWS" "$COLS"
|
|
NEED_FULL_REDRAW=0
|
|
else
|
|
# --- fast path ---
|
|
if [ "$SEL" = "$PREV_SEL" ] && [ "$OFFSET" = "$PREV_OFFSET" ]; then
|
|
return 2>/dev/null || true
|
|
fi
|
|
prev_row=$(( (PREV_SEL - OFFSET) + 2 ))
|
|
new_row=$(( (SEL - OFFSET) + 2 ))
|
|
|
|
if [ "$prev_row" -ge 2 ] && [ "$prev_row" -le $((LIST_ROWS + 1)) ]; then
|
|
render_row "$prev_row" "$PREV_SEL" 0 "$LIST_COLS"
|
|
fi
|
|
if [ "$new_row" -ge 2 ] && [ "$new_row" -le $((LIST_ROWS + 1)) ]; then
|
|
render_row "$new_row" "$SEL" 1 "$LIST_COLS"
|
|
fi
|
|
|
|
[ "$SHOW_PREVIEW" = "1" ] && draw_preview "$ROWS" "$COLS" "$PREV_COL" "$PREV_WIDTH"
|
|
|
|
draw_botbar "$ROWS" "$COLS"
|
|
fi
|
|
|
|
PREV_SEL=$SEL
|
|
PREV_OFFSET=$OFFSET
|
|
}
|
|
|
|
# --- input ---
|
|
read_key() {
|
|
IFS= read -r -n1 key 2>/dev/null || IFS= read -r key
|
|
if [ "$key" = "$(printf '\033')" ]; then
|
|
IFS= read -r -n1 -t 0.1 _k2 2>/dev/null || _k2=""
|
|
if [ -z "$_k2" ]; then
|
|
printf '\033'; return
|
|
fi
|
|
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"
|
|
}
|
|
|
|
setup_term() { old_stty=$(stty -g); stty -echo -icanon min 1 time 0; }
|
|
restore_term() { stty "$old_stty"; }
|
|
|
|
# --- actions ---
|
|
# --- smart file opener ---
|
|
# Detects file type by extension then mime, picks the right program.
|
|
# Terminal programs run in the foreground; GUI programs are backgrounded.
|
|
_run_tty() { "$@"; }
|
|
_run_gui() { "$@" >/dev/null 2>&1 & }
|
|
|
|
open_file() {
|
|
_f="$1"
|
|
|
|
# use user opener script if it exists and is executable
|
|
_opener="${XDG_CONFIG_HOME:-$HOME/.config}/sfm/opener"
|
|
if [ -x "$_opener" ]; then
|
|
"$_opener" "$_f"
|
|
return
|
|
fi
|
|
|
|
_ext="${_f##*.}"
|
|
_ext=$(printf '%s' "$_ext" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# --- by extension ---
|
|
case "$_ext" in
|
|
# text / code → editor
|
|
txt|md|markdown|rst|csv|tsv|log|conf|cfg|ini|toml|yaml|yml|\
|
|
sh|bash|zsh|fish|py|rb|pl|lua|js|ts|jsx|tsx|json|xml|html|\
|
|
htm|css|scss|sass|c|h|cpp|cc|cxx|hpp|rs|go|java|kt|swift|\
|
|
cs|php|r|sql|vim|diff|patch|makefile|dockerfile|gitignore|\
|
|
env|lock|mod|sum)
|
|
_run_tty ${EDITOR:-vi} "$_f"
|
|
return ;;
|
|
# images → GUI viewer
|
|
jpg|jpeg|png|gif|bmp|tiff|tif|webp|svg|ico|heic|heif|avif)
|
|
for _v in imv imvr feh sxiv nsxiv eog eom viewnior shotwell gimp; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
# video → GUI player
|
|
mp4|mkv|avi|mov|wmv|flv|webm|m4v|mpeg|mpg|3gp|ogv)
|
|
for _v in mpv vlc mplayer totem celluloid haruna; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
# audio → player (mpv/vlc run fine headless too)
|
|
mp3|flac|ogg|wav|aac|m4a|opus|wma|aiff)
|
|
for _v in mpv vlc mplayer cmus mocp; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
# PDF / documents
|
|
pdf)
|
|
for _v in zathura evince okular mupdf atril xreader; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
# office docs
|
|
odt|ods|odp|doc|docx|xls|xlsx|ppt|pptx)
|
|
for _v in libreoffice soffice; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
# archives → list contents in pager
|
|
zip|tar|gz|bz2|xz|zst|7z|rar)
|
|
if command -v atool >/dev/null 2>&1; then
|
|
atool -l "$_f" 2>&1 | ${PAGER:-less}; return
|
|
elif command -v bsdtar >/dev/null 2>&1; then
|
|
bsdtar -tf "$_f" 2>&1 | ${PAGER:-less}; return
|
|
fi ;;
|
|
esac
|
|
|
|
# --- fallback: try mime type via `file` ---
|
|
if command -v file >/dev/null 2>&1; then
|
|
_mime=$(file --mime-type -b "$_f" 2>/dev/null)
|
|
case "$_mime" in
|
|
text/*|application/json|application/xml|application/javascript)
|
|
_run_tty ${EDITOR:-vi} "$_f"; return ;;
|
|
image/*)
|
|
for _v in imv feh sxiv nsxiv eog gimp; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
video/*)
|
|
for _v in mpv vlc mplayer; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
audio/*)
|
|
for _v in mpv vlc mplayer; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
application/pdf)
|
|
for _v in zathura evince okular mupdf; do
|
|
command -v "$_v" >/dev/null 2>&1 && { _run_gui "$_v" "$_f"; return; }
|
|
done ;;
|
|
esac
|
|
fi
|
|
|
|
# --- last resort: xdg-open / open, else editor ---
|
|
if command -v xdg-open >/dev/null 2>&1; then
|
|
_run_gui xdg-open "$_f"
|
|
elif command -v open >/dev/null 2>&1; then
|
|
_run_gui open "$_f"
|
|
else
|
|
_run_tty ${EDITOR:-vi} "$_f"
|
|
fi
|
|
}
|
|
|
|
# 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="${_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
|
|
"$(printf '\033')")
|
|
# read with timeout to distinguish bare esc from sequences
|
|
IFS= read -r -n2 -t 0.1 _seq 2>/dev/null || _seq=""
|
|
hide_cursor
|
|
READ_LINE=""; return 1 ;;
|
|
"$(printf '\n')"|\
|
|
"$(printf '\r')")
|
|
hide_cursor
|
|
return 0 ;;
|
|
"$(printf '\177')"|\
|
|
"$(printf '\010')")
|
|
if [ -n "$READ_LINE" ]; then
|
|
READ_LINE="${READ_LINE%?}"
|
|
printf '\b \b'
|
|
fi ;;
|
|
*)
|
|
READ_LINE="${READ_LINE}${_ch}"
|
|
printf '%s' "$_ch" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
do_open_with() {
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
case "$entry" in
|
|
*/) INFO_MSG="cannot open-with a directory"; NEED_FULL_REDRAW=1; return ;;
|
|
esac
|
|
_target="${CWD}/${entry%@}"
|
|
goto "$(term_rows)" 1
|
|
printf '%s%s Open "%s" with (esc=cancel): %s' \
|
|
"${ERASE_LINE}" "${CYAN}${BOLD}" "$entry" "${RESET}"
|
|
if read_line && [ -n "$READ_LINE" ]; then
|
|
# check the program exists
|
|
if ! command -v "$READ_LINE" >/dev/null 2>&1; then
|
|
INFO_MSG="not found: ${READ_LINE}"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
restore_term; show_cursor
|
|
printf '\033[2J\033[H'
|
|
"$READ_LINE" "$_target"
|
|
setup_term; hide_cursor
|
|
NEED_FULL_REDRAW=1
|
|
else
|
|
NEED_FULL_REDRAW=1; draw
|
|
fi
|
|
}
|
|
|
|
do_newfile() {
|
|
goto "$(term_rows)" 1
|
|
printf '%s%s File name: %s' "${ERASE_LINE}" "${WHITE}${BOLD}" "${RESET}"
|
|
if read_line && [ -n "$READ_LINE" ]; then
|
|
touch "${CWD}/${READ_LINE}"
|
|
load_entries
|
|
else
|
|
NEED_FULL_REDRAW=1; draw
|
|
fi
|
|
}
|
|
|
|
# --- help overlay helpers (must be top-level for dash compatibility) ---
|
|
_help_bx=0; _help_by=0; _help_iw=0; _help_hl=""
|
|
|
|
help_row() {
|
|
_r=$1; _text=$2
|
|
goto "$(( _help_by + _r ))" "$_help_bx"
|
|
if [ "${#_text}" -gt "$_help_iw" ]; then
|
|
_text=$(printf '%s' "$_text" | cut -c1-"$_help_iw")
|
|
fi
|
|
_pl=$(( _help_iw - ${#_text} ))
|
|
_p=$(printf '%*s' "$_pl" '')
|
|
printf '%s|%s%s%s%s|%s' \
|
|
"${BOLD}${CYAN}" "${RESET}" "${_text}" "${_p}" \
|
|
"${BOLD}${CYAN}" "${RESET}"
|
|
}
|
|
|
|
help_sep() {
|
|
goto "$(( _help_by + $1 ))" "$_help_bx"
|
|
printf '%s|%s|%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}"
|
|
}
|
|
|
|
do_help() {
|
|
ROWS=$(term_rows)
|
|
COLS=$(term_cols)
|
|
bw=$((COLS - 4))
|
|
[ "$bw" -gt 52 ] && bw=52
|
|
[ "$bw" -lt 36 ] && bw=36
|
|
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))
|
|
_help_hl=$(printf '%*s' "$_help_iw" '' | tr ' ' '-')
|
|
|
|
goto "$_help_by" "$_help_bx"
|
|
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_help_hl" "${RESET}"
|
|
|
|
help_row 1 ""
|
|
help_row 2 " KEYBOARD SHORTCUTS"
|
|
help_sep 3
|
|
help_row 4 " j/k up/down g/G top/bottom"
|
|
help_row 5 " h/left go back"
|
|
help_row 6 " l/right/enter open / enter dir"
|
|
help_sep 7
|
|
help_row 8 " / search filter"
|
|
help_row 9 " esc clear filter"
|
|
help_row 10 " . toggle hidden files"
|
|
help_row 11 " i file info in status bar"
|
|
help_row 12 " s cycle sort: name/size/date"
|
|
help_row 13 " T toggle size/date details"
|
|
help_row 14 " P toggle preview pane"
|
|
help_sep 15
|
|
help_row 16 " space toggle multi-select"
|
|
help_row 17 " a select all / deselect all"
|
|
help_row 18 " y yank/copy (works on selection)"
|
|
help_row 19 " x cut (works on selection)"
|
|
help_row 20 " p paste"
|
|
help_row 21 " d delete (works on selection)"
|
|
help_sep 22
|
|
help_row 23 " r rename R refresh"
|
|
help_row 24 " m make directory"
|
|
help_row 25 " n new file"
|
|
help_row 26 " u trash file (safe delete)"
|
|
help_row 27 " U open trash directory"
|
|
help_row 28 " ! drop to shell in CWD"
|
|
help_row 29 " o open with custom program"
|
|
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}"
|
|
|
|
read_key > /dev/null
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_search() {
|
|
FILTER=""
|
|
SEARCHING=1
|
|
show_cursor
|
|
while true; do
|
|
apply_filter
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
NEED_FULL_REDRAW=1
|
|
draw
|
|
IFS= read -r -n1 _ch 2>/dev/null || IFS= read -r _ch
|
|
case "$_ch" in
|
|
"$(printf '\033')")
|
|
# read with short timeout to catch escape sequences
|
|
IFS= read -r -n2 -t 0.1 _seq 2>/dev/null || _seq=""
|
|
if [ -z "$_seq" ]; then
|
|
# bare esc — clear filter and exit search
|
|
FILTER=""; SEARCHING=0
|
|
apply_filter
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
hide_cursor; NEED_FULL_REDRAW=1; draw; return
|
|
fi
|
|
# else it was an arrow key or other sequence — ignore it
|
|
;;
|
|
"$(printf '\n')"|\
|
|
"$(printf '\r')")
|
|
SEARCHING=0; hide_cursor; NEED_FULL_REDRAW=1; return ;;
|
|
"$(printf '\177')"|\
|
|
"$(printf '\010')")
|
|
[ -n "$FILTER" ] && FILTER="${FILTER%?}" ;;
|
|
*)
|
|
FILTER="${FILTER}${_ch}" ;;
|
|
esac
|
|
done
|
|
}
|
|
do_clear_filter() {
|
|
FILTER=""
|
|
SEARCHING=0
|
|
apply_filter
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_toggle_hidden() {
|
|
if [ "$SHOW_HIDDEN" = "0" ]; then
|
|
SHOW_HIDDEN=1
|
|
INFO_MSG="hidden files shown"
|
|
else
|
|
SHOW_HIDDEN=0
|
|
INFO_MSG="hidden files hidden"
|
|
fi
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
}
|
|
|
|
do_toggle_details() {
|
|
if [ "$SHOW_DETAILS" = "0" ]; then
|
|
SHOW_DETAILS=1; INFO_MSG="details on"
|
|
else
|
|
SHOW_DETAILS=0; INFO_MSG="details off"
|
|
fi
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_toggle_preview() {
|
|
if [ "$SHOW_PREVIEW" = "0" ]; then
|
|
SHOW_PREVIEW=1; INFO_MSG="preview on"
|
|
else
|
|
SHOW_PREVIEW=0; INFO_MSG="preview off"
|
|
fi
|
|
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=""
|
|
SELECTED=""
|
|
PREV_CWD="$CWD"
|
|
LAST_CHILD="${CWD##*/}/" # name of current dir with trailing slash
|
|
CWD=$(cd "$(dirname "$CWD")" && pwd)
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
# restore selection to the directory we just came from
|
|
if [ -n "$LAST_CHILD" ]; then
|
|
idx=$(find_entry "$LAST_CHILD")
|
|
[ "$idx" -ge 0 ] 2>/dev/null && SEL=$idx
|
|
fi
|
|
LAST_CHILD=""
|
|
}
|
|
|
|
do_open() {
|
|
entry=$(get_entry "$SEL")
|
|
case "$entry" in
|
|
*/)
|
|
FILTER=""
|
|
SELECTED=""
|
|
_target=$(joinpath "${entry%/}")
|
|
if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then
|
|
INFO_MSG="permission denied: ${entry%/}"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
PREV_CWD="$CWD"
|
|
CWD=$(cd "$_target" && pwd); norm_cwd
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
;;
|
|
*@)
|
|
_name="${entry%@}"
|
|
_target=$(joinpath "$_name")
|
|
if [ -d "$_target" ]; then
|
|
if [ ! -x "$_target" ] || [ ! -r "$_target" ]; then
|
|
INFO_MSG="permission denied: ${_name}"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
FILTER=""; SELECTED=""
|
|
PREV_CWD="$CWD"
|
|
CWD=$(cd "$_target" && pwd); norm_cwd
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
else
|
|
if [ ! -r "$_target" ]; then
|
|
INFO_MSG="permission denied: ${_name}"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
restore_term; show_cursor
|
|
printf '\033[2J\033[H'
|
|
open_file "$_target"
|
|
setup_term; hide_cursor
|
|
NEED_FULL_REDRAW=1
|
|
fi
|
|
;;
|
|
*)
|
|
_target="${CWD}/${entry}"
|
|
if [ ! -r "$_target" ]; then
|
|
INFO_MSG="permission denied: ${entry}"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
restore_term; show_cursor
|
|
printf '\033[2J\033[H'
|
|
open_file "$_target"
|
|
setup_term; hide_cursor
|
|
NEED_FULL_REDRAW=1
|
|
;;
|
|
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 ---
|
|
_prompt="Delete ${_c} selected items?"
|
|
confirm_overlay; _yes=$?
|
|
if [ "$_yes" -eq 0 ]; then
|
|
_rest="$SELECTED"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"
|
|
_next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -z "$_line" ] && continue
|
|
_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
|
|
else
|
|
NEED_FULL_REDRAW=1
|
|
fi
|
|
else
|
|
# --- single delete ---
|
|
entry=$(get_entry "$SEL")
|
|
_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
|
|
_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
|
|
fi
|
|
}
|
|
|
|
do_rename() {
|
|
entry=$(get_entry "$SEL")
|
|
_name="${entry%@}"; _name="${_name%/}"
|
|
goto "$(term_rows)" 1
|
|
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
|
|
fi
|
|
}
|
|
|
|
do_mkdir() {
|
|
goto "$(term_rows)" 1
|
|
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
|
|
else
|
|
NEED_FULL_REDRAW=1; draw
|
|
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
|
|
_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
|
|
}
|
|
|
|
do_shell() {
|
|
restore_term; show_cursor
|
|
printf '\033[2J\033[H'
|
|
cd "$CWD" || true
|
|
printf '%s(type "exit" to return to fm)%s\n' "${YELLOW}" "${RESET}"
|
|
${SHELL:-sh}
|
|
# restore CWD in case user cd'd around
|
|
CWD="${PWD}"; norm_cwd
|
|
setup_term; hide_cursor
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_sort() {
|
|
case "$SORT_MODE" in
|
|
name) SORT_MODE="size" ;;
|
|
size) SORT_MODE="date" ;;
|
|
date) SORT_MODE="name" ;;
|
|
esac
|
|
INFO_MSG="sort: ${SORT_MODE}"
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
}
|
|
|
|
do_trash() {
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
_name="${entry%@}"; _name="${_name%/}"
|
|
target="${CWD}/${_name}"
|
|
_ts=$(date '+%Y%m%d_%H%M%S' 2>/dev/null || date '+%s')
|
|
_dest="${TRASH_DIR}/${_ts}_${_name}"
|
|
if mv "$target" "$_dest"; then
|
|
INFO_MSG="trashed: ${entry}"
|
|
[ "$SEL" -ge "$((COUNT - 1))" ] && SEL=$((COUNT - 2))
|
|
[ "$SEL" -lt 0 ] && SEL=0
|
|
load_entries
|
|
else
|
|
INFO_MSG="trash failed"
|
|
NEED_FULL_REDRAW=1
|
|
fi
|
|
}
|
|
|
|
do_open_trash() {
|
|
PREV_CWD="$CWD"
|
|
FILTER=""; SELECTED=""
|
|
CWD="$TRASH_DIR"; norm_cwd
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
}
|
|
|
|
do_copy_path() {
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
_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
|
|
printf '%s' "$_path" | xclip -selection clipboard
|
|
elif command -v xsel >/dev/null 2>&1; then
|
|
printf '%s' "$_path" | xsel --clipboard --input
|
|
elif command -v pbcopy >/dev/null 2>&1; then
|
|
printf '%s' "$_path" | pbcopy
|
|
else
|
|
INFO_MSG="no clipboard tool found"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
INFO_MSG="path copied: ${_path}"
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_jump_back() {
|
|
[ -z "$PREV_CWD" ] && { INFO_MSG="no previous directory"; NEED_FULL_REDRAW=1; return; }
|
|
_tmp="$CWD"
|
|
CWD="$PREV_CWD"; norm_cwd
|
|
PREV_CWD="$_tmp"
|
|
FILTER=""; SELECTED=""
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
}
|
|
|
|
do_bookmark_add() {
|
|
# toggle: if CWD already bookmarked, remove it; else add it
|
|
if grep -qx "$CWD" "$BOOKMARK_FILE" 2>/dev/null; then
|
|
# remove
|
|
_tmp=$(grep -vx "$CWD" "$BOOKMARK_FILE")
|
|
printf '%s\n' "$_tmp" > "$BOOKMARK_FILE"
|
|
INFO_MSG="bookmark removed: ${CWD}"
|
|
else
|
|
printf '%s\n' "$CWD" >> "$BOOKMARK_FILE"
|
|
INFO_MSG="bookmarked: ${CWD}"
|
|
fi
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_bookmark_jump() {
|
|
# count bookmarks
|
|
_bc=0
|
|
while IFS= read -r _l; do [ -n "$_l" ] && _bc=$((_bc+1)); done < "$BOOKMARK_FILE"
|
|
[ "$_bc" -eq 0 ] && { INFO_MSG="no bookmarks saved"; NEED_FULL_REDRAW=1; return; }
|
|
|
|
# draw a small picker overlay
|
|
ROWS=$(term_rows); COLS=$(term_cols)
|
|
_pw=$(( COLS * 2 / 3 )); [ "$_pw" -gt 70 ] && _pw=70; [ "$_pw" -lt 30 ] && _pw=30
|
|
_ph=$(( _bc + 4 )); [ "$_ph" -gt $((ROWS - 4)) ] && _ph=$((ROWS - 4))
|
|
_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 ' ' '-')
|
|
|
|
_bsel=1
|
|
_bvis=$((_ph - 3))
|
|
_boff=1
|
|
while true; do
|
|
# draw picker inline
|
|
goto "$_py" "$_px"
|
|
printf '%s+%s+%s' "${BOLD}${CYAN}" "$_hl" "${RESET}"
|
|
goto "$((_py+1))" "$_px"
|
|
_t=" BOOKMARKS"; _pl=$((_iw - ${#_t})); _p=$(printf '%*s' "$_pl" '')
|
|
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))
|
|
[ "$_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" '')
|
|
if [ "$_bi" -eq "$_bsel" ]; then
|
|
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${REV}${YELLOW}" "$_bt" "$_bp" "${RESET}${BOLD}${CYAN}" "${RESET}"
|
|
else
|
|
printf '%s|%s%s%s%s|%s' "${BOLD}${CYAN}" "${RESET}" "$_bt" "$_bp" "${BOLD}${CYAN}" "${RESET}"
|
|
fi
|
|
done < "$BOOKMARK_FILE"
|
|
goto "$((_py+_ph))" "$_px"
|
|
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
|
|
[ "$_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
|
|
[ "$_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
|
|
PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
|
|
FILTER=""; SELECTED=""
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
else
|
|
INFO_MSG="not found: ${_chosen}"; NEED_FULL_REDRAW=1
|
|
fi
|
|
return ;;
|
|
'[D') NEED_FULL_REDRAW=1; return ;; # left — close
|
|
*) NEED_FULL_REDRAW=1; return ;; # bare esc — close
|
|
esac ;;
|
|
"$(printf '\n')"|\
|
|
"$(printf '\r')"|\
|
|
l)
|
|
_chosen=$(awk -v n="$_bsel" 'NR==n&&NF{print;exit}' "$BOOKMARK_FILE")
|
|
if [ -n "$_chosen" ] && [ -d "$_chosen" ]; then
|
|
PREV_CWD="$CWD"; CWD="$_chosen"; norm_cwd
|
|
FILTER=""; SELECTED=""
|
|
SEL=0; OFFSET=0; PREV_SEL=0; PREV_OFFSET=0
|
|
load_entries
|
|
else
|
|
INFO_MSG="not found: ${_chosen}"; NEED_FULL_REDRAW=1
|
|
fi
|
|
return ;;
|
|
q|Q|h) NEED_FULL_REDRAW=1; return ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
do_toggle_select() {
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
toggle_selected "$entry"
|
|
_c=$(count_selected)
|
|
[ "$_c" -eq 0 ] && INFO_MSG="selection cleared" || INFO_MSG="${_c} selected"
|
|
SEL=$((SEL + 1))
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_select_all() {
|
|
_c=$(count_selected)
|
|
if [ "$_c" -gt 0 ]; then
|
|
# deselect all
|
|
SELECTED=""
|
|
INFO_MSG="selection cleared"
|
|
else
|
|
# select all visible entries (skip ../)
|
|
SELECTED=""
|
|
_rest="$ENTRIES"
|
|
while [ -n "$_rest" ]; do
|
|
_line="${_rest%%
|
|
*}"; _next="${_rest#*
|
|
}"
|
|
[ "$_next" = "$_rest" ] && _next=""
|
|
_rest="$_next"
|
|
[ -z "$_line" ] && continue
|
|
[ "$_line" = "../" ] && continue
|
|
if [ -z "$SELECTED" ]; then SELECTED="$_line"
|
|
else SELECTED="$SELECTED
|
|
$_line"; fi
|
|
done
|
|
_c=$(count_selected)
|
|
INFO_MSG="selected all: ${_c} items"
|
|
fi
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_yank() {
|
|
_c=$(count_selected)
|
|
if [ "$_c" -gt 0 ]; then
|
|
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
|
|
[ -z "$_e" ] && continue
|
|
_yname="${_e%@}"; _yname="${_yname%/}"
|
|
printf '%s\n' "${CWD}/${_yname}"
|
|
done)
|
|
CLIP_MODE="copy"
|
|
INFO_MSG="yanked ${_c} items"
|
|
SELECTED=""
|
|
else
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
_yname="${entry%@}"; _yname="${_yname%/}"
|
|
CLIPBOARD="${CWD}/${_yname}"
|
|
CLIP_MODE="copy"
|
|
INFO_MSG="yanked: ${entry}"
|
|
fi
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_cut() {
|
|
_c=$(count_selected)
|
|
if [ "$_c" -gt 0 ]; then
|
|
CLIPBOARD=$(printf '%s\n' "$SELECTED" | while IFS= read -r _e; do
|
|
[ -z "$_e" ] && continue
|
|
_cname="${_e%@}"; _cname="${_cname%/}"
|
|
printf '%s\n' "${CWD}/${_cname}"
|
|
done)
|
|
CLIP_MODE="cut"
|
|
INFO_MSG="cut ${_c} items"
|
|
SELECTED=""
|
|
else
|
|
entry=$(get_entry "$SEL")
|
|
[ -z "$entry" ] && return
|
|
_cname="${entry%@}"; _cname="${_cname%/}"
|
|
CLIPBOARD="${CWD}/${_cname}"
|
|
CLIP_MODE="cut"
|
|
INFO_MSG="cut: ${entry}"
|
|
fi
|
|
NEED_FULL_REDRAW=1
|
|
}
|
|
|
|
do_paste() {
|
|
if [ -z "$CLIPBOARD" ]; then
|
|
INFO_MSG="nothing to paste"
|
|
NEED_FULL_REDRAW=1
|
|
return
|
|
fi
|
|
printf '%s\n' "$CLIPBOARD" | while IFS= read -r _src; do
|
|
[ -z "$_src" ] && continue
|
|
_name="${_src##*/}"
|
|
_dest="${CWD}/${_name}"
|
|
if [ -e "$_dest" ]; then
|
|
_base="${_name%.*}"; _ext="${_name##*.}"
|
|
[ "$_ext" = "$_name" ] && _ext="" || _ext=".${_ext}"
|
|
_dest="${CWD}/${_base}_copy${_ext}"
|
|
fi
|
|
case "$CLIP_MODE" in
|
|
copy) cp -r "$_src" "$_dest" ;;
|
|
cut) mv "$_src" "$_dest" ;;
|
|
esac
|
|
done
|
|
CLIPBOARD=""; CLIP_MODE=""
|
|
INFO_MSG="pasted"
|
|
load_entries
|
|
}
|
|
|
|
# --- main ---
|
|
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
|
|
printf '\033[2J' # clear screen exactly once at startup
|
|
load_entries
|
|
|
|
while true; do
|
|
draw
|
|
key=$(read_key)
|
|
|
|
case "$key" in
|
|
j|"$(printf '\033[B')")
|
|
[ "$SEL" -lt $((COUNT - 1)) ] && SEL=$((SEL + 1)) ;;
|
|
k|"$(printf '\033[A')")
|
|
[ "$SEL" -gt 0 ] && SEL=$((SEL - 1)) ;;
|
|
g) SEL=0 ;;
|
|
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')"|\
|
|
l) do_open ;;
|
|
"$(printf '\033[D')"|\
|
|
h) do_go_back ;;
|
|
b) do_bookmark_add ;;
|
|
B) do_bookmark_jump ;;
|
|
'?') do_help ;;
|
|
R) load_entries; INFO_MSG="refreshed" ;;
|
|
/) do_search ;;
|
|
'.') do_toggle_hidden ;;
|
|
T) do_toggle_details ;;
|
|
P) do_toggle_preview ;;
|
|
i) do_info ;;
|
|
'+') do_chmod_x ;;
|
|
'-') do_chmod_nox ;;
|
|
o) do_open_with ;;
|
|
s) do_sort ;;
|
|
u) do_trash ;;
|
|
U) do_open_trash ;;
|
|
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 ;;
|
|
"$(printf '\033')") do_clear_filter ;;
|
|
' ') do_toggle_select ;;
|
|
a) do_select_all ;;
|
|
y) do_yank ;;
|
|
x) do_cut ;;
|
|
p) do_paste ;;
|
|
d) do_delete ;;
|
|
r) do_rename ;;
|
|
m) do_mkdir ;;
|
|
n) do_newfile ;;
|
|
q|Q) break ;;
|
|
'!') do_shell ;;
|
|
esac
|
|
done
|
|
|
|
restore_term
|
|
show_cursor
|
|
printf '\033[2J\033[H'
|
|
printf '%s\n' "$CWD"
|