#!/usr/bin/env bash # # fff - fucking fast file-manager. # https://github.com/dylanaraps/fff 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. # Use affixes provided by the user or use defaults, if necessary. if [[ $FFF_MARK_FORMAT == *%f* ]]; then mark_pre=${FFF_MARK_FORMAT/'%f'*} mark_post=${FFF_MARK_FORMAT/*'%f'} else mark_pre=" " mark_post="*" fi # Find supported 'file' arguments. file -I &>/dev/null || : "${file_flags:=biL}" } 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. w3m_paths=(/usr/{pkg/,}{local/,}{bin,lib,libexec,lib64,libexec64}/w3m/w3mi*) read -r w3m _ < <(type -p "$FFF_W3M_PATH" w3mimgdisplay "${w3m_paths[@]}") } get_mime_type() { # Get a file's mime_type. mime_type=$(file "-${file_flags:-biL}" "$1" 2>/dev/null) } 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[3%s;4%sm%*s\r%s %s%s\e[m\e[%sH\e[K\e8' \ "$((LINES-1))" \ "${FFF_COL5:-0}" \ "${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 # Set window name. printf '\e]2;fff: %s\e'\\ "$PWD" # If '$PWD' is '/', unset it to avoid '//'. [[ $PWD == / ]] && PWD= for item in "$PWD"/*; do if [[ -d $item ]]; then dirs+=("$item") # Find the position of the child directory in the # parent directory list. [[ $item == "$OLDPWD" ]] && ((previous_index=item_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 # standard 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)) ((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 current 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_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]}") # Subtract 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 } 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 ${FFF_KEY_YANK:=y}|${FFF_KEY_YANK_ALL:=Y}) file_program=(cp -iR) ;; ${FFF_KEY_MOVE:=m}|${FFF_KEY_MOVE_ALL:=M}) file_program=(mv -i) ;; ${FFF_KEY_LINK:=s}|${FFF_KEY_LINK_ALL:=S}) file_program=(ln -s) ;; # These are 'fff' functions. ${FFF_KEY_TRASH:=d}|${FFF_KEY_TRASH_ALL:=D}) file_program=(trash) ;; ${FFF_KEY_BULK_RENAME:=b}|${FFF_KEY_BULK_RENAME_ALL:=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" || cmd_line "error: Can't cd to trash directory." if cp -alf "$@" &>/dev/null; then rm -r "${@:1:$#-1}" else mv -f "$@" fi # 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 } clear_screen reset_terminal "${VISUAL:-${EDITOR:-vi}}" "$1" setup_terminal redraw ;; *) # 'nohup': Make the process immune to hangups. # '&': Send it to the background. # 'disown': Detach it from the shell. nohup "${FFF_OPENER:-${opener:-xdg-open}}" "$1" &>/dev/null & disown ;; 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}) read "${read_flags[@]}" -rsn 2 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' or '\eOC'). # '' is what bash sees when the enter/return key is pressed. ${FFF_KEY_CHILD1:=l}|\ ${FFF_KEY_CHILD2:=$'\e[C'}|\ ${FFF_KEY_CHILD3:=""}|\ ${FFF_KEY_CHILD4:=$'\eOC'}) open "${list[scroll]}" ;; # Go to the parent directory. # 'D' is what bash sees when the left arrow is pressed # ('\e[D' or '\eOD'). # '\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'}|\ ${FFF_KEY_PARENT5:=$'\eOD'}) # 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' or '\eOB'). ${FFF_KEY_SCROLL_DOWN1:=j}|\ ${FFF_KEY_SCROLL_DOWN2:=$'\e[B'}|\ ${FFF_KEY_SCROLL_DOWN3:=$'\eOB'}) ((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' or '\eOA'). ${FFF_KEY_SCROLL_UP1:=k}|\ ${FFF_KEY_SCROLL_UP2:=$'\e[A'}|\ ${FFF_KEY_SCROLL_UP3:=$'\eOA'}) # '\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:=$FFF_HIDDEN}>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 full ;; # 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 printf '\e[1mfff\e[m: %s\n' "Running ${file_program[0]}" "${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]}" "${FFF_STAT_CMD:-stat}" "${list[scroll]}" read -ern 1 redraw } ;; # Toggle executable flag. ${FFF_KEY_EXECUTABLE:=X}) [[ -f ${list[scroll]} && -w ${list[scroll]} ]] && { if [[ -x ${list[scroll]} ]]; then chmod -x "${list[scroll]}" status_line "Unset executable." else chmod +x "${list[scroll]}" status_line "Set executable." fi } ;; # 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" ;; # Refresh current dir. ${FFF_KEY_REFRESH:=e}) open "$PWD" ;; # 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 ((${FFF_HIDDEN:=0} == 1)) && shopt -s dotglob # 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 "$@"