You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1112 lines
31 KiB
Bash
1112 lines
31 KiB
Bash
#!/usr/bin/env bash
|
|
#
|
|
# fff - fucking fast file-manager.
|
|
|
|
get_os() {
|
|
# Figure out the current operating system to set some specific variables.
|
|
# '$OSTYPE' typically stores the name of the OS kernel.
|
|
case "$OSTYPE" in
|
|
# Mac OS X / macOS.
|
|
darwin*)
|
|
opener="open"
|
|
file_flags="bIL"
|
|
;;
|
|
|
|
haiku)
|
|
opener="open"
|
|
|
|
[[ -z $FFF_TRASH_CMD ]] &&
|
|
FFF_TRASH_CMD="trash"
|
|
|
|
[[ $FFF_TRASH_CMD == trash ]] && {
|
|
FFF_TRASH="$(finddir -v "$PWD" B_TRASH_DIRECTORY)"
|
|
mkdir -p "$FFF_TRASH"
|
|
}
|
|
;;
|
|
esac
|
|
}
|
|
|
|
setup_terminal() {
|
|
# Setup the terminal for the TUI.
|
|
# '\e[?1049h': Use alternative screen buffer.
|
|
# '\e[?7l': Disable line wrapping.
|
|
# '\e[?25l': Hide the cursor.
|
|
# '\e[2J': Clear the screen.
|
|
# '\e[1;Nr': Limit scrolling to scrolling area.
|
|
# Also sets cursor to (0,0).
|
|
printf '\e[?1049h\e[?7l\e[?25l\e[2J\e[1;%sr' "$max_items"
|
|
|
|
# Hide echoing of user input
|
|
stty -echo
|
|
}
|
|
|
|
reset_terminal() {
|
|
# Reset the terminal to a useable state (undo all changes).
|
|
# '\e[?7h': Re-enable line wrapping.
|
|
# '\e[?25h': Unhide the cursor.
|
|
# '\e[2J': Clear the terminal.
|
|
# '\e[;r': Set the scroll region to its default value.
|
|
# Also sets cursor to (0,0).
|
|
# '\e[?1049l: Restore main screen buffer.
|
|
printf '\e[?7h\e[?25h\e[2J\e[;r\e[?1049l'
|
|
|
|
# Show user input.
|
|
stty echo
|
|
}
|
|
|
|
clear_screen() {
|
|
# Only clear the scrolling window (dir item list).
|
|
# '\e[%sH': Move cursor to bottom of scroll area.
|
|
# '\e[9999C': Move cursor to right edge of the terminal.
|
|
# '\e[1J': Clear screen to top left corner (from cursor up).
|
|
# '\e[2J': Clear screen fully (if using tmux) (fixes clear issues).
|
|
# '\e[1;%sr': Clearing the screen resets the scroll region(?). Re-set it.
|
|
# Also sets cursor to (0,0).
|
|
printf '\e[%sH\e[9999C\e[1J%b\e[1;%sr' \
|
|
"$((LINES-2))" "${TMUX:+\e[2J}" "$max_items"
|
|
}
|
|
|
|
setup_options() {
|
|
# Some options require some setup.
|
|
# This function is called once on open to parse
|
|
# select options so the operation isn't repeated
|
|
# multiple times in the code.
|
|
|
|
# Format for normal files.
|
|
[[ $FFF_FILE_FORMAT == *%f* ]] && {
|
|
file_pre="${FFF_FILE_FORMAT/'%f'*}"
|
|
file_post="${FFF_FILE_FORMAT/*'%f'}"
|
|
}
|
|
|
|
# Format for marked files.
|
|
[[ $FFF_MARK_FORMAT == *%f* ]] && {
|
|
mark_pre="${FFF_MARK_FORMAT/'%f'*}"
|
|
mark_post="${FFF_MARK_FORMAT/*'%f'}"
|
|
}
|
|
}
|
|
|
|
get_term_size() {
|
|
# Get terminal size ('stty' is POSIX and always available).
|
|
# This can't be done reliably across all bash versions in pure bash.
|
|
read -r LINES COLUMNS < <(stty size)
|
|
|
|
# Max list items that fit in the scroll area.
|
|
((max_items=LINES-3))
|
|
}
|
|
|
|
get_ls_colors() {
|
|
# Parse the LS_COLORS variable and declare each file type
|
|
# as a separate variable.
|
|
# Format: ':.ext=0;0:*.jpg=0;0;0:*png=0;0;0;0:'
|
|
[[ -z $LS_COLORS ]] && {
|
|
FFF_LS_COLORS=0
|
|
return
|
|
}
|
|
|
|
# Turn $LS_COLORS into an array.
|
|
IFS=: read -ra ls_cols <<< "$LS_COLORS"
|
|
|
|
for ((i=0;i<${#ls_cols[@]};i++)); {
|
|
# Separate patterns from file types.
|
|
[[ ${ls_cols[i]} =~ ^\*[^\.] ]] &&
|
|
ls_patterns+="${ls_cols[i]/=*}|"
|
|
|
|
# Prepend 'ls_' to all LS_COLORS items
|
|
# if they aren't types of files (symbolic links, block files etc.)
|
|
[[ ${ls_cols[i]} =~ ^(\*|\.) ]] && {
|
|
ls_cols[i]="${ls_cols[i]#\*}"
|
|
ls_cols[i]="ls_${ls_cols[i]#.}"
|
|
}
|
|
}
|
|
|
|
# Strip non-ascii characters from the string as they're
|
|
# used as a key to color the dir items and variable
|
|
# names in bash must be '[a-zA-z0-9_]'.
|
|
ls_cols=("${ls_cols[@]//[^a-zA-Z0-9=\\;]/_}")
|
|
|
|
# Store the patterns in a '|' separated string
|
|
# for use in a REGEX match later.
|
|
ls_patterns="${ls_patterns//\*}"
|
|
ls_patterns="${ls_patterns%?}"
|
|
|
|
# Define the ls_ variables.
|
|
# 'declare' can't be used here as variables are scoped
|
|
# locally. 'declare -g' is not available in 'bash 3'.
|
|
# 'export' is a viable alternative.
|
|
export "${ls_cols[@]}" &>/dev/null
|
|
}
|
|
|
|
get_w3m_path() {
|
|
# Find the path to the w3m-img library.
|
|
# If w3mimgdisplay is already in path, use it.
|
|
type -p w3mimgdisplay &>/dev/null && {
|
|
w3m="w3mimgdisplay"
|
|
return
|
|
}
|
|
|
|
# Use a glob and expansion to find the path.
|
|
w3m_paths=(/usr/{local/,}{lib,libexec,lib64,libexec64}/w3m/w3mi*)
|
|
|
|
[[ -x ${w3m_paths[0]} ]] &&
|
|
w3m="${w3m_paths[0]}"
|
|
}
|
|
|
|
get_mime_type() {
|
|
# Get a file's mime_type.
|
|
mime_type="$(file "-${file_flags:-biL}" "$1")"
|
|
}
|
|
|
|
status_line() {
|
|
# Status_line to print when files are marked for operation.
|
|
local mark_ui="[${#marked_files[@]}] selected (${file_program[*]}) [p] ->"
|
|
|
|
# Escape the directory string.
|
|
# Remove all non-printable characters.
|
|
PWD_escaped="${PWD//[^[:print:]]/^[}"
|
|
|
|
# '\e7': Save cursor position.
|
|
# This is more widely supported than '\e[s'.
|
|
# '\e[%sH': Move cursor to bottom of the terminal.
|
|
# '\e[30;41m': Set foreground and background colors.
|
|
# '%*s': Insert enough spaces to fill the screen width.
|
|
# This sets the background color to the whole line
|
|
# and fixes issues in 'screen' where '\e[K' doesn't work.
|
|
# '\r': Move cursor back to column 0 (was at EOL due to above).
|
|
# '\e[m': Reset text formatting.
|
|
# '\e[H\e[K': Clear line below status_line.
|
|
# '\e8': Restore cursor position.
|
|
# This is more widely supported than '\e[u'.
|
|
printf '\e7\e[%sH\e[30;4%sm%*s\r%s %s%s\e[m\e[%sH\e[K\e8' \
|
|
"$((LINES-1))" \
|
|
"${FFF_COL2:-1}" \
|
|
"$COLUMNS" "" \
|
|
"($((scroll+1))/$((list_total+1)))" \
|
|
"${marked_files[*]:+${mark_ui}}" \
|
|
"${1:-${PWD_escaped:-/}}" \
|
|
"$LINES"
|
|
}
|
|
|
|
read_dir() {
|
|
# Read a directory to an array and sort it directories first.
|
|
local dirs
|
|
local files
|
|
local item_index
|
|
|
|
# If '$PWD' is '/', unset it to avoid '//'.
|
|
[[ $PWD == / ]] && PWD=
|
|
|
|
for item in "$PWD"/*; do
|
|
if [[ -d $item ]]; then
|
|
dirs+=("$item")
|
|
((item_index++))
|
|
|
|
# Find the position of the child directory in the
|
|
# parent directory list.
|
|
[[ $item == "$OLDPWD" ]] &&
|
|
((previous_index=item_index))
|
|
else
|
|
files+=("$item")
|
|
fi
|
|
done
|
|
|
|
list=("${dirs[@]}" "${files[@]}")
|
|
|
|
# Indicate that the directory is empty.
|
|
[[ -z ${list[0]} ]] &&
|
|
list[0]="empty"
|
|
|
|
((list_total=${#list[@]}-1))
|
|
|
|
# Save the original dir in a second list as a backup.
|
|
cur_list=("${list[@]}")
|
|
}
|
|
|
|
print_line() {
|
|
# Format the list item and print it.
|
|
local file_name="${list[$1]##*/}"
|
|
local file_ext="${file_name##*.}"
|
|
local format
|
|
local suffix
|
|
|
|
# If the dir item doesn't exist, end here.
|
|
if [[ -z ${list[$1]} ]]; then
|
|
return
|
|
|
|
# Directory.
|
|
elif [[ -d ${list[$1]} ]]; then
|
|
format+="\\e[${di:-1;3${FFF_COL1:-2}}m"
|
|
suffix+='/'
|
|
|
|
# Block special file.
|
|
elif [[ -b ${list[$1]} ]]; then
|
|
format+="\\e[${bd:-40;33;01}m"
|
|
|
|
# Character special file.
|
|
elif [[ -c ${list[$1]} ]]; then
|
|
format+="\\e[${cd:-40;33;01}m"
|
|
|
|
# Executable file.
|
|
elif [[ -x ${list[$1]} ]]; then
|
|
format+="\\e[${ex:-01;32}m"
|
|
|
|
# Symbolic Link (broken).
|
|
elif [[ -h ${list[$1]} && ! -e ${list[$1]} ]]; then
|
|
format+="\\e[${mi:-01;31;7}m"
|
|
|
|
# Symbolic Link.
|
|
elif [[ -h ${list[$1]} ]]; then
|
|
format+="\\e[${ln:-01;36}m"
|
|
|
|
# Fifo file.
|
|
elif [[ -p ${list[$1]} ]]; then
|
|
format+="\\e[${pi:-40;33}m"
|
|
|
|
# Socket file.
|
|
elif [[ -S ${list[$1]} ]]; then
|
|
format+="\\e[${so:-01;35}m"
|
|
|
|
# Color files that end in a pattern as defined in LS_COLORS.
|
|
# 'BASH_REMATCH' is an array that stores each REGEX match.
|
|
elif [[ $FFF_LS_COLORS == 1 &&
|
|
$ls_patterns &&
|
|
$file_name =~ ($ls_patterns)$ ]]; then
|
|
match="${BASH_REMATCH[0]}"
|
|
file_ext="ls_${match//[^a-zA-Z0-9=\\;]/_}"
|
|
format+="\\e[${!file_ext:-${fi:-37}}m"
|
|
|
|
# Color files based on file extension and LS_COLORS.
|
|
# Check if file extension adheres to POSIX naming
|
|
# stardard before checking if it's a variable.
|
|
elif [[ $FFF_LS_COLORS == 1 &&
|
|
$file_ext != "$file_name" &&
|
|
$file_ext =~ ^[a-zA-Z0-9_]*$ ]]; then
|
|
file_ext="ls_${file_ext}"
|
|
format+="\\e[${!file_ext:-${fi:-37}}m"
|
|
|
|
else
|
|
format+="\\e[${fi:-37}m"
|
|
fi
|
|
|
|
# If the list item is under the cursor.
|
|
(($1 == scroll)) &&
|
|
format+="\\e[1;3${FFF_COL4:-6};7m"
|
|
|
|
# If the list item is marked for operation.
|
|
[[ ${marked_files[$1]} == "${list[$1]:-null}" ]] && {
|
|
format+="\\e[3${FFF_COL3:-1}m${mark_pre:= }"
|
|
suffix+="${mark_post:=*}"
|
|
}
|
|
|
|
# Escape the directory string.
|
|
# Remove all non-printable characters.
|
|
file_name="${file_name//[^[:print:]]/^[}"
|
|
|
|
printf '\r%b%s\e[m\r' \
|
|
"${file_pre}${format}" \
|
|
"${file_name}${suffix}${file_post}"
|
|
}
|
|
|
|
draw_dir() {
|
|
# Print the max directory items that fit in the scroll area.
|
|
local scroll_start="$scroll"
|
|
local scroll_new_pos
|
|
local scroll_end
|
|
|
|
# When going up the directory tree, place the cursor on the position
|
|
# of the previous directory.
|
|
((find_previous == 1)) && {
|
|
((scroll_start=previous_index-1))
|
|
((scroll=scroll_start))
|
|
|
|
# Clear the directory history. We're here now.
|
|
find_previous=
|
|
}
|
|
|
|
# If current dir is near the top of the list, keep scroll position.
|
|
if ((list_total < max_items || scroll < max_items/2)); then
|
|
((scroll_start=0))
|
|
((scroll_end=max_items))
|
|
((scroll_new_pos=scroll + 1))
|
|
|
|
# If curent dir is near the end of the list, keep scroll position.
|
|
elif ((list_total - scroll < max_items/2)); then
|
|
((scroll_start=list_total - max_items + 1))
|
|
((scroll_new_pos=max_items - (list_total-scroll)))
|
|
((scroll_end=list_total+1))
|
|
|
|
# If current dir is somewhere in the middle, center scroll position.
|
|
else
|
|
((scroll_start=scroll-max_items/2))
|
|
((scroll_end=scroll_start+max_items))
|
|
((scroll_new_pos=max_items/2+1))
|
|
fi
|
|
|
|
# Reset cursor position.
|
|
printf '\e[H'
|
|
|
|
for ((i=scroll_start;i<scroll_end;i++)); {
|
|
# Don't print one too many newlines.
|
|
((i > scroll_start)) &&
|
|
printf '\n'
|
|
|
|
print_line "$i"
|
|
}
|
|
|
|
# Move the cursor to its new position if it changed.
|
|
# If the variable 'scroll_new_pos' is empty, the cursor
|
|
# is moved to line '0'.
|
|
printf '\e[%sH' "$scroll_new_pos"
|
|
((y=scroll_new_pos))
|
|
}
|
|
|
|
draw_img() {
|
|
# Draw an image file on the screen using w3m-img.
|
|
# We can use the framebuffer; set win_info_cmd as appropriate.
|
|
[[ $(tty) == /dev/tty[0-9]* && -w /dev/fb0 ]] &&
|
|
win_info_cmd="fbset"
|
|
|
|
# X isn't running and we can't use the framebuffer, do nothing.
|
|
[[ -z $DISPLAY && $win_info_cmd != fbset ]] &&
|
|
return
|
|
|
|
# File isn't an image file, do nothing.
|
|
get_mime_type "${list[scroll]}"
|
|
[[ $mime_type != image/* ]] &&
|
|
return
|
|
|
|
# w3m-img isn't installed, do nothing.
|
|
type -p "$w3m" &>/dev/null || {
|
|
cmd_line "error: Couldn't find 'w3m-img', is it installed?"
|
|
return
|
|
}
|
|
|
|
# $win_info_cmd isn't installed, do nothing.
|
|
type -p "${win_info_cmd:=xdotool}" &>/dev/null || {
|
|
cmd_line "error: Couldn't find '$win_info_cmd', is it installed?"
|
|
return
|
|
}
|
|
|
|
# Get terminal window size in pixels and set it to WIDTH and HEIGHT.
|
|
if [[ $win_info_cmd == xdotool ]]; then
|
|
IFS=$'\n' read -d "" -ra win_info \
|
|
< <(xdotool getactivewindow getwindowgeometry --shell)
|
|
|
|
declare "${win_info[@]}" &>/dev/null || {
|
|
cmd_line "error: Failed to retrieve window size."
|
|
return
|
|
}
|
|
else
|
|
[[ $(fbset --show) =~ .*\"([0-9]+x[0-9]+)\".* ]]
|
|
IFS=x read -r WIDTH HEIGHT <<< "${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
# Get the image size in pixels.
|
|
read -r img_width img_height < <("$w3m" <<< "5;${list[scroll]}")
|
|
|
|
# Substract the status_line area from the image size.
|
|
((HEIGHT=HEIGHT-HEIGHT*5/LINES))
|
|
|
|
((img_width > WIDTH)) && {
|
|
((img_height=img_height*WIDTH/img_width))
|
|
((img_width=WIDTH))
|
|
}
|
|
|
|
((img_height > HEIGHT)) && {
|
|
((img_width=img_width*HEIGHT/img_height))
|
|
((img_height=HEIGHT))
|
|
}
|
|
|
|
clear_screen
|
|
status_line "${list[scroll]}"
|
|
|
|
# Add a small delay to fix issues in VTE terminals.
|
|
((BASH_VERSINFO[0] > 3)) &&
|
|
read "${read_flags[@]}" -srn 1
|
|
|
|
# Display the image.
|
|
printf '0;1;%s;%s;%s;%s;;;;;%s\n3;\n4\n' \
|
|
"${FFF_W3M_XOFFSET:-0}" \
|
|
"${FFF_W3M_YOFFSET:-0}" \
|
|
"$img_width" \
|
|
"$img_height" \
|
|
"${list[scroll]}" | "$w3m" &>/dev/null
|
|
|
|
# Wait for user input.
|
|
read -ern 1
|
|
|
|
# Clear the image.
|
|
printf '6;%s;%s;%s;%s\n3;' \
|
|
"${FFF_W3M_XOFFSET:-0}" \
|
|
"${FFF_W3M_YOFFSET:-0}" \
|
|
"$WIDTH" \
|
|
"$HEIGHT" | "$w3m" &>/dev/null
|
|
|
|
redraw
|
|
}
|
|
|
|
redraw() {
|
|
# Redraw the current window.
|
|
# If 'full' is passed, re-fetch the directory list.
|
|
[[ $1 == full ]] && {
|
|
read_dir
|
|
scroll=0
|
|
}
|
|
|
|
clear_screen
|
|
draw_dir
|
|
status_line
|
|
}
|
|
|
|
run() {
|
|
# Run a program without disrupting the user-interface.
|
|
clear_screen
|
|
reset_terminal
|
|
"$@"
|
|
setup_terminal
|
|
redraw
|
|
}
|
|
|
|
mark() {
|
|
# Mark file for operation.
|
|
# If an item is marked in a second directory,
|
|
# clear the marked files.
|
|
[[ $PWD != "$mark_dir" ]] &&
|
|
marked_files=()
|
|
|
|
# Don't allow the user to mark the empty directory list item.
|
|
[[ ${list[0]} == empty && -z ${list[1]} ]] &&
|
|
return
|
|
|
|
if [[ $1 == all ]]; then
|
|
if ((${#marked_files[@]} != ${#list[@]})); then
|
|
marked_files=("${list[@]}")
|
|
mark_dir="$PWD"
|
|
else
|
|
marked_files=()
|
|
fi
|
|
|
|
redraw
|
|
else
|
|
if [[ ${marked_files[$1]} == "${list[$1]}" ]]; then
|
|
unset 'marked_files[scroll]'
|
|
|
|
else
|
|
marked_files[$1]="${list[$1]}"
|
|
mark_dir="$PWD"
|
|
fi
|
|
|
|
# Clear line before changing it.
|
|
printf '\e[K'
|
|
print_line "$1"
|
|
fi
|
|
|
|
# Find the program to use.
|
|
case "$2" in
|
|
y|Y) file_program=(cp -iR) ;;
|
|
m|M) file_program=(mv -i) ;;
|
|
s|S) file_program=(ln -s) ;;
|
|
|
|
# These are 'fff' functions.
|
|
d|D) file_program=(trash) ;;
|
|
b|B) file_program=(bulk_rename) ;;
|
|
esac
|
|
|
|
status_line
|
|
}
|
|
|
|
trash() {
|
|
# 'trash' a file.
|
|
cmd_line "trash [${#marked_files[@]}] items? [y/n]: " y n
|
|
|
|
[[ $cmd_reply != y ]] &&
|
|
return
|
|
|
|
if [[ $FFF_TRASH_CMD ]]; then
|
|
# Pass all but the last argument to the user's
|
|
# custom script. command is used to prevent this function
|
|
# from conflicting with commands named "trash".
|
|
command "$FFF_TRASH_CMD" "${@:1:$#-1}"
|
|
|
|
else
|
|
cd "$FFF_TRASH" && mv -f "$@"
|
|
|
|
# Go back to where we were.
|
|
cd "$OLDPWD" ||:
|
|
fi
|
|
}
|
|
|
|
bulk_rename() {
|
|
# Bulk rename files using '$EDITOR'.
|
|
rename_file="${XDG_CACHE_HOME:=${HOME}/.cache}/fff/bulk_rename"
|
|
marked_files=("${@:1:$#-1}")
|
|
|
|
# Save marked files to a file and open them for editing.
|
|
printf '%s\n' "${marked_files[@]##*/}" > "$rename_file"
|
|
"${EDITOR:-vi}" "$rename_file"
|
|
|
|
# Read the renamed files to an array.
|
|
IFS=$'\n' read -d "" -ra changed_files < "$rename_file"
|
|
|
|
# If the user deleted a line, stop here.
|
|
((${#marked_files[@]} != ${#changed_files[@]})) && {
|
|
rm "$rename_file"
|
|
cmd_line "error: Line mismatch in rename file. Doing nothing."
|
|
return
|
|
}
|
|
|
|
printf '%s\n%s\n' \
|
|
"# This file will be executed when the editor is closed." \
|
|
"# Clear the file to abort." > "$rename_file"
|
|
|
|
# Construct the rename commands.
|
|
for ((i=0;i<${#marked_files[@]};i++)); {
|
|
[[ ${marked_files[i]} != "${PWD}/${changed_files[i]}" ]] && {
|
|
printf 'mv -i -- %q %q\n' \
|
|
"${marked_files[i]}" "${PWD}/${changed_files[i]}"
|
|
local renamed=1
|
|
}
|
|
} >> "$rename_file"
|
|
|
|
# Let the user double-check the commands and execute them.
|
|
((renamed == 1)) && {
|
|
"${EDITOR:-vi}" "$rename_file"
|
|
|
|
source "$rename_file"
|
|
rm "$rename_file"
|
|
}
|
|
|
|
# Fix terminal settings after '$EDITOR'.
|
|
setup_terminal
|
|
}
|
|
|
|
open() {
|
|
# Open directories and files.
|
|
if [[ -d $1/ ]]; then
|
|
search=
|
|
search_end_early=
|
|
cd "${1:-/}" ||:
|
|
redraw full
|
|
|
|
elif [[ -f $1 ]]; then
|
|
# Figure out what kind of file we're working with.
|
|
get_mime_type "$1"
|
|
|
|
# Open all text-based files in '$EDITOR'.
|
|
# Everything else goes through 'xdg-open'/'open'.
|
|
case "$mime_type" in
|
|
text/*|*x-empty*|*json*)
|
|
# If 'fff' was opened as a file picker, save the opened
|
|
# file in a file called 'opened_file'.
|
|
((file_picker == 1)) && {
|
|
printf '%s\n' "$1" > \
|
|
"${XDG_CACHE_HOME:=${HOME}/.cache}/fff/opened_file"
|
|
exit
|
|
}
|
|
|
|
run "${VISUAL:-${EDITOR:-vi}}" "$1"
|
|
;;
|
|
|
|
*)
|
|
# 'nohup': Make the process immune to hangups.
|
|
# '&': Send it to the background.
|
|
# 'disown': Detach it from the shell.
|
|
if type -p "$FFF_OPENER" &>/dev/null; then
|
|
run "$FFF_OPENER" "$1"
|
|
|
|
else
|
|
nohup "${opener:-xdg-open}" "$1" &>/dev/null & disown
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
}
|
|
|
|
cmd_line() {
|
|
# Write to the command_line (under status_line).
|
|
cmd_reply=
|
|
|
|
# '\e7': Save cursor position.
|
|
# '\e[?25h': Unhide the cursor.
|
|
# '\e[%sH': Move cursor to bottom (cmd_line).
|
|
printf '\e7\e[%sH\e[?25h' "$LINES"
|
|
|
|
# '\r\e[K': Redraw the read prompt on every keypress.
|
|
# This is mimicking what happens normally.
|
|
while IFS= read -rsn 1 -p $'\r\e[K'"${1}${cmd_reply}" read_reply; do
|
|
case "$read_reply" in
|
|
# Backspace.
|
|
$'\177'|$'\b')
|
|
cmd_reply="${cmd_reply%?}"
|
|
|
|
# Clear tab-completion.
|
|
unset comp c
|
|
;;
|
|
|
|
# Tab.
|
|
$'\t')
|
|
comp_glob="$cmd_reply*"
|
|
|
|
# Pass the argument dirs to limit completion to directories.
|
|
[[ $2 == dirs ]] &&
|
|
comp_glob="$cmd_reply*/"
|
|
|
|
# Generate a completion list once.
|
|
[[ -z ${comp[0]} ]] &&
|
|
IFS=$'\n' read -d "" -ra comp < <(compgen -G "$comp_glob")
|
|
|
|
# On each tab press, cycle through the completion list.
|
|
[[ -n ${comp[c]} ]] && {
|
|
cmd_reply="${comp[c]}"
|
|
((c=c >= ${#comp[@]}-1 ? 0 : ++c))
|
|
}
|
|
;;
|
|
|
|
# Escape / Custom 'no' value (used as a replacement for '-n 1').
|
|
$'\e'|"${3:-null}")
|
|
cmd_reply=
|
|
break
|
|
;;
|
|
|
|
# Enter/Return.
|
|
"")
|
|
# If there's only one search result and its a directory,
|
|
# enter it on one enter keypress.
|
|
[[ $2 == search && -d ${list[0]} ]] && ((list_total == 0)) && {
|
|
# '\e[?25l': Hide the cursor.
|
|
printf '\e[?25l'
|
|
|
|
open "${list[0]}"
|
|
search_end_early=1
|
|
|
|
# Unset tab completion variables since we're done.
|
|
unset comp c
|
|
return
|
|
}
|
|
|
|
break
|
|
;;
|
|
|
|
# Custom 'yes' value (used as a replacement for '-n 1').
|
|
"${2:-null}")
|
|
cmd_reply="$read_reply"
|
|
break
|
|
;;
|
|
|
|
# Replace '~' with '$HOME'.
|
|
"~")
|
|
cmd_reply+="$HOME"
|
|
;;
|
|
|
|
# Anything else, add it to read reply.
|
|
*)
|
|
cmd_reply+="$read_reply"
|
|
|
|
# Clear tab-completion.
|
|
unset comp c
|
|
;;
|
|
esac
|
|
|
|
# Search on keypress if search passed as an argument.
|
|
[[ $2 == search ]] && {
|
|
# '\e[?25l': Hide the cursor.
|
|
printf '\e[?25l'
|
|
|
|
# Use a greedy glob to search.
|
|
list=("$PWD"/*"$cmd_reply"*)
|
|
((list_total=${#list[@]}-1))
|
|
|
|
# Draw the search results on screen.
|
|
scroll=0
|
|
redraw
|
|
|
|
# '\e[%sH': Move cursor back to cmd-line.
|
|
# '\e[?25h': Unhide the cursor.
|
|
printf '\e[%sH\e[?25h' "$LINES"
|
|
}
|
|
done
|
|
|
|
# Unset tab completion variables since we're done.
|
|
unset comp c
|
|
|
|
# '\e[2K': Clear the entire cmd_line on finish.
|
|
# '\e[?25l': Hide the cursor.
|
|
# '\e8': Restore cursor position.
|
|
printf '\e[2K\e[?25l\e8'
|
|
}
|
|
|
|
key() {
|
|
# Handle special key presses.
|
|
[[ $1 == $'\e' ]] && {
|
|
read "${read_flags[@]}" -rsn 2
|
|
|
|
# Handle a normal escape key press.
|
|
[[ ${1}${REPLY} == $'\e\e['* ]] &&
|
|
read "${read_flags[@]}" -rsn 1 _
|
|
|
|
local special_key="${1}${REPLY}"
|
|
}
|
|
|
|
case "${special_key:-$1}" in
|
|
# Open list item.
|
|
# 'C' is what bash sees when the right arrow is pressed ('\e[C').
|
|
# '' is what bash sees when the enter/return key is pressed.
|
|
"${FFF_KEY_CHILD1:=l}"|\
|
|
"${FFF_KEY_CHILD2:=$'\e[C'}"|\
|
|
"${FFF_KEY_CHILD3:=""}")
|
|
open "${list[scroll]}"
|
|
;;
|
|
|
|
# Go to the parent directory.
|
|
# 'D' is what bash sees when the left arrow is pressed ('\e[D').
|
|
# '\177' and '\b' are what bash sometimes sees when the backspace
|
|
# key is pressed.
|
|
"${FFF_KEY_PARENT1:=h}"|\
|
|
"${FFF_KEY_PARENT2:=$'\e[D'}"|\
|
|
"${FFF_KEY_PARENT3:=$'\177'}"|\
|
|
"${FFF_KEY_PARENT4:=$'\b'}")
|
|
# If a search was done, clear the results and open the current dir.
|
|
if ((search == 1 && search_end_early != 1)); then
|
|
open "$PWD"
|
|
|
|
# If '$PWD' is '/', do nothing.
|
|
elif [[ $PWD && $PWD != / ]]; then
|
|
find_previous=1
|
|
open "${PWD%/*}"
|
|
fi
|
|
;;
|
|
|
|
# Scroll down.
|
|
# 'B' is what bash sees when the down arrow is pressed ('\e[B').
|
|
"${FFF_KEY_SCROLL_DOWN1:=j}"|\
|
|
"${FFF_KEY_SCROLL_DOWN2:=$'\e[B'}")
|
|
((scroll < list_total)) && {
|
|
((scroll++))
|
|
((y < max_items)) && ((y++))
|
|
|
|
print_line "$((scroll-1))"
|
|
printf '\n'
|
|
print_line "$scroll"
|
|
status_line
|
|
}
|
|
;;
|
|
|
|
# Scroll up.
|
|
# 'A' is what bash sees when the up arrow is pressed ('\e[A').
|
|
"${FFF_KEY_SCROLL_UP1:=k}"|\
|
|
"${FFF_KEY_SCROLL_UP2:=$'\e[A'}")
|
|
# '\e[1L': Insert a line above the cursor.
|
|
# '\e[A': Move cursor up a line.
|
|
((scroll > 0)) && {
|
|
((scroll--))
|
|
|
|
print_line "$((scroll+1))"
|
|
|
|
if ((y < 2)); then
|
|
printf '\e[L'
|
|
else
|
|
printf '\e[A'
|
|
((y--))
|
|
fi
|
|
|
|
print_line "$scroll"
|
|
status_line
|
|
}
|
|
;;
|
|
|
|
# Go to top.
|
|
"${FFF_KEY_TO_TOP:=g}")
|
|
((scroll != 0)) && {
|
|
scroll=0
|
|
redraw
|
|
}
|
|
;;
|
|
|
|
# Go to bottom.
|
|
"${FFF_KEY_TO_BOTTOM:=G}")
|
|
((scroll != list_total)) && {
|
|
((scroll=list_total))
|
|
redraw
|
|
}
|
|
;;
|
|
|
|
# Show hidden files.
|
|
"${FFF_KEY_HIDDEN:=.}")
|
|
# 'a=a>0?0:++a': Toggle between both values of 'shopt_flags'.
|
|
# This also works for '3' or more values with
|
|
# some modification.
|
|
shopt_flags=(u s)
|
|
shopt -"${shopt_flags[((a=a>0?0:++a))]}" dotglob
|
|
redraw full
|
|
;;
|
|
|
|
# Search.
|
|
"${FFF_KEY_SEARCH:=/}")
|
|
cmd_line "/" "search"
|
|
|
|
# If the search came up empty, redraw the current dir.
|
|
if [[ -z ${list[*]} ]]; then
|
|
list=("${cur_list[@]}")
|
|
((list_total=${#list[@]}-1))
|
|
redraw
|
|
search=
|
|
else
|
|
search=1
|
|
fi
|
|
;;
|
|
|
|
# Spawn a shell.
|
|
"${FFF_KEY_SHELL:=!}")
|
|
reset_terminal
|
|
|
|
# Make fff aware of how many times it is nested.
|
|
export FFF_LEVEL
|
|
((FFF_LEVEL++))
|
|
|
|
cd "$PWD" && "$SHELL"
|
|
setup_terminal
|
|
redraw
|
|
;;
|
|
|
|
# Mark files for operation.
|
|
"${FFF_KEY_YANK:=y}"|\
|
|
"${FFF_KEY_MOVE:=m}"|\
|
|
"${FFF_KEY_TRASH:=d}"|\
|
|
"${FFF_KEY_LINK:=s}"|\
|
|
"${FFF_KEY_BULK_RENAME:=b}")
|
|
mark "$scroll" "$1"
|
|
;;
|
|
|
|
# Mark all files for operation.
|
|
"${FFF_KEY_YANK_ALL:=Y}"|\
|
|
"${FFF_KEY_MOVE_ALL:=M}"|\
|
|
"${FFF_KEY_TRASH_ALL:=D}"|\
|
|
"${FFF_KEY_LINK_ALL:=S}"|\
|
|
"${FFF_KEY_BULK_RENAME_ALL:=B}")
|
|
mark all "$1"
|
|
;;
|
|
|
|
# Do the file operation.
|
|
"${FFF_KEY_PASTE:=p}")
|
|
[[ ${marked_files[*]} ]] && {
|
|
[[ ! -w $PWD ]] && {
|
|
cmd_line "warn: no write access to dir."
|
|
return
|
|
}
|
|
|
|
# Clear the screen to make room for a prompt if needed.
|
|
clear_screen
|
|
reset_terminal
|
|
|
|
stty echo
|
|
"${file_program[@]}" "${marked_files[@]}" .
|
|
stty -echo
|
|
|
|
marked_files=()
|
|
setup_terminal
|
|
redraw full
|
|
}
|
|
;;
|
|
|
|
# Clear all marked files.
|
|
"${FFF_KEY_CLEAR:=c}")
|
|
[[ ${marked_files[*]} ]] && {
|
|
marked_files=()
|
|
redraw
|
|
}
|
|
;;
|
|
|
|
# Rename list item.
|
|
"${FFF_KEY_RENAME:=r}")
|
|
[[ ! -e "${list[scroll]}" ]] &&
|
|
return
|
|
|
|
cmd_line "rename ${list[scroll]##*/}: "
|
|
|
|
[[ $cmd_reply ]] &&
|
|
if [[ -e $cmd_reply ]]; then
|
|
cmd_line "warn: '$cmd_reply' already exists."
|
|
|
|
elif [[ -w ${list[scroll]} ]]; then
|
|
mv "${list[scroll]}" "${PWD}/${cmd_reply}"
|
|
redraw full
|
|
|
|
else
|
|
cmd_line "warn: no write access to file."
|
|
fi
|
|
;;
|
|
|
|
# Create a directory.
|
|
"${FFF_KEY_MKDIR:=n}")
|
|
cmd_line "mkdir: " "dirs"
|
|
|
|
[[ $cmd_reply ]] &&
|
|
if [[ -e $cmd_reply ]]; then
|
|
cmd_line "warn: '$cmd_reply' already exists."
|
|
|
|
elif [[ -w $PWD ]]; then
|
|
mkdir -p "${PWD}/${cmd_reply}"
|
|
redraw full
|
|
|
|
else
|
|
cmd_line "warn: no write access to dir."
|
|
fi
|
|
;;
|
|
|
|
# Create a file.
|
|
"${FFF_KEY_MKFILE:=f}")
|
|
cmd_line "mkfile: "
|
|
|
|
[[ $cmd_reply ]] &&
|
|
if [[ -e $cmd_reply ]]; then
|
|
cmd_line "warn: '$cmd_reply' already exists."
|
|
|
|
elif [[ -w $PWD ]]; then
|
|
: > "$cmd_reply"
|
|
redraw full
|
|
|
|
else
|
|
cmd_line "warn: no write access to dir."
|
|
fi
|
|
;;
|
|
|
|
# Show file attributes.
|
|
"${FFF_KEY_ATTRIBUTES:=x}")
|
|
[[ -e "${list[scroll]}" ]] && {
|
|
clear_screen
|
|
status_line "${list[scroll]}"
|
|
stat "${list[scroll]}"
|
|
read -ern 1
|
|
redraw
|
|
}
|
|
;;
|
|
|
|
# Show image in terminal.
|
|
"${FFF_KEY_IMAGE:=i}")
|
|
draw_img
|
|
;;
|
|
|
|
# Go to dir.
|
|
"${FFF_KEY_GO_DIR:=:}")
|
|
cmd_line "go to dir: " "dirs"
|
|
|
|
# Let 'cd' know about the current directory.
|
|
cd "$PWD" &>/dev/null ||:
|
|
|
|
[[ $cmd_reply ]] &&
|
|
cd "${cmd_reply/\~/$HOME}" &>/dev/null &&
|
|
open "$PWD"
|
|
;;
|
|
|
|
# Go to '$HOME'.
|
|
"${FFF_KEY_GO_HOME:=~}")
|
|
open ~
|
|
;;
|
|
|
|
# Go to trash.
|
|
"${FFF_KEY_GO_TRASH:=t}")
|
|
get_os
|
|
open "$FFF_TRASH"
|
|
;;
|
|
|
|
# Go to previous dir.
|
|
"${FFF_KEY_PREVIOUS:=-}")
|
|
open "$OLDPWD"
|
|
;;
|
|
|
|
# Directory favourites.
|
|
[1-9])
|
|
favourite="FFF_FAV${1}"
|
|
favourite="${!favourite}"
|
|
|
|
[[ $favourite ]] &&
|
|
open "$favourite"
|
|
;;
|
|
|
|
# Quit and store current directory in a file for CD on exit.
|
|
# Don't allow user to redefine 'q' so a bad keybinding doesn't
|
|
# remove the option to quit.
|
|
q)
|
|
: "${FFF_CD_FILE:=${XDG_CACHE_HOME:=${HOME}/.cache}/fff/.fff_d}"
|
|
|
|
[[ -w $FFF_CD_FILE ]] &&
|
|
rm "$FFF_CD_FILE"
|
|
|
|
[[ ${FFF_CD_ON_EXIT:=1} == 1 ]] &&
|
|
printf '%s\n' "$PWD" > "$FFF_CD_FILE"
|
|
|
|
exit
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
# Handle a directory as the first argument.
|
|
# 'cd' is a cheap way of finding the full path to a directory.
|
|
# It updates the '$PWD' variable on successful execution.
|
|
# It handles relative paths as well as '../../../'.
|
|
#
|
|
# '||:': Do nothing if 'cd' fails. We don't care.
|
|
cd "${2:-$1}" &>/dev/null ||:
|
|
|
|
[[ $1 == -v ]] && {
|
|
printf '%s\n' "fff 2.2"
|
|
exit
|
|
}
|
|
|
|
[[ $1 == -h ]] && {
|
|
man fff
|
|
exit
|
|
}
|
|
|
|
# Store file name in a file on open instead of using 'FFF_OPENER'.
|
|
# Used in 'fff.vim'.
|
|
[[ $1 == -p ]] &&
|
|
file_picker=1
|
|
|
|
# bash 5 and some versions of bash 4 don't allow SIGWINCH to interrupt
|
|
# a 'read' command and instead wait for it to complete. In this case it
|
|
# causes the window to not redraw on resize until the user has pressed
|
|
# a key (causing the read to finish). This sets a read timeout on the
|
|
# affected versions of bash.
|
|
# NOTE: This shouldn't affect idle performance as the loop doesn't do
|
|
# anything until a key is pressed.
|
|
# SEE: https://github.com/dylanaraps/fff/issues/48
|
|
((BASH_VERSINFO[0] > 3)) &&
|
|
read_flags=(-t 0.05)
|
|
|
|
((${FFF_LS_COLORS:=1} == 1)) &&
|
|
get_ls_colors
|
|
|
|
# Create the trash and cache directory if they don't exist.
|
|
mkdir -p "${XDG_CACHE_HOME:=${HOME}/.cache}/fff" \
|
|
"${FFF_TRASH:=${XDG_DATA_HOME:=${HOME}/.local/share}/fff/trash}"
|
|
|
|
# 'nocaseglob': Glob case insensitively (Used for case insensitive search).
|
|
# 'nullglob': Don't expand non-matching globs to themselves.
|
|
shopt -s nocaseglob nullglob
|
|
|
|
# Trap the exit signal (we need to reset the terminal to a useable state.)
|
|
trap 'reset_terminal' EXIT
|
|
|
|
# Trap the window resize signal (handle window resize events).
|
|
trap 'get_term_size; redraw' WINCH
|
|
|
|
get_os
|
|
get_term_size
|
|
get_w3m_path
|
|
setup_options
|
|
setup_terminal
|
|
redraw full
|
|
|
|
# Vintage infinite loop.
|
|
for ((;;)); {
|
|
read "${read_flags[@]}" -srn 1 && key "$REPLY"
|
|
|
|
# Exit if there is no longer a terminal attached.
|
|
[[ -t 1 ]] || exit 1
|
|
}
|
|
}
|
|
|
|
main "$@"
|
|
|