#!/bin/bash

#Scan for shares and make Rox apps for mount points. Public domain.
# merged with 01micko's update for slacko 20121028 (0.5)
# 201711 step: update AppRun only, versioned 1.0.0:
#   Support multiple servers with assorted shares/passwords; all dialogs yad; gettexted.
#   Three phases: scan loop; edit/save/load dialog; create RoxApps loop.
# 1.0.1 step: hide/show passwords.
# 2.0.0 step: +privacy, +usability, +gettext, +dev tools, bug fixing. Depend yad patched with github PR 26.
# 2.1.0 step: fix cosmetic issues; test with yad & yad-lib updates.
# 2.2.x step: see fossil:docs/zz-smb-browser-changelog.md
# 2.2.1 see changelog.md; also use yad_gtk2 explicitly - yad_gtk3 works as well but it isn't as well-tested

VERSION=2.2.1
# Highlight non-obvious dependencies (all items) of the Scanner and Share RoxApps
DEPEND="cifs-utils defaulttexteditor eztables-firewall giflib gifsicle gtksu mpscan samba4-cutdown yad_gtk2"

# Enable DEBUG {{{1
# User can enable debug level by passing positional parameter 'debug='<digits>
# debug > 0 : do not exit when no subnets found; keep progress log window open; enable ERR trap
# debug > 1 : log passwords, smbclient output and mount.cifs output
# debug > 3 : append caller info to show_progress lines
[[ $* =~ .*debug=([[:digit:]]+).* ]] && DEBUG=${BASH_REMATCH[1]} || DEBUG=0

handle_fatal() # {{{1
{
  trap - ERR
  local fifo=/tmp/smb-browser-fatal frame=0
  [ -e "$fifo" ] && rm -f "$fifo"; mkfifo "$fifo"
  yad_gtk2 \
    --title="Unexpected Error - SMB Browser" \
    --text="<b>Call Stack</b> - close app windows and quit\r" \
    --on-top --center --image=gtk-stop --window-icon=gtk-stop \
    --width=600 --height=400 --timeout=0 \
    --button=gtk-quit --buttons-layout=center \
    --text-info < "$fifo" &
  sleep 0.5
  while caller $((frame++)); do :; done > "$fifo" 2>&1
  wait
  rm -f "$fifo"
  exit 1
}

[ $DEBUG -gt 0 ] && trap handle_fatal ERR # for bug reports {{{1}}}

. "${BASH_SOURCE%/*}"/scripts/shared.sh || { echo >&2 ERR shared.sh; exit 1; }

init_app1 "$@" || exit 1 # {{{1}}}
export TEXTDOMAIN=fatdog OUTPUT_CHARSET=UTF-8
. gettext.sh
. ./scripts/i18n_table.sh && i18n_table || { echo >&2 ERR i18n_table.sh; exit 1; }
[ $EUID -ne 0 ] && exec gtksu "$i18n_GtksuTitle" "$0" "$@"
APPTITLE=$i18n_AppTitle APPWINSIZE=$i18n_WinSize
YAD_TITLE="$APPTITLE [$$]"
init_app2 # sets YAD_OPTIONS
. yad-lib.sh # {{{1}}} In: $YAD_TITLE
export YAD_GEOMETRY YAD_GEOMETRY_POPUP

HIDEPASSWORDS=${HIDEPASSWORDS:-1}
SEPARATOR=$'\b' # must not be one of the default $IFS characters
# set HIDE_RES=. to hide resource files or $MOUNTNAME icon of the Share RoxApps
MOUNTNAME=mnt-point RES=resources HIDE_RES=. HIDE_MOUNTNAME=
# sec=ntlm removed for 3.9 onwards
# scripts/part2 automatically converts gid=<name> to its numeric gid equivalent.
DEFAULT_MOUNT_OPTIONS="noserverino,file_mode=0775,dir_mode=0775,gid=users"
# set WITH_HOSTNAME_RESOLUTION not empty to attempt hostname resolution before falling back to plain IP.
WITH_HOSTNAME_RESOLUTION=

SMBCLIENT=smbclient
# $SMBCLIENT error 'not support EXTENDED_SECURITY.*spnego = yes.*ntlmv2 auth = yes'
# is harmless but if you want to control it set
# SMBCLIENT="smbclient --option=clientusespnego=no"
# (SMB1 client without extended security)

PROGRESS_BAR_WIDTH_PC=70 # % ${APPWINSIZE%x*}; 70% looks better
# [geometry]:[bg_color]:(animated)_GIF_path
PROGRESS_GIF="45x128+$YAD_BORDERS+$YAD_BORDERS:white:$APPDIR"/icons/juggling10-45x128.gif

# Set traps and create temporary folder. {{{1
TMPD=$(mktemp -d -p "${TMPDIR:-/tmp}" "smb-browser_XXXXXX")
chmod 700 "$TMPD" || exit 1
export TMPD
handler() # {{{1
{
  trap - HUP INT QUIT TERM ABRT 0 ERR
  [ $DEBUG = 0 ] && exec 1>/dev/null 2>&1
  TRAPPED=1
  close_preview_dialog
  close_progress_dialog
  [ -e "$TMPD"/tmp-mount ] && mountpoint -q "$TMPD"/tmp-mount && umount -n "$TMPD"/tmp-mount
  rm -rf "$TMPD"
}
trap handler HUP INT QUIT TERM ABRT 0

