#!/bin/bash # sixcat: Use sixel graphics to show an image inside a terminal. # sixgif: Use sixel graphics to play an animation inside a terminal. # Version 1.0 # hackerb9, June 2017 # Sixel graphics are supported by terminals such as the DEC VT340 and # emulators, such as 'xterm -ti vt340'. # ImageMagick is required. (apt-get install imagemagick) # The heart of this program is really just one line: # # convert foo.jpg sixel:- # # All the other lines are bells and whistles, mostly to make the image # look better by increasing the number of colors available. ###################################################################### # Configuration ###################################################################### # Number of colors in the terminal's sixel palette. # (But also see the -x and -n options). default_numcolors=16 # Genuine vt340's had a 16-color palette. # Maximum image size is terminal dependent, so we have to guess. geometry="-geometry 800x480>" # this was the actual vt340 resolution #geometry="-geometry 800x800>" # allows for portrait orientation #geometry="-geometry 1000x1000>" # maximum allowed in xterm322 # Should ImageMagick dither the image to make it look better? # (Use dither="" to disable.) dither="-dither floyd-steinberg" # Maximum number of seconds to wait for a response from the terminal # after a color escape sequence request. Usually, terminals respond # much faster than 0.1 seconds, but I could imagine it needing to be # increased for slow links (e.g., RS232C). timeout=0.1 ###################################################################### # Helper functions ###################################################################### usage() { cat < -x: Force the use of xterm escape sequences to detect and change the palette size instead of defaulting to $default_numcolors. If the palette is smaller than 1024, it is set to 1024. Note that -x is the default if \$TERM is "xterm". -X: Just assume palette size is $default_numcolors. Does not use xterm escape sequences regardless of \$TERM. -n INTEGER: Assume pallete size is INTEGER. Does not use xterm escape sequences regardless of \$TERM. Note: if this program is called sixgif, it will play GIF animations in the top left instead of scrolling all the frames as multiple images. EOF } # Some functions to deal with checking and changing the number of # colors the terminal can show. Likely xterm specific, but YMMV. getnumcolors() { # Query xterm for the palette size and print it. # (Negative numbers indicate errors.) colors 1 0 } resetnumcolors() { # Reset xterm back to its default number of colors. # Print the new number of colors (negative if an error occurred.) colors 2 0 } setnumcolors() { # Request xterm to change the number of colors in its palette. # Print the new number of colors (negative if an error occurred.) colors 3 $1 } colors() { # Send an xterm escape code request in the form: ^[[?1;3;1024S # Print the response value (or a negative number if an error occurs) csi=$'\e[' action=$1 # Action. 1 to get, 2 to reset, 3 to set value=$2 # Number of colors to set returnvalue=-1 # Raw response is in the same form as request: ^[[?1;0;1024S # Let's parse it to be more manageable. IFS=";" \ read -a REPLY -s -t $timeout -d "S" \ -p "${csi}?1;${action};${value}S" if [[ $? != 0 || ${#REPLY} == 0 ]]; then # No response to VT escape sequence. Maybe you are not an xterm? # Or, perhaps timeout is too short? returnvalue=-2 elif [[ "${REPLY[1]}" == "3" ]]; then # Response code 3 means error. Likely too many colors requested. returnvalue=-3 elif [[ "${REPLY[1]}" == "0" ]] ; then # 0 means okay. Yay! returnvalue=${REPLY[2]} else # If we get here, response was not in the format we expected. # Perhaps user typed during xterm response? Or cosmic rays? returnvalue=-4 fi echo $returnvalue } 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. # (E.g., if we want to run resetnumcolors at the end of the program.) # WARNING. While this should work with any terminal, if it fails, # it'll end up waiting for approximately *forever* (i.e., 60 seconds). read -s -t 60 -d "c" -p $'\e[c' } debugeta() { # Given a number of bytes, estimate the amount of time its going # to take to send at the current TTY baud rate. # Mostly for larks as this will be completely bogus on terminal emulators. len=$1 # Static variables to store TTY type and STTY speed. [[ "$TTY" ]] || TTY=$(tty) [[ "$SPEED" ]] || SPEED=$(stty speed) || SPEED=0 echo "sixel command length is $len bytes" if [[ "$SPEED" -gt 0 ]]; then if [[ "$TTY" =~ /pt || "$TTY" =~ ttyp ]]; then echo "pseudoterminal detected (speed estimates only valid for real hardware)" fi echo "terminal speed is $SPEED bps" echo "estimated time to send: $((len*8/SPEED)) seconds" fi } # If the user hits ^C, we don't want them stuck in SIXEL mode cleanup() { echo -n $'\e[?80h' # Reenable sixel scrolling echo -n $'\e\\' # Escape sequence to stop SIXEL exit 0 } trap cleanup SIGINT SIGHUP SIGABRT ###################################################################### # MAIN BEGINS HERE ###################################################################### if [ $# -eq 0 ]; then usage exit 1 else case "$1" in -n|--num-colors) shift nflag="$1" shift # Just assume the palette is n colors. Do not bother checking or changing. # (Might be needed for some terminal emulators. I do not know.) if [[ "$nflag" -gt 0 ]]; then default_numcolors=$nflag else usage exit 1 fi ;; -x|--force-xterm-colors) shift # Force xterm palette manipulation, regardless of $TERM # (Might be needed for some terminal emulators. I do not know.) xflag=yup ;; -X|--disable-xterm-colors) shift # Disable xterm palette manipulation, regardless of $TERM # (Might be needed for some terminal emulators. I do not know.) Xflag=yup ;; -*) usage exit 1 ;; esac fi numcolors=$default_numcolors # Optionally detect and set the number of palette colors if [[ "$xflag" || -z "$Xflag" && -z "$nflag" && "$TERM" =~ ^xterm ]]; then # Since the VT340 only had a palette of 16 colors, we can vastly # improve the image if we tell xterm to increase it to 1024. # This would fail on a real VT340. palettechanged=yup oldnumcolors=$(getnumcolors) if [[ $oldnumcolors -lt 1024 ]]; then numcolors=$(setnumcolors 1024) if [[ $numcolors -le 0 ]]; then echo -e "\rTerminal did not accept escape sequence to increase color palette. Are you really an xterm?" >&2 numcolors=$default_numcolors fi else # At least 1024 colors, so do not change anything numcolors=$oldnumcolors palettechanged= fi fi # Small optimization (saves half a second) if [[ $numcolors == 1024 ]]; then # Imagemagick already defaults to 1024 colors for sixel colors="" else colors="-colors $numcolors" fi # Print image filename after showing it? Yes, if multiple files. if [[ $# -gt 1 ]]; then showname=yup else showname="" fi # PAST IS PROLOGUE... NOW TO THE MEAT. for f; do x=$(convert "$f" $geometry $colors sixel:- 2>/dev/null) [[ ${#x} != 0 ]] || continue # The sixel command length better be >0 : debugeta ${#x} case $0 in *sixgif) # If this program is named "sixgif" play files as animations, # instead of displaying each frame and scrolling down. echo -n $'\e[?80l' # Disable sixel scrolling for t in {1..3}; do echo -n "$x" # sixgif plays repeatedly done echo -n $'\e[?80h' # Reenable sixel scrolling ;; *) echo -n "$x" # sixcat just shows once [[ "$showname" ]] && echo "$f" ;; esac done # TIDY UP waitforterminal if [[ "$palettechanged" ]]; then # Reset color palette numcolors=$(resetnumcolors) : echo Number of colors reset to $numcolors >&2 fi