#!/bin/ash
# vim: foldmarker=[[[,]]]:

# =============================================================================
# Findnrun - a progressive finder
  Version=2.4.1
# authors: Copyright (C)2015-2017 step; (C)2015 SFR, L18L
# license: GNU GPL applies
# depends: gtk-dialog 0.8.3+, gawk, ash (or bash), mdview (NLS help only)
# source: https://github.com/step-/find-n-run
# forum: http://www.murga-linux.com/puppy/viewtopic.php?t=102811
# =============================================================================

# Localization settings. [[[1
export TEXTDOMAIN=findnrun
export OUTPUT_CHARSET=UTF-8
>/dev/null gettext 'INSTRUCTIONS FOR TRANSLATORS
1 Download the latest commented translation template from:
  https://raw.githubusercontent.com/step-/find-n-run/master/usr/share/doc/nls/findnrun/findnrun.pot
2 Follow the translation tutorial at:
  https://github.com/step-/find-n-run/blob/master/usr/share/doc/findnrun/TRANSLATING.md'

# Initialize variables that can be changed. [[[1
# i18n Main window title
APP_NAME=$(gettext "Findnrun")
APP_TITLE="${APP_NAME}"

XDG_DATA_DIRS=${XDG_DATA_DIRS:-/usr/share:/usr/local/share}
XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local}

{ read DESKTOP_FILE_DIRS; read ICON_DIRS; } << EOF
$(
  IFS=:
  set -- ${XDG_DATA_HOME} ${XDG_DATA_DIRS}
  for i; do echo -n ":$i/applications"; done; echo
  for i; do echo -n ":$i/icons"; done; echo
)
EOF
DESKTOP_FILE_DIRS=${DESKTOP_FILE_DIRS#:} ICON_DIRS=${ICON_DIRS#:}
# Ref. http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout
# Note: Search by icon *theme* isn't implemented.
# Additional Fatdog64-specific locations start at .../midi-icons.
ICON_DIRS="$HOME/.icons:${ICON_DIRS}:/usr/share/pixmaps:/usr/share/midi-icons:/usr/share/mini-icons"

[ -f "${CONFIG}" ] || CONFIG="${HOME}/.findnrunrc"
FNRHELPINDEX=${FNRHELPINDEX:-/usr/share/doc/findnrun/index.md:/usr/share/doc/findnrun/help[en].tar.gz:/usr/share/doc/findnrun/index.html}

# Initialize variables that should not be changed. [[[1
REMARK= # leave unset
FNRDEBUG=${FNRDEBUG:-} # unset to disable debug trace to stderr; set verbosity (integer) 1-10
if [ -n "${FNRDEBUG}" ]; then # set DEBUG1, DEBUG2, ... [[[
  i= c=
  while true; do
    i=$(($i +1))
    [ $i -le 10 -a $i -le ${FNRDEBUG} ] && c=" $c DEBUG$i=1" || break
  done
  eval "$c"
  unset c i
fi
#]]]
trap_handler() # [[[
{
  trap - HUP INT QUIT TERM ABRT 0
  local tmpd ids id
  tmpd=$(readlink -f "${TMPD}")
  [ -d "${tmpd}" ] || exit
  if cd "${tmpd}"; then
    # kill process / process group pids; guard system processes such as init (1).
    for id in $(grep --include .pidof_\* -hr .); do
      case ${id} in 0|1) ;; *) ids="${ids} ${id}" ;; esac
    done
    [ -n "${ids}" ] && /bin/kill -TERM -- ${ids} 2>/dev/null # not the shell built-in!
    cd - >/dev/null
  fi
  rm -rf "${tmpd}"* ${DEBUG1:+/tmp/varSEARCH}
  exit
}
#]]]
trap trap_handler INT QUIT TERM HUP ABRT 0
TMPD=`mktemp -d -p "${TMPDIR:-/tmp}" ${0##*/}_XXXXXX` && chmod 700 "${TMPD}"
TMP0="${TMPD}/.FNRstart"; mkdir "${TMP0}" # built-in source resources
DATF="${TMP0}/.dat" # database of .desktop file entries
AWKB="${TMP0}/.build.awk" # builds (and queries) database
AWKQ="${TMP0}/.query.awk" # queries database (faster)
AWK4="${TMPD}/.saveflt.awk" # filters saved items
HSTF="${TMPD}/.hist-FNR.sh"; >"${HSTF}" # global command line history
FCSF="${TMPD}/.fcs"; >"${FCSF}" # triggers varSEARCH focus grabber
F2SF="${TMPD}/.f2s"; >"${F2SF}" # varCMD/varSEARCH focus cycler
F3SF="${TMPD}/.f3s"; >"${F3SF}" # built-in/plugin source list view cycler
F4SF="${TMPD}/search-output" # saved items
RWMF="${TMPD}/.msg_restart" # RESTART_WINDOW message
ETAP="${TMPD}/.eta"; >"${ETAP}" # tap invocation event
FNRRPC="${TMPD}/.rpc"; >"${FNRRPC}" # remote procedure call mailbox
SEP=`echo -e "\b"` # field separator of packed values
XDGHOMEICONS="${HOME}/.icons" # freedesktop.org standard user's icon location

# -----------------------------------------------------------------------------
#LANG=de_DE.UTF-8 # just for demo
LR=${LANG%.*} #ex:pt_BR
LL=${LANG%_*} #ex:pt

# -----------------------------------------------------------------------------
# Unexport these variables before running the selected tap-command.
UNEXPORT="unset GUI_ABOUT varSEARCH varFOCUSGRABBER varCMD varCOMMENT varOPEN varICONS varFOCUSSEARCH varF2 TEXTDOMAIN OUTPUT_CHARSET"
# unset varLIST and varF3 separately

# Prepare the configuration file. [[[1
# Defaults - match same variable/value pairs in the next block. [[[2
# Leave alone the next three configuration values for historical reasons.
defOPEN=false
export varICONS=false
export varFOCUSSEARCH=false

# Read / supplement $CONFIG. [[[2
[ -e "$CONFIG" ] || > "$CONFIG"; . "$CONFIG"
# Add missing/hidden defaults.
# Define and initialize ALL configuration values here.
gawk '
/^ICONCACHE=/{f1=1}
/^SEARCHCOMMENTS=/{f2=1}
/^SEARCHFROMLEFT=/{f3=1}
/^SEARCHREGEX=/{f4=1}
/^CASEDEPENDENT=/{f5=1}
# GEOMETRY=WxH+X+Y has no default, environment is overridden
# DESKTOP_FILE_DIRS initialized from system values, environment overrides
# ICON_DIRS initialized from system values, environment overrides
/^SEARCHCATEGORIES=/{f6=1}
/^SEARCHCOMPLETE=/{f7=1}
/^IBOL=/{f8=1}
/^HOTKEY_F2=/{f9=1}
/^HOTKEY_F3=/{f10=1}
/^HOTKEY_F12=/{f11=1}
/^SOURCES=/{f12=1}
/^SHOWNODISPLAY=/{f13=1}
/^SEARCHFILENAMES=/{f14=1}
/^HOTKEY_F4=/{f15=1}
END{ # Follow desired configuration file order
  # Hotkey format: accel-mods":"key-sym":"accel-key
  # cf. https://github.com/01micko/gtkdialog/blob/wiki/menuitem.md
  if(!f9) print "HOTKEY_F2=\"0:F2:0xffbf\"">>ARGV[1]
  if(!f10) print "HOTKEY_F3=\"0:F3:0xffc0\"">>ARGV[1]
  if(!f15) print "HOTKEY_F4=\"0:F4:0xffc1\"">>ARGV[1]
  if(!f11) print "HOTKEY_F12=\"0:F12:0xffc9\"">>ARGV[1]
  if(!f1) print "ICONCACHE=\"'"${XDGHOMEICONS}"'\"">>ARGV[1]
  if(!f14) print "SEARCHFILENAMES=false">>ARGV[1]
  if(!f2) print "SEARCHCOMMENTS=false">>ARGV[1]
  if(!f6) print "SEARCHCATEGORIES=false">>ARGV[1]
  if(!f7) print "SEARCHCOMPLETE=true">>ARGV[1]
  if(!f3) print "SEARCHFROMLEFT=false">>ARGV[1]
  if(!f4) print "SEARCHREGEX=false">>ARGV[1]
  if(!f5) print "CASEDEPENDENT=false">>ARGV[1]
  if(!f13) print "SHOWNODISPLAY=false">>ARGV[1]
  # IBOL+IBOL in search field ignores all characters to beginning of line.
  # 0xAD, dec(173), soft-hyphen, gtk-dialog invisible
  if(!f8) printf "IBOL=\"%c\" # reserved\n", 0xAD >>ARGV[1] # requires gawk
  # Built-in sources - space separated list
  if(!f12) print "SOURCES=\"FNRstart FNRsc\"">>ARGV[1]
}
' "${CONFIG}" && . "$CONFIG"

# Values that depend on defaults.
# Icons [[[2
# Note [ANCHOR_ICON_PATH]:
# For XDG/GTK to be able to find icons in a non-standard icon path,
# the path must end in '/icons' and XDG_DATA_DIRS must include the
# parent folder path. Gtkdialog window icons (attribute icon-name)
# must be copied to '/icons/hicolor/32x32/apps/'
# Findnrun copies a plugin $ICON in gawk function try_store_source().
XDG_DATA_DIRS="${XDG_DATA_DIRS}:${TMPD}" # for plugin ICONS
if [ -z "${ICONCACHE}" ]; then
  ICONCACHE="${XDGHOMEICONS}"
else
  XDG_DATA_DIRS="${XDG_DATA_DIRS}:${ICONCACHE}"
  ICONCACHE="${ICONCACHE}/icons"
fi
# Prefix to worked-around icons.
ICONSTEM="${ICONCACHE}/findnrun-" # NOT a dirpath
ICONSTEM2="${TMPD}/icons/" # slash-terminated, for plugin ICONS
ICONSTEM2A="${ICONSTEM2}/hicolor/scalable/apps/" # ditto
mkdir -p "${ICONCACHE}" "${ICONSTEM2}" "${ICONSTEM2A}"

# Parse command line --options. [[[1
while ! [ "${1#--}" = "$1" ]; do
  case "$1" in
    --geometry=*) # Override GEOMETRY set in $CONFIG, if any.
      o=${1#--geometry=}; GEOMETRY=${o%--geometry}
      ;;
    --perm=*|--perm) # Change tempdir permissions.
      o=${1#--perm=}; o=${o%--perm}; chmod "${o:-700}" "${TMPD}"
      ;;
    --) shift; CMDLINEOPTS="$@"; break ;; # pass CMDLINEOPTS to gtkdialog
    --stdout) ENABLESTDOUT=1 ;; # Don't redirect gtkdialog's stdout to null
    --*) echo "${0##*/}: invalid option $1" >&2; exit 1
    ;;
  esac
  shift
done

# Initialize more variables [[[1
# Some environment variables may affect operations:
# BROWSER, GEOMETRY, LANG, XCLIP, and more.

which gtkdialog4 >/dev/null 2>&1 && GTKDIALOG=gtkdialog4 || GTKDIALOG=gtkdialog

# Declare built-in sources. [[[1
# $BUILTIN_SOURCES get special treatment
# pipe-separated list
BUILTIN_SOURCES="FNRstart|FNRsc"

