#!/usr/bin/env bash # lsix: like ls, but for images. # Shows thumbnails of images with titles directly in terminal. # Requirements: just ImageMagick (and a Sixel terminal, of course) # Version 1.6.2 # B9 December 2018 # See end of file for USAGE. # The following defaults may be overridden if autodetection succeeds. numcolors=16 # Default number of colors in the palette. background=white # Default montage background. foreground=black # Default text color. width=800 # Default width of screen in pixels. # Feel free to edit these defaults to your liking. tilesize=120 # Width and height of each tile in the montage. tilewidth=$tilesize # (or specify separately, if you prefer) tileheight=$tilesize # If you get questionmarks for Unicode filenames, try using a different font. # You can list fonts available using `convert -list font`. #fontfamily=Droid-Sans-Fallback # Great Asian font coverage #fontfamily=Dejavu-Sans # Wide coverage, comes with GNU/Linux #fontfamily=Mincho # Wide coverage, comes with MS Windows # Default font size is based on width of each tile in montage. fontsize=$((tilewidth/10)) #fontsize=16 # (or set the point size directly, if you prefer) timeout=0.25 # How long to wait for terminal to respond # to a control sequence (in seconds). # Sanity checks and compatibility if [[ ${BASH_VERSINFO[0]} -eq 3 ]]; then if bash --version | head -1 | grep -q "version 3"; then cat <<-EOF >&2 Error: The version of Bash is extremely out of date. (2007, the same year Steve Jobs announced the iPhone!) This is almost always due to Apple's MacOS being silly. Please let Apple know that their users expect current UNIX tools. In the meantime, try using "brew install bash". EOF exit 1 else exec bash "$0" "$@" || echo "Exec failed" >&2 exit 1 fi fi if ! command -v montage >/dev/null; then echo "Please install ImageMagick" >&2 exit 1 fi if command -v gsed >/dev/null; then alias sed=gsed # Use GNU sed for MacOS & BSD fi cleanup() { echo -n $'\e\\' # Escape sequence to stop SIXEL. stty echo # Reset terminal to show characters. exit 0 } trap cleanup SIGINT SIGHUP SIGABRT EXIT autodetect() { # Various terminal automatic configuration routines. # Don't show escape sequences the terminal doesn't understand. stty -echo # Hush-a Mandara Ni Pari # IS TERMINAL SIXEL CAPABLE? # Send Device Attributes IFS=";" read -a REPLY -s -t 1 -d "c" -p $'\e[c' >&2 for code in "${REPLY[@]}"; do if [[ $code == "4" ]]; then hassixel=yup break fi done # 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/lsix/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 # ENABLE SIXEL SCROLLING so image will appear right after cursor. if [[ $TERM != "mlterm" ]]; then echo -ne $'\e[?80h' else # Except... mlterm (as of 3.5.0) has a bug that reverses the sense echo -ne $'\e[?80l' fi # TERMINAL COLOR AUTODETECTION. # Find out how many color registers the terminal has IFS=";" read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?1;1;0S' >&2 [[ ${REPLY[1]} == "0" ]] && numcolors=${REPLY[2]} # Bug workaround: mlterm does not report number of colors. if [[ $TERM =~ mlterm ]]; then numcolors=1024; fi # YAFT is vt102 compatible, cannot respond to vt220 escape sequence. if [[ "$TERM" == yaft* ]]; then numcolors=256; fi # Increase colors, if needed if [[ $numcolors -lt 256 ]]; then # Attempt to set the number of colors to 256. # This will work for xterm, but fail on a real vt340. IFS=";" read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?1;3;256S' >&2 [[ ${REPLY[1]} == "0" ]] && numcolors=${REPLY[2]} fi # Query the terminal background and foreground colors. IFS=";:/" read -a REPLY -r -s -t ${timeout} -d "\\" -p $'\e]11;?\e\\' >&2 if [[ ${REPLY[1]} =~ ^rgb ]]; then # Return value format: $'\e]11;rgb:ffff/0000/ffff\e\\'. # ImageMagick wants colors formatted as #ffff0000ffff. background='#'${REPLY[2]}${REPLY[3]}${REPLY[4]%%$'\e'*} IFS=";:/" read -a REPLY -r -s -t ${timeout} -d "\\" -p $'\e]10;?\e\\' >&2 if [[ ${REPLY[1]} =~ ^rgb ]]; then foreground='#'${REPLY[2]}${REPLY[3]}${REPLY[4]%%$'\e'*} # Check for "Reverse Video" (DECSCNM screen mode). IFS=";?$" read -a REPLY -s -t ${timeout} -d "y" -p $'\e[?5$p' if [[ ${REPLY[2]} == 1 || ${REPLY[2]} == 3 ]]; then temp=$foreground foreground=$background background=$temp fi fi fi # YAFT is vt102 compatible, cannot respond to vt220 escape sequence. if [[ "$TERM" == yaft* ]]; then background=black; foreground=white; fi # Send control sequence to query the sixel graphics geometry to # find out how large of a sixel image can be shown. IFS=";" read -a REPLY -s -t ${timeout} -d "S" -p $'\e[?2;1;0S' >&2 if [[ ${REPLY[2]} -gt 0 ]]; then width=${REPLY[2]} else # Nope. Fall back to dtterm WindowOps to approximate sixel geometry. IFS=";" read -a REPLY -s -t ${timeout} -d "t" -p $'\e[14t' >&2 if [[ $? == 0 && ${REPLY[2]} -gt 0 ]]; then width=${REPLY[2]} fi fi # BUG WORKAROUND: XTerm cannot show images wider than 1000px. # Remove this hack once XTerm gets fixed. Last checked: XTerm(327) if [[ $TERM =~ xterm && $width -ge 1000 ]]; then width=1000; fi # Space on either side of each tile is less than 0.5% of total screen width tilexspace=$((width/201)) tileyspace=$((tilexspace/2)) # Figure out how many tiles we can fit per row. ("+ 1" is for -shadow). numtiles=$((width/(tilewidth + 2*tilexspace + 1))) } main() { # Discover and setup the terminal autodetect if [[ $# == 0 ]]; then # No command line args? Use a sorted list of image files in CWD. shopt -s nullglob nocaseglob nocasematch set - *{jpg,jpeg,png,gif,tiff,tif,p?m,x[pb]m,bmp,ico,svg,eps} [[ $# != 0 ]] || exit readarray -t < <(printf "%s\n" "$@" | sort) # Only show first frame of animated GIFs if filename not specified. for x in ${!MAPFILE[@]}; do if [[ ${MAPFILE[$x]} =~ gif$ ]]; then MAPFILE[$x]=":${MAPFILE[$x]}[0]" fi done set - "${MAPFILE[@]}" fi # Resize on load: Save memory by appending this suffix to every filename. resize="[${tilewidth}x${tileheight}]" imoptions="-tile ${numtiles}x1" # Each montage is 1 row x $numtiles columns imoptions+=" -geometry ${tilewidth}x${tileheight}>+${tilexspace}+${tileyspace}" # Size of each tile and spacing imoptions+=" -background $background -fill $foreground" # Use terminal's colors imoptions+=" -auto-orient " # Properly rotate JPEGs from cameras if [[ $numcolors -gt 16 ]]; then imoptions+=" -shadow " # Just for fun :-) fi # See top of this file to change fontfamily and fontsize. [[ "$fontfamily" ]] && imoptions+=" -font $fontfamily " [[ "$fontsize" ]] && imoptions+=" -pointsize $fontsize " # Create and display montages one row at a time. while [ $# -gt 0 ]; do # While we still have images to process... onerow=() goal=$(($# - numtiles)) # How many tiles left after this row while [ $# -gt 0 -a $# -gt $goal ]; do len=${#onerow[@]} onerow[len++]="-label" onerow[len++]=$(processlabel "$1") onerow[len++]="$1" shift done montage "${onerow[@]}" $imoptions gif:- \ | convert - -colors $numcolors sixel:- done } processlabel() { # This routine is all about appeasing ImageMagick. # 1. Remove silly [0] suffix and : prefix. # 2. Quote percent backslash, and at sign. # 3. Replace control characters with question marks. # 4. If a filename is too long, remove extension (.jpg). # 5. Split long filenames with newlines (recursively) span=15 # filenames longer than span will be split echo -n "$1" | sed 's|^:||; s|\[0]$||;' | tr '[:cntrl:]' '?' | awk -v span=$span -v ORS="" ' function halve(s, l,h) { # l and h are locals l=length(s); h=int(l/2); if (l <= span) { return s; } return halve(substr(s, 1, h)) "\n" halve(substr(s, h+1)); } { if ( length($0) > span ) gsub(/\..?.?.?.?$/, ""); print halve($0); } ' | sed 's|%|%%|g; s|\\|\\\\|g; s|@|\\@|g;' } #### main "$@" # Send an escape sequence and wait for a response from the terminal # so that the program won't quit until images have finished transferring. read -s -t 60 -d "c" -p $'\e[c' >&2 ###################################################################### # NOTES: # Usage: lsix [ FILES ... ] # * FILES can be any image file that ImageMagick can handle. # # * If no FILES are specified the most common file extensions are tried. # (For now, lsix only searches the current working directory.) # # * Non-bitmap graphics often work fine (.svg, .eps, .pdf, .xcf). # # * Files containing multiple images (e.g., animated GIFs) will show # all the images if the filename is specified at the command line. # Only the first frame will be shown if "lsix" is called with no # arguments. # # * Because this uses escape sequences, it works seamlessly through ssh. # # * If your terminal supports reporting the background and foreground # color, lsix will use those for the montage background and text fill. # # * If your terminal supports changing the number of color registers # to improve the picture quality, lsix will do so. # * Only software needed is ImageMagick (e.g., apt-get install imagemagick). # Your terminal must support SIXEL graphics. E.g., # # xterm -ti vt340 # * To make vt340 be the default xterm type, set this in .Xresources: # # ! Allow sixel graphics. (Try: "convert -colors 16 foo.jpg sixel:-"). # xterm*decTerminalID : vt340 # * Xterm does not support reporting the screen size in pixels unless # you add this to your .Xresources: # # ! Allow xterm to read the terminal window size (op #14) # xterm*allowWindowOps : False # xterm*disallowedWindowOps : 1,2,3,4,5,6,7,8,9,11,13,18,19,20,21,GetSelection,SetSelection,SetWinLines,SetXprop # * Be cautious using lsix on videos (lsix *.avi) as ImageMagick will # try to make a montage of every single frame and likely exhaust # your memory and/or your patience. # BUGS # * Directories are not handled nicely. # * ImageMagick's Montage doesn't handle long filenames nicely. # * Some transparent images (many .eps files) presume a white background # and will not show up if your terminal's background is black. # * This file is getting awfully long for a one line kludge. :-) # LICENSE INFORMATION # (AKA, You know your kludge has gotten out of hand when...) # Dual license: # * You have all the freedoms permitted to you under the # GNU GPL >=3. (See the included LICENSE file). # * Additionally, this program can be used under the terms of whatever # license 'xterm' is using (now or in the future). This is primarily # so that, if the xterm maintainer (currently Thomas E. Dickey) so # wishes, this program may be included with xterm as a Sixel test. # However, anyone who wishes to take advantage of this is free to do so.