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

#!/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 "$@"