add_resources() # $1-path-to-appdir; In/Out {{{1
# In: $HIDE_RES $RES
# Out: $ICON_MOUNTED $ICON_UNMOUNTED $ICON_SHOW $ICON_HIDE
# Return: status
{
  local p AP=${1%%/} resd
  ICON_MOUNTED=mounted.svg ICON_UNMOUNTED=drive48.png ICON_SHOW=show.svg ICON_HIDE=hide.svg
  resd="$AP/$HIDE_RES$RES" && mkdir -p "$resd" &&
  (cd "$APPDIR"/icons && cp $ICON_UNMOUNTED smb_overlay.png $ICON_SHOW $ICON_HIDE CREDITS "$resd") &&
  # Create self-contained $ICON_MOUNTED: image data is embedded in the file.
  # For a smaller but not self-contained icon replace lines image1 and image2 below with:
  # <image xlink:href="$resd/$ICON_UNMOUNTED" width="48" height="48" id="image1"/>
  # <image xlink:href="$resd/smb_overlay.png" width="48" height="48" id="image2"/>
  cat > "$resd/$ICON_MOUNTED" << EOF &&
<svg width="48" height="48" id="svg1" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs id="defs1">
<linearGradient id="LG1">
   <stop style="stop-color:#00ff00;stop-opacity:1;" id="stop1"/>
   <stop style="stop-color:#333333;stop-opacity:1;" offset="1" id="stop2"/>
</linearGradient>
<radialGradient xlink:href="#LG1" id="RG1" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5"/>
</defs>
<image xlink:href="data:image/png;base64,$(base64 "$resd/$ICON_UNMOUNTED")" width="48" height="48" id="image1"/>
<circle cx="40" cy="8" r="8" style="fill:url(#RG1);" id="ellipse1"/>
<image xlink:href="data:image/png;base64,$(base64 "$resd/smb_overlay.png")" width="48" height="48" id="image2"/>
</svg>
EOF
  if mountpoint -q "$AP/$HIDE_MOUNTNAME$MOUNTNAME" 2>/dev/null; then
    ln -sfT ${resd#$AP/}/$ICON_MOUNTED "$AP"/.DirIcon
  else
    ln -sfT ${resd#$AP/}/$ICON_UNMOUNTED "$AP"/.DirIcon
  fi

}

scan_subnets() # $1-list-of-subnets {{{1
{
  local net pids

  # The for loop runs parallel mpscans, one per subnet, each mpscan scans ports in parallel.
  # Moreover, mpscan was hacked to time out faster (pkg mpscan-0.1.0kirk).

  # The while loop redirects server IPs to the caller via stdout, and to the progress
  # dialog via write_progress
  for net in $1; do
    stdbuf -oL mpscan $TIMEOUT -p $PORT $net.1 - $net.254 2>/dev/null &
    pids+=" $!"
  done > >(while read a1 a2 a3 a4 tail; do
      [ OK = "$a4" ] && echo "$a2"    # all samba servers
      write_progress "$a2"            # all nodes
    done)

  trap ": $FUNCNAME handling USR1; kill $pids 2>/dev/null; close_progress_dialog" USR1
  wait $pids 2>/dev/null
  trap - USR1
  true # for DEBUG>0
}

get_server_name() # $1-IP {{{1
# Print NETBIOS name ("" if the firewall blocks UDP port 137)
{
  # Historic note: Couldn't reliably get server name with smbclient. -- 2020 Is this still true?
  nmblookup -A $1 | awk '/<20>/ { sub(/^[ \t]+/, "", $1); print $1; exit }'
}

get_share_names() # In/Out {{{1
# In: $Username $Password $SMBCLIENT $IP $NAME
# Out: $NSHARES $NADMIN_SHARES (number of all shares = NSHARES + NADMIN_SHARES) $NAME2 $TMPD/open-share-access
{
  NSHARES=0 NADMIN_SHARES=0 NAME2=$NAME
  local ret out="$TMPD"/$RANDOM err="$TMPD"/$RANDOM count="$TMPD"/$RANDOM
  # First try with access credentials
  if ! USER="$Username" PASSWD="$Password" $SMBCLIENT -g -L $IP 1>"$out" 2>"$err"; then
    # Nope, try open access
    $SMBCLIENT -U % -g -L $IP 1>>"$out" 2>>"$err" &&
      # If it worked then share access is open on this server
      echo "$IP:" >> "$TMPD"/open-share-access
  fi

  echo 0 0 > "$count"
  awk -F\| -v COUNT="$count" -v IP="$IP" -v OPEN_SHARE="$TMPD"/open-share-access '
# ----- $out -----
$1 == "Disk" { # share name
  # Filter automatic Windows administrative shares https://en.wikipedia.org/wiki/Administrative_share.
  if($2 ~ /[A-Za-z][$]$/) {
    ++nadmin_shares
  } else {
    ++nshares
    print $2
  }
}
$1 == "Server" { # server name
  server_name = $2
}
# ----- $err -----
# This error message reveals open share access on this server
/requested LANMAN password.*but \x27client lanman auth = no\x27/ {
  print IP":"$0 >> OPEN_SHARE
}
END {
  print 0+nshares, 0+nadmin_shares, "/"server_name > COUNT
}' "$out" "$err" && ret=$? || ret=$?

  read NSHARES NADMIN_SHARES NAME2 < "$count"
  NAME2=${NAME2#/}
  # Bounce error messages to stderr
  [ -s "$err" ] && grep -H --label "$NAME2 $IP" . < "$err" >&2
  rm -f "$auth" "$out" "$err" "$count"
}

prompt_to_set_Username_Password_for() # $1='SHARE'|'NAME' [$2-preamble] [$3-invalid] ; In/Out {{{1
# In: $SHARE $NAME $SEPARATOR $HIDEPASSWORDS $IP
# Out: $Username $Password (unchanged on Cancel) $HIDEPASSWORDS
{
  local key=$1 preamble=$2 invalid=$3 has_share has_cancel button_showhide IFS ret msg
  case $key in
    NAME ) printf -v msg "$i18n_NAME" "$preamble" "$NAME" "$IP"
      has_cancel=TRUE ;;
    SHARE) printf -v msg "$i18n_SHARE" "$preamble" "$SHARE" "$NAME" "$IP"
      has_share=TRUE ;;
  esac
  [ "$HIDEPASSWORDS" ] &&
    button_showhide="$i18n_p2_Show!$APPDIR/icons/show.svg!$i18n_p2_Show_ttip" ||
    button_showhide="$i18n_p2_Hide!$APPDIR/icons/hide.svg!$i18n_p2_Hide_ttip"
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE"
  IFS=$SEPARATOR
  set -- $(yad_gtk2 \
    ${YAD_GEOMETRY_POPUP// /$IFS} --on-top \
    --text="$invalid${invalid:+\r}$msg\r${i18n_credentials_help//\*/  $'\u26AB'}" \
    --form --separator="$IFS" \
    --field="$i18n_p2_User_name": "$Username" \
    --field="$i18n_p2_Password":${HIDEPASSWORDS:+H} "$Password" \
    --button=gtk-ok:0 \
    "${has_cancel:+--button=gtk-cancel:1}" \
    --button="$button_showhide:4" \
    "${has_share:+--button=$i18n_Skip!gtk-go-forward!$i18n_Skip_ttip $i18n_Go_to_next_share_for_server:1}" \
    --button="$i18n_Jump!gtk-media-forward!$i18n_Jump_ttip:2" \
    --button="$i18n_Leave!gtk-media-stop!$i18n_Leave_ttip:3" \
    --button="gtk-help:defaultbrowser /usr/share/doc/faqs/smb-browser.html" \
    ;echo "${IFS}$?") #yad exit status
  ret=${@: -1} #yad exit status 0(OK) 1(Skip/Cancel) 2(Jump) 3(Leave) 4(Toggle show/hide passwd)
  if [ $DEBUG -gt 1 ]; then
    local btn
    case $ret in 0) btn=OK;; 2) btn=$i18n_Jump;; 3) btn=Leave;; 4) :;; 1|*) btn=$i18n_Skip;; esac
    [ "$btn" ] && write_progress "$btn pressed"
  fi
  # Only 0(OK) and 4(Toggle show/hide) store Username and Password.
  case $ret in
    0 ) Username="$1" Password="$2" ;;
    4 ) Username="$1" Password="$2" ret=4 #caller matches '4'
      [ "$HIDEPASSWORDS" ] && unset HIDEPASSWORDS || HIDEPASSWORDS=1 ;;
    70|252 ) ret=1 ;; #map timeout and ESC key => skip to the next share
  esac
  return $ret
}

declare -A PASSWORD_STORE #{{{1}}}