# Built-in save-filter-command. [[[2
FNRSAVEFLT="gawk -f \"$AWK4\""
# usage:
#   SAVEFLT_<source-id>="CUT\"=i,j,k,...\" RDR="redirection" \$FNRSAVEFLT \"${file}\""
# CUT and RDR explained at script AWK4 - see examples for FNRstart and Filmstrip source.

# save-filter-command setup for built-in sources. [[[2
# If xclip command exists then copy stdout to clipboard else to awk's stderr.
# User preference XCLIP can override/disable clipboard copying.
if [ -z "$XCLIP" ]; then
  XCLIP=$(which xclip 2>/dev/null)
  [ -e "$XCLIP" ] && XCLIP="|$XCLIP"
fi
if [ -z "$XCLIP" -o none = "$XCLIP" ]; then
  XCLIP=">/dev/stderr"
fi

# Default built-in source "Desktop Apps" [[[2
SOURCE_FNRstart="FNRstart::FNRstart:FNRstart:FNRstart:::FNRstart"
TAP_FNRstart="gawk -f \"${AWKQ}\" -v GREP=\"\${term}\" \"${DATF}\"${REDIRECT2}"
TITLE_FNRstart=$(gettext "application finder")
ICON_FNRstart='/usr/share/doc/findnrun/findnrun.svg' # Filmstrip plugin expects.
# save item label, .desktop file fullpath, exec line, comments, categories
SAVEFLT_FNRstart="CUT=\"-4,1,3,4,5\" RDR=\"\$FNRXCLIP\" \$FNRSAVEFLT \"\${file}\""

# Built-in source "Shell Completion" [[[2
SOURCE_FNRsc="FNRsc::FNRsc:FNRsc:FNRsc:::FNRsc"
[ true = "${CASEDEPENDENT}" ] && unset i || i=i
x="${TMPD}/.FNRsc"; mkdir "$x" &&
# Embedded newline characters not allowed.
TAP_FNRsc="t='$x/.dat';"' if ! [ -s "$t" ]; then set +f; IFS=:; for i in $PATH; do { cd "$i" && ls -1 *"${term}"* ;} 2>/dev/null; done | findnrun-formatter -- -O su | tee "$t"; else grep -F'$i' "${term}" "$t"; fi;'
ICON_FNRsc='/usr/share/doc/findnrun/FNRsc.svg'
TITLE_FNRsc=$(gettext "shell completion")
# save item label == exec line
SAVEFLT_FNRsc="CUT=\"-4\" RDR=\"\$FNRXCLIP\" \$FNRSAVEFLT \"\${file}\""

# Prepare sources. [[[1
# Don't change next two formats; functions generate_invoke*()
# assume them, and so may also findnrun-formatter in the future.
SRCSTEM="${TMPD}/.source-" # prefix to source filenames
SRCFMT='%d-%s.sh' # source filename format, i.e., printf "%s${SRCFMT}" "${SRCSTEM}" 0 FNRstart
store_valid_sources() # [[[
{
# In: $SOURCES
# Out: list of valid sources that got stored for gtkdialog sh's use.
# Return value:   <number of valid sources> <list of valid sources>
#   The list of valid sources is a subset of $SOURCES.
# Return code: [[[
# 1-99: fatal; 101-199: recoverable; 201-299: warning.
# If the offending subject is detected it is printed to stderr.
# Fatal errors => gawk exit(code).
# Recoverable errors => print code, disable source and continue.
# Warning => print code and continue.
# 101 source-id isn't a valid shell variable name (SOURCES=)
# 102 null tap-command
# 103 invalid tap-command sh syntax
# 104 invalid drain-command sh syntax
# 111 broken tap-id (leads nowhere)    TODO 111 and up
# 112 broken drain-id
# 113 broken title-id
# 114 broken icon-id
# 201 unreferenced TAP_ (linked by no one)
# 202 unreferenced DRAIN_
# 203 unreferenced TITLE_
# 204 unreferenced ICON_
# 205 unreferenced SOURCE_
# 206 unreferenced INITSEARCH_
# 207 unreferenced MODE_
# 208 unreferenced PLGDIR_
# 208 unreferenced SAVEFLT_
#]]]
# i18n Source plugin validation.
  set | gawk -v SOURCES="${SOURCES}" -v BUILTIN="${BUILTIN_SOURCES}" \
    -v MSG1="$(gettext "fatal source error: %d%s\n")" \
    -v MSG2="$(gettext "recoverable source error: %d%s\n")" \
    -v MSG3="$(gettext "source warning: %d%s\n")" \
'#!/usr/bin/gawk -f
BEGIN {
  TEXTDOMAIN="'"${TEXTDOMAIN}"'"
  if('${DEBUG1:-0}') print "\n=== PROCESS SOURCE DECLARATIONS (set FNRDEBUG= 3-5 for more)" > "/dev/stderr"
  # Invalid SOURCES syntax? 101 [[[
  nE = split(SOURCES, E)
  for(j=1; j <= nE; j++) {
    s = E[j] # source-id
    if(match(s, /[^[:alnum:]_ ]|^[[:digit:]]/))
      recoverable(101, s) # exclude invalid source-id
    else
      # include syntactically valid source-id
      valid_sources = valid_sources" "s
  }
  valid_sources = substr(valid_sources, 2)
  # Fall back to BUILTIN source.
  if("" == valid_sources) valid_sources = "FNRstart"
  #]]]
  # id types
  ordered_G = "SOURCE|TAP|DRAIN|ICON|TITLE|INITSEARCH|MODE|PLGDIR|SAVEFLT"
  # G[] array of types
  nG = split(ordered_G, G, /\|/)
  pattern = "^("ordered_G")_([^=]+)=(.*)"
}
{
  if(match($0, pattern, a)) {
    # Parse declarations [[[
    # I[] array of any-type ids a[2]
    # S[] array of source-ids a[1] contained in I[]
    # VGI[] values a[3] by (group a[1], id a[2])
    I[a[2]] = a[2]
    if("SOURCE" == a[1]) S[a[2]] = a[2]
    # Assert: Input stream ("set |") single quoted all values.
    # Note the difference between ash and bash: bash "env" single quotes
    # just the values that include spaces, while ash "env" single
    # quote all values regardless. So we strip exterior single quotes
    # conditionally to keep compatibility with both shells.
    VGI[a[1], a[2]] = unquote(a[3])
    if('${DEBUG5:-0}') print a[1],"**",a[2],"**",a[3] > "/dev/stderr"
    #]]]
  }
}
END {
    if('${DEBUG4:-0}') {
      printf "# I:" >"/dev/stderr"; for(i in I) printf(" %s", i) >"/dev/stderr"
      print "" >"/dev/stderr"
      printf "# S:" >"/dev/stderr"; for(s in S) printf(" %s", s) >"/dev/stderr"
      print "" >"/dev/stderr"
      for(i in I) {
        for(j=1; j<=nG; j++) {
          g = G[j]
          if((g, i) in VGI)
            printf("# <%s, %s> = %s\n", g, i, VGI[g, i]) >"/dev/stderr"
        }
      }
    }
  # TS[] array of tap-ids by source-id
  # DS[] array of drain-ids by source-id
  for(s in S) {
    split(VGI["SOURCE",s], a, /:/)
    if(a[1]) TS[s] = a[1] # TODO should use symbols, not number
    if(a[2]) DS[s] = a[2] # ditto
    # Localize title; ditto
    if(a[4]) VGI["TITLE", a[4]] = dcgettext(VGI["TITLE", a[4]], "findnrun-plugin-"s)
    if('${DEBUG5:-0}')
      printf "source %s: TAP %s DRAIN %s\n", s, \
        (s in TS) ?TS[s] :"NULL", (s in DS) ?DS[s] :"NULL" >"/dev/stderr"
  }
  # Null tap-command? 102 [[[
  for(s in S) if(! s in TS || ! (("TAP", TS[s]) in VGI) || ! VGI["TAP", TS[s]]) {
    recoverable(102, s)
    delete S[s]
  }
  #]]]
  # Invalid tap-command sh syntax? 103 [[[
  syntax_check("TAP", TS, S, 103) # deletes elements of S
  #]]]
  # Invalid drain-command sh syntax? 104 [[[
  syntax_check("DRAIN", DS, S, 104) # deletes elements of S
  #]]]
  # Store remaining valid sources for gtkdialog shell. [[[
  # Treat sources as an ordered list.
  nE = split(valid_sources, E)
  SH_FOR_ICONS="sh" # try_store_source() will pipe into it
  for(j=1; j <= nE; j++) {
    if(! (E[j] in S)) continue
    s = E[j] # source-id
    ret = try_store_source(s, S, Store)
    # Cumulate binned sources: enabled, disabled, hidden.
    bin[ret] = bin[ret]" "s
  }
  close(SH_FOR_ICONS) # actually executes piped commands.
  #]]]
  # Output binned sources. [[[
  for(x = 0; x <= 2; x++) {
    bin[x] = substr(bin[x], 2)
    nbin[x] = split(bin[x], source)
    # Output enabled (visible/hidden) sources only.
    if(x != 0x1) {
      for(j = 1; j <= nbin[x]; j++) {
        buf = Store[source[j]]
        p = index(buf, "\n")
        saveas = substr(buf, 1, p-1)
        # While saving source to disk add NSOURCES= number of visible sources
        printf("%s\nNSOURCES='"'%d'"'\n", substr(buf, p+1), nbin[0]) > saveas
        close(saveas)
      }
    }
  }
  #]]]
  # Return binned sources and their counts.
  for(j = 0; j <= 2; j++) printf("%d:%s:", nbin[j], bin[j])
  printf "\n"
}
function syntax_check(group, A, I, code,   i, checker, status) { # [[[
  # Checks all items of A. Deletes items of I.

  # The best syntax checker that we can get, but still quite limited.
  checker = "sh -n"

  if('${DEBUG4:-0}') { #[[[
    print "\nsyntax_check",group,":" >"/dev/stderr"
    for(i in A) print "source",i, \
      (i in I) ?"to check" :"is already invalid", \
      "(declares", group, A[i]")" >"/dev/stderr"
  } #]]]
  # For each declared source-id as i that is associated with a non-null group-id...
  #   If source-id i is still in the set of valid ids (I)
  #   && its group-id A[i] has an associated value in VGI...
  for(i in A) if(i in I && ((group, A[i]) in VGI)) {
    if(match(BUILTIN, i)) continue # optimization: skip built-in sources
    if('${DEBUG4:-0}') #[[[
      print "now checking",i"'"'"'s", group, A[i],"=>", \
        VGI[group, A[i]] >"/dev/stderr" #]]]
    print VGI[group, A[i]] | checker
    if(close(checker)) { # non-zero exit code on invalid syntax
      recoverable(code, group": "VGI[group, A[i]], i)
      delete I[i]
    }
  }
}
#]]]
function try_store_source(s, S, Store, buf, A,   a, g, i, j, I, iconpath, cachedpath) { #[[[
  # try_store_source stores BY REFERENCE in map Store the current
  # source s unless the source "disabled" bit is on.
  # Upon storing s, try_store_source increments global static variable _global_try_store_source.
  # Call try_store_source for the ORDERED list of valid sources.
  # SH_FOR_ICONS is pre-defined as "sh"
  # In => Out: (all output values are wrapped in SINGLE quotes)
  #   s      source-id
  #   S      global array of valid source ids
  #   Store  return array of composed sources
  #   buf==""      => compose from globals G and VGI for s; OR
  #   buf=="array" => compose from A["SOURCE"]=source-id, A["TAP"]=tap-command, ...
  #   A    see buf="array"
  # Return value: 0:enabled-in-Store 1:disabled 2:hidden-in-Store

  # Assert: The calling outer loop has stripped all exterior single quotes.
  if("" == buf) {
    buf = sprintf("ID='"'%s'"'", s)
    split(s":"VGI["SOURCE",s], I, /:/)
    for(j = 1; j <= nG; j++) {
      g = G[j]; i = (j in I) ?I[j] :"N\x08NULL" # \x08 => never "(g,i) in VGI"
      if('${DEBUG3:-0}') printf ("<%s, %s> ", g, i) > "/dev/stderr"
      if((g, i) in VGI) {
          buf = buf sprintf("\n%s='"'%s'"'", g, VGI[g, i])
      } else { # the plugin does not declare a value for group id g
        if("TITLE" == g) {
          # on no title-id output source-id as title value
          buf = buf sprintf("\n%s='"'%s'"'", g, s)
        } else if("MODE" == g) {
          # on no mode-id output MODE="0" for bit mask
          buf = buf sprintf("\n%s='"'%d'"'", g, 0)
        } else {
          # on no "other" id output null value, i.e., VAR="" << IMPORTANT
          buf = buf sprintf("\n%s='"''"'", g)
        }
      }
    }
  } else if("array" == buf) { # NOT USED
    buf = sprintf("ID='"'%s'"'", s)
    for(a in A)
      buf = buf sprintf("\n%s='"'%s'"'", a, A[a])
  }
  # Get MODE: [1] bit mask
  match(buf, /MODE=\x27([^\x27]+)\x27/, a); mode = a[1]
  # Do not store the current source if MODE "disabled" bit (0x1) is set
  if(and(mode, 0x1)) {
    if('${DEBUG3:-0}') print "\n!!! Plugin mode DISABLED [[\n"buf"\n]]" > "/dev/stderr"
    return 1 # DISABLED source
  }
  # Else continue to store the current source
  Store[s] = sprintf("%s\n%s", # filepath[.hide] + contents
    format_saveas(_global_try_store_source++, s, mode), buf)
  # Copy plugin icon, ref. note [ANCHOR_ICON_PATH] [[[
  # Get ICON: [1] fullpath, [2] dirname, [3] basename.
  match(buf, /ICON=\x27(([^\x27]*\/)([^\x27]+))\x27/, iconpath)
  if("" != iconpath[3]) {
    # Cache the icon. Read why at [ANCHOR_ICON_PATH].
    cachedpath = "'"${ICONSTEM2}"'" iconpath[3]
    # For gtkdialog tree widget column icon.
    printf("ln -sf \x27%s\x27 \x27%s\x27\n", \
      iconpath[1], cachedpath) | SH_FOR_ICONS
    # For gtkdialog window widget (attribute icon-name).
    cachedpath = "'"${ICONSTEM2A}"'" iconpath[3]
    printf("ln -sf \x27%s\x27 \x27%s\x27\n", \
      iconpath[1], cachedpath) | SH_FOR_ICONS
  }
  #]]]

  if(and(mode, 0x2)) {
    if('${DEBUG3:-0}') print "\n!!! Plugin mode HIDDEN [[\n"buf"\n]]" > "/dev/stderr"
    return 2 # HIDDEN source
  } else {
    if('${DEBUG3:-0}') print "\nPlugin is enabled [[\n"buf"\n]]" > "/dev/stderr"
    return 0
  }
}
#]]]
function format_saveas(num, name, mode,   saveas) { #[[[
  # Return source fullpath according to source number, name and mode bit mask
  saveas = sprintf("%s'"${SRCFMT}"'%s", "'"${SRCSTEM}"'", 0+num, name,
    and(mode, 0x2) ? ".hide" : "")
  return saveas
}#]]]
function fatal(code, subject, source) { #[[[
  printf(MSG1, code, \
    (source ?" "source":" :"") (subject ?" "subject :"")) > "/dev/stderr"
  exit(code)
}#]]]
function recoverable(code, subject, source) { #[[[
  printf(MSG2, code, \
    (source ?" "source":" :"") (subject ?" "subject :"")) > "/dev/stderr"
}#]]]
function warning(code, subject, source) { #[[[
  printf(MSG3, code, \
    (source ?" "source":" :"") (subject ?" "subject :"")) > "/dev/stderr"
}#]]]
function unquote(s,   p,t,l) { # [[[
  if((p = index("\x27", t = substr(s,1,1))) && p < 2) {
    if(t == substr(s, l = length(s)))
      return(substr(s, 2, l-2))
  }
  return(s)
}#]]]
  '
}
#]]]
list_diff() # $1-list1 $2-list2 [[[
# In: space-separated lists of words. Out: $1 - $2
{
  local e1 l1 l2 res
  l1=$1; l2=" $2 "
  for e1 in $l1; do : $e1; [ "${l2##* $e1 }" = "$l2" ] && res="$res $e1"; done
  printf %s "${res#?}"
}
#]]]

