From 0fdedde73154a0802e38333dfb0013a15058b51a Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Fri, 16 Aug 2024 11:33:48 +0700 Subject: [PATCH] added fff and shfm --- bin/fff | 1145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ bin/shfm | 428 ++++++++++++++++++++ 2 files changed, 1573 insertions(+) create mode 100755 bin/fff create mode 100755 bin/shfm diff --git a/bin/fff b/bin/fff new file mode 100755 index 0000000..16766e3 --- /dev/null +++ b/bin/fff @@ -0,0 +1,1145 @@ +#!/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. + # 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 "$@" diff --git a/bin/shfm b/bin/shfm new file mode 100755 index 0000000..aee611e --- /dev/null +++ b/bin/shfm @@ -0,0 +1,428 @@ +#!/bin/sh + +esc() { + case $1 in + # vt100 (IL is vt102) (DECTCEM is vt520) + CUD) printf '%s[%sB' "$esc_c" "$2" ;; # cursor down + CUP) printf '%s[%s;%sH' "$esc_c" "$2" "$3" ;; # cursor home + CUU) printf '%s[%sA' "$esc_c" "$2" ;; # cursor up + DECAWM) printf '%s[?7%s' "$esc_c" "$2" ;; # line wrap + DECRC) printf '%s8' "$esc_c" ;; # cursor restore + DECSC) printf '%s7' "$esc_c" ;; # cursor save + DECSTBM) printf '%s[%s;%sr' "$esc_c" "$2" "$3" ;; # scroll region + DECTCEM) printf '%s[?25%s' "$esc_c" "$2" ;; # cursor visible + ED[0-2]) printf '%s[%sJ' "$esc_c" "${1#ED}" ;; # clear screen + EL[0-2]) printf '%s[%sK' "$esc_c" "${1#EL}" ;; # clear line + IL) printf '%s[%sL' "$esc_c" "$2" ;; # insert line + SGR) printf '%s[%s;%sm' "$esc_c" "$2" "$3" ;; # colors + + # xterm (since 1988, supported widely) + screen_alt) printf '%s[?1049%s' "$esc_c" "$2" ;; # alternate buffer + esac +} + +term_setup() { + stty=$(stty -g) + stty -icanon -echo + esc screen_alt h + esc DECAWM l + esc DECTCEM l + esc ED2 + esc DECSTBM 1 "$((LINES - 2))" +} + +term_reset() { + esc DECAWM h >&2 + esc DECTCEM h >&2 + esc ED2 >&2 + esc DECSTBM >&2 + esc screen_alt l >&2 + stty "$stty" + + # needed for cd-on-exit + printf '%s\n' "$PWD" >&1 +} + +term_resize() { + # false-positive, behavior intentional, globbing is disabled. + # shellcheck disable=2046 + { + set -f + set +f -- $(stty size) + } + + LINES=$1 COLUMNS=$2 + + # space for status_line + bottom=$((LINES - 2)) +} + +term_scroll_down() { + case $((y - $#)) in + [0-9]*) return + esac + + y=$((y + 1)) + y2=$((y2 + 1 < bottom ? y2 + 1 : bottom)) + + line_print "$((y - 1))" "$@" + printf '\n' + line_print "$y" "$@" + status_line "$#" +} + +term_scroll_up() { + case $y in + -*|0|1) return + esac + + y=$((y - 1)) + + line_print "$((y + 1))" "$@" + + case $y2 in + 1) esc IL ;; + *) esc CUU; y2=$((y2 > 1 ? y2 - 1 : 1)) + esac + + line_print "$y" "$@" + status_line "$#" +} + +cmd_run() { + stty "$stty" + esc DECTCEM h + esc DECSTBM + esc ED2 + "$@" ||: + esc DECSTBM 1 "$((LINES - 2))" + esc DECTCEM l + stty -icanon -echo + hist=2 +} + +file_escape() { + tmp=$1 safe= + + # loop over string char by char + while c=${tmp%"${tmp#?}"}; do + case $c in + '') return ;; + [[:cntrl:]]) safe=$safe\? ;; + *) safe=$safe$c ;; + esac + + tmp=${tmp#?} + done +} + +hist_search() { + hist=0 j=1 + + for file do + case ${PWD%%/}/$file in + "$old_pwd") y=$j y2=$((j >= bottom ? mid : j)) cur=$file + esac + + j=$((j + 1)) + done +} + +list_print() { + esc ED2 + esc CUP + + i=1 + end=$((bottom + 1)) + mid=$((bottom / 4 < 5 ? 1 : bottom / 4)) + + case $# in + 1) [ -e "$1" ] || [ "$1" = 'no results' ] || set -- empty + esac + + case $hist in + 2) # redraw after cmd run + shift "$((y > y2 ? y - y2 : 0))" + ;; + + 1) # redraw after go-to-parent + hist_search "$@" + shift "$((y >= bottom ? y - mid : 0))" + ;; + + *) # everything else + shift "$((y >= bottom ? y - bottom : 0))" + ;; + esac + + for file do + case $i in + "$y2") esc SGR 0 7 + esac + + case $((i - end)) in + -*) + line_format "$file" + esc CUD + ;; + esac + + i=$((i + 1)) + done + + esc CUP "$((y > y2 ? y2 : y))" +} + +redraw() { + list_print "$@" + status_line "$#" +} + +status_line() { + esc DECSC + esc CUP "$LINES" + + case $USER in + root) esc SGR 31 7 ;; + *) esc SGR 34 7 ;; + esac + + printf '%*s\r%s ' "$COLUMNS" "" "($y/$1)" + + case $ltype in + '') printf %s "$PWD" ;; + *) printf %s "$ltype" + esac + + esc SGR 0 0 + esc DECRC +} + +prompt() { + esc DECSC + esc CUP "$LINES" + printf %s "$1" + esc DECTCEM h + esc EL0 + + case $2 in + r) + stty icanon echo + read -r ans ||: + stty -icanon -echo + ;; + esac + + esc DECRC + esc DECTCEM l + status_line "($y/$#) $PWD" +} + +line_print() { + offset=$1 + + case $offset in + "$y") esc SGR 0 7 + esac + + shift "$offset" + + case $offset in + "$y") cur=$1 + esac + + line_format "$1" +} + +line_format() { + file_escape "$1" + [ -d "$1" ] && esc SGR 1 31 + printf %s "$safe" + [ -d "$1" ] && printf / + esc SGR 0 0 + esc EL0 + printf '\r' +} + +main() { + set -e + + case $1 in + -h|--help) + printf 'shfm -[hv] \n' + exit 0 + ;; + + -v|--version) + printf 'shfm 0.4.2\n' + exit 0 + ;; + + *) + cd -- "${1:-"$PWD"}" + ;; + esac + + esc_c=$(printf '\033') + bs_char=$(printf '\177') + + set -- * + cur=$1 + + term_resize + term_setup + + trap 'term_reset' EXIT INT + trap 'term_resize; term_setup; y=1 y2=1; redraw "$@"' WINCH + + y=1 y2=1 + redraw "$@" + + while key=$(dd ibs=1 count=1 2>/dev/null); do + case $key${esc:=0} in + k?|A2) + term_scroll_up "$@" + ;; + + j?|B2) + term_scroll_down "$@" + ;; + + l?|C2|"$esc") # ARROW RIGHT + if [ -d "$cur" ] && cd -- "$cur" >/dev/null 2>&1; then + set -- * + y=1 y2=1 cur=$1 ltype= + redraw "$@" + + elif [ -e "$cur" ]; then + cmd_run "${SHFM_OPENER:="${EDITOR:=vi}"}" "$cur" + redraw "$@" + fi + ;; + + h?|D2|"$bs_char"?) # ARROW LEFT + old_pwd=$PWD + + case $ltype in + '') cd .. || continue ;; + *) ltype= ;; + esac + + set -- * + y=1 y2=1 cur=$1 hist=1 + redraw "$@" + ;; + + g?) + case $y in + 1) continue + esac + + y=1 y2=1 cur=$1 + redraw "$@" + ;; + + G?) + y=$# + y2=$(($# < bottom ? $# : bottom)) + line_print "$y" "$@" + redraw "$@" + ;; + + .?) + case ${hidden:=1} in + 1) hidden=0; set -- .* ;; + 0) hidden=1; set -- * + esac + + y=1 y2=1 cur=$1 + redraw "$@" + ;; + + :?) + prompt "cd: " r + + # false positive, behavior intentional + # shellcheck disable=2088 + case $ans in + '~') ans=$HOME ;; + '~/'*) ans=$HOME/${ans#"~/"} + esac + + cd -- "${ans:="$0"}" >/dev/null 2>&1|| continue + set -- * + y=1 y2=1 cur=$1 + redraw "$@" + ;; + + /?) + prompt / r + + IFS= + # globbing intentional, word splitting is disabled. + # shellcheck disable=2086 + set -- $ans* + unset IFS + + case $1$# in + "$ans*1") set -- 'no results' + esac + + y=1 y2=1 cur=$1 ltype="search $PWD/$ans*" + redraw "$@" + status_line "$#" + ;; + + -?) + cd -- "$OLDPWD" >/dev/null 2>&1|| continue + set -- * + y=1 y2=1 cur=$1 + redraw "$@" + ;; + + \~?) + cd || continue + set -- * + y=1 y2=1 cur=$1 + redraw "$@" + ;; + + \!?) + export SHFM_LEVEL + SHFM_LEVEL=$((SHFM_LEVEL + 1)) + cmd_run "${SHELL:=/bin/sh}" + redraw "$@" + ;; + + \??) + set -- 'j - down' \ + 'k - up' \ + 'l - open file or directory' \ + 'h - go up level' \ + 'g - go to top' \ + 'G - go to bottom' \ + 'q - quit' \ + ': - cd to ' \ + '/ - search current directory *' \ + '- - go to last directory' \ + '~ - go home' \ + '! - spawn shell' \ + '. - toggle hidden files' \ + '? - show keybinds' + + y=1 y2=1 cur=$1 ltype=keybinds + redraw "$@" + status_line "$#" + ;; + + q?) exit 0 ;; + + # handle keys which emit escape sequences + "$esc_c"*) esc=1 ;; + '[1') esc=2 ;; + *) esc=0 ;; + esac + done +} + +main "$@" >/dev/tty