try_mount() # [--leave-unmounted] [--no_retry] ; In/Out {{{1
# In: $IP $SHARE $NAME $Username $Password $IOCHARSET $AKEY $DEFAULT_MOUNT_OPTIONS
# Out: $NetworkResource[$AKEY] $PASSWORD_STORE $Username $Password $MountOptions[$AKEY]
{
  local then_unmount no_retry i=0 retval=0 opt_ip
  [[ $* =~ "--leave-unmounted" ]] && leave_unmounted=$((++i))
  [[ $* =~ "--no-retry" ]] && no_retry=$((++i))
  ##shift $i
  MountOptions[$AKEY]="$DEFAULT_MOUNT_OPTIONS$IOCHARSET"
  [ "$SV" = "on" ] && MountOptions[$AKEY]+=",vers=1.0"
  # Experimentally adding ip=$IP enhances ability to connect to open shares.
  # Note that scripts/part2 mounts //$IP/$SHARE.
  [ "guest" = "$Username" ] && opt_ip="ip=$IP"
  try_mount_cmd_and_set_NetworkResource "$opt_ip" 2> "$TMPD"/error 1>&2
  retval=$?
  [ $DEBUG -gt 1 ] && write_progress "$(uniq "$TMPD"/error)"
  rm -f "$TMPD"/error
  [ -n "$leave_unmounted" ] && umount -n "$TMPD"/tmp-mount 2>/dev/null
  if [ 0 = $retval ]; then #store password for possible re-use
    if [ guest != "$Username" ]; then
      PASSWORD_STORE[$Username]="$Password"
      [ $DEBUG -gt 1 ] && write_progress "PASSWORD_STORE[$Username]=($Password)"
    fi
  elif [ -z "$no_retry" ]; then #try stored passwords
    local usave="$Username" psave="$Password"
    set -- "${!PASSWORD_STORE[@]}"
    for Username; do
      Password="${PASSWORD_STORE[$Username]}"
      [ $DEBUG -gt 1 ] && write_progress "REUSE Username($Username) Password($Password)"
      try_mount --no-retry "$@"
      retval=$?
      if [ 0 = $retval ]; then
        return 0
      else
        # This assignment "takes" at the last iteration.
        Username="$usave" Password="$psave"
      fi
    done
  fi
  [ 0 = $retval -a guest = "$Username" ] && MountOptions[$AKEY]="ip=$IP,${MountOptions[$AKEY]}"
  return $retval
}

try_mount_cmd_and_set_NetworkResource() # $1-options In/Out {{{1
# In: $NAME $SHARE $IP $Username $Password $MountOptions $AKEY $WITH_HOSTNAME_RESOLUTION
# Out: $NetworkResource[$AKEY]
{
  local options=$1 ret UNC
  for UNC in ${WITH_HOSTNAME_RESOLUTION:+"//$NAME/$SHARE"} "//$IP/$SHARE"; do
    USER="$Username" PASSWD="$Password" mount.cifs "$UNC" "$TMPD"/tmp-mount -v -o "$options,${MountOptions[$AKEY]}"
    ret=$?
    [ 0 = $ret ] && break
  done
  NetworkResource[$AKEY]="$UNC"
  return $ret
}

printf_VARS() # $1-index $2-field-separator; In {{{1
# In: $VARS arrays.
{
# Note the special handling of $1=0, which allows printing a data-frame
# "record" of VARS, for $1>0, and a list of variables named $VARS, for $1==0
# (recall that bash treats 'VarName[0]' as 'VarName').
  local k=$1 fmt="${2:-$SEPARATOR}%s"
  # When index is zero the caller prints an index value, otherwise we do.
  [ 0 != $k ] && printf "%d" $k
  printf "$fmt" \
    "${Attach[$k]}" \
    "${Error[$k]}" \
    "${NetworkResource[$k]}" \
    "${MountOptions[$k]}" \
    "${ServerName[$k]}" \
    "${FolderName[$k]}" \
    "${ServerIP[$k]}" \
    "${UserName[$k]}" \
    "${UserPassword[$k]}" \
    ;
  printf "\n" # record sep
}
# ---------------------------------------------------------------------
# Reminder: Sync $VARS order with printf_VARS() and i18n_VARS !!! {{{1
# ---------------------------------------------------------------------
# Don't change \t separator.
VARS=$'Attach\tError\tNetworkResource\tMountOptions\tServerName\tFolderName\tServerIP\tUserName\tUserPassword'
declare -a $VARS # vectors of the "mount data" matrix, table columns.
# ---------------------------------------------------------------------

declare -A YADCOLUMNS #{{{1}}}

set_YADCOLUMNS() # In/Out {{{1
# In: $i18n_VARS
# OUT: $YADCOLUMNS
{
  local IFS=$'\t' name
  set -- $i18n_VARS
  unset IFS YADCOLUMNS
  YADCOLUMNS=( "--column=$1:CHK" ) # Attach
  shift
  for name; do
    YADCOLUMNS+=( "--column=$name" )
  done
}

table_VARS() # $1-field-separator ; In/Out {{{1
# stdout: table of VARS
# In: $AKEY, all data-frame arrays.
{
  local k sep="$1"
  k=1; while [ $k -le ${AKEY:-0} ]; do
    printf_VARS $((k++)) "$sep"
  done
}

tr_SEPARATOR_to_x() # $1-x(\t|\n) $2-infile $3-outfile {{{1
# In: $SEPARATOR
# Background: the data flows in this script:
# a. yad --list --separator $SEPARATOR > records
# b. Map(records, tr_separator_to_\n) | yad --list
# c. Map(records, tr_separator_to_\t) > savefile.tsv
# d. savefile.tsv | tr \t $SEPARATOR > records
{
  case $1 in
    $'\n') # $2: IRS=$'\n' IFS=$SEPARATOR; $3: ORS='' OFS=$'\n'
      local -a A
      while IFS="$SEPARATOR" read -r -a A; do
        printf "%s\n" "${A[@]}"
      done < "$2" > "$3" ;;
    $'\t') # $2: IRS=$'\n' IFS=$SEPARATOR; $3: ORS=$'\n' OFS=$'\t'
      tr "$SEPARATOR" "$1" < "$2" > "$3" ;;
    *) return 1 ;;
  esac
}

# Document/work around yad bugs {{{1
# ignore: yad version 0.40.3: --ellipsize=<position> always ellipsizes at the start of a string.
# work-around: yad version < 0.40.3: --confirm-overwrite is buggy.
awk -v V="`yad --version`" 'BEGIN{split(V,a,/[., ]/);exit((a[1]>0||a[2]>40||a[2]==40&&a[3]>=30)?0:1)}' &&
  YAD_CONFIRM_OVERWRITE=--confirm-overwrite || unset YAD_CONFIRM_OVERWRITE