store_valid_sources > "${TMPD}/.$$"
x=$?; [ 0 -lt $? -a $? -lt 100 ] && exit $x # fatal errors
# Get lists of valid sources.
ifs=$IFS; IFS=:
read NSOURCES VISIBLE_SOURCES x DISABLED_SOURCES x HIDDEN_SOURCES x < "${TMPD}/.$$"
IFS=$ifs
# Initialize gtkdialog source defaults from the first source.
. "${SRCSTEM}0-"*.sh

# Display warning dialog if some source declarations were found invalid. [[[
invalid=$(list_diff "$SOURCES" "$VISIBLE_SOURCES $DISABLED_SOURCES $HIDDEN_SOURCES")
if [ -n "$invalid" ]; then
  # TODO display main window then Xdialog window over.
  Xdialog --title "${APP_NAME}" --msgbox \
    "$(printf \
      "$(gettext 'Invalid source plugins found and disabled:\n%s')" \
      "$invalid")" 0x0 \
      || ntf -a  "$(gettext 'Invalid source plugins found and disabled:\n%s')" "$invalid "
fi
#]]]
# Prepare the database builder script. [[[1
# Usage: gawk -f "${AWKB}" [-v GREP="string"] [-v ALL_ICONS=true] [-v SHOWNODISPLAY=true|false] [-v LIST_FILE="list_file" | files ]
# Specify input file names either as lines of LIST_FILE or as command arguments
# The former style allows for handling read file errors gracefully.
[ -x /bin/dash ] && SH=/bin/dash || SH=/bin/ash
> "${AWKB}" echo '#!/usr/bin/gawk -f
BEGIN { # [[[2
  if('${DEBUG1:-0}') print "\n=== DB BUILDER (set FNRDEBUG= 2 for more)\n[[ ALL_ICONS="ALL_ICONS >"/dev/stderr"
  RS="^~cannot~match~me~" # enable slurp read mode.
  # Choose a shell for ongoing command execution - see icon_workaround().
  sh = "'${SH}'" # not used as a coprocess
  ICONSTEM = "'"${ICONSTEM}"'" # prefix to worked-around icons.
  if('${DEBUG2:-0}') print "ICONSTEM="ICONSTEM >"/dev/stderr"
  if(""==SHOWNODISPLAY) SHOWNODISPLAY = "false"
  MSG1="'"$(gettext "SHOWNODISPLAY false excludes file '%s'")"'"
  MSG2="'"$(gettext "filename '%s': Icon '%s' not found.")"'"
  # Read files specified by list of file names.
  if(LIST_FILE) {
    NFILES = read_list_file(LIST_FILE, file)
    exit # go to END - skip main loop
  }
}
# main loop # [[[2
# Read files specified by command line arguments.
# Use when you are certain that all files are readable.
{
  # Slurp NFILES .desktop files.
  file[++NFILES]=";"FILENAME"\n"$0
}
END { # [[[2
  # Is the icon work-around enabled and up-to-date?
  if(ICONUPDATED = is_icon_workaround_uptodate()) {
    # Speed up icon_workaround() by reading the icon index file.
    read_icon_index() # creates ICONINDEX map
  }
  # Decode .desktop files.
  if('${DEBUG1:-0}') print "awk decoding",NFILES,"files..." > "/dev/stderr"
  for(i=1; i<=NFILES; i++) {
    fil=file[i]
    name=exec=icnpath=icnname=icnext=comment=category=""
    match(fil, /^;([^\n]+)/, m); filename=m[1]
    match(fil, /\nName=([^\n]+)/, m); name=m[1]
    if(match(fil, /\nName\[('"${LR:-@}|${LL:-@}"')\]=([^\n]+)/, m)) name=m[2]
    if(!name) continue # trap bogus .desktop files
    if(SHOWNODISPLAY != "true" && index(fil, "NoDisplay=true")) {
      printf(MSG1"\n", filename) > "/dev/stderr"
      continue
    }
    match(fil, /\nExec=([^\n]+)/, m); exec=m[1]
    if(!exec) continue # trap bogus .desktop files
    # Delete freedesktop.org %F parameter since Exec= value is going to sh.
    sub(/[ \t]*%[a-zA-Z][ \t]*$/, "", exec)
    match(fil, /\nIcon=([^\n]*\/)?([^\n.]+)([.][^\n]*)?/, m)
    icnpath=m[1]; icnname=m[2]; icnext=substr(m[3],2)
    if(index(icnext, ".")) {
      # case "filename.any[.any ...].EXT"
      nic = split(icnext, ic, /\./)
      icnname = icnname "." substr(icnext, 1, length(icnext)-length(ic[nic])-1)
      icnext = ic[nic]
    }
    if(icnext && ! match(icnext, /png|svg|xpm/)) {
      # case "file.name" implicit icon EXT
      icnname = icnname "." icnext
      icnext = ""
    }
    match(fil, /\nComment=([^\n]+)/, m); comment=m[1]
    if(match(fil, /\nComment\[('"${LR:-@}|${LL:-@}"')\]=([^\n]+)/, m)) comment=m[2]
    match(fil, /\nCategories=([^\n]+)/, m); category=";"m[1]
    # narrow matches by GREP pattern and store for sorting step
    if(GREP && index(tolower(name), GREP) || !GREP) {
      if(key[k = tolower(name)]) { # case-independent sort; assert name != ""
        # Handle key clash by appending "%<" to the key N times on N-th clash.
        while(key[k = k"%<"]);
      }
      key[k] = k
      if(ALL_ICONS == "true") icon_workaround(sh)
      out[k] = format_item()
    }
  }
  # Sort by name and print.
  nkey = asort(key) # Note: asort is a GNU awk (gawk) extension.
  for(i=1; i<=nkey; i++) {
    print substr(out[key[i]], 1, 510)
    # 510 works around gtkdialog tree widget buffer overflow limit
  }
  # Remember if we performed a complete icon work-around.
  mark_icon_workaround_uptodate()
  # Close shell.
  print "exit" | sh
  close(sh)
  if('${DEBUG1:-0}') print "]] BUILDER" >"/dev/stderr"
}

function format_item(   ic,cols) { # [[[2
# Format tree widget item - columns: icon, name, all-packed-values.
# Note: assert data does not include characters "|" and $SEP.
  ic = format_icon_cell()
  # The second column, always empty, is reserved for future expansion.
  # The tree widget does display its value.
  cols = sprintf("%s||%s|%s", \
    ic, name, \
    sprintf("%s'${SEP}'%s'${SEP}'%s'${SEP}'%s'${SEP}'%s", \
    filename,name,exec,comment,category))
    # tree widget exports all packed values as a single column
  return(cols)
}

