#!/bin/bash
### A universal file viewer
### MochiMoppel 2017-03-20

TEMPDIR=/tmp/mm_view                    # For temp files. Will be cleaned upon mm_view exit
CNFGDIR=$HOME/tmp/mm_view               # For user settings. ($HOME/tmp not saved to savefile. Eventually should be $HOME/.config/<appname>
[[ -d $TEMPDIR ]] || mkdir -p $TEMPDIR
[[ -d $CNFGDIR ]] || mkdir -p $CNFGDIR
export APPPATH=$(realpath $0)

### Files for TEMPDIR
export SYMLINK=$TEMPDIR/symlink.tmp     # <input file> source for <edit> and  <pixmap> widgets.
export MSGFILE=$TEMPDIR/msgfile.tmp     # contains info text
export NB_PAGE=$TEMPDIR/nb_page.tmp     # contains notebook page
export LASTCHO=$TEMPDIR/lastcho.tmp     # contains last selected item (=last vCHOOSER)
export PID_FFP=$TEMPDIR/pid_ffp.tmp     # contains PID of ffplay/avplay
export P2_TEXT=$TEMPDIR/p2_text.tmp     # contains text for page 2 (scale % of image)
export K_SHIFT=$TEMPDIR/shiftkey.tmp    # shift key flag

### Files for CNFGDIR
export LASTDIR=$CNFGDIR/lastdir         # contains last accessed directory
export LASTGEO=$CNFGDIR/lastgeo         # contains last geometry
export IMG_FIT=$CNFGDIR/set_scale_img   # image scale flag 
export AUTOAUD=$CNFGDIR/set_play_audio  # autoplay audio  flag
export AUTOVID=$CNFGDIR/set_play_video  # autoplay video  flag
export BOXMODE=$CNFGDIR/set_vertical    # vertical layout flag

### Window settings
DEFAULT_WIDTH=1000  # Default window width
DEFAULT_HIGHT=500   # Default window height
CHOOSER_WIDTH=500   # Effective only in horizontal layout (default 500)
CHOOSER_HIGHT=200   # Effective only in vertical layout (default 200)
MENUBAR_HIGHT=25    # Should not be set to 0 (disables shortcuts). Value 1 hides mbar but still allows access via F10 (default 25)
WINDOW_MARGIN=0     # Inner margin of window (default 0)
WIDGETSPACING=3     # Spaces between menu - chooser&viewer - statusbar  (default 3)
CENTERSPACING=0     # Space between chooser - viewer (default 0)
L_EDIT_MARGIN=5     # Left margin of the text viewer pane (default 5)
STATUS_MARGIN=5     # Margin around bottom box containing statusbar & close button (default 5)
X_AXIS_ADJUST=2     # Compensates for width of separator. Increase value to reduce width of scaled images (default 2)
Y_AXIS_ADJUST=47    # Compensates for height of status bar, image page text and separator. Increase value to reduce height of scaled images(default 47)  

### Determine utilities to be used
type -p ffprobe && PROBE=ffprobe || { type -p avprobe && PROBE=avprobe || PROBE=false ;} ; export PROBE
type -p ffplay  && PLAY=ffplay   || { type -p avplay  && PLAY=avplay   || PLAY=false  ;} ; export PLAY
type -p xclip   && CLIP=xclip    || { type -p xsel    && CLIP=xsel     || CLIP=false  ;} ; export CLIP

### Determine startup folder. Priorities: 1) Passed argument 2) Last used folder 3) Default '/'
if      [[ -e $1 ]];then
        [[ -d $1 ]]       && DEFAULT_DIRECTORY=$1            || DEFAULT_DIRECTORY=$(dirname "$1")
else    [[ -f $LASTDIR ]] && DEFAULT_DIRECTORY=$(< $LASTDIR) || DEFAULT_DIRECTORY=/ 
fi

### Determine startup modes
[[ -f $LASTGEO ]] && GEOMETRY=$(< $LASTGEO)    || GEOMETRY=${DEFAULT_WIDTH}x${DEFAULT_HIGHT}  # window size and position
[[ -f $BOXMODE ]] && LAYOUT_V=true BOX=v SEP=h || LAYOUT_V=false BOX=h SEP=v # orientation (horizontal or vertical)
[[ -f $IMG_FIT ]] && SCALE_IMAGE=true          || SCALE_IMAGE=false          # image scaling
[[ -f $AUTOAUD ]] && AUTO_PLAY_A=true          || AUTO_PLAY_A=false          # autoplay audio
[[ -f $AUTOVID ]] && AUTO_PLAY_V=true          || AUTO_PLAY_V=false          # autoplay video

