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