function format_icon_cell( ) { # [[[2
# Format tree row icon cell.
# Use within <input icon-column="0"> or <stock-column="0">
# Note: tree widget does not support icons with paths anyway.
  return(icnpath ? icnpath icnname "." icnext : icnname)
}

function icon_workaround(sh,    a,c,IFP,ifp,x,lnk,ext) { # [[[2
# Workaround for gtkdialog tree widget not displaying icons with path.
  # Supposedly, IFP is the icon full path (it is used as the icon index key).
  IFP = icnpath icnname (icnext ?".":"") icnext
  if("" == IFP) return
  # If the work-around is not up-to-date run sh/find/ln to create icon links.
  if(!ICONUPDATED) {
    #printf "L" > "/dev/stderr"
    ifp = IFP
    c = ""
    if(-1 == getline < ifp) { # file ifp does not exist
      # This could happen because .desktop file sets Icon=name-only (valid), but
      # we cannot trust gtkdialog to show icons by name without extension, so
      # get the full pathname; ref. http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup
      # next 3 lines: expand colon-separated list to shell words
      icon_dirs = "'"$ICON_DIRS"'"
      gsub(/:/, "\" \"", icon_dirs)
      icon_dirs = "\"" icon_dirs "\""
      c = "2>/dev/null find -L " icon_dirs \
        " -type f \\( -name \""icnname".png\" -o -name \""icnname".svg\" -o -name \""icnname".xpm\" \\) -print0 -quit"
        # ! -path "*/.*/*" excludes all matches within any hidden dir, neat!
        # -print0 ensures match string does not include \n character
        # -quit exits after first match, if any; GNU awk (gawk) extension.
      # Read found path into ifp, which stays == "" if nothing found.
      ifp=""; c | getline ifp; close(c)
      # Note that ifp is null-terminated. See \x00 below.
    } else { # file ifp exists
      close(ifp)
    }
    if("" != ifp) {
      sub(/\x00/,"",ifp)
    } else {
      printf(MSG2"\n", filename, icnpath icnname ("" != icnext ? "."icnext : "")) >"/dev/stderr"
      return
    }
    # try symlinking target ifp
    x = split(ifp, a, /\//)
    lnk = a[x] # link name lnk <- icon-name[.ext] (no path)
    ext = (x = split(lnk, a, /\./)) ?"."a[x] :"" # ".ext" if any
    if(ext) x-- # forget ext from split link name
    # implode link name while replacing all dots with underscores
    lnk = ""; for(c=1; c<=x; c++) lnk = lnk "_" a[c]; lnk = substr(lnk, 2)
    # Symlink the icon.
    print "2>/dev/null ln -s \""ifp"\" \"" ICONSTEM lnk ext "\"" | sh
    # Progressively build the icon index file.
    printf "%s\x00%s\x00%s\x00%s\n", IFP, ifp, lnk, ext >> (ICONSTEM".index")
  } else {
    #printf "I" > "/dev/stderr"
    # The work-around is up-to-date so use ICONINDEX map to speed things up
    split(ICONINDEX[IFP], a, "\x00")
    lnk = a[3]
  }

  icnpath = "" # this tells format_item() to use icnname only
  icnname = "findnrun-" lnk # no ext
  # gtkdialog is very finicky: icon name must not include dots nor extension
  # but the link name must include an extension!
}

function is_icon_workaround_uptodate(   uptodate,x) { # [[[2
# Is the icon work-around enabled and up-to-date? - icon_workaround() helper.
  if(uptodate = ("true" == ALL_ICONS)) {
    if(uptodate = (-1 != (getline < (x = ICONSTEM".uptodate"))))
      close(x)
  }
  if('${DEBUG2:-0}') print "is_icon_workaround_uptodate="uptodate > "/dev/stderr"
  return(uptodate)
}

function read_icon_index(   a,b,data,i,x) { # [[[2
# Read the index file - icon_workaround() helper
  if('${DEBUG2:-0}') print "in read_icon_index" >"/dev/stderr"
  if(1 == (getline data < (x = ICONSTEM".index"))) {
    close(x)
    na = split(data, a, /\n/) # we slurped the index file
    for(i=1; i<=na; i++) {
      split(a[i], b, "\x00") # index record fields: IFP ifp lnk ext
      ICONINDEX[b[1]] = a[i]
    }
  }
}

function mark_icon_workaround_uptodate( ) { # [[[2
# If the icon work-around is enabled, mark whether it is up-to-date.
  if("true" == ALL_ICONS) {
    print "" > (ICONSTEM".uptodate")
    if('${DEBUG3:-0}') print "\nmark_icon_workaround_uptodate() created", ICONSTEM".uptodate" > "/dev/stderr"
  }
}

function read_list_file(list_file, acontent,   name,nname,nfiles,f,i,s) { # [[[2
# File names from list_file; slurp file contents into acontent; return nfiles.
  if(0 < (getline s < list_file)) {
    close(list_file)
    if(nname = split(s, name, /\n/)) {
      # Slurp nfiles .desktop files.
      for(i=1; i<nname; i++) {
        f = name[i]
        if(0 < (getline s < f)) {
          close(f)
          file[++nfiles]=";"f"\n"s
        } else {
          printf(MSG2"\n", f, "") >"/dev/stderr"
        }
      }
    }
  }
  return(0+nfiles)
}

'

# Prepare the database query script. [[[1
# Usage: gawk -f "${AWKQ}" [-v GREP="string"] ${DATF}
unset case subject commleft left categ REDIRECT2 func statements
[ true = "${CASEDEPENDENT}" ] || case=tolower
if ! [ true = "${SEARCHCOMPLETE}" ]; then
  # Split out the last field of the record that $AWK's format_item() printed.
  statements="
  split(\$(NF), a, /${SEP}/) # a <- {fullpathname,name,exec,comment,category}"
  if [ true = "$SEARCHFILENAMES" ]; then
    statements="$statements
    gsub(/^.*[/]|[.][^.]+$/, \"\", a[1]) # basename(fullpathname, .ext)"
  fi
  subject="${case}(a[2])"
else
  # Keep basename(fullpathname, .ext) & sub-fields 'name'..'category'.
  statements="
  a=\$(NF); x=index(a,\"${SEP}\"); a1=substr(a,1,x-1)
  gsub(/^.*[/]|[.][^.]+$/, \"\", a1); a=a1 substr(a,x)"
  subject="${case}(a)" # ="${case}(basename(a[1], .ext)\";\"a[2]\";\"a[3]\";\"a[4]\";\"a[5])"
fi
if [ true = "${SEARCHREGEX}" ]; then
  REDIRECT2=" 2>/dev/null" # Quiet awk's "fatal: invalid regex" message
  func=match
  [ true = "${SEARCHFROMLEFT}" ] && left="&& RSTART==1"
else
  func=index
  [ true = "${SEARCHFROMLEFT}" ] && left="==1"
fi
subject="${func}(${subject},GREP)"
if ! [ true = "${SEARCHCOMPLETE}" ]; then
  [ true = "${SEARCHFILENAMES}" ] && fnameleft="|| ${func}(${case}(a[1]),GREP)${left}"
  [ true = "${SEARCHCOMMENTS}" ] && commleft="|| ${func}(${case}(a[4]),GREP)${left}"
  case $SEARCHCATEGORIES in true|hidden) categ="|| ${func}(${case}(a[5]),GREP)";; esac
fi
> "${AWKQ}" echo '#!/usr/bin/gawk -f
BEGIN {
  if('${DEBUG3:-0}') {
    print "\n=== DB QUERY [[ GREP=\""GREP"\"" >"/dev/stderr"
    print "expression=('"${subject}${left} ${fnameleft} ${commleft} ${categ}"')" >"/dev/stderr"
  }
  FS="|"
}
{'"${statements}"'
  # narrow matches by GREP pattern
  if(GREP && ('"${subject}${left} ${fnameleft} ${commleft} ${categ}"') || !GREP) {
    print substr($0, 1, 510)
    # 510 works around gtkdialog tree widget buffer overflow limit
  }
}
END {
  if('${DEBUG3:-0}') print "]] QUERY" >"/dev/stderr"
} '

# Build database of .desktop file entries. [[[1
LIST_FILE="${AWKB}.list-file"
ifs=$IFS; IFS=:; set -- $DESKTOP_FILE_DIRS
# Recursive search supports common wine games.
find -L "$@" -type f -name '*.desktop' > "$LIST_FILE" 2>/dev/null
IFS=$ifs
gawk -v ALL_ICONS=${varICONS} -v SHOWNODISPLAY=${SHOWNODISPLAY} -f "${AWKB}" -v LIST_FILE="$LIST_FILE" > "${DATF}"
[ "${DEBUG1}" ] && { >/tmp/varSEARCH; echo >&2 === READING SEARCH INPUT ALSO FROM /tmp/varSEARCH; }
NDATF=$(wc -l "${DATF}"); NDATF=${NDATF%% *}

# Prepare Help system and its GUI button (About). [[[1
# i18n About dialog.
find_help() #[[[
{
  # In:  ${FNRHELPINDEX} paths.
  # Out: ${helpviewer} command, ${helpbutton} gtk code.
  local IFS=:
  set ${FNRHELPINDEX}
  for i; do
    unset x p s helpviewer helpbutton; hf="$i"
    case "${hf}" in *\[*\]*) p="${hf%[*}["; s="]${hf##*]}" # search for translation
      [ -e "$p${LL}$s" ] && hf="$p${LL}$s"; [ -e "$p${LR}$s" ] && hf="$p${LR}$s"; [ -e "$p${LANG}$s" ] && hf="$p${LANG}$s" ;;
    esac
    if [ -e "${hf}" ]; then
      hd="${TMPD}/help" && mkdir -p "${hd}" &&
      case "${hf##*.}" in t[gx]z|[gx]z) tar -C "${hd}" -xaf "${hf}" && hf=$(set +f; echo "${hd}"/index.*) ;; esac &&
      case "${hf##*.}" in md) x=${FNRMDVIEW:-mdview} ;; htm*) x=defaultbrowser;; esac
      read -t 1 helpviewer << EOF
$(which $x www-browser x-www-browser defaulttexteditor geany leafpad 2>&-)
EOF
      case ${helpviewer} in
        '') helpviewer="${BROWSER:-xdg-open} '${hf}'";; # catchall
        *mdview) helpviewer='v() { for a; do [ -e "$a" ] && cd "${a%/*}" && exec "'"$helpviewer"'" "${a%/*}" "${a##*/}" "'"$APP_TITLE"'" & sleep 0.4 || sleep 1; done ;}; v "'"$hf"'"' ;;
        *browser) helpviewer="'${helpviewer}' 'file://${hf}'" ;;
        *) # dup help files to protect sources from text editors
          if ! [ "${hf%/*}" = "${hd}" ]; then cp -fr "${hf%/*}/"* "${hd}/"; fi &&
          helpviewer="cd '${hd}' && '${helpviewer}' 'no-help.md'" ;;#${hf##*.}'";;
      esac &&
      helpbutton="
<button use-underline=\"true\">
  <label>$(gettext "_Help")</label>
  <input file stock=\"gtk-help\"></input>
  <action>${helpviewer} &</action>
  <action>closewindow:GUI_ABOUT</action>
</button>"
    break # on first valid help file path $hf found
  fi
  done
}
#]]]
find_help