dialog_VARS() # $@-yad-options-override ; In/Out {{{1
# stdin: table of VARS
# In: $AKEY, $SEPARATOR, $YADCOLUMNS, $APPDIR, all data-frame arrays.
# Out: $TMPD/vars, $TMPD/dialog.xid, $SAVEFILE
{
  local k ret warn1 count=0
  k=1; while [ $k -le ${AKEY:-0} ]; do [ "${Attach[$k]}" != TRUE ] && : $((++count)); : $((++k)); done
  if [ $count -gt 0 ]; then
    warn1="$(printf "<span bgcolor='red' fgcolor='white'><b> ! </b></span> $(ngettext "$i18n_unchecked_row" "$i18n_unchecked_rows" $count)" $count) $i18n_A_share_could_not_be_mounted $i18n_Double_click_customize_entry $i18n_Folder_could_appear_empty\r"
  fi
  set_YADCOLUMNS
  local x=$YAD_LIB_SCREEN_WIDTH y=$YAD_LIB_SCREEN_HEIGHT w=${APPWINSIZE%x*} h=${APPWINSIZE#*x}
  x=$(( ($x - $w) / 2 )) y=$(( ($y - $h) / 2 ))
  yad_gtk2 ${YAD_GEOMETRY:- --posx=$x --posy=$y --width=$w --height=$h} \
    --no-escape --editable --list --scroll \
    --print-all --separator="$SEPARATOR" --ellipsize=MIDDLE \
    --text="<b><big>$i18n_Review_mount_icons</big></b>\n$i18n_Mp_icn_hlp1\n$i18n_Mp_icn_hlp2\n$i18n_Mp_icn_hlp3\n$i18n_Dclick_to_edit\n\n$i18n_Click_to_accept_default\n$warn1" \
    --column='':NUM --hide-column=1 "${YADCOLUMNS[@]}" \
    --button=gtk-ok --button=gtk-cancel --button=gtk-save:2 --button="$i18n_Load!gtk-open":4 \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button="gtk-help:defaultbrowser /usr/share/doc/faqs/smb-browser.html" \
    --print-xid="$TMPD"/dialog.xid \
    "$@" > "$TMPD"/vars
  ret=$?

  ### Save discovery results

  if [ 2 = $ret ]; then
    yad_gtk2 ${YAD_GEOMETRY:- --posx=$x --posy=$y --width=$w --height=$h} \
      --file --save $YAD_CONFIRM_OVERWRITE \
      --button=gtk-go-back:1 --button=gtk-ok:0 \
      --filename="${APPDIR%/*}/shares.tsv" \
      --text="$i18n_Save_result\n" > "$TMPD"/savefile
    [ 0 != $? ] && return 21 # saving cancelled
    read SAVEFILE < "$TMPD"/savefile
    if [ "$SAVEFILE" ]; then
      # to tab-separated file .tsv
      tr_SEPARATOR_to_x $'\t' "$TMPD"/vars "$SAVEFILE"
      ret=$?
      [ 0 = $ret ] && return 20 # ok
      yad_gtk2 ${YAD_GEOMETRY_POPUP:- --center} --text="$(printf "$i18n_File_save_error" $ret)" \
        --button=gtk-go-back
      return 23
    fi
    ret=23

  ### Load archived discovery results

  elif [ 4 = $ret ]; then
    yad_gtk2 ${YAD_GEOMETRY:- --posx=$x --posy=$y --width=$w --height=$h} \
      --file --filename="${APPDIR%/*}/shares.tsv" \
      --button=gtk-go-back:1 --button=gtk-ok:0 \
      --text="$i18n_Load_result\n" > "$TMPD"/savefile
    [ 0 != $? ] && return 41 # loading cancelled
    read SAVEFILE < "$TMPD"/savefile
    if [ "$SAVEFILE" ]; then
      # from tab-separated file .tsv
      tr $'\t' "$SEPARATOR" < "$SAVEFILE" > "$TMPD"/vars
      ret=$?
      [ 0 = $ret ] && return 40 # ok
      yad_gtk2 ${YAD_GEOMETRY_POPUP:- --center} --text="$(printf "$i18n_File_load_error" $ret)" \
        --button=gtk-go-back
      return 43
    fi
    ret=43
  fi
  return $ret
}

options_dialog() # In/Out {{{1
# In: $APPDIR/options
# Out: PT SV CS SN TO SUBNETSALT TIMEOUT
{
  local c subnets
  # Since yad uses TRUE/FALSE we need to convert values from on/off.
  if [ -e "$APPDIR"/options ] &&
    # Temporarily map on/off => TRUE/FALSE for yad's use.
    sed -e 's/on/TRUE/' -e 's/off/FALSE/' "$APPDIR"/options > "$TMPD"/options
  then
    . "$TMPD"/options
  fi
  PT=${PT:-FALSE} SV=${SV:-TRUE} CS=${CS:-FALSE} SN=${SN:-FALSE} TO=${TO:-FALSE}
  [ -z "$TIMEOUT" ] && TIMEOUT="-t 5"
  [ TRUE = "$SN" ] && subnets="$SUBNETSALT"
  [ -z "$subnets" ] && subnets="$(get_subnets)"
  [ -z "$subnets" ] && subnets="<b>$i18n_network_configuration_error</b>"
  YAD_OPTIONS+=" --center --on-top"
  set -- $(yad_gtk2 --text="<b>$i18n_Options</b>" --text-align=center \
    --form --separator=" " \
    --field="<b>$i18n_Shares:</b>":LBL \
    --field="$i18n_Connect_SMB1":CHK \
    --field="$i18n_Use_utf8":CHK \
    --field="<b>$i18n_Scan:</b>":LBL \
    --field="$i18n_Scan_445":CHK \
    --field="$(printf "$i18n_Scan_another_subnet" "$subnets")":CHK \
    --field="$(printf "$i18n_Port_scan_timeout" "${TIMEOUT##* }")":CHK \
    lbl:1 "$SV" "$CS" lbl:4 "$PT" "$SN" "$TO"
  )
  [ "" = "$*" ] && exit # Cancel or ESC
  SV=$1 CS=$2 PT=$3 SN=$4 TO=$5
  while [ TRUE = $SN ]; do
    subnets="$(get_subnets)"
    c=$(yad_gtk2 --form --separator= \
      --field="$i18n_Subnet" "$SUBNETSALT" \
      --text="$(printf "$i18n_Enter_subnet" $subnets)\n" \
    )
    if [ -n "$c" ]; then
      ! [[ $c =~ [0-9]{1,3}"."[0-9]{1,3}"."[0-9]{1,3} ]] && continue
      SUBNETSALT="$c"
    fi
    break
  done
  if [ TRUE = $TO ]; then
    c=$(yad_gtk2 --form --separator= \
      --field="$i18n_New_port_scan_timeout":NUM ${TIMEOUT##* } \
    )
    [ "$c" = 0 -o -z "$c" ] && c=5
    TIMEOUT="-t $c"
  fi
  printf '%s\n' "PT=$PT" "SV=$SV" "CS=$CS" "SN=$SN" "TO=$TO" \
    "SUBNETSALT=\"$SUBNETSALT\"" "TIMEOUT=\"$TIMEOUT\"" \
    > "$TMPD"/options &&
    # map yad's TRUE/FALSE to options file's on/off
    sed -e 's/TRUE/on/' -e 's/FALSE/off/' "$TMPD"/options > "$APPDIR"/options
}

show_progress() # $1-parent-pid $2-intro ; In/Out ; Signal USR1 {{{1
# In: $TMPD/progress-pipe $PROGRESS_GIF $YAD_GEOMETRY_POPUP
# Out: $TMPD/progress.pids, $TMPD/progress.xid, Signal USR1
# Display progress in a scrolling text area. Append lines asynchronouly with write_progress().
# Trap USR1 to be paged for handling the Cancel button.
#
# Instead of scrolling text, a single progress line can be implemented by replacing "--text-info --tail --listen"
# with "--progress" and echoing "#$line" instead of "$line" (possibly with padding); see step's git commit b88ab82.
{
	local ppid=$1 title="$2" notice="$3" geometry bg_color gif_path p="$PROGRESS_GIF"
	geometry=${p%%:*}; p=${p#*:}
	bg_color=${p%%:*}; p=${p#*:}
	gif_path=${p%%:*}; p=${p#*:}
	: > "$TMPD"/progress.pids
	: > "$TMPD"/progress.xid

	# set a uniform white background for the whole Gtk dialog to make the gif background blend in
	echo > "$TMPD"/progress-gtkrc '
style "uniform-background"
{
bg[NORMAL]        = "#ffffff"
}
widget_class "<GtkWindow>" style "uniform-background"
widget_class "<GtkWindow>.*.<GtkSourceView>" style "uniform-background"
'

	# scan_subnets traps USR1 and will be paged when Cancel is pressed
	# yad --image displays a static image; gifview -a animates the image.
	yad_gtk2 < "$TMPD"/progress-pipe \
		$YAD_GEOMETRY_POPUP \
		--title="$i18n_ProgressTitle" \
		--on-top --no-escape \
		--text-info --tail --listen \
		--button=gtk-cancel:"sh -c \"kill -USR1 $$\"" \
		--image="${gif_path%gif}1.gif" \
		--print-xid="$TMPD"/progress.xid \
		--gtkrc="$TMPD"/progress-gtkrc \
		&
	echo $! >> "$TMPD"/progress.pids

	# yad's window won't be viewable until yad reads something from the pipe
	write_progress "$intro"
	# give yad some time to show its window and write its xid file
	sleep 0.5

	# animation
	local xid
	read xid < "$TMPD"/progress.xid
	if [ "$xid" ]; then
		gifview -a --new-window $xid ${geometry:+-g $geometry} ${bg_color:+--bg $bg_color} "$gif_path" &
		echo $! >> "$TMPD"/progress.pids
	fi
}

write_progress() # [--highlight] $1-line ; Reserved: file descriptor 5 {{{1
# Write line to the progress dialog window.
# It's OK to call this even when the progress dialog isn't running.
{
	[ -e "$TMPD"/progress-pipe ] || return 0
	local IFS highlight s="$1"
	[ --highlight = "$1" ] && highlight=1 s=$2 && shift
	# keep file descriptor open so the pipe won't close and the reader won't exit
	exec 5> "$TMPD"/progress-pipe

	# Highlight regular lines
	if [ $DEBUG -le 2 ]; then
		if ! [ "$highlight" ] || ! [ "$s" ]; then
			echo "$s" >&5
		else
			IFS=$'\n'
			set -- ${s^^}
			s=$1
			set -- ${@/#/$'\u2502' }
			set -- ${@/%/ $'\u2502'}
			unset IFS
			printf %s\\n $'\u250c\u2500'${s//?/$'\u2500'}$'\u2500\u2510' "$@" $'\u2514\u2500'${s//?/$'\u2500'}$'\u2500\u2518' >&5
		fi
		return
	fi

	# Debug is never highlighted
	local c0="`caller`" c1="`caller 1`" c2 c3
	[ "$c1" ] && c2="`caller 2`"
	[ "$c2" ] && c3="`caller 3`"
	printf '%s [%s%s%s%s]\n' "$s" "$c0" "${c1:+ « $c1}" "${c2:+ « $c2}" "${c3:+ « $c3}" >&5
}

open_progress_dialog() # $1-title ; Out {{{1
# Out: $TMPD/progress-pipe
{
  mkfifo "$TMPD"/progress-pipe || return 1
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE" $PROGRESS_BAR_WIDTH_PC:1::::100
  show_progress $$ "$1" "$2"
}

close_progress_dialog() # {{{1
# It's OK to call this even when the progress dialog isn't running.
{
  [ $DEBUG -gt 0 ] && ! [ "$TRAPPED" ] && return
  local p
  if [ -s "$TMPD"/progress.pids ]; then
    while read p; do
      kill $p 2>/dev/null; wait $p 2>/dev/null
    done < "$TMPD"/progress.pids
    rm -f "$TMPD"/progress.pids
  fi
  exec 5>&-
  rm -f "$TMPD"/progress-pipe
}

open_preview_dialog() # $1-message [$2@-yad-options] {{{1
# Live preview the list of scanned shares.
{
  local message=$1; shift
  coproc DLG_PREVIEW {
    dialog_VARS "$@" --no-buttons --text="<b><big>$message</big></b>" >/dev/null
  }
  sleep 0.5 # keep for callers of yad_lib_set_YAD_GEOMETRY
}

close_preview_dialog() # {{{1
{
  [ -z "$DLG_PREVIEW_PID" ] && return
  # stash YAD_GEOMETRY{,_POPUP} for other dialogs
  yad_lib_set_YAD_GEOMETRY '' "$YAD_TITLE"
  eval "exec ${DLG_PREVIEW[1]}>&-"
  pkill -P $DLG_PREVIEW_PID yad_gtk2
  kill $DLG_PREVIEW_PID; wait $DLG_PREVIEW_PID 2>/dev/null
  true
}

discovery_stats() # [--tsv] [$1-button] ; In {{{1
# In: "$TMPD"/{servers,server-ips,nshares,shares-<ip>}
{
  local opt_tsv
  [ "$1" = --tsv ] && opt_tsv=$'\t' && shift
  local -a IP; readarray -t IP < "$TMPD"/server-ips
  local button=$1 nserver=${#IP[@]} opt_list paths_shares_by_ip
  printf -v paths_shares_by_ip " $TMPD/shares-%s" "${IP[@]}"
  paste -s -d '|' $paths_shares_by_ip > "$TMPD"/shares-by-ip
  paste -d "${opt_tsv:-$'\n'}" "$TMPD"/{servers,server-ips,nshares,shares-by-ip} > "$TMPD"/stats
  [ "$opt_tsv" ] && return
  yad_gtk2 ${YAD_GEOMETRY_POPUP:- --center} --image=gtk-info --text="<b>$i18n_Discovery_Stats</b>\r$i18n_Discovery_note\r" \
    --text-align=center --button="${button:-gtk-ok}" \
    --column="$i18n_Server" --column="$i18n_IP" --column="$i18n_SharesNum" \
    --column=tt:HD --tooltip-column=-4 --list --no-selection \
    < "$TMPD"/stats || true
}
export -f discovery_stats
export i18n_Discovery_Stats i18n_Discovery_note i18n_Server i18n_IP i18n_SharesNum

get_share_roxapp_config() # In/Out {{{1
# Read the configuration of an existing Share RoxApp
# In: $MNTDIR, $SHAREAPPNAME
# Out: $SACFG[] dictionary
{
  local k v conf="$MNTDIR/$SHAREAPPNAME"/.config
  unset SACFG; declare -gA SACFG
  if [ -r "$conf" ]; then
    while read line; do
      if [[ $line =~ ^([[:alnum:]_]+)=(.*) ]]; then
        k=${BASH_REMATCH[1]} v=${BASH_REMATCH[2]}
        [[ $v == \'*\' || $v == \"*\" ]] && SACFG[$k]=${v:1:-1} || SACFG[$k]=$v
      fi
    done < "$conf"
  fi
}

set_MNTDIR__SHAREAPPNAME() # [[$1-server-NAME] $2-SHARE-name] ; In/Out #{{{1
# This function defines the location and format of Share RoxApp's folders.
# It's called at various stages before/after discovery and after user's edits.
# In: $APPDIR
# Out: $MNTDIR $SHAREAPPNAME
{
  local server="$1" share="$2"
  MNTDIR=${APPDIR%/*}
  ! [ "$server$share" ] && return # called at start of Main before discovery

  # sanity
  SHAREAPPNAME=${share//[\!@#$%^&*()\']/}     # chop troublesome characters
  SHAREAPPNAME=${SHAREAPPNAME//[[:space:]]/_} # squeeze white space

  # $server == "" after user's edits
  SHAREAPPNAME="${server:+$server--}$SHAREAPPNAME"
}

# Main {{{1

set_MNTDIR__SHAREAPPNAME

# APPDIR start-rox {{{2
[[ $PARAMS == *start-rox* ]] && ( sleep 1; defaultrox "$MNTDIR" ) &

# APPDIR options {{{2
[ -e "$APPDIR"/options ] && . "$APPDIR"/options
if [[ $PARAMS == *options* ]]; then options_dialog; exit $?; fi
[ -z "$TIMEOUT" ] && TIMEOUT="-t 5"
[ "$PT" = on ] && PORT=445 || PORT=139
[ "$CS" = on ] && IOCHARSET=",iocharset=utf8" || IOCHARSET=""
[ "$SN" = on ] && SUBNETS="$SUBNETSALT" || SUBNETS="$(get_subnets)"
! [ "$SUBNETS" ] && [ $DEBUG = 0 ] && exit_no_subnets

#{{{2}}}
mkdir -p "$TMPD"/tmp-mount

# Scan subnets for servers {{{2
intro=( $SUBNETS )
intro=$(ngettext "$i18n_Scanning_Network" "$i18n_Scanning_Networks" ${#intro[*]})
is_firewalled && notice="$i18n_p2_Firewalled" || notice=
open_preview_dialog "$intro" 2>/dev/null
open_progress_dialog "$intro" 2>/dev/null
bar_text="${SUBNETS// /$'\n'}"
write_progress "$bar_text"
write_progress --highlight "$notice"
scan_subnets "$SUBNETS" > "$TMPD"/server-ips # traps USR1; outputs also to progress dialog
trap 'close_progress_dialog' USR1            # must trap while progress dialog is open

# Scan servers for shares {{{2
# "$TMPD"/server₋ips lists servers in $IP order.
if [ -s "$TMPD"/server-ips ]; then
  SEQ=0
  close_preview_dialog
  open_preview_dialog "$i18n_Gathering_Authentications" --no-escape
  write_progress "$i18n_Gathering_Authentications"
  while read IP; do
    # Note that Samba on linux with user level security rejects Username="guest" and causes problems with smbclient-3.6.12

    # Quick NETBIOS name lookup -- LAN only
    NAME=$(get_server_name $IP); [ -z "$NAME" ] && NAME=$IP # when UDP 137 is firewalled
    write_progress "$IP ${NAME%$IP}"
    # get_share_names will look up the NETBIOS NAME by another method

    echo "${NAME#$IP}" >> "$TMPD"/servers  # "$TMPD"/servers lists servers by $NAME, "" if NAME unknown
    : > "$TMPD"/shares-$IP # for discovery_stats
    # Test for open access server (anonymous login)
    $SMBCLIENT -g -N -L $IP > "$TMPD"/error 2>&1 || true
    [ $DEBUG -gt 1 ] && write_progress "$(uniq "$TMPD"/error)"
    if grep -qm1 'failed\|NT_STATUS_ACCESS_DENIED' "$TMPD"/error; then
      # Access not open so request credentials.
      Username=guest Password=
      ret=4; while [ $ret = 4 ]; do # while show/hide passwords
        prompt_to_set_Username_Password_for NAME "<span color='red'>$(uniq "$TMPD"/error)</span>\n\n" && ret=$? || ret=$?
      done
      case $ret in
        1|2 ) echo 0 >> "$TMPD"/nshares; continue ;; #to the next server
        3 ) break ;; # Leave clicked
      esac
    fi

    get_share_names > "$TMPD"/shares # "$TMPD"/shares lists verbatim non-admin $SHAREs of server $NAME at address $IP
    echo $NSHARES >> "$TMPD"/nshares # "$TMPD"/nshares counts server's $SHAREs
    cp "$TMPD"/shares{,-$IP} # for discovery_stats
    # Replace the server name if a better name was discovered
    [ -n "$NAME2" -a "$NAME2" != "$NAME" ] && NAME=$NAME2 && sed -i -e "\$i$NAME" -e '$d' "$TMPD"/servers

    ! [ -s  "$TMPD"/shares ] && continue  # no visible shares discovered for this server

    # SEQ is a sequence index
    # AKEY is the key (index in this case) for bash arrays
    # NAME is the server name
    # SHARE is the verbatim share name for mount.cifs
    # SHAREAPPNAME is the SHARE name formatted as a Share RoxApp's valid folder name

    while read SHARE; do
      # Test SHARE for valid mount, and aggregate from VARS + print the AKEYth row to the main window DLG_PREVIEW.

      AKEY=$((++SEQ))
      set_MNTDIR__SHAREAPPNAME "$NAME" "$SHARE"
      [ $DEBUG -gt 1 ] && write_progress "========="
      write_progress "$SHAREAPPNAME"
      unset invalid

      # To minimize repeated password requests we use the following strategy:
      # CURRENT Username and Password are loop variables that hold the most recently-used credentials (TRY).
      # The idea is to try mounting with the current credentials because they are likely to be valid from
      # the second to the last shares of a multi-share server, such as a home NAS.
      # However, if a Share RoxApp for the current SHARE is available, we use the credentials stored in
      # the RoxApp folder instead of the CURRENT credentials. SACFG[] holds such credentials.
      # If neither CURRENT nor SACFG credentials are set, we start with Username 'guest' hoping that
      # it will suffice.  We also start with 'guest' when we know that it must suffice. This is the case
      # when "$NAME $IP" is listed in $TMPD/open-share-access (see get_share_names).

      [ $DEBUG -gt 1 ] && write_progress "CURRENT Username($Username) Password($Password)"
      get_share_roxapp_config # if existing in $MNTDIR/$SHAREAPPNAME

      ServerName[$AKEY]=$NAME ServerIP[$AKEY]=$IP FolderName[$AKEY]=$SHAREAPPNAME Error[$AKEY]=0

      # If the server grants open share access then validate and proceed to the next share
      if [ -s "$TMPD"/open-share-access ] && grep -q "^$IP:" "$TMPD"/open-share-access; then
        usave="$Username" psave="$Password"
        Username=guest Password=
        [ $DEBUG -gt 1 ] && write_progress "OPEN UserName[$AKEY]=($Username) UserPassword[$AKEY]=($Password)"
        if try_mount --leave-unmounted; then
          UserName[$AKEY]=$Username UserPasswork[$AKEY]=$Password Attach[$AKEY]=TRUE
          Username="$usave" Password="$psave"
          [ $DEBUG -gt 1 ] && write_progress "APPROVE:$LINENO UserName[$AKEY]=(${UserName[$AKEY]}) UserPassword[$AKEY]=(${UserPassword[$AKEY]})"
          continue
        fi
        Username="$usave" Password="$psave"
      fi

      # Try to reuse the Password from an existing configuration
      if [ "${SACFG[Password]}" ]; then
        [ "${SACFG[ClearText]}" ] &&
          Password=${SACFG[Password]} ||
          Password="$(echo "${SACFG[Password]}" | base64 -d)"
      fi
      # Try to reuse the Username from an existing configuration...
      if [ "${SACFG[Username]}" ]; then
        Username=${SACFG[Username]}
        [ $DEBUG -gt 1 ] && write_progress "LOAD Username($Username) Password($Password)"
      fi
      # ...if not available then use the CURRENT Username otherwise use "guest"
      [ "$Username" = "" ] && Username="guest"

      [ $DEBUG -gt 1 ] && write_progress "ELECT Username($Username) Password($Password)"

      while : "SHARE isn't mounted"; do
        # $Username and $Password are In & Out parameters for try_mount
        UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
        [ $DEBUG -gt 1 ] && write_progress "TRY UserName[$AKEY]=(${UserName[$AKEY]}) UserPassword[$AKEY]=(${UserPassword[$AKEY]})"
        try_mount --leave-unmounted && Error[$AKEY]=$? || Error[$AKEY]=$?
        if [ 0 = ${Error[$AKEY]} ]; then # we have valid credentials
          [ $DEBUG -gt 1 ] && write_progress "SUCCESS UserName[$AKEY]=(${Username}) UserPassword[$AKEY]=(${Password})"
          UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
          Attach[$AKEY]=TRUE  # don't change "TRUE"
          [ $DEBUG -gt 1 ] && write_progress "APPROVE:$LINENO UserName[$AKEY]=(${UserName[$AKEY]}) UserPassword[$AKEY]=(${UserPassword[$AKEY]})"
          break 1 #to the next share for the current server
        else
          [ $DEBUG -gt 1 ] && write_progress "FAIL(${Error[$AKEY]}) UserName[$AKEY]=(${Username}) UserPassword[$AKEY]=(${Password})"
          Attach[$AKEY]=FALSE # don't change "FALSE"
          ret=4; while [ $ret = 4 ]; do # while show/hide passwords
            close_progress_dialog
            prompt_to_set_Username_Password_for SHARE '' "$invalid" && ret=$? || ret=$?
            invalid="<span color=\"red\">$i18n_invalid_credentials</span>"
          done
          case $ret in
            0) if ! [ "$Password" ]; then
                # guest access or ask me later for my password
                Error[$AKEY]=0 Attach[$AKEY]=TRUE
                UserName[$AKEY]="$Username" UserPassword[$AKEY]="$Password"
                [ $DEBUG -gt 1 ] && write_progress "APPROVE:$LINENO UserName[$AKEY]=(${UserName[$AKEY]}) UserPassword[$AKEY]=(${UserPassword[$AKEY]})"
                break 1
              fi ;;
            1) break 1 ;; #to the next share for the current server
            2) break 2 ;; #to the next server
            3) break 3 ;; #to the final edit/save/load dialog
          esac
        fi
      done #trying to mount the current SHARE    break 1:
      # Append input form data to the preview dialog, which could be
      # closed without negatively impacting this loop.
      [ "$DLG_PREVIEW_PID" ] && printf_VARS $AKEY "\n" >&${DLG_PREVIEW[1]}
    done < "$TMPD"/shares     # break 2:
  done < "$TMPD"/server-ips   # break 3:
  close_preview_dialog
fi
close_progress_dialog

# Edit/Save/Load "mount data" records. {{{2
# This block can be commented out and still RoxApps will be created.
table_VARS $'\n' > "$TMPD"/table-vars
while true || [ -s "$TMPD"/table-vars ]; do
  unset update show_final_dialog
  dialog_VARS < "$TMPD"/table-vars && ret=$? || ret=$?
  case $ret in # 2x(Save) 4x(Load) x: 0(success) 1(cancel) 3(error)
    0) : OK;   show_final_dialog=1; break ;;
   2?) : Save; update=1 ;;
    3) : Back; update=1 ;;
   43) : Load error ;;
   4?) : Load ok/cancel; update=1 ;;
    *) : Cancel/unexpected; exit $ret ;;
  esac
  [ $update ] && tr_SEPARATOR_to_x $'\n' "$TMPD"/vars "$TMPD"/table-vars
done

# Create Share RoxApps {{{2
# . The created AppRun and AppInfo.xml import some variables/functions from this script.
# . The goal is for a Share RoxApp to be self-contained and movable to another computer where this script isn't installed.
# . The shebang must be /bin/sh -- to signify POSIX compatibility.
# . Gettexted messages are resolved when this script runs. Therefore they are bound to the current language.

# . The loop reads the records that dialog_VARS wrote to "$TMPD"/vars, rather than
#   reading $VARS directly, because the user can change yad rows via "right-click edit".
# . The loop abandons creating the current RoxApp if an error occurs; see the ERR trap.

unset $VARS # clear arrays
#DEBUG while IFS="$SEPARATOR" read -r k $VARS; do printf "%d" $k; printf_VARS 0; done >&2 < ""$TMPD"/vars"; exit

declare -i count_started=0 count_created=0
: > "$TMPD"/error
if [ -s "$TMPD"/vars ]; then
  # The trap accumulates names of Share RoxApps that could not be created due to errors in the while loop.
  trap $'echo "${SHAREAPPDIR##*/} \u25ac\u25b6 $NetworkResource" >> "$TMPD"/error; continue' ERR
  while IFS="$SEPARATOR" read -r k $VARS; do
    ! [ TRUE = $Attach ] && continue # SHARE could not be mounted/is not checked
    : $((++count_started))
    # ----- Mount variables for scripts/part2 -----{{{
    SHARE="${NetworkResource##*/}" # strip off server name (or server IP address)
    set_MNTDIR__SHAREAPPNAME "" "$FolderName" # sanity check after user's edits
    NAME="$ServerName"   # normally a hostname; an IP if smblookup didn't return a name
    Username="$UserName"
    Password="$UserPassword"
    ClearText=''
    IP="$ServerIP"
    # scripts/part2 should never add mount options of its own. Mount options
    # can be added in $DEFAULT_MOUNT_OPTIONS, and the user can edit yad list
    # cells for specific shares.
    MountOptions=$MountOptions # guest includes ip=<IP addr>
    # ---------------------------------------------}}}

    SHAREAPPDIR="$MNTDIR/$SHAREAPPNAME"
    mkdir -p "$SHAREAPPDIR/$HIDE_MOUNTNAME$MOUNTNAME"
    add_resources "$SHAREAPPDIR"
    #-------------------------------------------------------
    exec 3>&1 1>"$SHAREAPPDIR"/AppRun # save+redirect stdout
    #-------------------------------------------------------{{{
    # A Share RoxApp is coded for a POSIX shell and embeds functions and variables from this script.
    # POSIXLY_CORRECT constrains bash.
    echo "#!/bin/sh
POSIXLY_CORRECT=on

FALLBACK_VERSION='$VERSION'
DEPEND='$DEPEND'
"
    # Embed functions.
    awk '/FALLBACK-BEGIN/{f=1;next}/FALLBACK-END/{f=0;next}f' "$APPDIR"/scripts/shared.sh
    # Load functions.
    echo "
# Override functions to provide script upgrades.
. '$APPDIR/scripts/shared.sh' || true
"
    cat "$APPDIR"/scripts/part1
    if [ "$Username" = guest -o -z "$Password" ]; then
      passwd=$Password
    else
      passwd=$(printf %s "$Password"|base64)
    fi
    if [ "$SN" = on ]; then
      salt=$SUBNETSALT
    else
      unset salt
    fi
    echo "
. ./.config
MOUNTNAME='$HIDE_MOUNTNAME$MOUNTNAME'
ICON_MOUNTED='$HIDE_RES$RES/$ICON_MOUNTED'
ICON_UNMOUNTED='$HIDE_RES$RES/$ICON_UNMOUNTED'
ICON_SHOW='$HIDE_RES$RES/$ICON_SHOW'
ICON_HIDE='$HIDE_RES$RES/$ICON_HIDE'
"
    # Embed translations.
    T="$APPDIR"/scripts/i18n_table.sh $BASH --posix -c '. "$T" && i18n_table && set' | grep '^i18n_\(p2\|export\)_'
    echo
    cat $APPDIR/scripts/part2
    #-------------------------------------------------------}}}
    exec 1>"$SHAREAPPDIR"/.config
    #-------------------------------------------------------{{{
    echo "
#begin share
Server='$NAME'
Share='$SHARE'
MountOptions='$MountOptions'
Username='$Username'
Password='$passwd'
# Set \$ClearText not empty if \$Password is clear text
ClearText='$ClearText'
#end share

#begin scan
# Uncomment next line to force an IP address instead of doing an IP lookup on the server name
# See also 'ip=', if any, in MountOptions above.
#IP='$IP'
# Define SUBNETSALT x.y.z to scan an address range -- (skips server name to IP lookup)
PORT='$PORT'
SUBNETSALT='$salt'
TIMEOUT='$TIMEOUT'
#end scan"
    #-------------------------------------------------------}}}
    exec 1>&3 # restore stdout
    #-------------------------------------------------------
    printf -v x "$i18n_UMOUNT" "${SHAREAPPDIR#$MNTDIR/}" &&
    sed -e "s|@SUMMARY@|$i18n_SUMMARY|" -e "s|@UMOUNT@|$x|" -e "s|@EDITCONF@|$i18n_EDITCONF|" \
      $APPDIR/scripts/AppInfo.xml.tpl > "$SHAREAPPDIR"/AppInfo.xml &&
    chmod uga+x "$SHAREAPPDIR"/AppRun
    echo 'exec "$(dirname "$0")"/AppRun' > "$SHAREAPPDIR/$i18n_mount_it"
    chmod +x "$SHAREAPPDIR/$i18n_mount_it"
    echo 'exec "$(dirname "$0")"/AppRun unmount' > "$SHAREAPPDIR/$i18n_unmount_it"
    chmod +x "$SHAREAPPDIR/$i18n_unmount_it"
    : $((++count_created))
  done < "$TMPD"/vars
fi
trap - ERR

# Show final feedback {{{2
is_firewalled && firewall_info="\r<b>$i18n_p2_Firewalled:</b>\r$i18n_p2_port_139\r$i18n_p2_port_445" || unset firewall_info
trap handle_fatal ERR
if ! [ -s "$TMPD"/server-ips ]; then
  yad_gtk2 ${YAD_GEOMETRY_POPUP:- --center} --image=gtk-info --text="$i18n_found_no_servers$firewall_info" --button=gtk-quit || true
elif [ 0 = $(awk '{n+=$0}END{print 0+n}' "$TMPD"/nshares) ]; then
  yad_gtk2 --center --image=gtk-info --text="$i18n_found_no_shares$firewall_info" \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button=gtk-quit || true
elif [ $count_started -gt 0 -o "$show_final_dialog" ]; then
  # Display (warning) dialog with save results button.
  keepfile="$HOME/${MOUNTNAME#${HIDE_MOUNTNAME}}-$(date +%F-%H-%M).tsv"
  save_results() {
    tr_SEPARATOR_to_x $'\t' "$TMPD"/vars "$keepfile" &&
    if [ -s "$TMPD"/error ]; then cp "$TMPD"/error "${keepfile%.tsv}"-errors.txt; fi &&
    discovery_stats --tsv &&
    cp "$TMPD"/stats "${keepfile%.tsv}"-stats.tsv &&
    kill -USR1 $YAD_PID ||
    yad_gtk2 --center --text="$(printf "$i18n_File_save_error" $?)" --button=gtk-ok || true
  }
  export -f save_results tr_SEPARATOR_to_x
  export SEPARATOR i18n_File_save_error keepfile

  printf -v ask_save_file "$i18n_final_ask_save_file" "$keepfile"
  printf -v share_icon_created "$(ngettext "$i18n_share_icon_created" "$i18n_share_icons_created" $count_created)" $count_created
  [ $count_created -gt 0 ] && usage=" $i18n_final_usage" || unset usage
  count_incomplete=$((count_started - count_created))
  printf -v warning "$(ngettext "$i18n_share_icon_incomplete" "$i18n_share_icons_incomplete" $count_incomplete)" $count_incomplete
  if [ $count_incomplete = 0 ]; then
    button=gtk-ok warning=
  else
    button=gtk-dialog-warning warning="\r\r<span color='red'>$warning</span>\r\r$(cat "$TMPD"/error)"
  fi
  yad_gtk2 --center \
    --image="$button" \
    --text="$share_icon_created$usage$warning\r\r$ask_save_file\r" \
    --button="gtk-save:bash -c save_results" \
    --button="$i18n_Stats!gtk-info!$i18n_Stats_ttip:bash -c discovery_stats" \
    --button=gtk-quit || true
fi