### Determine startup size of viewer pane (for image scaling) 
set -- ${GEOMETRY//[x+-]/ }                                                  # e.g. GEOMETRY=980x616+-232+592
    GEO_X=$1    
    GEO_Y=$2
set --
if [[ $LAYOUT_V == true ]] ;then
    VIEWER_X=$((GEO_X - WINDOW_MARGIN *2))
    VIEWER_Y=$((GEO_Y - Y_AXIS_ADJUST - MENUBAR_HIGHT - CHOOSER_HIGHT - WINDOW_MARGIN - ( CENTERSPACING + WIDGETSPACING + STATUS_MARGIN) * 2 ))
else
    VIEWER_X=$((GEO_X - X_AXIS_ADJUST - CHOOSER_WIDTH - (WINDOW_MARGIN + CENTERSPACING) * 2))
    VIEWER_Y=$((GEO_Y - Y_AXIS_ADJUST - MENUBAR_HIGHT - (WINDOW_MARGIN + WIDGETSPACING + STATUS_MARGIN) * 2))
fi
export VIEWER_X VIEWER_Y

### Initialize files and prepare exitus
ln -fsn  /dev/null  $SYMLINK
echo  0          >  $NB_PAGE
echo -n          >  $LASTCHO
echo -n          >  $P2_TEXT
trap "rm -r $TEMPDIR/*;busybox pkill -f \"$PLAY -nodisp\"" EXIT # pkill does not depend on PID. More reliable here

function fill_statusbar {
    MIME=$(file --separator "   │ " -i "$vCHOOSER" 2>&1)
    if [[ "$MIME" = *│*symlink* ]];then
         echo -n "$vCHOOSER"  " ➡"  $(file --separator "  │ " -i "$(realpath "$vCHOOSER")"  2>&1)
    else echo -n $MIME
    fi
};export -f fill_statusbar

function fill_viewer {                          # called when user selects item with single click or UP/DOWN key
    if [[ -s $PID_FFP ]];then                   # kill any ffplay/avplay that might still be running
        kill $(<$PID_FFP)
        echo -n > $PID_FFP
    fi
    echo -n "$vCHOOSER" > $LASTCHO              # write path of currently selected item to $LASTCHO 
    echo -n "$vCHOOSER" | $CLIP -i              # mimic ROX-Filer and send selected item to PRIMARY buffer
    ((    vNOTE    )) && echo  0 > $NB_PAGE     # if notebook page != 0 reset it to 0
    [[ -s $K_SHIFT ]] && echo -n > $K_SHIFT     # reset Shift key flag (mainly to recover from envoking search box with shift key and then using arrow keys to select next item...far fetched scenario?)

    case $vSTATUSBAR in
    ### DIRECTORIES AND EMPTY FILES ###
    *│*directory*)
        if  [[ $(echo "$vCHOOSER"/*) == "$vCHOOSER/*" ]];then
            MESSAGE=EMPTY
        else
            MESSAGE=$(du -hs  "$vCHOOSER")
            [[ ${MESSAGE::1} = 0 ]] && LNKMSG=". Always 0 in case of link"
            MESSAGE="${MESSAGE%%[[:blank:]]*}       (disk usage$LNKMSG)"
        fi
        ;;
    *│*empty*)
        if [[ "$vCHOOSER" =~ (^/proc/) ]] ;then         # /proc contains seemingly empty virtual files that can't be read by linking them
                MESSAGE=$(< "$vCHOOSER")
        else    vCHOOSER=/dev/null
        fi
        ;;

    ### IMAGES ###
    *'.xpm '*)                                          # xpm=us-ascii, must precede 'ascii' case; not scalable
        echo 1 > $NB_PAGE
        ;;
    *│*image/svg*|*'.svg '*)
        if [[ $vMENU_SCALE == true ]];then
             echo -n "SVG image, scaled to fit viewer" > $P2_TEXT
             echo  2 > $NB_PAGE
        else echo  1 > $NB_PAGE
        fi
        ;;
    *│*image*)
        if [[ $vMENU_SCALE == true ]] ;then
            EXIV=$(exiv2 "$vCHOOSER" 2>/dev/null)
            set -- ${EXIV#*Image size}                  # '      : 1600 x 1200 ...'
            IMAGE_X=$2                                  # '1600'
            IMAGE_Y=$4                                  # '1200'
            set --
            if (( IMAGE_X > VIEWER_X || IMAGE_Y > VIEWER_Y ));then
                 SX=$(( 100 * VIEWER_X / IMAGE_X))      # scale in % ;Unrounded integer only, but good enough
                 SY=$(( 100 * VIEWER_Y / IMAGE_Y))
                 SCALE=$((SY < SX ? SY : SX))           # SCALE = SX or SY, whichever is smaller
                 echo -n "Scaled to ${SCALE}% of ${IMAGE_X}x${IMAGE_Y}" > $P2_TEXT
                 echo 2 > $NB_PAGE
            else echo 1 > $NB_PAGE
            fi
        else echo 1 > $NB_PAGE
        fi
        ;;

    ### TEXT ###
     *charset=utf-8)
        ;;
     *charset=*ascii)
        [[ "$vCHOOSER" =~ (^/sys/) ]] && MESSAGE=$(< "$vCHOOSER" )
        ;;
    *charset=iso*|*charset=*unknown*)
        CHARSET=${vSTATUSBAR#*charset=}
        [[ $CHARSET = *unknown* ]] && CHARSET=iso-8859-1    # Seems to work best
        OOPS=$(iconv -f $CHARSET -t UTF-8 -o $MSGFILE -c "$vCHOOSER" 2>&1>/dev/null )
        if [[ $OOPS ]] ;then
            MESSAGE=$'Something went wrong.\n\nMaybe "iconv" is not installed or could not convert charset '"$CHARSET"$'\nHere is the error message:\n\n'"$OOPS"
        else ln -fsn $MSGFILE  $SYMLINK; return
        fi
        ;;

    ### AUDIO/VIDEO ###
    *│*video*|*'.mkv '*)
        if [[ $vMENU_VIDEOPLAY == true && $PLAY != false ]] ;then
            $PLAY -x 320 -y 180 -i "$vCHOOSER"  &
            echo -n $! > $PID_FFP
        fi
        MESSAGE=$($PROBE "$vCHOOSER" 2>&1)
        [[ $MESSAGE ]] || MESSAGE='Display of video specs requires "ffprobe" or "avprobe"'
        ;; 
    *│*audio*|*'.mp3 '*|*'.ogg '*|*'.wma '*)
        if [[ $vMENU_AUDIOPLAY == true && $PLAY != false ]] ;then
            $PLAY -nodisp -i "$vCHOOSER"  &
            echo -n $! > $PID_FFP
        fi
        if [[ $PROBE != false ]];then
            FFP=$($PROBE "$vCHOOSER" 2>&1)
            DUR=Duration:${FFP#*Duration:}
            ID3=${FFP##*Metadata:$'\n'}
            if [[ "$ID3" = "$FFP" ]] ;then          # No Metadata found
                ID3=""
            else
                EN_SPACE=$'\xe2\x80\x82'            # Unicode U+2002 EN SPACE (" ")
                ID3=${ID3/$DUR}                     # Strip trailing $DUR
                ID3=${ID3// /$EN_SPACE}             # replace SPACE with EN SPACE
                ID3=${ID3//$EN_SPACE:/$'\t:'}       # replace single EN SPACE followed by colon with a tab
            fi
            MESSAGE=$(echo -n "$DUR"$'\n\n'"$ID3" | sed "s/^[[:blank:]]*//")
        else
            MESSAGE='Display of audio specs requires "ffprobe" or "avprobe"'
        fi
        ;;    

    ### EXECUTABLES ###
    *│*executable*)
        if [[ $(grep -i "usage:" "$vCHOOSER") ]] ;then
                MESSAGE=$'\n\tUsage info may exist. Try F1'
        else    vCHOOSER=/dev/null
        fi
        ;;

    ### EVERYTHING ELSE ###
    *)  vCHOOSER=/dev/null ;;
    esac

    if [[ "$MESSAGE" ]];then
         echo "$MESSAGE"    > $MSGFILE
         ln -fsn  $MSGFILE    $SYMLINK
    else ln -fsn "$vCHOOSER"  $SYMLINK
    fi
};export -f fill_viewer

function on_demand_info {
   (( vNOTE )) && echo  0 > $NB_PAGE                        # set to notebook page 0
   case $vSTATUSBAR in 
    *│*executable*)
        grep -iq "usage:" "$vCHOOSER" || return             # exit function if 'usage:' not found
        for ARG in --help -help;do 
            urxvt -g 1x1+0+0  -e bash -c "                  # open console in minimum size in upper left corner to reduce flicker
            { sleep 0.5; kill \$$ ;} &                      # let console commit suicide after 0.5sec.
            \"$vCHOOSER\" $ARG  &> $MSGFILE                 # redirect all output to MSGFILE
            "
            grep -iqm1 "usage:" $MSGFILE && break           # leave loop if output contains 'usage:' 
        done
        ln -fsn  $MSGFILE  $SYMLINK
        ;;
    esac
};export -f on_demand_info