# i18n About dialog widgets: window text; number of apps text (singular/plural)
export GUI_ABOUT='
<window title="'"${APP_TITLE}"'" icon-name="edit-find" window-position="2">
  <vbox>
    <frame>
      <text justify="0" selectable="true" can-focus="false">
        <label>"'"$(printf "$(gettext "%s %s
authors: %s
Open source - GNU GPL license applies

%s
%s

configuration: %s
")" "${APP_NAME}" "${Version}" "step, SFR, L18L" \
"http://www.murga-linux.com/puppy/viewtopic.php?t=98330" \
"https://github.com/step-/find-n-run" "${CONFIG}")
$(n="${NDATF}"; printf "$(ngettext "%s application found" "%s applications found" "$n")" "$n")
$(n="${NSOURCES}"; printf "$(ngettext "%s source loaded" "%s sources loaded" "$n")" "$n"
)"'"</label>
      </text>
    </frame>
    <hbox homogeneous="true">
      <text space-fill="true" space-expand="true"><label>""</label></text>
      <button use-underline="true">
        <label>'"$(gettext "_OK")"'</label>
        <input file stock="gtk-ok"></input>
        <action>closewindow:GUI_ABOUT</action>
      </button>
      '"${helpbutton}"'
      <text space-fill="true" space-expand="true"><label>""</label></text>
    </hbox>
  </vbox>
  <variable>GUI_ABOUT</variable>
  <action signal="key-press-event" condition="command_is_true([ $KEY_SYM = Escape ] && echo true )">closewindow:GUI_ABOUT</action>
</window>'

# Prepare the save filter script. [[[1
# Usage: gawk -f "$AWK4" "\${file}" with $CUT and $RDR environment variables
> "${AWK4}" echo '#!/usr/bin/gawk -f
# "CUT=i,j,k,..." selects which fields to print:
# Positive indexes select source-defined fields.
# Negative indexes select tree widget item fields in reverse order.
# Thus -1 is the icon, and -4 is the item label.
# "\t" (tab) is the output field separator.
#
# Typical redirections:
# RDR=">/dev/stderr" or RDR=">/path/to/output/file"
# RDR="|xclip" or RDR="|/path/to/executable-file"

BEGIN {
  FS = "|"
  CUT=ENVIRON["CUT"]
  RDR=ENVIRON["RDR"]
  n = split(CUT, f, /,/)
  if(match(RDR, /^[ \t]*[|]/)) {
    redirect = "piper"
  } else if(match(RDR, /^[ \t]*[>]/)) {
    redirect = "printer"
  } else {
    printf "'"$(gettext "%s: invalid SAVEFLT redirection '%s'\n")"'", \
      ENVIRON["ID"], RDR > "/dev/stderr"
    exit(1)
  }
  RDR = substr(RDR, RSTART+RLENGTH)
}
{
  # positive indexes
  _ = split($(NF), a, /'"$SEP"'/)
  # negative indexes
  w = 0
  while(!(++w>=NF)) {
    a[-w] = $(w)
  }
  @redirect(f,a,n)
}
function piper(f,a,n,   i) { # [[[2
  while(!(++i>=n)) {
    printf "%s\t", a[f[i]] | RDR
  }
  if(n) {
    print a[f[n]] | RDR
  }
}
function printer(f,a,n,   i) { # [[[2
  while(!(++i>=n)) {
    printf "%s\t", a[f[i]] > RDR
  }
  if(n) {
    print a[f[n]] > RDR
  }
}
'

# Helpers [[[1

generate_get_drain_cmd() # [[[2
{
  # Echo sh code that extracts the drain command of the selected list item.
  # Command is returned in "$@" - Command defined per plugin-dev.md.

  # KLUDGE work around the peculiar Windows paths that winemenubuilder generates.
  echo -n 'case $varLIST in *wine\ *\\\\\\\\*)'
  echo -n ' varLIST=$(echo -n "$varLIST" | sed -e "s/\\\\\\\\/\\\\/g") ;;'
  echo -n 'esac'

  echo -n '; ifs="${IFS}"'
  echo -n '; IFS="'"${SEP}"'"; set -f; set -- $varLIST; set -- $3; set +f'
  echo -n '; IFS="${ifs}"'
}

generate_get_source() # [$1-event] [[[2
{
  # Echo sh code that loads the current source's field values.
  # Values are returned as shell variables - Values defined per plugin-dev.md.
  local eventfmt
  eventfmt="${1:+ on event:}$1${1:+ }"
  [ -n "${DEBUG2}" ] && echo -n 'eval ">&2 echo \"GET SOURCE \${varF3:-0}'"${eventfmt}"'\"";'
  [ -n "${DEBUG3}" ] && echo -n '>&2 echo "[["; set -x;'
  echo -n ". \"${SRCSTEM}\"\${varF3:-0}-*.sh"
  [ -n "${DEBUG3}" ] && echo -n '; set +x; echo >&2 "]]"'
}

generate_invoke_source_save_filter() # [[[2
{
  # Echo sh code that invokes source[varF3]'s save-filter-command.
  # Unexport all vars.
  echo -n "$UNEXPORT varLIST varF3 invokeTAP invokeDRAIN"
  echo -n "; if [ -n \"\$SAVEFLT\" ]; then"
  echo -n   " file=\"$F4SF\""
  echo -n   "; FNRDEBUG=$FNRDEBUG"
  echo -n   " FNRTMP=\"$TMPD\" FNRPID=$FNRPID FNRRPC=\"$FNRRPC\""
  echo -n   " FNRSAVEFLT=\"$FNRSAVEFLT\" FNRXCLIP=\"$XCLIP\"" # specifically for save-filter
  echo -n   " eval \$SAVEFLT"
  echo -n '; fi'
}

generate_invoke_source_tap() # [[[2
{
  # Echo sh code that invokes source[varF3]'s tap-command.
  # Extract search input value as $term with IBOL+IBOL correction. [[[
  echo -n 'ifs="${IFS}"; IFS="'"${IBOL}"'"; set -f; set -- ${varSEARCH}; for i; do term="$i"; done; set +f; set IFS="${ifs}";'
  #]]]
  # Get source[varF3]'s values. [[[
# Optimization: use cached <variable>TAP</variable>.
  generate_get_source invoke_source_tap
  # ]]]
  # Start tap-command. [[[
  echo -n ' && fnrevent=${invokeTAP:-Search} && fnrevent=${fnrevent%% *}' # trim timestamp
  if [ -n "${DEBUG1}" ]; then #[[[
    echo -n ' && >&2 printf %s "$(date +%H:%M:%S.%N) INVOKE TAP event=${fnrevent}"'
    [ -n "${DEBUG3}" ] && echo -n ' && eval ">&2 echo -n \" term(hex)=\$(echo -n \${term} |xxd -p)\""'
    echo -n ' && eval ">&2 echo \" term=\${term} \${TAP}\""'
  fi
  #]]]
  # Unexport all vars.
  # cf. UNEXPORT in generate_invoke_source_drain.
  echo -n " && ${UNEXPORT} varLIST varF3 invokeTAP invokeDRAIN"
  echo -n ' && FNREVENT="${fnrevent}" FNRDEBUG='"${FNRDEBUG}"
  echo -n ' FNRTMP="'"${TMPD}"'" FNRPID=${FNRPID} FNRRPC="'"${FNRRPC}"'"'
  echo -n ' eval ${TAP}'
  #]]]
}

generate_invoke_source_drain() # [[[2
{
  # Echo sh code that invokes source[varF3]'s drain-command.
  # Extract selected list item value as "$@".
  generate_get_drain_cmd
  # Unexport all vars but varF3 and varLIST.
  # cf. 'UNEXPORT' in generate_invoke_source_tap and 'unset' further down
  echo -n "; ${UNEXPORT}"
  # Load source[varF3]'s values into drain's invocation environment. [[[
  echo -n '; '; generate_get_source invoke_source_drain
  # ]]]
  # Save drain-command to history lists. [[[
  # To the source's own history list file and to the global history list file
  # $HSTF, which is the history widget input file (varCMD).
  echo -n "; echo \$DRAIN\ \"\$@\" >> \"$TMPD/.hist-\$ID.sh\""
  echo -n "; echo \$DRAIN \"\$@\" >> '$HSTF'"
  #]]]
  # Start drain-command. [[[
  echo -n '; set -- ${DRAIN} "$@"'
  # NOTE: v.2.0.0 main window's gtkdialog doesn't implement variable
  # invokeDRAIN. So, for the plugin interface, here it's sufficient to
  # pretend that invokeDRAIN is implemented, and handle it similarly to
  # what we do for invokeTAP, which instead is fully implemented.
  echo -n '; fnrevent=${invokeDRAIN:-Activate} && fnrevent=${fnrevent%% *}' # trim timestamp
  if [ -n "${DEBUG1}" ]; then #[[[
    echo -n ' && >&2 printf %s $(date +%H:%M:%S.%N)'
    echo -n ' && eval ">&2 echo \" INVOKE DRAIN event=${fnrevent} \${DRAIN} \$@ \""'
  fi
  #]]]
  echo -n '; unset varF3 varLIST invokeTAP invokeDRAIN TAP DRAIN ICON TITLE INITSEARCH MODE PLGDIR SAVEFLT'
  echo -n '; FNREVENT="${fnrevent}" FNRDEBUG='"${FNRDEBUG}"
  echo -n ' FNRTMP="'"${TMPD}"'" FNRPID=${FNRPID} FNRRPC="'"${FNRRPC}"'"'
  echo -n ' eval "$@" &'
  #]]]
}

