2439 lines
73 KiB
Bash
Executable file
2439 lines
73 KiB
Bash
Executable file
#!/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=&img_url=https%3A//www.fotopolis.pl/i/images/4/2/3/d2FjPTIzNDB4MQ%3D%3D_src_179423-1555946732rlzp7d96a9729_2048px22_1100mv.jpg&rpt=imagedups"
|
||
resultPage=$(
|
||
echo "$output" |
|
||
sed -rn '
|
||
s/&/\&/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
|