function save_settings {
        ACTWINID=$(xprop -root _NET_ACTIVE_WINDOW)
        ACTWINID=${ACTWINID##* }
        XWININFO=$(xwininfo -id $ACTWINID)
        XWIN_ARR=($(awk '/^ *Abs|^ *Rel|^ *Width|^ *Height/ { print $NF }' <<< "$XWININFO"))
        WINDOW_W=${XWIN_ARR[4]}
        WINDOW_H=${XWIN_ARR[5]}
        WINDOW_X=$(( ${XWIN_ARR[0]} - ${XWIN_ARR[2]} ))
        WINDOW_Y=$(( ${XWIN_ARR[1]} - ${XWIN_ARR[3]} ))
        GEOMETRY=${WINDOW_W}x${WINDOW_H}+${WINDOW_X}+${WINDOW_Y}
        echo -n $GEOMETRY              > $LASTGEO
        echo -n $(dirname "$vCHOOSER") > $LASTDIR
};export -f save_settings

function open_editor   {
    echo -n > $K_SHIFT                  # reset shiftkey flag
    [[ -f "$vCHOOSER" ]] || return      # editor should open only regular files ...
    [[ $vSTATUSBAR =~ (x-empty|charset=[^b][^i][^n]) ]] && defaulttexteditor "$vCHOOSER" # ...and only non-binaries or empty files
};export -f open_editor

TT01=$'Executables:    Usage message (if available)'

MENU='
<vbox space-expand="false">
    <menubar height-request="'$MENUBAR_HIGHT'">
        <menu label="File">
            <menuitem label="Open in default editor Shift+click" accel-key="0xffbf" accel-mods="0"  icon-name="geany">
                <action>open_editor defaulttexteditor</action>
            </menuitem>
            <menuitem label="Show in ROX            MiddleClick" accel-key="0x72" accel-mods="4"  stock-id="gtk-jump-to">
                <action>rox -s "$vCHOOSER"</action>
            </menuitem>
            <menuitemseparator></menuitemseparator>
            <menuitem label="Exit" accel-key="0x71" accel-mods="4" stock-id="gtk-quit"> <action>activate:vBTN_CLOSE</action>           </menuitem>
        </menu>
        <menu label="View">
            <menuitem label="Additional info (see tooltip)" tooltip-text="'$TT01'"stock-id="gtk-info" accel-key="0xffbe" accel-mods="0">
                <action>on_demand_info</action> 
                <action>refresh:vEDIT</action> 
                <action>refresh:vNOTE</action> 
            </menuitem>
        </menu>
        <menu label="Preferences">
            <menuitem label="Fit images to viewer size" checkbox="'$SCALE_IMAGE'">
                <variable>vMENU_SCALE</variable>
                <action>if true >   $IMG_FIT</action>
                <action>if false rm $IMG_FIT</action>
                <action condition="command_is_true( ((vNOTE)) &&  > $LASTCHO || echo true)">break:</action>
                <action>activate:vBTN_ACTION</action>
            </menuitem>
            <menuitem label="Autoplay audio files" checkbox="'$AUTO_PLAY_A'">
                <variable>vMENU_AUDIOPLAY</variable>
                <action>if true >   $AUTOAUD</action>
                <action>if false rm $AUTOAUD</action>
               <action>fill_viewer</action> 
            </menuitem>
            <menuitem label="Autoplay video files" checkbox="'$AUTO_PLAY_V'">
                <variable>vMENU_VIDEOPLAY</variable>
                <action>if true >   $AUTOVID</action>
                <action>if false rm $AUTOVID</action>
                <action>fill_viewer</action> 
            </menuitem>
            <menuitem label="Vertical layout (viewer below chooser)" checkbox="'$LAYOUT_V'" >
                <action>if true >   $BOXMODE</action>
                <action>if false rm $BOXMODE</action>
                <action>save_settings; $APPPATH &</action>
                <action>exit:</action>
            </menuitem>
        </menu>
    </menubar>
</vbox>
'

export MAIN_DIALOG='
<window title="'${0##*/}'"allow-shrink="true" margin="'$WINDOW_MARGIN'">
<vbox space-expand="true" space-fill="true"  spacing="'$WIDGETSPACING'">
'$MENU' 
    <'$BOX'box space-expand="true"  spacing="'$CENTERSPACING'">
        <chooser  width-request="'$CHOOSER_WIDTH'" height-request="'$CHOOSER_HIGHT'" space-expand="false" space-fill="true">
            <variable>
"vCHOOSER"
            </variable>
            <default>"'$DEFAULT_DIRECTORY'"</default>
            <action>"[[ -s $PID_FFP ]] && { kill $(<$PID_FFP); echo -n > $PID_FFP ;} "</action> 
            <action>rox "$vCHOOSER"</action> 
            <action when="update-preview">activate:vBTN_ACTION</action>
            <action signal="button-release-event">[[ $PTR_BTN == 1 && -s $K_SHIFT ]] && open_editor &</action> 
            <action signal="button-release-event">[[ $PTR_BTN == 2                ]] && rox -s  "$vCHOOSER" &</action> 
            <action signal="key-press-event">     [[ $KEY_SYM == Shift_[LR]       ]] && echo    > $K_SHIFT</action>
            <action signal="key-release-event">   [[ $KEY_SYM == Shift_[LR]       ]] && echo -n > $K_SHIFT</action>
        </chooser>
        <'$SEP'separator space-expand="false"></'$SEP'separator>
        <notebook space-fill="true" space-expand="true" show-tabs="false" show-border="false">
            <vbox>
                <edit left-margin="'$L_EDIT_MARGIN'">
                    <variable export="false">
"vEDIT"
                    </variable>
                    <input file>'$SYMLINK'</input>
                </edit>
            </vbox>
            <vbox>
                <text label="Original size" xalign="2"></text>
                <vbox scrollable="true">
                    <pixmap space-expand="true">
                        <variable>
"vPM01"
                        </variable>
                        <input file>'$SYMLINK'</input>
                    </pixmap>
                </vbox>
            </vbox>
            <vbox>
                <text xalign="2">
                    <variable>
"vTXT2"
                    </variable>
                    <input file>"'$P2_TEXT'"</input>
                </text>
                <pixmap space-expand="true" space-fill="true">
                    <variable>
"vPM02"
                    </variable>
                    <width>'$VIEWER_X'</width>
                    <height>'$VIEWER_Y'</height>
                    <input file>'$SYMLINK'</input>
                </pixmap>
            </vbox>
            <variable>
"vNOTE"
            </variable>
            <input file>'$NB_PAGE'</input>
        </notebook>
    </'$BOX'box>
    <hbox space-expand="false" margin="'$STATUS_MARGIN'" visible="true"> 
        <entry  space-expand="true">
            <variable>
"vSTATUSBAR"
            </variable>
            <input>fill_statusbar</input>
        </entry>
        <button label="   Close   ">
            <variable>
"vBTN_CLOSE"
            </variable>
            <action>save_settings</action>
            <action>exit:OK</action>
        </button>
    </hbox>
    <button label="This button should be invisible" visible="false" sensitive="false">
        <variable>
"vBTN_ACTION"
        </variable>
        <action condition="command_is_true([[ \"$(<$LASTCHO)\" == \"$vCHOOSER\" ]] && echo true )">break:</action>
        <action>refresh:vSTATUSBAR</action>
        <action>fill_viewer</action>
        <action condition="command_is_true([[ $(<$NB_PAGE) != $vNOTE ]] && echo true )">refresh:vNOTE</action>
        <action condition="command_is_true([[ $vNOTE       == 0      ]] && echo true )">refresh:vEDIT</action>
        <action condition="command_is_true([[ $vNOTE       == 1      ]] && echo true )">refresh:vPM01</action>
        <action condition="command_is_true([[ $vNOTE       == 2      ]] && echo true )">refresh:vPM02</action>
        <action condition="command_is_true([[ $vNOTE       == 2      ]] && echo true )">refresh:vTXT2</action>
    </button>
</vbox>
<action signal="key-press-event" condition="command_is_true([[ $KEY_SYM == Escape ]] && echo true )">busybox pkill -f "$PLAY -nodisp" && > $PID_FFP</action>
</window>
'
gtkdialog -G $GEOMETRY