dotfiles/scripts/vv
2024-02-18 11:40:03 -06:00

2439 lines
73 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# vv: VT View
# Version 1.9.3
# B9 May 2023
# Use sixel graphics to show images inside a terminal (e.g., xterm).
# Copyright (c) 2019..2023 hackerb9, under the terms of GNU GPL 3+.
# Some nice features:
# * Can display any image type. (Via ImageMagick).
# * Detects images by magic, not by extension.
# * Shows everything in the current working directory by default.
# * Loads directories recursively (default: ask; -r: always; -R: never).
# * Scales down large images to fit within terminal.
# * Defaults to fast viewing [-F] to quickly preview and delete (d) images,
# or use [-f] to default to full-size (slower, high quality) view.
# * Hit (f) to view full-size (slower, high quality) for current image only,
# or use (F) to toggle between fast and full-size viewing mode.
# * Preview size can be increased (+), decreased (-), set (=), or reset (0).
# * Supports "fit width" (w) mode; portrait pictures scroll vertically.
# * Can view at "100% zoom" (1). Cuts large picture into pieces, if necessary.
# * Has "best fit" (B), autorotate to use as much of the screen as possible.
# * Deleted (d) images are moved to Trashcan (Freedesktop standard).
# * Can undo (u) last trashed file.
# * Reverse image search using w3m (R). (Currently scrapes yandex).
# * Slideshow mode (S).
# * Renaming a file (r) uses readline for easier editing.
# * Move (m) and save-a-copy (s) remember previously used directory.
# * Edit embedded comments (C). Works with JPEG, PNG, TIFF, GIF, and more.
# * Embedded comments can have multiple lines (use ^V^J for a new line).
# * Sets xterm to use more color registers for higher quality pictures.
# * Should work on true DEC hardware (e.g., VT340).
# * Resizes current image when terminal resizes. (SIGWINCH).
# * Pixel height of preview is a multiple of the current text font height,
# so previews should be the right size no matter your screen resolution.
# * Pixel width of preview can use the full width of the terminal.
# * View videos (v) such as animated GIFs. (Requires 'sixvid' and ffmpeg).
# * Icons and small images can be zoomed in to screen size. (z to toggle).
# * Breadth first search. Images in current dir are shown before subdirs.
# Required Dependencies:
# * A sixel capable terminal, such as 'xterm -ti vt340'.
# * ImageMagick
# Recommended but not required:
# * ncurses-bin (for tput)
# * file (determine filetype by magic numbers rather than extension)
# * mediainfo (quickly count number of frames in a video)
# * GNU coreutils (for realpath, for deleting images to trashcan)
# * curl, w3m and w3m-img for text based reverse image search
# * Bash version >=5 (read bug workaround)
# * sixvid & ffmpeg: VT viewer so viewing an mp4 will play an animation
# * poppler-utils (for pdfinfo to count pages in PDF files)
# Note: This is designed mainly for quickly previewing, deleting,
# and renaming images over an ssh connection. For that reason,
# fast, small, low-quality images are emphasized and deletion
# is a single keystroke. (Note, undeletion is also a single key).
#
# Design:
#
# This should feel like a command-line program that just happens
# to have graphics. In particular, it should *NOT* take over the
# whole screen.
#
# Current work is appended at the bottom. Old images should
# scroll off the top.
#
# Keep hands on home row. Allow harder to reach keys -- like Esc
# and arrows -- only as aliases for easier to hit keys.
#
# Common functions are mapped to a single key, without shift.
# The most frequently used functions are on the home row.
# Less common functions may include shift.
#
# As much as possible, reduce repetitive work. (Temporal locality.)
#
# Go as fast as possible, trading off quality if necessary.
#
# Should work even over a laggy ssh connection with a vintage VT340.
#
# Unix keys ^C, ^Z, ^W, ^L act reasonably and do not mess up the terminal.
#
# The user isn't a dummy, don't hide info merely to seem simple.
#
# Reduce cognitive load:
#
# * User shouldn't have to memorize which boolean flags are set.
# Show status every time an image is displayed.
#
# * Help should not be overwhelming. Give just the most salient
# bits, with ways the user can ask for more help.
#
# * "Don't make me read." As much as possible, make the state
# predictable so an expert could type keys without seeing the
# prompts and know exactly how vv will react. (Note: This is
# the opposite of current touch screen interfaces).
#
#
# For more info or to report a bug, please see
# https://github.com/hackerb9/vv
# Defaults you can change.
declare -g timeout=0.25 # Max secs to wait for control sequence response.
declare -gi ssdelay=5 # Num secs to linger during slideshow.
declare -g viewmode="fast" # Show "fast" or "full" images.
declare -gi previewlines=8 # Height of fast preview image (in text lines).
declare -g background="gray20" # Default for bg (alpha) if autodetection fails
declare -g foreground="gray80" # Default for fg (text) if autodetection fails
declare -g rflag="ask" # Empty string means do not recurse dirs.
declare -g sortorder="-v" # `ls` flag to sort files in "natural" order.
#declare -g sortorder="-tru" # `ls` flag to sort files. Least recent viewed.
#declare -g sortorder="-t" # `ls` flag to sort files. Most recent changed.
declare -g traversal="breadth" # dir recursion: breadth/depth first or neither.
declare -g shownotices="y" # Show optional messages.("Install X to get Y").
#declare -g shownotices="" # Empty string to never mention such frippery.
# Defaults for image processing flags. Note that opposite of "y" is "", not "n".
# To disable, set to empty string. To enable, set to any string.
declare -g dither="y" # Improve image quality by dithering if viewmode==full.
declare -g aalias="y" # Smooths jaggies when zooming in on small images.
declare -g enhance="" # Lightens dark images with gamma +1.5. Set by (e)
declare -g zoomin="y" # Magnify small images to screen size (slower).
# Defaults if terminal autodetection fails.
declare -gi numcolors=16 # Num colors the terminal can show.
declare -gi defaultwidth=800 defaultheight=480 # Default sixel screen size.
# Global variables
declare -gi LINES COLUMNS # Screen width and height in rows, cols of text.
declare -gi width height # Screen width and height in pixels.
declare -gi fontheight # Height of terminal's font in pixels.
declare -gi previewsize=0 # Height of preview in pixels. (C.f., previewlines, above)
declare -gi forcepsize=0 # Size of preview, if given on commandline.
declare -g tmpdir # Directory for storing temporary files.
declare -gA termkey # Array to hold terminfo key input sequences.
declare -g currentview=$viewmode # Most recently used viewmode.
declare -gi FrameCount # Number of image frames in file. >1 for videos.
declare -gi ImageWidth # Image width in pixels.
declare -gi ImageHeight # Image height in pixels.
# Debugging stuff
declare -g DEBUG=${DEBUG} # Set to anything to enable debugging
declare -g TIME= # Set to anything to benchmark sixel creation
debug() {
if [[ "${DEBUG:-}" ]]; then
echo "$@" >&2
fi
}
cleanup() {
# If the user hits ^C, we don't want them stuck in SIXEL mode
echo -n $'\e\\' # Escape sequence to stop SIXEL
stty -F/dev/tty echo # Reset terminal to show input characters.
stty -F/dev/tty icanon # Allow backspace to work again.
stty -F/dev/tty sane # For bash 4.4's SIGWINCH-in-read bug
echo -en "\r"; tput el # Clear line for prompt.
tput rmkx # Disable terminal application key sequences
tput cnorm # Show the cursor.
[[ -d "$tmpdir" ]] && rm -r "$tmpdir" # Erase temporary files.
exit 0
}
trap cleanup SIGINT SIGHUP SIGABRT EXIT
usage() {
cat <<EOF
Usage: vv [OPTION]... [FILE | DIR]...
Rapidly view and delete image files in a Sixel-capable terminal.
With no arguments, searches current directory for images.
-r, --recursive[=yes|ask] # Search image directories recursively [ask].
-R, --recursive=no # Do not search sub directories recursively.
-f, --full # View images full-size of terminal (${width}x${height}).
-F, --fast # View fast ${previewlines} line high previews [DEFAULT].
-B, --best-fit # Like full, but rotate to maximize pixel usage.
-S, --slideshow[=DELAY] # Change images every $ssdelay seconds.
-n, --num-colors=N # Set max colors. (Currently: $numcolors).
-g, --geometry=WIDTHxHEIGHT # Set screen width, height in pixels. (${width}x${height}).
-P, --preview-size=PIXELS # Set height of previews (pixels). (${previewsize}).
-p, --preview-lines=LINES # Same, but specify in lines of text. ($previewlines).
-a, --antialias # Enable smoothing when resizing image [DEFAULT].
-A, --no-antialias # Disable smoothing, show pixels as sharp.
-?, --help
EOF
}
error() {
echo >&2
echo "$(basename $0): FATAL ERROR: $@" >&2
}
notice() {
# Display a "notice" — a minor message, such as a missing
# optional program that would improve functionality, but
# isn't strictly required.
if [[ "$shownotices" ]]; then
echo >&2
echo "$(basename $0): notice: $@" >&2
fi
}
prerequisites() {
# Check that all necessary programs are installed.
local -i returncode=0
# Critical dependencies which cause fatal errors
if ! command -v convert >/dev/null; then
error "Please install ImageMagick for convert and identify"
returncode=1
fi
# Non-fatal errors that simply degrade performance or capability
if ! command -v tput >/dev/null; then
notice "Please install ncurses-bin for tput and infocmp"
# 'tput el' is used to clear the line.
# 'infocmp' is used to interpret the arrow key sequences.
alias tput=true; alias infocmp=true
fi
if ! command -v file >/dev/null; then
notice "Please install 'file' for autodetecting images by magic"
alias file=fakefile
fi
if ! command -v mediainfo >/dev/null; then
# Note cannot use 'alias' here because it is run in a subshell.
mediainfo() { fakemediainfo "$@" ; }
fi
if ! command -v realpath >/dev/null; then
notice "Please install GNU coreutils for realpath"
# Note cannot use 'alias' here because it is run in a subshell.
realpath() { fakerealpath "$@" ; }
fi
if ! command -v w3m >/dev/null || command -v w3m-img >/dev/null; then
function w3m {
notice "Please install w3m and w3m-img for reverse image search"
}
alias w3m-img=w3m
fi
if ! command -v curl >/dev/null; then
notice "Please install curl for reverse image search"
fi
if ! command -v numfmt >/dev/null; then
numfmt() { tr '\t' ' '; } # Only needed for nicely grouping thousands
fi
if ! command -v fmt >/dev/null; then
fmt() { cat ; } # Only needed for wrapping long comments
fi
if ! command -v pdfinfo >/dev/null; then
pdfinfo() {
notice "Page count is slow. Please install pdfinfo (poppler-utils)."
echo "Pages: $(identify "$1" | grep -c PDF)"
}
fi
if [[ $BASH_VERSINFO -lt 5 ]]; then
notice "Kludging read for Bash4"
BASH4BUG="-e"
else
BASH4BUG=""
fi
if ! tmpdir=$(mktemp -d --tmpdir "$(basename $0).$$.XXXXXX"); then
echo "Error: Could not create temporary directory! Disk full?" >&2
returncode=1
fi
if [[ returncode -eq 0 ]]; then
return 0
else
# Die on fatal errors
exit $returncode
fi
}
autodetect() {
# Various terminal initialization and configuration routines.
# Before querying the terminal, wait for it to be ready.
waitforterminal
# Don't show escape sequences the terminal doesn't understand.
stty -echo # Hush-a Mandara Ni Pari
# Don't consume backspace key before 'read'.
stty -icanon
# Hide the cursor so it doesn't dash around when we show the prompt.
tput civis
# Make sure the terminal supports sixel graphics.
hassixelordie
# Terminal color palette ("registers") autodetection.
read numcolors < <(getnumcolors $numcolors)
# BUG: ImageMagick 6.9 can't send more than 256 colors anyhow.
[[ numcolors -le 256 ]] || numcolors=256
# Don't dither if we have enough colors. (Dither adds +1s to processing).
# BUG: Unfortunately, we have to always dither since max colors is 256.
[[ numcolors -le 256 ]] && dither="yup" || dither=""
# Detect terminal background color (for alpha channel).
background=$(getbg)
# Autodetect the sixel screen size .
windowchange
# Make terminfo keys valid so we can catch arrow keys.
inittermkey
# Configure readline for input with 'read -e'
configreadline
}
getbg() {
# Send the xterm escape sequence to get the background color of
# the terminal. Terminal responds: $'\e]11;rgb:ffff/0000/ffff\e\\'.
# Prints results as hexadecimal (16-bit per channel) in the form
# #RRRRGGGGBBBB (useful for ImageMagick).
local p=11 # 11 is bg, 10 is fg in escape sequence
# wait for terminal to be ready (up to 5 seconds)
waitforterminal 5
# Check for reverse video
IFS=";?$" read -a REPLY -s -t $timeout -d "y" -p $'\e[?5$p'
if [[ $? -le 128 && ( ${REPLY[2]} == 1 || ${REPLY[2]} == 3 ) ]]; then
# Reversed colors!
p=10
fi
# Query the terminal background (or foreground) color.
IFS=";:/" read -a REPLY -r -s -t $timeout -d "\\" -p $'\e]'$p$';?\e\\'
if [[ $? -le 128 && ${REPLY[1]:-} =~ ^rgb ]]; then
echo '#'${REPLY[2]}${REPLY[3]}${REPLY[4]%%$'\e'*}
else
echo $background
fi
}
getfg() {
# Send the xterm escape sequence to get the fg color of the terminal.
local p=10 # 11 is bg, 10 is fg in escape sequence
# wait for terminal to be ready (up to 5 seconds)
waitforterminal 5
# Check for reverse video
IFS=";?$" read -a REPLY -s -t $timeout -d "y" -p $'\e[?5$p'
if [[ ${REPLY[2]} == 1 || ${REPLY[2]} == 3 ]]; then
# Reversed colors!
p=11
fi
# Query the terminal background (or foreground) color.
IFS=";:/" read -a REPLY -r -s -t $timeout -d "\\" -p $'\e]'$p$';?\e\\'
if [[ ${REPLY[1]} =~ ^rgb ]]; then
echo '#'${REPLY[2]}${REPLY[3]}${REPLY[4]%%$'\e'*}
else
echo $background
fi
}
windowchange() {
# SIGWINCH handler: Called when terminal window is resized.
# Send control sequence to query the sixel graphics geometry to
# find out how large of a sixel image can be shown.
# Sets global variables LINES, COLUMNS, width, height, and fontheight.
local IFS=""; unset IFS # In case IFS was set before SIGWINCH
read width height < <(getwindowsize)
# Calculate height of terminal font in pixels
# Note: this could be wrong because xterm returns max == 1000x1000.
# TODO: maybe use echo $'\e[16t' to get width, height of font.
LINES=$(tput lines); COLUMNS=$(tput cols)
[[ $LINES -gt 0 ]] || LINES=24 # Default 24 rows of text.
fontheight=height/LINES
[[ fontheight -gt 0 ]] || fontheight=20 # Default 20px high font.
debug "font height is $fontheight"
# Trim returned height so we have room for two lines of text
height=$(( fontheight * (LINES-2) ))
debug "usable screen height is $height"
if [[ $forcepsize == 0 ]]; then
# Preview size height changes based on font size.
if [[ $previewsize -ne $((fontheight*previewlines)) ]]; then
[[ $previewsize ]] && SHOULDREDRAW=yup
previewsize=fontheight*previewlines
debug "preview height = fontheight × preview lines = $fontheight × $previewlines = $previewsize"
fi
else
# Preview size forced by command line.
previewsize=$forcepsize
fi
debug "preview size is ${width}x${previewsize} pixels"
if [[ "$viewmode" == full || "$currentview" == full ]]; then
SHOULDREDRAW=yup # Redraw the current image
fi
}
trap windowchange SIGWINCH
hassixelordie() {
# Returns true if terminal is Sixel capable.
# Otherwise, prints an error message and dies.
local hassixel="" code IFS=";?c"
# IS TERMINAL SIXEL CAPABLE? # Send Device Attributes
if read -a REPLY -s -t 1 -d "c" -p $'\e[c' >&2; then
for code in "${REPLY[@]}"; do
if [[ $code == "4" ]]; then
hassixel=yup
break
fi
done
fi
# YAFT is vt102 compatible, cannot respond to vt220 escape sequence.
if [[ "$TERM" == yaft* ]]; then hassixel=yeah; fi
if [[ -z "$hassixel" ]]; then
cat <<-EOF >&2
Error: Your terminal does not report having sixel graphics support.
Please use a sixel capable terminal, such as xterm -ti vt340, or
ask your terminal manufacturer to add sixel support.
You may test your terminal by viewing a single image, like so:
convert foo.jpg -geometry 800x480 sixel:-
If your terminal actually does support sixel, please file a bug
report at http://github.com/hackerb9/vv/issues
EOF
read -s -t 1 -d "c" -p $'\e[c' >&2
if [[ "$REPLY" ]]; then
echo
cat -v <<< "(Please mention device attribute codes: ${REPLY}c)"
fi
exit 1
fi
}
getnumcolors() {
# Detects number of colors the terminal can show and attempts to
# increase the number if it's too low.
# Argument $1: default number of colors if detection fails.
# Output: Prints how many color registers the terminal has.
local -i n=0
local IFS=";" # Split reads on semicolons
if read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?1;1;0S' >&2; then
[[ ${REPLY[1]} == "0" ]] && n=${REPLY[2]}
fi
# YAFT is vt102 compatible, cannot respond to vt220 escape sequence.
if [[ "$TERM" == yaft* ]]; then n=256; fi
# Increase colors, if needed
if [[ $n -lt 256 ]]; then
# Attempt to set the number of colors to 256.
# This will work for xterm, but fail on a real vt340.
if read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?1;3;256S' >&2; then
[[ ${REPLY[1]} == "0" ]] && n=${REPLY[2]}
fi
fi
# Return the results (or the default if querying didn't work)
[[ $n -gt 0 ]] || n=${1:-16}
echo $n
return
}
getwindowsize() {
# Send control sequences to find how large of a sixel image can be shown.
# Uses globals $defaultwidth, $defaultheight if terminal does not respond.
# Outputs geometry as width then height. (echo $w $h)
waitforterminal # Wait for terminal to be ready before querying.
# Necessary for Xterm-344 due to slow redraw bug.
local -i w=0 h=0 # for reading width and height integers
local IFS=";" # temporarily split on semicolons
local REPLY=(0 0 0 0) # array of results from terminal
# Send control sequence to query the sixel graphics geometry.
if read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?2;1;0S' >&2; then
if [[ ${#REPLY[@]} -ge 4 && ${REPLY[2]} -gt 0 && ${REPLY[3]} -gt 0 ]]; then
w=${REPLY[2]}
h=${REPLY[3]}
else
# Nope. Fall back to dtterm WindowOps to approximate sixel geometry.
waitforterminal
if read -a REPLY -s -t ${timeout} -d "t" -p $'\e[14t' >&2; then
if [[ $? == 0 && ${#REPLY[@]} -ge 3 && ${REPLY[2]} -gt 0 ]]; then
w=${REPLY[2]}
h=${REPLY[1]}
fi
fi
fi
fi
# Discard responses slow to arrive responses from the terminal.
flushstdin
# Return the results (or the default if querying didn't work)
[[ w -gt 0 ]] || w=${defaultwidth}
[[ h -gt 0 ]] || h=${defaultheight}
echo $w $h
debug "window size is $w x $h"
return
}
waitforterminal() {
# Send an escape sequence and wait for a response from the terminal.
# This routine will let us know when an image transfer has finished
# and it's okay to send escape sequences that request results.
# WARNING. This *should* work with any terminal, but if it fails
# it'll end up waiting for approximately forever (i.e., 60 seconds).
flushstdin
read -s -t ${1:-60} -d "c" -p $'\e[c'
flushstdin
}
inittermkey() {
# Build termkey array so we can recognize when the user hits keys
# that send multiple characters. E.g., \eOB maps to "Down Arrow".
local a x k
# To be valid, we must enable terminal application mode key sequences.
if tput smkx; then
for k in $(infocmp -L1 | egrep 'key_|cursor_'); do
a=""; x=""
# Example "k: key_down=\EOB,"
a=${k#*key_}
a=${a#*cursor_}
a=${a%=*} # a: down
x=${k#*=}
x=${x%,*} # x: \EOB
#debug termkey["$x"]="$a"
termkey["$x"]="$a"
done
fi
}
configreadline() {
# Configure input from the user so that 'read -e' (readline) works.
# Note, bind will complain that "line editing is not enabled", but
# we toss that spurious warning in /dev/null.
# Editing happens on only one line of text, scrolling horizontally.
bind "set horizontal-scroll-mode on" 2>/dev/null
# XXX If I knew how to disable readline adding a space after
# filename completion I would do so here. (See saverenamemove()).
}
parseescape() {
# Parse terminal escape sequence for F-keys, arrows.
# Read all the characters following an escape key has been hit
# and then look it up in the termkey array.
while read -t 0; do
read -n 1 -s temp
REPLY+="$temp"
done
esc=$'\E'
REPLY=${REPLY/$esc/\\E} # Replace escape with backslash E
REPLY=${termkey["$REPLY"]:-"$REPLY"}
}
flushstdin() {
# flush stdin in case a key was hit twice by accident
local REPLY
while read -s -n1 -t .001; do :; done
}
e() {
# Clear current line and print text on it without a linefeed
echo -en "\r"
tput el
echo -en "$(tildify "$*")" # Not using $@ so I can pass args to echo.
}
E() {
# Clear current line and print text on it with a linefeed
e "$@"
echo
}
tildify() {
# Any arguments that look like $HOME/ get shortened to ~/
local arg
for arg; do
arg=${arg/#$HOME\//\~/} # At beginning of line.
arg=${arg// $HOME\// \~/} # After a space.
arg=${arg//\n$HOME\//\n\~/} # After a newline escape.
arg=${arg//\t$HOME\//\t\~/} # After a tab escape.
arg=${arg// $HOME\// \~/} # After a tab.
echo -n "$arg"
done
echo
}
s() {
# Plural(s)
[[ ${1%.*} -ne 1 ]] && echo -n "s"
}
declare -g lastuseddir="" # Static variable for default save/move dir.
saverenamemove() {
# Interactively save/rename/move image file $2 to a new name or dir.
# "save" and "move" default to prompting for a directory name,
# presuming the filename will be the same.
# "rename" defaults to prompting for a filename, presuming the
# directory will be the same.
# Move and rename both use 'mv', so are functionally equivalent.
# Move prompts with a directory, rename suggests a filename.
# They both set outer scope "$f", which may be a bad idea.
# (This allows user to rename again if need be.)
# [TODO: This will get cleared up once we have a proper array of files.]
local mode="$1"
local file="$2"
if [[ "$mode" == "rename" ]]; then
local path=$(realpath --relative-base="$PWD" "$file")
else
local path=$(realpath "$file")
fi
local dir=$(dirname "$path")
local ext=${f##*.}; [[ "$f" != "$ext" ]] || ext=""
local base=$(basename "$file" ".$ext")
local IFS=$'\n' # Tildify shouldn't split on spaaces
tput el
stty echo # Temporarily show characters the user types.
tput cnorm # Temporarily show the cursor.
if [[ "$mode" == "rename" ]]; then
read -d $'\r' -p "Rename to: " -e -i $(tildify "$path") newname
elif [[ "$mode" == "save" ]]; then
read -d $'\r' -p "Save a copy of $base in: " -e \
-i $(tildify "${lastuseddir:-$dir}/") newname
else
read -d $'\r' -p "Move to: " -e -i "$(tildify "${lastuseddir:-$dir}/")" newname
fi
tput civis # Hide cursor.
stty -echo # Hide input keys.
newname=${newname/#~\//$HOME/} # untildify (for realpath.)
# Remove trailing space from tab completion.
if [[ "${newname}" =~ (.*[^ ])([ ]+)$ ]]; then
newname="${BASH_REMATCH[1]}"
fi
if [[ $mode == "save" || $mode == "move" ]]; then
[[ "$newname" == /* ]] || newname="$dir/$newname" # Make path absolute.
fi
[[ -d "$newname" ]] && newname+="/." # Use existing directory.
[[ "$newname" != */ ]] || newname+="." # Never end with a "/" (for dirname.)
mkdir -p $(dirname "$newname")
newname=$(realpath "$newname" 2>/dev/null) # Canonicalize
if [[ "$newname" && "$newname" != "$dir" && ! "$newname" -ef "$path" ]]
then
if [[ -d "$newname" ]]; then
# New directory specified, so keep same filename
newname="$newname/$base.$ext"
fi
local newext="${newname##*.}"
if [[ "$newext" =~ jpe?g && "$ext" =~ jpe?g ]]; then
# Special case: Allow renaming .jpg to .jpeg and vice-versa.
ext="$newext"
fi
if [[ "$newname" != *$ext ]]; then
# User specified filename without the extension.
newname="$newname.$ext"
fi
local overwrite=""
if [[ -e "$newname" ]]; then
# Hmm... a file of that name already exists.
if cmp -s "$path" "$newname"; then
E "Notice: image already saved in $newname."
return 1
else
E "Warning: A different image already exists with that filename."
sidebyside "$path" "$newname"
overwrite="(overwrite!) "
fi
fi
# Extra warning to double check dangerous operations.
if [[ "$overwrite" ]]; then
# Okay to save/rename/move (possibly overwriting)?
e "$mode $file to $overwrite$newname (y/N)? "
read -s -n1
case "$REPLY" in
y|Y) # Yes, just continue.
;;
n|N|*)
E "$mode cancelled."
return 1
;;
esac
fi
if [[ $mode == "save" ]]; then
if cp -p "$path" "$newname"; then
E "Saved a copy of $file to $newname"
else
return 1
fi
elif [[ $mode == "rename" || $mode == "move" ]]; then
if mv "$file" "$newname"; then
E "Filename is now $newname";
f="$newname" # Write to outerscope variable "$f". (!!!)
else
return 1
fi
fi
else
E "$mode cancelled."
return
fi
if [[ $mode == "save" || $mode == "move" ]]; then
# Remember the directory for easy saving next time.
lastuseddir=$(dirname "$newname")
fi
return
}
sidebyside() {
# Show two images side by side for quick comparison.
local -i half=width/2
montage -background $background -fill $foreground \
-auto-orient \
-geometry ">${half}x>$((height-fontheight))" \
-label "$1" "$1" -label "$2" "$2" sixel:-
viewmode=ss
SHOULDREDRAW=yup
}
editproperty() {
# Given a property name and a filename,
# show the property and let the user edit it.
# Example property names: "comment", "caption", "title", "description"
local propname="$1"
local file="$2"
local frame0="file://$file[0]" # Without file://, IM barfs on colons.
local property=$(identify -format "%[$propname]" "$frame0" 2>/dev/null)
local prompt="New $propname: "
if [[ $(echo "$property" | wc -l) -gt 1 ]]; then
prompt=""
echo "WARNING: Existing $propname has multiple lines."
echo " Use ^V^J to add new lines."
fi
stty echo # Temporarily show characters the user types.
tput cnorm # Temporarily show the cursor.
IFS= read -d $'\r' -e -i "$property" -p "$prompt" newproperty
tput civis # Hide cursor.
stty -echo # Hide input keys.
if [[ "$newproperty" == "$property" || -z "$newproperty" ]]; then
E "$propname editing cancelled."
return
fi
e "Writing comment into $file..."
local t=$(mktemp "/tmp/$(basename $0).XXXXXX") || return 1
cp "$file" "$t" && convert "$t" -set "$propname" "$newproperty" "$file"
rm "$t"
}
showproperty() {
# Given a property name (e.g., "comment") and a filename,
# Show the property. Return failure if the property doesn't exist.
local propname="$1"
local file="$2"
local frame0="file://$file[0]"
local property=$(identify -format "%[$propname]" "$frame0" 2>/dev/null) &&
echo "$propname: $property"
}
reverseimagesearch() {
# Given a filename of an image, do a web search for it to see if
# it has been seen anywhere else.
# Tip to future self: to screen scrape XHR / JSON, use Firefox's
# Network inspector while uploading image to search engine. Right
# click on the POST or GET and go to "COPY cURL". Then, from the
# command line, prune that command down to the bare minimum.
# Use json_pp to pretty print JSON data.
yandeximagesearch "$1"
}
yandeximagesearch() {
# Reverse image search using yandex and w3m
# Yandex is not the best but has a simple interface we can access
# without javascript. (Unlike Tineye, Bing, and Google.)
local file="$1"
local output resultPage deleteme="" # Local vars
local scale=">1000x>1000" # Image size to scale to before searching.
tput cnorm # Temporarily show cursor for w3m
if [[ $FrameCount -gt 1 ]]; then
# It's a video, so search only for first frame.
echo "searching only first frame of video" >&2
file="$tmpdir/frame0.jpg"
convert "file://$1[0]" "$file"
deleteme="$file"
fi
url="https://yandex.com/images/search?rpt=imageview"
url+="&format=json"
url+='&request=%7B%22blocks%22%3A%5B%7B%22block%22%3A%22cbir-controller__upload%3Aajax%22%7D%5D%7D'
cookie="yp=999999999999.sp.aflt%3A999999999999.szm.1; my=YywBAAA="
# Upload the image and save the output to search through.
# Note: Curl's "@filename" splits filenames with commas & semicolons.
output=$(convert -sample "$scale" "$file" jpeg:- |
curl --max-time 30 --cookie "$cookie" \
--form upfile='@-;filename=foo.jpg' $url)
if [[ $? -gt 0 ]]; then return; fi
# First, check for Content-Based Image Recognition.
# Look for "params":{"url":"cbir_id=4725%2FkrTprMr_05694&rpt=imageview"
resultPage=$(echo "$output" | grep -o '"cbir_id=[^"]*"' )
if [[ "$resultPage" ]]; then
resultPage=${resultPage#\"}; resultPage=${resultPage%\"}
resultPage="/images/search?$resultPage"
fi
if [[ -z "$resultPage" ]]; then
echo "Couldn't find CBIR result from Yandex, trying img_url." >&2
# Look for, eg: "nextPageUrl":"/images/search?text=&amp;img_url=https%3A//www.fotopolis.pl/i/images/4/2/3/d2FjPTIzNDB4MQ%3D%3D_src_179423-1555946732rlzp7d96a9729_2048px22_1100mv.jpg&amp;rpt=imagedups"
resultPage=$(
echo "$output" |
sed -rn '
s/&amp;/\&/g;
s/.*img_url=(http[^"&;]*).*/\1/p;
')
if [[ "$resultPage" ]]; then
echo "Oh good, img_url worked." >&2
resultPage="/images/search?url=$resultPage&rpt=imageview"
fi
fi
if [[ "$resultPage" ]]; then
echo "Found $(unuriescape "$resultPage")" >&2
w3m https://yandex.com$resultPage
else
echo "Error parsing results from Yandex." >&2
fi
# Old way. Maybe delete this.
## Use cookies to get page, then show in w3m
# curl --cookie "$cookie" --form upfile=@"$file" $url | tee debug |
# sed 's#<head>#<head><base href="'$url'">#' |
# w3m -T text/html
if [[ "$deleteme" ]]; then
rm -f "$deleteme"
fi
tput civis # Hide cursor again
}
numframes() {
# Given a filename, print the number of images in the file.
# (For most images, this will be 1.)
local file="$1"
# ImageMagick can count frames in a gif or apng, but doesn't do well when
# faced with a video. Rely on mediainfo/ffprobe instead.
case $(mimetype "$file") in
video/*) # («Radio killed?»)
# MediaInfo is faster than convert, but doesn't handle GIF, APNG.
mediainfo --Inform='Video;%FrameCount%' "$file"
;;
*|image/*) # An image file. (default)
shopt -s nocasematch
if [[ $file == *gif || $file == *png ]]; then
# Note, it is much faster to pipe to 'wc' than to use
# `convert "$file[-1]" -format "%[scene]" info:-`
identify "$file" | wc -l
else
echo 1
fi
shopt -u nocasematch
;;
esac
}
fakemediainfo() {
notice "Please install 'mediainfo' for counting video frames"
# If mediainfo isn't installed, fake it using ffprobe.
# This is slow and could take a quarter of a second to run.
# (Mediainfo takes only a twentieth of a second.)
while [[ $1 == -* ]]; do shift; done
# Although the ffprobe documentation claims to have a -count_frames option,
# it doesn't actually work. We must resort to an ugly kludge:
# print everything and count the word "video".
ffprobe -show_packets "$1" 2>/dev/null | grep -c video
}
fakerealpath() {
if [[ "$1" == -* ]]; then
notice "Please install GNU coreutils for realpath"
while [[ "$1" == -* ]]; do shift; done
fi
[[ "$1" && "$1" != /* ]] && echo -n "$PWD/"
echo "$1"
}
isimage() {
# Returns true (0) if $1 is an image or video.
# We use "file" first to check for magic cookies identifying images to
# prune out formats that ImageMagick's "identify" would churn needlessly
# turning into an image (ASCII text, word processing docs).
local file="$1"
local type=$(mimetype "$file")
if [[ $type == image/svg* ]]; then
# SVG files are special: ImageMagick might call inkscape which is slow.
# if identify "msvg:$file[0]" >/dev/null 2>&1; then
# return 0
# else
return 1
# fi
elif [[ "$file" == *sixel ]]; then
# The magic database doesn't know sixel files yet, so go by fileext.
return 0
elif [[ $type == image/* ]]; then
# Return true for an image.
if identify "file://$file[0]" >/dev/null 2>&1 \
|| identify "$file" >/dev/null 2>&1; then
return 0
fi
elif [[ $type == application/pdf || $type == application/postscript ]]; then
# We could pretend PDFs are images by returning 0 here.
# But, they ought to be treated like directories which the
# user is asked if they wish to open.
return 1
elif [[ $type == application/dicom ]]; then
# ImageMagick can view DICOM (Medical Imaging Data)
return 0
elif [[ $type == video/* ]]; then
# Return true for videos.
if mediainfo "$file" >/dev/null 2>&1; then
return 0
fi
fi
# Not viewable, return false.
return 1
}
trim() {
# Chop leading and trailing spaces from named variables.
local IFS; unset IFS
for var; do
read -rd '' $var <<<"${!var}"
done
}
imageinfo() {
# Show name of image plus width, height, etc.
# This is called every time a new image is shown.
# Sets global variables FrameCount, ImageWidth, ImageHeight.
# If file is invalid, returns 1 (FALSE in Bourne shell).
local file="$1"
local type=$(mimetype "$file")
local key value
FrameCount=0
# Kludge since magic database doesn't recognize sixel file format yet.
if [[ "$file" =~ \.six(el)?$ ]]; then
type=image/x-sixel
fi
# Based on the filetype, detect parameters and print info.
case $type in
image/*|application/dicom)
# NB: Running sed repeatedly is faster than "identify -format".
local id=$(identify "$file[0]" 2>/dev/null)
# sample.miff MIFF 100x100 3072x2304+1486+1102 8-bit Palette sRGB 33c 33507B 0.000u 0:00.000
local wxh=$(sed -rn 's/.* ([0-9]+x[0-9]+) .*/\1/p' <<<$id)
local bitdepth=$(sed -rn 's/.* ([0-9]+-bit) .*/\1/p' <<<$id)
local colorspace=$(sed -rn 's/.*-bit ([^0-9]+) .*/\1/p' <<<$id)
local colors=$(sed -rn 's/.* ([0-9]+c) .*/\1/p' <<<$id)
ImageWidth=${wxh%x*}; ImageHeight=${wxh#*x}
echo -n " $wxh $bitdepth $colorspace $colors"
FrameCount=$(numframes "$file")
[[ $FrameCount -gt 1 ]] && echo -n " $FrameCount frames"
;;
# NOTE: I used sed because identify -format can be very slow.
# For example, %w is fine, but %[width] takes 30x longer.
# Also slow: %z, %[bit-depth], %r, %[colorspace].
application/pdf)
declare -A info
while IFS=: read key val
do
trim key val
info["$key"]="$val"
done < <(pdfinfo "$file")
local p=${info["Pages"]:-1}
local s=${info["Page size"]:-}
local t=${info["Title"]:-}
echo -n " $p page$(s $p)${s:+, }$s${t:+, }$t"
FrameCount=1 # Fake a FrameCount, else image will not be shown.
ImageWidth=0; ImageHeight=0; # Fake width and height
;;
application/postscript)
FrameCount=1 # Fake a FrameCount, else image will not be shown.
ImageWidth=0; ImageHeight=0; # Fake width and height
;;
video/*)
local -A a=(["Width"]= ["Height"]= ["Frame rate"]= ["Duration"]=)
local IFS=$'\n'
for line in $(mediainfo "$file"); do
key=${line%:*}
value=${line#*:}
trim key
trim value
a[$key]="$value"
done
# Cleanup "1 366 pixels" -> "1366".
a["Width"]="${a["Width"]% pixels}"
a["Height"]="${a["Height"]% pixels}"
a["Width"]="${a["Width"]// }" # Mediainfo puts spaces in numbers!
a["Height"]="${a["Height"]// }"
# Set global variables.
FrameCount=$(mediainfo --Inform='Video;%FrameCount%' "$file")
ImageWidth=${a["Width"]}
ImageHeight=${a["Height"]}
# Okay, finally show the video info.
echo -n ${ImageWidth}x${ImageHeight} pixels, " "
echo -n ${FrameCount} frame$(s $FrameCount), " @"
echo -n ${a["Frame rate"]% FPS*} "fps, " ${a["Duration"]}.
;;
application/zip|application/rar|application/tar|application/7z)
case "$file" in
*.cb?|*.CB?)
echo -e " - ComicBook archives not supported (yet).\n"
return 1
;;
*)
echo -e " - Skipping filetype '$type'.\n"
return 1
;;
esac
;;
*)
echo -e " - Skipping filetype '$type'.\n"
return 1
;;
esac
if [[ $FrameCount -eq 0 ]]; then
e "No images in $file\r"
return 1
else
echo # New line for sixel image to start at.
return 0
fi
}
mimetype() {
# Given an image filename, report its MIME type. (e.g., "image/jpeg").
local type=$(file --mime-type --brief --dereference -- "$file")
# Rarely, some file formats will not have a good MIME type.
if [[ $type == application/octet-stream ]]; then
case $file in
*miff|*MIFF) type="image/miff" ;; # ImageMagick.
*rgb|*RGB) type="image/sgi" ;; # SGI RLE color.
*bw|*BW) type="image/sgi" ;; # SGI RLE bw.
esac
fi
echo "$type"
}
id() {
# Identify an image tersely, but more verbosely than imageinfo.
# Relies on imageinfo already having been called to set FrameCount.
local f="$1"
local size=$(du -h "$f")
size=${size%%$'\t'*}
local rp=$(realpath "$f")
if [[ $rp =~ ^$HOME ]]; then
rp=${rp/$HOME/\~} # Replace /home/username with ~
fi
echo -n "$rp: "
local format="%wx%h %m, ${size}B"
# format+=", %[colors] colors" # Calculated (too slow?)
format+="\n"
if [[ "$f" != *svg ]]; then
identify -format "$format" "file://$f[0]"
else
identify -format "$format" "msvg:$f[0]"
fi
local comment=$(identify -format "%[comment]" "file://$f[0]" 2>/dev/null)
if [[ "$comment" ]]; then
echo "Comment: $comment" | fmt -w "${COLUMNS:-75}" -s
fi
}
fakefile() {
# A fake version of file that doesn't use magic cookies or look
# inside the file at all. Instead, it just considers the filename
# and returns "image/emagi" if the filename has a well-known
# extension (e.g., "bar.jpg"). [Note: "Emagi" are the magicians
# of the matrix and the makers of magic cookies.]
while [[ $1 == -* ]]; do shift; done
ext=${1##*.}
shopt -s nocasematch
case "$ext" in
six|sixel|miff|mvg|\
jpg|jpeg|jp2|jpx|\
png|apng|gif|tif|tiff|\
webp|\
svg|eps|ps|pdf|psd|\
heif|jxr|jpeg2000|avif|\
raw|dng|\
pnm|ppm|pbm|xpm|xbm|\
pcx|sgi|yuv|dcx|dpx|avs|palm|pcd|pcds|fits|cin|\
targa|tga|\
macpaint|mac|pntg|pict|pic|\
amigapaint|iff|ilm|ilbm|lbm|\
bmp|ico|icon)
echo "image/$ext"
;;
mp4|webm|mkv|avi|\
ogm|ogv|vp8|\
mov|m4v|mp2|\
wmv|asf|flv|3gp)
echo "video/$ext"
;;
*)
echo "who/knows"
;;
esac
shopt -u nocasematch
}
showimage() {
# Shows the file named in $1 using sixel.
# Global variable $viewmode is either "full", which enlarges it to
# the full size of the terminal window, or it is "fast", which
# scales the image down, removes colors, and takes other shortcuts
# for the fastest preview.
# Note that "full size" is not exactly the same as "full screen".
# In a terminal emulator, the sixel graphics are just a window.
# However, if the terminal emulator is then maximized (F11 key, often),
# vv will automatically rescale the image to use the entire space,
# minus two lines for the title and prompt.
[[ "$1" ]] || echo "Error, showimage() called without filename">&2
# Show filename and other info
e "$f\t"
imageinfo "$f" || return 1 # Return error if 0 frames in file.
if [[ "$f" =~ \.six(el)?$ ]]; then
echo "Warning: ImageMagick may show blackness when scaling sixel images." >&2
fi
# dorotate gets set if viewmode is best fit and rotating the image
# would allow us to see more of it on the screen.
local dorotate=""
# Maybe use ">$width" in geometry to disable zooming on tiny images
local gt=""
if [[ "$zoomin" ]]; then gt=""; else gt=">"; fi
# Centering an image can slow down convert by <200ms. XXX Worth it?
local centering="-gravity center -extent ${width}x${height}"
# crop gets set in "fit width" mode, below.
local crop=""
# -flatten is needed for transparent PNGs,
# but would interfere with fitwidth's -crop option.
local flatten="-flatten"
# geometry for ImageMagick's -geometry flag.
local geometry="$gt${width}x$gt${height}"
local extract="-extract >${width}x>${height}" # Speed up large images
# Configure options based on viewmode
case $viewmode in
fast)
geometry="$gt${width}x${previewsize}"
;;
full)
;;
bestfit)
dorotate=$(mayberotate "$1")
if [[ "$dorotate" ]]; then
extract="-extract >${height}x>${width}"
fi
;;
width)
# fit width fits image width to screen, but not height
geometry="$gt${width}x"
# Round crop down to the height of a line of text to avoid gaps
crop="-crop x$(( (height/fontheight) * fontheight ))"
# Disable centering's -extent
centering=""
# Disable flattening multiple layers
flatten=""
# Don't shrink to screen height, as we'll be showing multiple crops.
extract=${extract%x*}
;;
*)
error "Unknown viewmode '$viewmode'"
exit 1
esac
currentview=$viewmode # Remember last used viewmode.
# Save the sixel output so we can clear "Processing..." message.
local output=""
# OSC 8 URI linking so user can click on image to open it
local osc8uri="file://`hostname --fqdn``realpath "$1"`"
debug "OSC 8 URI link goes to $osc8uri"
# All set let's show it.
case $viewmode in
full|width|bestfit) # Large, high quality and slow. (about 1 second)
e "preparing $viewmode"
# If antialiasing is off, use -sample to resize the image.
if [[ $aalias ]]; then resize=geometry; else resize=sample; fi
echo -n "${dorotate:+, rotated}"
echo -n "${enhance:+, enhanced}"
echo -n "${dither:+, dithered}"
echo -n "${aalias:+, antialiased}"
echo -en " view...\r"
output=$( ${DEBUG:+debugdo} ${TIME} \
convert -background $background -auto-orient ${dorotate} \
$extract \
-$resize "$geometry" \
$crop \
${enhance:+-channel rgb -auto-level -gamma 1.5} \
+dither ${dither:+-dither floyd-steinberg} \
$centering \
-colors $numcolors \
"file://$1[0]" \
$flatten \
sixel:-)
# Note: -crop is used by fitwidth and interferes with -flatten.
;;
*|fast) # Fast and cruddy
echo -en "preparing fastest preview\r" # About 0.15 seconds
if [[ $aalias ]]; then resize=geometry; else resize=sample; fi
# Note that -sample is faster for large images, but super cruddy.
# 3,000,000 pixels empirically found as where time benefit began.
[[ ImageWidth*ImageHeight -lt 3000000 ]] || resize=sample
output=$(
echo -en "\e]8;;${osc8uri}\e\\" # Begin OSC 8 URI linking
${DEBUG:+debugdo} ${TIME} \
convert "file://$1[0]" \
-auto-orient \
-$resize "$geometry" \
+repage -background $background \
-flatten +dither -colors $numcolors \
sixel:-
echo -en "\e]8;;\e\\" # End OSC 8 URI linking
)
# Note: -flatten and -background is needed for transparency.
# Note: +repage is needed for images with virtual canvases.
# Note: +dither (disable dither) saves ~200ms sixel creation time.
# Note: -colors 256 saves another ~200ms sixel creation time.
;;
esac
tput el
if [[ "$output" ]]; then
echo -n "$output"
SHOULDREDRAW="" # Reset redraw flag until SIGWINCH trips it
else
# Error. ImageMagick must have failed as $output is empty.
return 1
fi
return
}
mayberotate() {
# Checks global variables $ImageWidth and $ImageHeight
# against the screen size in $width x $height.
# Prints "-rotate +90" if the image should be rotated to best fit
# in the current screen. Prints nothing otherwise.
if [[ $ImageHeight == $ImageWidth || $height == $width ]]; then
return
fi
if [[ -z "$zoomin" \
&& $ImageHeight -le $height \
&& $ImageWidth -lt $width ]]; then
# Small enough to fit in screen and not enlarging.
return
fi
local iorient=$((ImageHeight > ImageWidth))
local sorient=$((height > width))
if [[ $iorient != $sorient ]]; then
echo "-rotate +90"
fi
}
showprompt() {
if [[ $COLUMNS -lt 80 ]]; then
e "vv: " # Terse prompt
else
# Show long prompt after image.
echo -en "\r"
echo -n "Press space to keep, 'd' to delete, "
if [[ FrameCount -gt 1 ]]; then
echo -n "'v' for video, "
elif [[ $currentview != full ]]; then
echo -n "'f' for full, "
fi
echo -n "'h' for help, 'q' to quit."
tput el
fi
}
declare -ig recursionlevel=0 # How many times doit() has called itself.
declare -g abortrecursion="" # Used to pop back up to the top level dir.
declare -g poprecursion="" # Used by checkrflag to pop up one level.
declare -g slideshow="" # Gets set to "yes" when running slideshow.
doit () {
# For every file, show it and ask what is to be done.
# This is the main loop of the program.
# With "-r", doit() calls itself recursively for subdirectories.
for f; do
if [[ "$abortrecursion" ]]; then
if [[ $recursionlevel -gt 1 ]]; then
recursionlevel=recursionlevel-1
return
else
abortrecursion=""
fi
fi
# Found a directory, so maybe recurse
if [[ -d "$f" && ! -L "$f" ]]; then
# Allow Enter key to switch recursion flag to 'ask'.
if [[ $rflag ]] && read -t 0; then
rflag="ask"
fi
f=$(echo "$f" | tr -s /) # Squeeze repeated /// to single /.
f=${f%/} # No / at end.
# Recurse if directory was specified on command line or rflag≅yes.
if [[ $recursionlevel -eq 0 ]] || checkrflag "$f/"; then
e "$f/"
recursionlevel=recursionlevel+1
mapfile -t < <(sortrecur "$f"/*)
doit "${MAPFILE[@]}"
continue
else
# Special case for checkrflag: user hit 'x' to exit recursion.
if [[ "$poprecursion" ]]; then
poprecursion=""
recursionlevel=recursionlevel-1
return
fi
fi
fi
[[ -f "$f" ]] || continue;
# Skip non-image files discovered through recursion.
# (Always attempts to show files specified on command line).
if [[ recursionlevel -gt 0 ]] && ! isimage "$f"; then
# The current file isn't an image.
throb
continue
fi
# Show the image using sixels.
showimage "$f" || continue # Can return error if 0 frames in file.
# Discard any keystrokes hit before read loop started.
flushstdin
# Read a key loop.
while true; do
if [[ $SHOULDREDRAW ]]; then
# Sigwinch interrupt handler resized screen
SHOULDREDRAW=""
echo
viewmode=$currentview showimage "$f"
continue 1
fi
showprompt # Show "Press space to keep..."
# Use read timeout to check several times a second for window resize.
REPLY=
while [[ -z $REPLY && -z $SHOULDREDRAW ]]; do
read $BASH4BUG -s -n1 -t 0.25 < /dev/tty
local e=$?
if [[ $e -gt 0 ]]; then
# Read command returned an error (usually timeout).
if [[ "$slideshow" ]]; then
if doslideshow "$f"; then
continue 3 # User hit q to quit vv
else
continue 2 # User hit ESC to stop slideshow
fi
elif [[ $e -gt 128 ]]; then
# normal read timeout (just loop to allow for SIGWINCH)
continue 1 # Read a key loop.
else
echo "Error in read: $e. This shouldn't happen." >&2
continue 1
fi
fi
done
if [[ $SHOULDREDRAW ]]; then
# Sigwinch interrupt handler resized screen
SHOULDREDRAW=""
echo
viewmode=$currentview showimage "$f"
continue 1
fi
# if [[ "$slideshow" ]]; then
# if doslideshow "$f"; then
# continue 2
# fi
# fi
# If we get here, then 'read' successfully got a key.
echo -en "\r"
tput el
# Parse terminal escape sequence for F-keys, arrows.
if [[ "$REPLY" == $'\e' ]]; then parseescape; fi
flushstdin # Remove extraneous keys. (E.g., SPC held down).
case $REPLY in
" ") continue 2 # Space: Skip to next image.
;;
d|t) mygio trash "$f" && echo "trashed $f"
sleep 0.25; flushstdin # Slow down deletes
continue 2
;;
u) f=$(untrash) && echo "untrashed '$(tildify $f)'" && showimage "$f"
;;
T) # Undocumented tool to show Trashcan in GUI browser.
mygio open trash://
;;
# $'\cD') # Immediate, unrevocable delete. Too dangerous?
# rm "$f" && echo "deleted $f"
# sleep 0.25; flushstdin # Slow down deletes
# continue 2
# ;;
f) E "Fitting to full size once ('F' for always)"
viewmode=full showimage "$f"
;;
F) # Toggle full/fast view mode.
[[ $viewmode != full ]] && viewmode=full || viewmode=fast
E "Switching default viewmode to $viewmode."
if [[ $currentview != $viewmode ]]; then
showimage "$f"
fi
;;
w) E "Fitting to screen width once ('W' for always)"
viewmode=width showimage "$f"
;;
W) # Toggle fitwidth/fast view mode.
[[ $viewmode != width ]] && viewmode=width || viewmode=fast
E "Switching default viewmode to $viewmode."
if [[ $currentview != $viewmode ]]; then
showimage "$f"
fi
;;
v) if [[ $FrameCount -le 1 ]]; then
# Just a single image, so temporarily use full mode.
viewmode=full showimage "$f"
else
# Allow lowercase 'v' for files we detect as videos.
e "Playing video\r"
sixvid "$f"
flushstdin
fi
;;
V) e "Playing video\r"
sixvid "$f"
flushstdin
;;
M) e "Playing video\r"
# Undocumented for now. Should this use xdg-open?
mpv -loop "$f" || animate "$f"
flushstdin
;;
b|$'\c?'|$'\cH') E "Back not implemented yet"
# b reserved for "back" once we implement it.
;;
c) identify -format \
"Comment: %[comment]\n" "file://$f[0]" 2>/dev/null \
| fmt -w "${COLUMNS:-75}" -s
;;
C) editproperty comment "$f"
;;
i) id "$f" # Quick info
;;
I) # Verbose info.
convert "file://$f[0]" \
-print "%[option:*]%[artifact:*]%[*]" \
null:- | grep -v '^filename='
echo $(realpath "$f")
local size=$(du -b "$f" | numfmt --grouping)
size=${size%% *}
echo "$size bytes"
identify -format '%[colors] unique colors\n' "$f"
;;
r) # rename "$f"
saverenamemove rename "$f"
;;
m) # rename "$f" # XXX do document.
saverenamemove move "$f"
;;
s) #saveacopy "$f"
saverenamemove save "$f"
;;
S|f5) # Allow F5 for folks used to other viewers.
if [[ "$slideshow" ]]; then
E "Slide show stopped"
slideshow=""
viewmode=${oldvmss:-fast}
else
E "Slide show mode. Hit 'p' to pause, 'Escape' to stop."
slideshow="yup"
oldvmss=$viewmode
viewmode="full"
fi
showimage "$f"
;;
A) # Toggle antialiasing
if [[ "$aalias" ]]; then
E "Turning antialiasing off (show pixels when zooming in on small images)"
aalias=""
else
E "Turning antialiasing on (smooth small images when blown up)"
aalias=yup
fi
viewmode=$currentview showimage "$f"
;;
B) # Toggle autorotate image to fit screen (portrait/landscape).
[[ $viewmode != bestfit ]] && viewmode=bestfit || viewmode=fast
E "Switching default viewmode to $viewmode."
if [[ $currentview != $viewmode ]]; then
showimage "$f"
fi
;;
$'\cb') # Ctrl-B: Rotate this one image to fit the screen.
viewmode=bestfit showimage "$f"
;;
D) # Toggle dither
if [[ "$dither" ]]; then
E "Turning dither off (load faster by not dithering)"
dither=""
else
E "Turning dither on (visually smooth fullscreen images)"
dither=yup
fi
if [[ $currentview != "fast" ]]; then
viewmode=$currentview showimage "$f"
fi
;;
E) # Toggle enhance view
if [[ "$enhance" ]]; then
E "Turning enhanced view off"
enhance=""
else
E "Turning enhanced view on"
enhance=yup
fi
# XXX should enhance apply to fast view?
if [[ $currentview != "fast" ]]; then
viewmode=$currentview showimage "$f"
fi
;;
e) e "temporarily showing enhanced view\r"
enhance=yup viewmode=full showimage "$f"
;;
=) # Set preview size to specific number
if [[ $currentview == "fast" ]]; then
stty echo # Temporarily show characters the user types.
tput cnorm # Temporarily show the cursor.
echo -en "\r"; tput el # Clear line for prompt.
IFS= read -e -r -p "Height of preview in lines [$previewlines] ? "
tput civis
stty -echo
if [[ "$REPLY" -le 0 || "$REPLY" -eq $previewlines ]]; then
E "Preview lines unchanged"
else
[[ "$REPLY" -lt $LINES ]] || REPLY=$((LINES-2)) # Max is #rows - 2
previewlines=$REPLY
E "Preview lines changed to $previewlines"
windowchange
fi
fi
;;
+) # Zoom in preview
if [[ $currentview == "fast" ]]; then
previewlines=previewlines+1
[[ previewlines -le $((LINES-2)) ]] || previewlines=$((LINES-2))
windowchange
e "Preview lines changed to $previewlines"
fi
;;
-) # Zoom out preview
if [[ $currentview == "fast" ]]; then
previewlines=previewlines-1
[[ previewlines -ge 1 ]] || previewlines=1
windowchange
e "Preview lines changed to $previewlines"
fi
;;
0) # Reset previewlines
previewlines=8
windowchange
e "Preview lines changed to $previewlines"
;;
1) # Show 100% dot-for-dot view
dotfordot "$f"
;;
Z|z) # Magnify small images up to size of screen
togglezoomin "$f"
;;
l|$'\cL') # l or ^L to redraw screen.
clear # Note: ^L doesn't work with bash4.
showimage "$f"
;;
"") # ENTER key: show imageinfo and preview again.
# (XXX: Maybe not a good use of such a convenient key?)
viewmode=fast showimage "$f"
;;
x) if [[ $recursionlevel -gt 0 ]]; then
E "Exiting directory $(dirname "$f")"
recursionlevel=recursionlevel-1
return
else
continue 2 # Allow x to also skip to next image
fi
;;
X) if [[ $recursionlevel -gt 0 ]]; then
E "Exiting to top level directory"
abortrecursion=yes
recursionlevel=recursionlevel-1
return
fi
;;
R) reverseimagesearch "$f"
showimage "$f"
;;
y) yandeximagesearch "$f"
showimage "$f"
;;
h|'?')
E "$(basename $0): Rapidly view and delete image files"
showcommonkeys
;;
H)
E "$(basename $0): Advanced options"
showadvancedkeys
;;
q|Q|'\E') exit
;;
,) enabledebugging
;;
*)
echo "Unknown key: $REPLY" |cat -v >&2
;;
esac;
done
done
recursionlevel=recursionlevel-1
return
}
showcommonkeys() {
cat << EOF
MOST USEFUL KEYS:
SPACE: skip to next image.
f: view just the current image as full-size, higher quality.
F: toggle between always showing full-size images and fast previews.
v: Plays video using sixel (no sound).
+/-/0/=: increase/decrease/reset/set fast preview size.
d: trash image (also try 'gio list trash:' and 'gio trash --empty')
u: undo previous trash (can be used multiple times)
r: rename image file.
m: move image file to another directory.
s: save a copy of the image to another directory or filename.
q: quit.
H: SHOW ADVANCED KEYS.
EOF
}
showadvancedkeys() {
cat <<EOF
ADVANCED KEYS
w: fit width of current image (tall images to scroll off screen).
W: toggle between always fitting width and fast previews.
i: display basic image info (filename, type, geometry, filesize)
I: verbosely show all image metadata (EXIF, IPTC, etc.)
c: display comments and image size.
C: edit image comment.
R: reverse image search. Uses w3m text browser by default.
S: slide show (full-size, every $ssdelay seconds).
e: enhanced view just this image (improves contrast and brightness).
E: toggle enhance on/off (currently auto-level and gamma +1.5).
D: toggle dither on (visually smoother) or off (loads faster).
1: 100% zoom. 1 to 1, screen to image pixels. (Crops large images).
Z: toggle zooming small images up to the size of the screen.
A: toggle antialias when zooming in on small images.
B: toggle best fit on/off (autorotate portrait image to landscape screen).
x: exit directory (pops up one level of recursion).
X: exit all directories (pops up to top level directory).
h: help (SHOW MOST COMMON KEYS)
EOF
}
dotfordot() {
# Given an image, show it at 100% zoom (image pixel == screen pixel).
# If the image is smaller than the screen, this is trivial.
# If the image is larger, then break it into pieces.
# TODO: Use cursor or vi keys to navigate pieces. Escape to leave. XXX
local -i w=$ImageWidth h=$ImageHeight # Image width and height
local f="$1"
if [[ $w -le $width && $h -le $height ]]; then
# Trivial case: image is small, so just show it.
viewmode=full showimage "$f"
return
fi
### Split image into pieces. (Warning: high disk usage)
# Calculate number of pieces needed to ensure 1:1 pixel scaling
local -i wzoom=(w+width-1)/width
local -i hzoom=(h+height-1)/height
echo "Cutting image into tiles ($wzoom horizontally and $hzoom vertically)"
# Note that using ImageMagick's "-crop @" operator means the size
# is calculated for us. Eventually, we'd like to make sure the
# pieces line up with the terminal text so there are no gaps.
# Needs to be a multiple of the terminal's font height and width in pixels.
convert "$f" -crop ${wzoom}x${hzoom}@ -set filename:offset "%X %Y %w %h" \
+adjoin "$tmpdir/%[filename:offset] crop.ppm"
### Show pieces
local IFS=$'\n'
for f in $(ls -1v $tmpdir/*crop.ppm); do
echo -en "$f\r"
convert "$f" "$f.sixel"
cat "$f.sixel"
done
### All done
rm $tmpdir/*crop.*
}
max() {
[[ "$1" -gt "$2" ]] && echo "$1" || echo "$2"
}
declare -gi throbidx=0 # Static index for throbber[]
throb() {
# A spinning doohickey while recursively scanning directories.
local -a throbber=('/' '-' '\' '|')
echo -n "${throbber[throbidx++]}"
echo -n $'\b'
[[ throbidx -lt ${#throbber[@]} ]] || throbidx=0
}
checkrflag() {
# Return false if $rflag is empty string.
# If $rflag is "ask", then ask the user about entering directory $1.
# Otherwise, return true.
# When asking, allow for several special cases.
# In particular, if user hit 'x', they want to abort the current
# recursion, so set the global "poprecursion" flag.
case $rflag in
"") return 1 # False
;;
ask)
if [[ "$slideshow" ]]; then return 0; fi # Don't ask if showing slideshow.
echo -en "\r"; tput el # Clear line for prompt.
read -n1 -s -p "Recurse into $1 [Y/n/?] ? "
echo -en "\r"; tput el
flushstdin # Remove extraneous keys. (E.g., SPC held down).
case "$REPLY" in
'?'|h)
cat <<-EOF
Press
y Recurse into “$(basename $1)”. [DEFAULT]
(Space and Enter are the same as 'y').
n Skip directory “$(basename $1)”.
Y Always answer Yes to recursion.
N Always answer No to recursion.
EOF
if [[ $recursionlevel -gt 1 ]]; then
cat <<-EOF
x Exit one level of recursion
(Skip directory “$(basename $(dirname $1))”).
X Pop up to the top level directory.
EOF
fi
checkrflag "$1" # Ask them again.
return # Return same as previous command.
;;
y|""|" ")
return 0 # ENTER and SPACE mean yes.
;;
Y)
echo "Will now always recurse without asking."
rflag=yup
return 0
;;
N)
echo "Disabling recursion"
rflag=""
;;
x) if [[ $recursionlevel -gt 1 ]]; then
E "Exiting directory $(dirname "$f")"
poprecursion=yup
fi
return 1
;;
X) if [[ $recursionlevel -gt 1 ]]; then
E "Exiting to top level directory"
abortrecursion=yes
poprecursion=yup
fi
return 1
;;
q|Q) exit # Quit everything!
;;
n|*)
return 1;
;;
esac
;;
*)
return 0 # True
;;
esac
}
sortrecur() {
# Sort before recursing on a new directory.
# Input: a list of file and directory names, one per line.
# Output: the same list, sorted by 'ls $sortorder'.
# Directories are placed first, last, or neither based on $traversal.
# Note: if you have newlines in your filenames, you are silly.
# First, pre-sort files based on time or filename.
# Default of -v means "natural sort" so 9.png comes before 10.png.
mapfile -t < <(ls -d $sortorder -- "$@" 2>/dev/null)
debug "$traversal"
case $traversal in
bfs|[Bb]readth*|[Bb]reath*)
# Breadth First Search: show images before opening subdirs.
sortdirslast "${MAPFILE[@]}"
;;
dfs|[Dd]epth*|[Dd]eath*)
# "Death First Search" -- Prof. Vazirani.
echo "XXX Depth First Search: does anyone actually want this?" >&2
sortdirsfirst "${MAPFILE[@]}"
;;
neither|*)
# Dirs are not special and are recursed into whenever seen.
showargs "${MAPFILE[@]}"
;;
esac
}
sortdirslast() {
# Input: a list of file and directory names.
# Output: list with files before directories.
# Breadth First Search makes sense when entering a directory, we
# usually want to see every image in it first before recursing
# into subdirectories.
local -a directories=
local name
local IFS=$'\n' # Don't split on spaces in filenames.
for name; do
if [[ -d "$name" ]]; then
directories+=($name)
else
echo "$name"
fi
done
for name in "${directories[@]}"; do
echo "$name"
done
}
sortdirsfirst() {
# Input: a list of file and directory names.
# Output: list with directories before files.
# Depth First Search makes no sense. Just implementing it for completeness.
local -a files=
local name
local IFS=$'\n' # Don't split on spaces in filenames.
for name; do
if [[ -d "$name" ]]; then
echo "$name"
else
files+=($name)
fi
done
for name in "${files[@]}"; do
echo "$name"
done
}
showargs() {
# Input: a list of file and directory names.
# Output: same list, with newlines between each item
local name
local IFS=$'\n' # Don't split on spaces in filenames.
for name; do
echo "$name"
done
}
togglezoomin() {
# Toggle zooming in on small images and update the view if necessary.
# Uses global variable "currentview".
# Sets global variable "zoomin".
local f="$1"
if [[ -z "$zoomin" ]]; then
echo "Zooming in on small images enabled."
zoomin=yup
else
echo "Zooming in on small images disabled"
zoomin=""
fi
viewmode=$currentview showimage "$f"
}
doslideshow() {
# Wait for the user to hit a key or timeout after $ssdelay seconds.
# Returns false only if user wants to stop the slideshow.
local file="$1"
e "Slideshow: p to pause, Esc to stop, Q to quit."
while true; do
if read -s -n1 -t $ssdelay; then
tput el
if [[ "$REPLY" == $'\e' ]]; then parseescape; fi
case $REPLY in
" ") #Space for next image
break;
;;
S|f5|'\E') # quit slideshow, but not whole program.
slideshow=""
viewmode=${oldvmss:-fast}
return 1
;;
q|Q) exit # Quit everything!
;;
p|P|*) # pause
e "Slideshow paused. Hit any key to continue or Esc to stop, Q to quit.\r"
read -s -n1
tput el
if [[ "$REPLY" == $'\e' ]]; then parseescape; fi
case $REPLY in
" ") break ;;
S|f5|'\E') # Stop slideshow.
slideshow=""
viewmode=${oldvmss:-fast}
return 1
;;
q|Q) exit # Quit program completely.
;;
esac
e "Slideshow: Press p to pause, Esc to stop, Q to quit."
;;
esac
else
# Normal screensaver delay timeout; show the next image.
return 0
fi
done
}
enabledebugging() {
# Debugging gobbledygook
DEBUG=yup
TIME=time
set -o functrace
trap '{ for ((i=${#FUNCNAME[@]}-1; i>=0; i--)); do echo -en "${BASH_LINENO[i]}: ${FUNCNAME[i]} " >&2 ; [[ $i != 0 ]] && echo -n " --> " ; done; echo ; } >&2' RETURN
}
debugdo() {
# Both show the command and execute it
echo >&2
echo >&2
echo "$@" >&2
echo >&2
"$@"
}
# ----------------------------------------------------------------------
# START OF TRASH SCRIPTS TO REPLACE 'gio trash' command.
mygio() {
# If the libglib2.0-bin's gio command isn't installed, we fake it.
if command -v gioXXXXX >/dev/null; then
# gio exists, so use it.
# Ugh. Buggy when a filename has a colon in it. Prepending "file:" doesn't fix it.
gio "$@"
else
# gio from libglib2.0-bin is missing, so use my shell script version.
case "$1" in
trash)
shift
trash "$@"
;;
open)
shift
xdg-open "$@"
;;
*) echo "Unsupported mygio command '$1'" >&2
;;
esac
fi
}
trash() {
# trash: Move files to ~/.local/share/Trash/files/.
# Well-nigh-compliant with FreeDesktop Trash Specification.
# https://specifications.freedesktop.org/trash-spec/trashspec-latest.html
#
# Handles same filename deletions, race conditions, URI-style escaping.
# Bugs: Does not handle $topdir for foreign mounts (e.g., removable drives).
local trashdir=${XDG_DATA_HOME:-$HOME/.local/share}/Trash
if ! mkdir -p $trashdir/files || ! mkdir -p $trashdir/info; then
echo "Error: trash() could not create $trashdir/{files,info}.">&2
echo "Not deleting $@." >&2
return 1
fi
for file; do
local path=$(uriescape "$(realpath "$file")")
local newname=$(realpath "$file" | tr / \!)
# Allow same filename to be deleted repeatedly without loss.
# Creates trashinfo file first (atomically, to avoid race collisions).
if ! atomictouch "$trashdir/info/$newname.trashinfo"; then
local -i i=1
while ! atomictouch "$trashdir/info/$newname.$i.trashinfo"; do
i=i+1
done
newname=$newname.$i
fi
# Write metadata to .trashinfo file
cat > "$trashdir/info/$newname.trashinfo" << EOF
[Trash Info]
Path=$path
DeletionDate=$(date +Y-%m-%dT%H:%M:%S)
EOF
mv "$file" "$trashdir/files/$newname"
done
}
atomictouch() {
# If the file named by $1 does not exist, this creates it atomically.
# Otherwise, it return an error.
# This is atomic because Bash uses O_EXCL for noclobber.
( set -C; >"$1"; ) 2>&-
}
uriescape() {
# FROM RFC 2396, Uniform Resource Identifiers
# urireserved=";/?:@&=+$,"
# alnum="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# mark="\-\_\.\!\~\*\'\(\)"
# unreserved="$alnum$mark"
# okaychars="$unreserved$urireserved"
# excluded='] <>#%"{}|\^[`' # also exclude control (00-1F and 7F hex).
local -i i
for (( i=0; i<${#1}; i++ )); do
local c=${1:i:1}
case $c in
a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|\;|\/|\?|\:|\@|\&|\=|\+|\$|\,|\-|\_|\.|\!|\~|\*|\'|\(|\))
echo -n "$c"
;;
*) echo -n "$c" | hexdump -ve '/1 ":%02X"' | tr : %
;;
esac
done
echo
}
unuriescape() {
# FROM RFC 2396, Uniform Resource Identifiers
# urireserved=";/?:@&=+$,"
# alnum="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
# mark="\-\_\.\!\~\*\'\(\)"
# unreserved="$alnum$mark"
# okaychars="$unreserved$urireserved"
# excluded='] <>#%"{}|\^[`' # also exclude control (00-1F and 7F hex).
local -i i
for (( i=0; i<${#1}; i++ )); do
local c=${1:i:1}
case $c in
%) # Next two letters must be hex digits.
local -i j=i+1; local -i k=i+2
local h="${1:j:1}${1:k:1}"
i=i+2
c=$(echo -en '\x'"$h")
;;
esac
echo -n "$c"
done
echo
}
untrash() {
# untrash: Take the most recent file out of ~/.local/share/Trash/files.
#
# Puts file back where it came from, unless it would clobber another file.
# If a file of that name already exists, rename existing as .1, .2, ...
#
# If directory it was in is gone, then recreates the directory.
#
# Outputs the filename the of the untrashed file.
# Returns error if trash is empty.
# Find last created infofile that points at an existing file
local trashdir=${XDG_DATA_HOME:-$HOME/.local/share}/Trash
local infofile=/dev/null.d/nonexistant
while true; do
local infofile=$(ls -tr "$trashdir/info/"* 2>/dev/null | tail -1)
if [[ ! "$infofile" ]]; then
E "Trashcan is empty." >&2
return 1
fi
# Sanity check
[[ -e "$infofile" ]] || return 1 # No info, nothing to untrash.
local trashname="${infofile%.trashinfo}"
trashname="${trashname#$trashdir/info/}"
trashname="$trashdir/files/$trashname"
if [[ -e "$trashname" ]]; then
break
else
rm -f "$infofile" # Delete an infofile that points at nothing.
fi
done
local Path=$(awk -e '/^Path=/ {print substr($0,6); exit;}' "$infofile")
Path=$(unuriescape "$Path")
# Does directory even existing for us to write to?
if ! mkdir -p $(dirname "$Path") || ! [[ -w $(dirname "$Path") ]]; then
echo "Warning: Cannot write to '$(dirname "$Path")'" >&2
echo -n "Warning: Restoring to " >&2
if [[ -w "$PWD" ]]; then
tildify "$PWD" >&2
Path="$PWD/$(basename "$Path")"
else
tildify "$HOME" >&2
Path="$HOME/$(basename "$Path")"
fi
fi
# Finally, we can restore the file.
if mv --backup=numbered "$trashname" "$Path" && rm "$infofile"; then
echo "$Path" # Return filename untrashed
return 0
else
return 1
fi
}
# END OF TRASH SCRIPTS
#----------------------------------------------------------------------
### MAIN ###
set -o nounset # Just for laughs, let's die on typos. ;-)
# Check prerequisites
prerequisites
# Determine window size, set up terminal
autodetect
# Parse command line args
TEMP=$(getopt -o 'r::R,fF,S::,B,n:,dD,aA,g:,p:P:,?' \
--long 'recursive::,full-size,fast-view' \
--long 'slide-show::,slideshow::,best-fit' \
--long 'num-colors:,dither,no-dither' \
--long 'antialias,no-antialias' \
--long 'geometry:,preview-size:,preview-lines:' \
--long 'help' \
-n 'vv' -- "$@")
if [ $? -ne 0 ]; then
echo >&2
usage >&2
exit 1
fi
# Use getopt's results from TEMP
eval set -- "$TEMP"
unset TEMP
while true; do
case "$1" in
-r|--recursi*)
case "$2" in
'') # No optional argument, so do nothing special.
rflag=yup
;;
*) # Optional argument: yes, no, ask.
rflag=$2
;;
esac
case "$rflag" in
ask) echo "Recursion will ask before entering each directory."
;;
""|no|off|false|False|F|0|nil|disable*)
rflag=""
echo "Recursion disabled."
;;
*)
echo "Recursion enabled." | cat -v
;;
esac
shift 2
continue
;;
-R)
rflag=""
echo "Recursion disabled."
shift
continue
;;
-f|--full*)
echo 'Full screen mode (slower, better quality)'
viewmode="full"
shift
continue
;;
-F|--fast*)
echo 'Fast view mode (small, quick, low quality)'
viewmode="fast"
shift
continue
;;
-B|--best-fit)
echo "Best-fit (full screen, auto-rotate to maximize screen usage)"
viewmode="bestfit"
shift
continue
;;
-S|--slide-show|--slideshow)
slideshow=yup
oldvmss=fast
viewmode=full
case "$2" in
'') # No optional argument, so do nothing special.
true
;;
*) # Optional argument sets time delay.
ssdelay=$2
;;
esac
echo "Slideshow mode ($ssdelay second delay)"
shift 2
continue
;;
-n|--num-colors)
numcolors=$2
echo "Number of colors: $numcolors. (Not used in Fast view)."
if [[ numcolors -eq 0 ]]; then usage; exit 1; fi
shift 2
continue
;;
-d|--dither)
dither=yup
echo "Dithering on. (Smoother gradients in Full screen mode)."
shift
continue
;;
-D|--no-dither)
dither=""
echo "Dithering off. (Faster rendering in Full screen mode)."
shift
continue
;;
-a|--antialias)
aalias=yup
echo "Antialias on. (Smooth jaggies when scaling image)."
shift
continue
;;
-A|--no-antialias)
aalias=""
echo "Antialiasing off. (Show pixel art as sharp)."
shift
continue
;;
-g|--geometry)
width=${2%x*}
height=${2#*x}
echo "Terminal gemetry set to ${width}x${height} px."
if [[ width -eq 0 || height -eq 0 ]]; then usage; exit 1; fi
shift 2
continue
;;
-P|--preview-size)
forcepsize=${2#*x} # Ignore width if given geometry (e.g., 800x256)
echo "Fast-view's preview size set to ${width}x${forcepsize}."
if [[ $forcepsize -eq 0 ]]; then usage; exit 1; fi
previewsize=$forcepsize
shift 2
continue
;;
-p|--preview-lines)
previewlines=$2
if [[ $previewlines -eq 0 ]]; then usage; exit 1; fi
previewsize=fontheight*previewlines
echo "Fast-view's preview size set to ${width}x${previewsize}."
shift 2
continue
;;
'-?'|--help)
usage
exit 0
;;
--)
shift
break
;;
*)
echo 'Internal error!' >&2
exit 1
;;
esac
done
if [[ $# -gt 0 ]]; then
# Show all files/dirs mentioned on command line.
recursionlevel=0 # Allow one level of recursion for any
doit "$@" # directories named on command line.
else
# No arguments, so show everything in current working directory.
recursionlevel=1 # Do not complain about non-image files.
mapfile -t < <(sortrecur *) # Sort files for recursion.
doit "${MAPFILE[@]}" # Start recursing.
fi
# Don't quit until images have finished transferring.
waitforterminal