generate_status_bar() # [[[2
{
  # Echo sh code that loads the current and next source's titles
  # and sets them as navigation hotkey labels.
  set -- ${VISIBLE_SOURCES:-FNRStart}
  [ $# -lt 2 ] && return # no status bar unless:
  # Multiple sources. [[[
  #i18n Status bar: Ctrl+0... and F3... hotkeys
  local ctrlnkey funckey keysymF3 tooltip
  keysymF3=${HOTKEY_F3#*:}; keysymF3=${keysymF3%:*}
  ctrlnkey=$(gettext "[ctrl+%d]") funckey=$(gettext "[%s]")
  tooltip=$(printf "$(gettext \
    "Click the status bar or press a hotkey to activate the next source.\n%s\n%s")" \
    "$(n="${NSOURCES}"; printf "$(ngettext "%s source loaded" "%s sources loaded" "$n")" "$n")" \
    "$(n="${NDATF}"; printf "$(ngettext "%s application found" "%s applications found" "$n")" "$n")" \
  )
  echo -n '
    <vbox>
      <eventbox>
        <statusbar has-resize-grip="false" sensitive="false" tooltip-text="'"$tooltip"'">
          <variable export="false">varSBAR</variable>
          <input>'
  [ -n "${DEBUG3}" ] && echo -n 'echo >&2 "=== GENERATE STATUS BAR [["; set -x;'
  echo -n 'title="${TITLE}"' # This is a gtkdialog sh variable and...
  # [[[ ... it gets its value from the <input> tag, which is activated only
  # after refresh:TITLE. Adding a <default> tag value for TITLE doesn't
  # initialize its **sh value**. It only initializes its **widget** value.
  # Again, here we deal with the gtkdialog sh value, so we need to be
  # prepared to the null value case, which manifests itself only once,
  # when gtkdialog starts. Here it goes.
  #]]]
  echo -n '; [ -z "${title}" ] && . "'"${SRCSTEM}"'"${varF3:-0}-*.sh && title="${TITLE}"'
  # Load next source's values
  echo -n '; . "'"${SRCSTEM}"'"$(((${varF3:-0} + 1) % ${NSOURCES:-'${NSOURCES}'}))-*.sh'
  echo -n '; next="${TITLE}"'
  # Send formatted data to SBAR.
  echo -n '; printf "'"%s ${ctrlnkey}  »  ${funckey} %s"'" "${title}" $((${varF3}+1)) "'"${keysymF3}"'" "${next}"'
  [ -n "${DEBUG3}" ] && echo -n 'set +x; echo >&2 "]]";'
  echo "</input>
        ${DEBUG2:+<action>echo>&2 ,,, varSBAR ,,,</action>}
        </statusbar>
        <action signal=\"button-press-event\">set -- $VISIBLE_SOURCES; echo -n \$(((\${varF3} + 1) % \${#}))>'$F3SF'</action>
        <action signal=\"button-press-event\">refresh:varF3</action>
      </eventbox>
    </vbox>" # Copy <action>s from <menuitem> HOTKEY_F3.
  #]]]
}

generate_source_hotkeys() # [[[2
{
  # Actions for keys Ctrl+1..Ctrl+9
  local h i
  h=0
  for i in 1 2 3 4 5 6 7 8 9; do
    echo '<menuitem accel-key="0x003'"$i"'" accel-mods="4">'
    printf "  <action>%s</action>\n" "echo -n $h >\"${F3SF}\"" "refresh:varF3"
    echo "</menuitem>"
    [ $i = $NSOURCES ] && break
    h=$i
  done
}

start_window() # Returns when gtkdialog terminates [[[2
{
  local dialog findnrun
  dialog="${TMPD}/.main.xml"
  findnrun="$TMPD/${0##*/}"
  ln -sf "$(command -v $GTKDIALOG)" "$findnrun"
  cat > "${dialog}" &&
  if [ ${FNRDEBUG:-0} -gt 9 ]; then
    cat
    return
  fi &&
  if [ "${ENABLESTDOUT}" ]; then
    "$findnrun" ${GEOMETRY:+--geometry=}${GEOMETRY} ${CMDLINEOPTS} -f "${dialog}" &
  else
    "$findnrun" ${GEOMETRY:+--geometry=}${GEOMETRY} ${CMDLINEOPTS} -f "${dialog}" >/dev/null &
  fi
  mv "$TMPD/.$$" "$TMPD/.$$-$!" # track PPID-PID
  wait # for traps to PID
}

generate_restart_window() # [[[2
{
  # Restart findnrun dialog with all command line arguments.
  printf %s \
    "for pid in $TMPD/.$$-*; do :; done" \
    '; pid=${pid##*-}' \
    "; cat /proc/$$/cmdline | xargs -0 env -- &" \
    ' kill $pid'
}

# Restart window dialog. [[[1
# i18n restart window dialog
export RESTART_WINDOW='
<window title="'"$APP_TITLE"'" icon-name="edit-find" resizable="false" window-position="2">
  <vbox>
    <text wrap="true" xalign="0">
      <variable export="false">RESTART_WINDOW_MESSAGE</variable>
      <input>2>/dev/null head -n 1 "'"$TMPD/.msg_restart"'"</input>
    </text>
    <text justify="0">
      <label>"     '"$(gettext "Restart Findnrun?")"'     "</label>
    </text>
    <text><label>""</label></text>
    <hbox>
      <text space-fill="true" space-expand="true"><label>""</label></text>
      <button yes>
        <action>'"$(generate_restart_window)"'</action>
      </button>
      <button no>
        <action>closewindow:RESTART_WINDOW</action>
      </button>
      <text space-fill="true" space-expand="true"><label>""</label></text>
    </hbox>
  </vbox>
  <variable>RESTART_WINDOW</variable>
  <action signal="key-press-event" condition="command_is_true([ $KEY_SYM = Escape ] && echo true )">closewindow:RESTART_WINDOW</action>
</window>'

# Prepare and show the main window. [[[1
# Tip: if your /bin/sh is bash you can use the following stanza in
# <action>s, <input>s, etc. to print the execution line on stderr:
#   <action>${DEBUG1:+echo >&2 \$BASH_EXECUTION_STRING;}commands...</action>
unset showcategories
[ true = "$SEARCHCATEGORIES" -o true = "$SEARCHCOMPLETE" ] && showcategories=true
[ hidden = "${SEARCHCATEGORIES}" ] && unset showcategories
# i18n Main window widgets
# i18n "0" (invisible, disregard).
gettext 0 >/dev/null # work around an xgettext's limitation
start_window << EOF
<window title="${APP_TITLE}" icon-name="edit-find" window-position="2">
  <vbox>
    ${REMARK# [[[. varSEARCH: progressive typing search input field.}
    <hbox spacing="0">
      <entry auto-refresh="${DEBUG1:+true}" tooltip-text="$(gettext "Press ENTER to select")">
        ${REMARK# [[[. Entering IBOL+IBOL makes the search input field}
        ${REMARK# ignore all characters to the left of IBOL+IBOL included.}
        ${REMARK# IBOL stands for Ignore To Beginning Of Line. Its default}
        ${REMARK# value is the soft-hyphen character, which is invisible in}
        ${REMARK# gtkdialog. Here we append IBOL+IBOL to the default search}
        ${REMARK# term so that the entire value --which is simply a help tip}
        ${REMARK# is ignored, and the search engine can perform a clean query.}
        <default>$(gettext "Type some letters to refine the list")${IBOL}${IBOL}</default>
        ${REMARK# ]]]}
        <variable>varSEARCH</variable>
        ${REMARK# The entry widget ignores initial input unless it is refreshed, see varSEARCH0.}
        <input>echo -n "\${INITSEARCH}"</input>
        ${DEBUG1:+<input file>/tmp/varSEARCH</input>}
        ${DEBUG2:+<action>echo>&2 ,,, varSEARCH ,,,</action>}
        <action>refresh:varLIST</action>
        <action signal="activate">grabfocus:varLIST</action>
        <action signal="activate">echo false>"${FCSF}"</action>
      </entry>
      <button tooltip-text="$(gettext "Clear entry")" stock-icon-size="1">
        <input file stock="gtk-clear"></input>
        <action>grabfocus:varSEARCH</action>
        <action>clear:varSEARCH</action>
      </button>
    </hbox>
    ${REMARK# ]]]}
    ${REMARK# [[[. varLIST: list tap-data records; invoke selected.}
    <tree enable-search="false" exported-column="2" column-visible="1|1|0" headers-visible="false" icon-column-name="gtk-apply" hscrollbar-policy="1" vscrollbar-policy="1" tooltip-text="$(gettext "Press ENTER or double-click to run the selected item")">
      ${REMARK# Column names below. Only Reserved and Label are visible.}
      ${REMARK# Consider also that there is an icon column, so a full input}
      ${REMARK# record is defined as Icon|Reserved|Label|PackedValues }
      <label>Reserved|Label|PackedValues</label>
      <variable>varLIST</variable>
      <output file>$F4SF</output>
      ${REMARK# [[[. Populate list view.}
      <input icon-column="0">$(generate_invoke_source_tap)</input>
      ${REMARK# ]]]}
      ${DEBUG2:+<action>echo>&2 ,,, varLIST ,,,</action>}
      ${REMARK# [[[. Invoke list view selected item.}
      <action signal="row-activated">$(generate_invoke_source_drain)</action>${REMARK# which saves item to history.}
      <action signal="row-activated">refresh:varCMD</action>
      ${REMARK# ]]]}
      <action condition="active_is_false(varOPEN)">exit:EXIT</action>
      <action signal="changed">refresh:varCMD</action>
      <action signal="changed">clear:varCOMMENT</action>
      <action signal="changed">refresh:varCOMMENT</action>
      ${DEBUG2:+<action>echo >&2 auto-refreshing varFOCUSGRABBER</action>}
      <action>( sleep 0.1 || sleep 1; echo "\${varFOCUSSEARCH}">"${FCSF}"; ) &</action>
    </tree>
    ${REMARK# ]]]}
    ${REMARK# [[[. varFOCUSGRABBER: handle varLIST on(EnterEnter|double-click).}
    ${REMARK# input file auto-refresh rate cannot be configured. http://code.google.com/p/gtkdialog/source/detail?r=453}
    ${REMARK# gtkdialog compiled w/o inotify refreshes about once a second. With inotify refreshing is instantaneous.}
    <checkbox auto-refresh="true" visible="false">
      <default>false</default>
      <variable>varFOCUSGRABBER</variable>
      <input file>${FCSF}</input>
      ${DEBUG2:+<action>echo>&2 ,,, varFOCUSGRABBER ,,,</action>}
      ${DEBUG2:+<action>if true echo >&2 'grabfocus:varSEARCH'</action>}
      <action>if true grabfocus:varSEARCH</action>
      <action>if true echo false>"${FCSF}"</action>
      <action>if true clear:varFOCUSGRABBER</action>
    </checkbox>
    ${REMARK# ]]]}
    ${REMARK# [[[. varCMD: history editing.}
    <hbox space-fill="false" space-expand="false">
      ${REMARK# This widget doubles as a varLIST item grabber and as a pull-down history list.}
      ${REMARK#  [[[. Pull-down list and input entry field combo.}
      <comboboxentry space-expand="true" space-fill="true" tooltip-text="$(gettext "Press the Down Arrow key to grab the selected item and move through the history list. You can edit the grabbed entry. History persists while the program is running. History is cleared on exit.")">
        <variable>varCMD</variable>
        <default>$(gettext "Press Down Arrow to grab selected item")</default>
        ${REMARK# Append a space to the grabbed varLIST item \$@ so we can tell it from history items.}
        ${REMARK# Note that the entry widget discards trailing spaces, so our space is a unique mark.}
        <input>$(generate_get_drain_cmd); echo "\$@ "; awk '{a[++i]=\$0}END{while(i>0)print a[i--]}#tac' "$HSTF"</input>
        <output file>$HSTF</output>
        ${DEBUG2:+<action>echo>&2 ,,, varCMD ,,,</action>}
        <action signal="activate" condition="command_is_true(echo \${varCMD:-true})">break:</action>
        <action signal="activate">set -- \$varCMD; echo "\$@" >> '$HSTF'; echo "\$@" >> "$TMPD/.hist-\$ID.sh"; $UNEXPORT varLIST varF3; eval "\$@" &</action>
        <action signal="activate" condition="active_is_false(varOPEN)">exit:EXIT</action>
        <action signal="activate" condition="command_is_true(echo \$varFOCUSSEARCH)">grabfocus:varSEARCH</action>
        <action signal="activate" condition="command_is_false(echo \$varFOCUSSEARCH)">grabfocus:varLIST</action>
        <action signal="activate">refresh:varCMD</action>
      </comboboxentry>
      ${REMARK#  ]]]}
      ${REMARK#  [[[. History item delete button.}
      <button tooltip-text="$(gettext "Remove entry from history")" stock-icon-size="1">
        <input file stock="gtk-remove"></input>
        <action>grabfocus:varCMD</action>
        ${REMARK# pull-down-list ::= varLIST-item + history-file-contents.}
        <action>removeselected:varCMD</action>${REMARK# pull-down-list -= user-selected-item.}
        <action>save:varCMD</action>${REMARK# Save pull-down-list (including space-terminated varLIST-item}
        ${REMARK# ^^^ Gtkdialog 0.8.4: save:varCMD does not append LF to the last file line.}
        ${REMARK# Now delete the space-terminated varLIST item from the saved history file.}
        ${REMARK# Thankfully awk is oblivious to the missing LF.}
        ${REMARK# history-file-contents -= space-terminated-items.}
        <action>awk '/[^ ]\$/{a[++i]=\$0}END{printf "">FILENAME;while(i>0)print a[i--]>FILENAME}#tac' '$HSTF'</action>
        <action>refresh:varCMD</action>${REMARK# widget <- varLIST-item + history-file-contents.}
      </button>
      ${REMARK#  ]]]}
    </hbox>
    ${REMARK# ]]]}
    ${REMARK# [[[. varCOMMENT: display item comment.}
    <entry sensitive="false" tooltip-text="$(gettext "Comment about current item")">
      <variable>varCOMMENT</variable>
      <input>IFS=${SEP}; set -- \${varLIST}; echo "\$4${showcategories:+ \$5}"</input>
    </entry>
    ${REMARK# ]]]}
    ${REMARK# [[[. Options and tools (bottom bar)}
    <hbox space-fill="false" space-expand="false">
      ${REMARK# [[[. EXP0 expander activates EXP1}
      <expander expanded="false" tooltip-text="$(gettext "More...")">
        <text visible="false"></text>
        <variable export="false">EXP0</variable>
        <action>if false show:EXP1</action>${REMARK# show the other expander and...}
        <action>if false activate:EXP1</action>${REMARK# ...expand it}
        <action>if true activate:EXP0</action>${REMARK# shrink myself and...}
        <action>hide:EXP0</action>${REMARK# ...hide myself immediately}
        <label>""</label>
      </expander>
      ${REMARK# ]]]}
      ${REMARK# [[[. varOPEN checkbox}
      <checkbox use-underline="true" tooltip-text="$(gettext "Keep this window open after activating an item. Keep the window open to use the history feature, or to avoid startup delays.")">
        <label>$(gettext "_Keep window")</label>
        <default>${defOPEN}</default>
        <variable>varOPEN</variable>
        ${DEBUG2:+<action>echo>&2 ,,, varOPEN ,,,</action>}
        <action>awk -v s=defOPEN=\${varOPEN} '/^defOPEN=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
      </checkbox>
      ${REMARK# ]]]}
      ${REMARK# [[[. varICONS checkbox}
      <checkbox use-underline="true" tooltip-text="$(gettext "Display all available icons instead of displaying just the icons that do not need to be cached. Caching all icons may take some time. Disabling this option clears the existing cache.")">
        <label>$(gettext "_Show all icons")</label>
        <default>${varICONS}</default>
        <variable>varICONS</variable>
        ${DEBUG2:+<action>echo>&2 ,,, varICONS ,,,</action>}
        <action>awk -v s=varICONS=\${varICONS} '/^varICONS=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
        <action>clear:varSEARCH</action>
        ${DEBUG2:+<action>ls /usr/share/pixmaps/findnrun-.uptodate ~/.icons/findnrun-.uptodate >&2; cat ~/.findnrunrc >&2</action>}
        <action condition="command_is_true([ -f '${ICONSTEM}.uptodate' -a true = \${varICONS} ] && echo true)">break:</action>
        ${DEBUG2:+<action>if true echo >&2 in true rebuilding database...</action>}
        <action>if true gawk -v ALL_ICONS=\${varICONS} -v SHOWNODISPLAY=\${SHOWNODISPLAY} -f '${AWKB}' -v LIST_FILE='$LIST_FILE' > '${DATF}'</action>
        ${DEBUG2:+<action>if true echo >&2 in true clear:varLIST</action>}
        <action>if true clear:varLIST</action>
        ${DEBUG2:+<action>if true echo >&2 in true refresh:varLIST</action>}
        <action>if true refresh:varLIST</action>
        ${DEBUG2:+<action>if false echo >&2 in false rm -f \{~/.icons\|/usr/share/pixmaps\}/findnrun-\*</action>}
        <action>if false rm -f '${ICONSTEM:-/tmp/dummy}'*</action>
        ${DEBUG2:+<action>if false echo >&2 in false rebuilding database...</action>}
        <action>if false gawk -v ALL_ICONS=\${varICONS} -v SHOWNODISPLAY=\${SHOWNODISPLAY} -f '${AWKB}' -v LIST_FILE='$LIST_FILE' > '${DATF}'</action>
        ${DEBUG2:+<action>if false echo >&2 in false clear:varLIST</action>}
        <action>if false clear:varLIST</action>
        ${DEBUG2:+<action>if false echo >&2 in false refresh:varLIST</action>}
        <action>if false refresh:varLIST</action>
      </checkbox>
      ${REMARK# ]]]}
      ${REMARK# [[[. varFOCUSSEARCH checkbox}
      <checkbox use-underline="true" tooltip-text="$(gettext "Return the keyboard focus to the search input field after activating an item instead of keeping the keyboard focus on the activated list item.")">
        <label>$(gettext "_Focus search")</label>
        <default>${varFOCUSSEARCH}</default>
        <variable>varFOCUSSEARCH</variable>
        ${DEBUG2:+<action>echo>&2 ,,, varFOCUSSEARCH ,,,</action>}
        <action>awk -v s=varFOCUSSEARCH=\${varFOCUSSEARCH} '/^varFOCUSSEARCH=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
      </checkbox>
      ${REMARK# ]]]}
      ${REMARK# This widget makes the widgets to its left float left and distribute evenly when the window is widened.}
      <text space-fill="true" space-expand="true"><label>""</label></text>
      ${REMARK# [[[. 'help' button}
      <button tooltip-text="$(gettext "About and help")" stock-icon-size="1">
        <input file stock="gtk-about"></input>
        <action>launch:GUI_ABOUT</action>
      </button>
      ${REMARK# ]]]}
      ${REMARK# [[[. 'exit' button}
      <button tooltip-text="$(gettext "Exit")" stock-icon-size="1">
        <input file stock="gtk-quit"></input>
        <action>exit:EXIT</action>
      </button>
      ${REMARK# ]]]}
    </hbox>
    ${REMARK# ]]]}
    ${REMARK# [[[. EXP1 expander shows More options}
    <hbox>
      <expander expanded="false" visible="false" space-fill="true" space-expand="true" label-fill="false" tooltip-text="$(gettext "Click to hide.")">
        <hbox space-fill="false" space-expand="false">
          ${REMARK# [[[. SHOWNODISPLAY checkbox}
          <checkbox use-underline="true" tooltip-text="$(gettext "Show also hidden system applications.")">
            <label>$(gettext "Show _hidden")</label>
            <default>${SHOWNODISPLAY}</default>
            <variable>SHOWNODISPLAY</variable>
            ${DEBUG2:+<action>echo>&2 ,,, SHOWNODISPLAY ,,,</action>}
            <action>awk -v s=SHOWNODISPLAY=\${SHOWNODISPLAY} '/^SHOWNODISPLAY=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
            <action>gawk -v ALL_ICONS=\${varICONS} -v SHOWNODISPLAY=\${SHOWNODISPLAY} -f '${AWKB}' -v LIST_FILE='$LIST_FILE' > '${DATF}'</action>
            <action>refresh:varLIST</action>
          </checkbox>
          ${REMARK# ]]]}
          ${REMARK# [[[. SEARCHCOMPLETE checkbox}
          <checkbox use-underline="true" tooltip-text="$(gettext "Search in application names, file names, command lines, comments and categories all at once.")">
            <label>$(gettext "Search _complete")*</label>
            <default>${SEARCHCOMPLETE}</default>
            <variable>SEARCHCOMPLETE</variable>
            ${DEBUG2:+<action>echo>&2 ,,, SEARCHCOMPLETE ,,,</action>}
            <action>awk -v s=SEARCHCOMPLETE=\${SEARCHCOMPLETE} '/^SEARCHCOMPLETE=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
            <action>: >"$RWMF"</action>
            <action>launch:RESTART_WINDOW</action>${REMARK# restart findnrun? w/ args}
          </checkbox>
          ${REMARK# ]]]}
          ${REMARK# [[[. SEARCHREGEX checkbox}
          <checkbox use-underline="true" tooltip-text="$(gettext "Interpret the search pattern as a POSIX Basic regular expression.")">
            <label>$(gettext "_Regex")*</label>
            <default>${SEARCHREGEX}</default>
            <variable>SEARCHREGEX</variable>
            ${DEBUG2:+<action>echo>&2 ,,, SEARCHREGEX ,,,</action>}
            <action>awk -v s=SEARCHREGEX=\${SEARCHREGEX} '/^SEARCHREGEX=/{\$0=s;f=1}{a[++n]=\$0}END{if(!f)a[++n]=s;++n;for(i=1;i!=n;i++)print a[i]>ARGV[1]}' '${CONFIG}'</action>
            <action>: >"$RWMF"</action>
            <action>launch:RESTART_WINDOW</action>${REMARK# restart findnrun? w/ args}
          </checkbox>
          ${REMARK# ]]]}
          ${REMARK# This widget makes the widgets to its left float left and distribute evenly when the window is widened.}
          <text space-fill="true" space-expand="true"><label>""</label></text>
          ${REMARK# [[[. 'edit' button}
          <button tooltip-text="$(gettext "Edit configuration file directly. Restart required for changes to take effect.*")" stock-icon-size="1">
            <input file stock="gtk-edit"></input>
            <action>open=$({ command -v rox || command -v xdg-open; } 2>/dev/null); \$open "$CONFIG" &</action>
            <action>gettext -s "When you are finished making your changes come back to this window and restart findnrun." >"$RWMF"</action>
            <action>launch:RESTART_WINDOW</action>
          </button>
          ${REMARK# ]]]}
        </hbox>
        <label>"$(gettext "application finder options       * = restart required")"</label>
        <variable export="false">EXP1</variable>
        <action>if false show:EXP0</action>
        <action>if false hide:EXP1</action>
      </expander>
    </hbox>
    ${REMARK# ]]]}
    ${REMARK# [[[. Menubar (nearly hidden 1x1).}
    ${REMARK# Implementing hotkeys as key-press-event handlers would slow down typing way too much.}
    ${REMARK# So the menubar implements hotkey handlers as menu accelerators, which are fast.}
    ${REMARK# Note that menu accelerators act globally, so these hotkeys really apply to all widgets.}
    <menubar height-request="1" width-request="1">
      <menu>
      ${REMARK#  [[[. Hotkeys for varENTRY.}
        ${REMARK#   [[[. Keys PageUp/PageDown paginate the current source plugin tap.}
        ${REMARK# Note that we serialize (%s) the event name to ensure invokeTAP detects a change.}
        <menuitem accel-key="0xff55" accel-mods="0">
          <action>date '+PageUp %s'>"${ETAP}"</action>
        </menuitem>
        <menuitem accel-key="0xff56" accel-mods="0">
          <action>date '+PageDown %s'>"${ETAP}"</action>
        </menuitem>
        ${REMARK#   ]]]}
        ${REMARK#   [[[. Key F12 activates the top varLIST item.}
        <menuitem accel-key="${HOTKEY_F12##*:}" accel-mods="${HOTKEY_F12%%:*}">
          <action>grabfocus:varLIST</action>
          <action>$(generate_invoke_source_drain)</action>${REMARK# which saves item to history.}
          <action condition="command_is_true(echo \${varFOCUSSEARCH})">grabfocus:varSEARCH</action>
          <action condition="command_is_false(echo \${varFOCUSSEARCH})">grabfocus:varLIST</action>
        </menuitem>
        ${REMARK#   ]]]}
      ${REMARK#  ]]]}
      ${REMARK#  [[[. Hotkeys for varLIST.}
        ${REMARK#   [[[. Key F4 saves search results and invokes a filter.}
        <menuitem accel-key="${HOTKEY_F4##*:}" accel-mods="${HOTKEY_F4%%:*}">
          <action>save:varLIST</action>
          <action>$(generate_invoke_source_save_filter)</action>
        </menuitem>
        ${REMARK#   ]]]}
      ${REMARK#  ]]]}
      ${REMARK#  [[[. Hotkeys for the main window widget.}
        ${REMARK#   [[[. Key ESC terminates findnrun.}
        <menuitem accel-key="0xff1b" accel-mods="0">
          <action>exit:EXIT</action>
        </menuitem>
        ${REMARK#   ]]]}
        ${REMARK#   [[[. Key F1 invokes the help viewer.}
        <menuitem accel-key="0xffbe" accel-mods="0">
          <action>${helpviewer:-:} "\${PLGDIR:-...}/index.md" &</action>
        </menuitem>
        ${REMARK#   ]]]}
        ${REMARK#   [[[. Key F2 cycles focus between the search and the command entry field.}
        <menuitem accel-key="${HOTKEY_F2##*:}" accel-mods="${HOTKEY_F2%%:*}">
          <action>case \${varF2:-false} in true) echo false;; false) echo true;; esac >"${F2SF}"</action>
          <action>refresh:varF2</action>
        </menuitem>
        ${REMARK#   ]]]}
        ${REMARK#   [[[. Key F3 cycles the list view among built-in and plugin sources.}
        <menuitem accel-key="${HOTKEY_F3##*:}" accel-mods="${HOTKEY_F3%%:*}">
          <action>set -- $VISIBLE_SOURCES; echo -n \$(((\${varF3} + 1) % \${#}))>'${F3SF}'</action>
          <action>refresh:varF3</action>
        </menuitem>
        ${REMARK#   ]]]}
      ${REMARK#  ]]]}
      ${REMARK#  [[[. Keys Ctrl+1 through Ctrl+9 select the first 9 source plugins directly.}
        $(generate_source_hotkeys)
      ${REMARK#  ]]]}
        <label>""</label>
      </menu>
    </menubar>
    ${REMARK# ]]]}
    ${REMARK# [[[. varSBAR statusbar}
    ${REMARK# Do not vbox the status bar here; keep the vbox under the control}
    ${REMARK# of generate_status_bar to be able to hide the status bar without}
    ${REMARK# leaving a tiny empty strip at the bottom of the main window.}
    $(generate_status_bar)
    ${REMARK# ]]]}
  </vbox>
  ${REMARK# Do not vbox/hbox hidden widgets because boxes add some unnecessary (tiny) padding.}
  ${REMARK# Keeping hidden items after visible ones prevents issues when resizing window vertically.}
  ${REMARK# [[[. Timers (hidden).}
  ${REMARK#  [[[. varSEARCH0: refresh initial search input.}
  <timer milliseconds="true" interval="100" visible="false">
    <variable export="false">varSEARCH0</variable>
    <action>refresh:varSEARCH</action>
    ${REMARK# varF3 re-enables this timer when another plugin gets loaded.}
    <action>disable:varSEARCH0</action>
  </timer>
  ${REMARK# -  ]]]}
  ${REMARK# - ]]]}
  ${REMARK# [[[. Cached source declaration variables (hidden).}
  <entry visible="false" sensitive="false">
    <variable>TAP</variable>
    <default>${TAP}</default>
    ${REMARK# Cache TAP until next refresh:TAP.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${TAP}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE TAP [\$varF3] \$TAP</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>DRAIN</variable>
    <default>${DRAIN:-null}</default>
    ${REMARK# Cache DRAIN until next refresh:DRAIN.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${DRAIN}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE DRAIN [\$varF3] \$DRAIN</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>ICON</variable>
    <default>${ICON:-null}</default>
    ${REMARK# Cache ICON until next refresh:ICON.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${ICON}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE ICON [\$varF3] \$ICON</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>TITLE</variable>
    <default>${TITLE:-null}</default>
    ${REMARK# Cache TITLE until next refresh:TITLE.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${TITLE}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE TITLE [\$varF3] \$TITLE</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>SOURCE</variable>
    <default>${SOURCE}</default>
    ${REMARK# Cache SOURCE until next refresh:SOURCE}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${SOURCE}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE SOURCE [\$varF3] \$SOURCE</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>ID</variable>
    <default>${ID}</default>
    ${REMARK# Cache ID until next refresh:ID.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${ID}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE ID [\$varF3] \$ID</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>NSOURCES</variable>
    <default>${NSOURCES}</default>
    ${REMARK# Cache NSOURCES until next refresh:NSOURCES.}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${NSOURCES}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE NSOURCES [\$varF3] \$NSOURCES</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>INITSEARCH</variable>
    ${REMARK# Cache INITSEARCH until next refresh:INITSEARCH}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${INITSEARCH}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE INITSEARCH [\$varF3] \$INITSEARCH</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>MODE</variable>
    ${REMARK# Cache MODE until next refresh:MODE}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${MODE}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE MODE [\$varF3] \$MODE</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>PLGDIR</variable>
    ${REMARK# Cache PLGDIR until next refresh:PLGDIR}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${PLGDIR}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE PLGDIR [\$varF3] \$PLGDIR</action>}
  </entry>
  <entry visible="false" sensitive="false">
    <variable>SAVEFLT</variable>
    ${REMARK# Cache SAVEFLT until next refresh:SAVEFLT}
    <input>. "${SRCSTEM}\${varF3:-0}-"*.sh && echo "\${SAVEFLT}"</input>
    ${DEBUG2:+<action>echo>&2 === CACHE SAVEFLT [\$varF3] \$SAVEFLT</action>}
  </entry>
  ${REMARK# ]]]}
  ${REMARK# [[[. Read-only values (hidden).}
  <entry visible="false" sensitive="false">
    <variable>FNRPID</variable>
    ${REMARK# gtkdialog process id.}
    <input>ps -ho ppid:1 \$\$</input>
  </entry>
  ${REMARK# ]]]}
  ${REMARK# [[[. Remote interface (hidden).}
  ${REMARK# Note that input events are serialized to ensure RPC detects the call event.}
  ${REMARK# External processes call RPC with: date '+FUNCTION1[ FUNCTION2...] %s'>"$FNRRPC"}
  <entry visible="false" auto-refresh="true">
    <default>ACCEPTING</default>
    <variable>RPC</variable>
    <input file>${FNRRPC}</input>
    ${DEBUG1:+<action>echo>&2 "RPC [ \$RPC ]"</action>}
    ${REMARK#  [[[. PresentMainWindow}
    <action condition="command_is_true(case \${RPC% *} in *PresentMainWindow*) echo 1;; esac)">presentwindow:MAINWINDOW</action>
    ${REMARK#  ]]]}
    ${REMARK#  [[[. PresentMainSearchInput}
    <action condition="command_is_true(case \${RPC% *} in *PresentMainSearchInput*) echo 1;; esac)">presentwindow:MAINWINDOW</action>
    <action condition="command_is_true(case \${RPC% *} in *PresentMainSearchInput*) echo 1;; esac)">grabfocus:varSEARCH</action>
    ${REMARK#  ]]]}
    ${REMARK#  [[[. RestartSearch}
    ${REMARK# This call forces a tap-command invocation with INITSEARCH input.}
    <action condition="command_is_true(case \${RPC% *} in *RestartSearch*) echo 1;; esac)">clear:varSEARCH</action>
    <action condition="command_is_true(case \${RPC% *} in *RestartSearch*) echo 1;; esac)">refresh:varSEARCH</action>
    ${REMARK#  ]]]}
    ${REMARK#   [[[. PageUp/PageDown paginate the current source plugin tap.}
    ${REMARK# Note that we serialize (%s) the event name to ensure invokeTAP detects a change.}
    <action condition="command_is_true(case \${RPC% *} in *PageUp*) echo 1;; esac)">date '+PageUp %s'>"${ETAP}"</action>
    <action condition="command_is_true(case \${RPC% *} in *PageDown*) echo 1;; esac)">date '+PageDown %s'>"${ETAP}"</action>
    ${REMARK#   ]]]}
    ${REMARK#  [[[. Exit}
    <action condition="command_is_true(case \${RPC% *} in *ExitFNR*) echo 1;; esac)">exit:EXIT</action>
    ${REMARK#  ]]]}
  </entry>
  ${REMARK# ]]]}
  ${REMARK# [[[. Hotkey targets (hidden).}
  ${REMARK#  [[[. refresh:varF2 to cycle focus between the search field and the command entry field.}
  ${REMARK# Key F2 demonstrates how.}
  <checkbox visible="false">
    <default>false</default>
    <label>F2</label>
    <variable>varF2</variable>
    <input file>${F2SF}</input>
    ${DEBUG2:+<action>echo>&2 ,,, varF2 ,,,</action>}
    <action>if true grabfocus:varCMD</action>
    <action>if false grabfocus:varSEARCH</action>
  </checkbox>
  ${REMARK#  ]]]}
  ${REMARK#  [[[. refresh:varF3 to switch the list view to another source.}
  ${REMARK# Key F3 demonstrates how.}
  <entry visible="false" sensitive="false">
    <variable>varF3</variable>
    <default>0</default>
    <input file>${F3SF}</input>
    ${DEBUG2:+<action>echo>&2 ,,, varF3 ,,,</action>}
    ${REMARK# [[[. Optimization: Cache current source values.}
    <action>refresh:TAP</action>
    <action>refresh:DRAIN</action>
    <action>refresh:ICON</action>
    <action>refresh:TITLE</action>
    <action>refresh:INITSEARCH</action>
    <action>refresh:MODE</action>
    <action>refresh:PLGDIR</action>
    <action>refresh:SAVEFLT</action>
    <action>refresh:SOURCE</action>
    <action>refresh:ID</action>
    ${REMARK# ]]]}
    ${DEBUG2:+<action>echo>&2 ,,, refresh varSBAR [[</action>}
    <action>refresh:varSBAR</action>
    ${DEBUG2:+<action>echo>&2 ]] ,,,</action>}
    <action>clear:varSEARCH</action>
    <action>enable:varSEARCH0</action>
    <action>refresh:varLIST</action>
  </entry>
  ${REMARK#  ]]]}
  ${REMARK#  [[[. Write event name to file $ETAP to invoke the current tap.}
  ${REMARK# Keys PageDown/PageUp demonstrate how.}
  <entry visible="false" auto-refresh="true">
    <default>Search</default>
    <variable>invokeTAP</variable>
    <input file>${ETAP}</input>
    ${COMMENT# Do not invoke if input is empty.}
    <action condition="command_is_true(echo \${invokeTAP:-true})">break:</action>
    ${DEBUG2:+<action>echo>&2 ,,, invokeTAP -\$invokeTAP- ,,,</action>}
    <action>refresh:varLIST</action>${COMMENT# Invoke tap.}
    <action>clear:invokeTAP</action>${COMMENT# Clear cached value.}
  </entry>
  ${REMARK#  ]]]}
  ${REMARK# ]]]}
  ${REMARK# [[[. Hotkey actions (hidden).}
  ${REMARK# DEBUG <action signal="key-press-event">exec 1>&2;echo;date;env|grep KEY_</action>}
  ${REMARK# ]]]}
  <action signal="delete-event">exit:abort</action>
  <variable export="false">MAINWINDOW</variable>
</window>
EOF

# i18n Findnrun own .desktop file. [[[1
# i18n Translate the Name[xx] field to be added to file findnrun.desktop
Name=$(gettext "Findnrun.")
# i18n Translate the Comment[xx] field to be added to file findnrun.desktop
Comment=$(gettext "Find applications and much more.")

