#!/bin/dash

# Copyright (C) step, 2019-2021 and the Fatdog team
# Fatdog Control Panel (or Menu or Applet copier)
#
# License: GNU GPL Version 3 or later
# Depends: gawk, patched yad, gtkmenuplus
# Version 20210601

# Applet format and panel view are 100% compatible with the original fatdog-control-panel.sh
# Menu and applet copier are new features; invoke with -h for short help

### std localisation stanza
export TEXTDOMAIN=fatdog
#. gettext.sh

### configuration variables - set here and/or in user's profile
CP_MENU="${CP_MENU:-}" # set =1 to default to showing a menu
CP_MENU_ICON_SIZE="${CP_MENU_ICON_SIZE:-22}"

### configuration variables from original fatdog-control-panel.sh
APPTITLE="$(gettext 'Fatdog64 Control Panel')"
APPICON="/usr/share/pixmaps/midi-icons/controlpanel48.png"
APPICON2="/usr/share/pixmaps/midi-icons/go48.png"
DESKTOP_FILES_DIRS="/usr/share/applications /usr/local/share/applications $XDG_DATA_HOME/applications"
ICON_DIRS="/usr/share/pixmaps/ /usr/share/midi-icons/ /usr/share/mini-icons/ /usr/share/icons/ $XDG_DATA_HOME/icons"
ICON_DISPLAY_SIZE="32 32"   # in pixels, width x height
ICON_WIDTH="100"            # in pixels, max size of each grid cell
WINDOW_SIZE="600 300"       # in pixels, startup window width x height
HIDPI_WINDOW_SIZE="1100 600"

SYSTEM_APPLETS=/etc/control-panel-applets
SYSTEM_APPLETS_DIR=${SYSTEM_APPLETS}.dir
USER_APPLETS=$FATDOG_STATE_DIR/control-panel-applets
USER_APPLETS_DIR=${USER_APPLETS}.dir

### Parse options
# Option format: -x[=parm] | --opt-x[=parm]
# Short options can't be combined together. Space can't substitute '=' before option value.
unset opt_start_tab opt_tabset opt_menu opt_menu_export_to opt_menu_icon_size opt_copy_applets
opt_menu_one_level=0
while ! [ "${1#-}" = "$1" ]; do
	# Note: keep cases as self-documented one-liners; see the sed regex under --help
	case "$1" in
		#begin-help
		--start-tab=* ) opt_start_tab=${1#*=} ;; #: * is either a 1-based index or an uppercase English tab name
		--tabset=* ) opt_tabset=${1#*=} ;; # include only these tabs #: * is a comma-separated list of tabs as in --start-tab

		#;; #: --tabset also applies to --menu and --copy-applets
		--menu ) opt_menu=1 ;; #: show a menu instead of a tabbed dialog
		--menu-export-to=* ) opt_menu_export_to=${1#*=} ;; #: export menu definition to *=filepath ("-" for stdout)
		--menu-icon-size=* ) opt_menu_icon_size=${1#*=} ;;
		--menu-one-level ) opt_menu_one_level=1 ;; #: show a flat menu
		--copy-applets=* ) opt_copy_applets=${1#*=} ;; #: copy applet tree to *=directory_path
		--help|--help=*|-h|-h=*)
		#end-help
			printf "$(gettext "%s [OPTIONS]\nOPTIONS:")\n" "${0##*/}"
			sed -nE '/#begin[-]help/,/#end[-]help/ s/^[[:space:]]*(--[^)]+)?.+;;(.+#)?(:.*)?/\t\1\t\3/p' "$0"
			exit ;;
		--) shift; break ;;
		-*)
			echo >&2 "$(gettext "unknown option:")" "$1"
	esac
	shift
done

### init parameters - this app can be called with parameters and masquerade as launcher too!
[ "$1" ] && APPTITLE="$1" && APPICON=$APPICON2 && shift

### adjust gui for large screens
DPI=$(sed '/Xft.dpi/!d; s/.*:[ \t]*//' ~/.Xresources 2>/dev/null)
[ "$DPI" ] && [ $DPI -ge 132 ] && WINDOW_SIZE="$HIDPI_WINDOW_SIZE" # double the size

### find applets to load - either from command line or system applet {{{1
if [ "$1" ]; then
	while [ "$1" ]; do
		[ -e "$1" ] && . "$1"
		shift
	done
else
	[ -e "$SYSTEM_APPLETS" ] && . "$SYSTEM_APPLETS"
	[ -e "$SYSTEM_APPLETS_DIR" ] &&
	for control_file in "$SYSTEM_APPLETS_DIR"/*; do [ -r "$control_file" ] && . "$control_file"; done

	[ -e "$USER_APPLETS" ] && . "$USER_APPLETS"
	[ -e "$USER_APPLETS_DIR" ] &&
	for control_file in "$USER_APPLETS_DIR"/*; do [ -r "$control_file" ] && . "$control_file"; done
fi

### Define yad dialogs {{{1
TMPS="/tmp/.${0##*/}-$$" KEY=$$
p="$TMPS-yad-tabs.sh"
: > "$p" && exec 3< "$p" && rm "$p" && YAD_TABS=/proc/self/fd/3
p="$TMPS-yad-notebook"
: > "$p" && exec 4< "$p" && rm "$p" && YAD_NOTEBOOK=/proc/self/fd/4
printf "'%s' " >> "$YAD_NOTEBOOK" \
	"yad" \
	"--key=$KEY" \
	"--notebook" \
	"--tab-pos=left" \
	"--width=${WINDOW_SIZE% *}" \
	"--height=${WINDOW_SIZE#* }" \
	"--center" \
	"--buttons-layout=center" \
	"--no-buttons" \
	"--title=$APPTITLE" \
	"--window-icon=$APPICON" \
	;
# work-around: plugged --icons needs vscroll=always otherwise it may hang upon resizing the notebook
YAD_ICONBOX="\
--plug=$KEY \
--icons \
--listen \
--vscroll-policy=always \
--item-width=$ICON_WIDTH \
--icon-size=${ICON_DISPLAY_SIZE% *}"

### Define gtkmenuplus menu {{{1
p="$TMPS.gtkmenuplus"
: > "$p" && exec 5< "$p" && rm "$p" && MENU=/proc/self/fd/5
echo >> "$MENU" "
format=mnemonic=\"1\" # enable label mnemonics throughout
configure=noicons # reserve empty space for missing icons
configure=endsubmenu # nest menu by keywords not by indentation
${CP_MENU_ICON_SIZE:+iconsize=$CP_MENU_ICON_SIZE}
${opt_menu_icon_size:+iconsize=$opt_menu_icon_size}
"

### Define desktop files copier script {{{1
p="$TMPS-applet-copier.sh"
echo > "$p" "mkdir -p \"$opt_copy_applets\" && cd \"$opt_copy_applets\" || exit 1" &&
	exec 6< "$p" && rm "$p" && APPLET_COPIER=/proc/self/fd/6

notebook_ripper() # $1-key {{{1
{
	local key=$1
	pkill -f "yad.*--(key|plug)=$key "
	if [ "$NOTEBOOK_PID" ]; then # release shared memory just in case
		ipcs -mp | while read shmid owner cpid lpid; do
			[ $NOTEBOOK_PID = "$cpid" ] && ipcrm shm $shmid 2>/dev/null
		done
	fi
}

list_applet_items() # {{{1
{
# ----------------------------------------------------------------------------
# each applet is a shell variable named "TAB_"<index>_<canonical_name>
# 1. read each applet's value with set | gawk
# value is <applet_name> followed by a list of <applet_item_short_name>s
# an <applet_name> is wrapped with $(gettext ...) so we distinguish between
# the ... part, named "canonical", and gettext's result, named "localized"
# ----------------------------------------------------------------------------
	set | gawk -F "[=|\x22\x27]" '
#{{{gawk
# ----------------------------------------------------------------------------
# 2. make tab`s <meta> = {<canonical_index>, <canonical_name>, <localized_name>}
# ----------------------------------------------------------------------------
/^TAB[0-9]+/ { # e.g. TAB3_NETWORK
	p = index($1, "_")
	canonical_index = substr($1, 4, p - 4)
	canonical_tabname = substr($1, p + 1)
	if("" != $3) {
		localized_tabname = $3
	} else { # stale package-installed applets enter here
		localized_tabname = canonical_tabname"\\n"i18n_migrated
		# avoid potential clash between stale and up-to-date tab numbers
		canonical_index = 100 + canonical_index # stale > 100 arbitrary offset
	}
	items = $4 # space separated
	# escape XML entities
	gsub(/&/, "\\&amp;", localized_tabname)
	gsub(/</, "\\&lt;", localized_tabname)
	gsub(/>/, "\\&gt;", localized_tabname)
	# freeze \\r and \\n - melted by BEGINFILE, open_tab and add_item_to_tab
	gsub(/\\r/, "\\\\r", localized_tabname)
	gsub(/\\n/, "\\\\n", localized_tabname)
# ----------------------------------------------------------------------------
# 3. sow "APPLET_META="<meta> "\n" [<applet_item_shortname> "\n" ...]
# <applet_item_shortname> is the short name of a .desktop file -- it won`t be resolved here
# ----------------------------------------------------------------------------
	Meta[canonical_index] = sprintf("APPLET_META=%d\b%s\b%s\n", canonical_index, localized_tabname, canonical_tabname)
	# list tab`s applet items, e.g. "hwclock" -- short name of .desktop file
	Meta[canonical_index ":nItem"] = nf = split(items, a, / /)
	for(i = 1; i <= nf; i++) { Item[canonical_index, i] = a[i] }
	CNI[canonical_index] = canonical_tabname # table: canonical_name by canonical_index
}
END {
# ----------------------------------------------------------------------------
# 4. sort Meta by ascending canonical_index; canonical_index is the applet`s
# <index> including migrated stale applets` (index offset +100), if any.
# ----------------------------------------------------------------------------
	# make index Tx sorted by tab_index so that the i-th tab`s
	# canonical_tabname is CNI[canonical_index = Tx[tab_index]]
	nTx = asorti(CNI, Tx, "@ind_num_asc")
# ----------------------------------------------------------------------------
# 5. select applets; they can be selected either by canonical_tabname or by
# notebook tab index with the --*tab* command-line options.
# ----------------------------------------------------------------------------
	# helpers
	has_tabset  = "" != TABSET  # --tabset
	for(i = split(TABSET, _, /,/); i > 0; i--) { Tabset[_[i]] = 1 }

	# delete non-selected
	for(tab_index = 1; tab_index <= nTx; tab_index++) {
		if( has_tabset && ! ((tab_index in Tabset) || (CNI[Tx[tab_index]] in Tabset)) )
			{ delete Tx[tab_index] }
	}
	# compact index Tx -> Cx
	nCx = asort(Tx, Cx, "@val_num_asc")
# ----------------------------------------------------------------------------
# 6. print Meta & its Items for selected applets; Meta and Item are indexed by
# canonical_index = Cx[tab_index]
# ----------------------------------------------------------------------------
	for(tab_index = 1; tab_index <= nCx; tab_index++) {
		ci = Cx[tab_index]
		print Meta[ci]
		for(n = 1; n <= Meta[ci ":nItem"]; n++) { print Item[ci, n] }
	}
}
#gawk}}}
' TABSET="$opt_tabset" YAD_NOTEBOOK="$YAD_NOTEBOOK" \
	i18n_migrated="$(gettext "[migrated]")"
}

### Main {{{1}}}
MSG1=$(gettext "Warning: %s:%d: Exec= line embeds double quotes (not allowed)")
LR=${LANG%.*} LL=${LANG%_*} # i.e., LANG=de_DE.UTF-8 : LR==de_DE LL==de

# ----------------------------------------------------------------------------
# 1. get list of "APPLET_META=<meta>" "\n" [ <shortname> "\n" ... ] from list_applet_items
# 2. pipe: convert short names into .desktop file full paths with gawk
# 3. output "APPLET_META=<meta>" "\n" [ pathname "\n" ... ] for next xargs
# ----------------------------------------------------------------------------
list_applet_items |
	gawk '
#{{{gawk
BEGIN {
	nD = split("'"$DESKTOP_FILES_DIRS"'", D, " ")
	ORS = "\0"
}
# this will set variable APPLET_META in the next gawk`s command line
/^APPLET_META=/ { print; next }
{ if(path = find_desktop_file($0)) { print path; next } }
function find_desktop_file(name,   i, path) {
	for(i = 1; i <= nD; i++) {
		if(is_readable(path = D[i] "/" name ".desktop")) { return path }
		return "" # not found
	}
}
function is_readable(path,   s, ret) {
	if(ret = (0 < (getline s < path))) { close(path) }
	return ret
}
#gawk}}}
' |
# ----------------------------------------------------------------------------
# 4. pipe: Process .desktop files with xargs gawk; xargs builds this kind of command line:
# gawk <options> APPLET_META=<meta_1> <pathname_1> APPLETA_META=<meta_2> <pathname_2> ...
# 5. gawk: for each p in <pathname>s:
#	read p'<meta>; decode p's .desktop file; output all layouts corresponding to p
# the layouts are:
#	sh file with yad command (default, referred to as HEREDOC);
#	gtkmenuplus file for --menu;
#	sh file for --copy-applets
# ----------------------------------------------------------------------------
	xargs -0 gawk -F= -v WARN="$MSG1" '
#{{{gawk
BEGINFILE { # this marks the beginning of a new tab
	if(APPLET_META_PREV != APPLET_META) { # APPLET_META set via ARGV[]
		split(APPLET_META, m, /\b/)
		index_canonical   = m[1]
		tabname_localized = m[2]
		tabname_canonical = m[3]

		tabname_multiline = tabname_localized
		gsub(/(\\[rn])+/, "\n", tabname_multiline)
		printf "\x27%s\x27 ", "--tab="tabname_multiline >> YAD_NOTEBOOK

		APPLET_META_PREV = APPLET_META
		if(IN_TAB) { close_tab(TABNUM) }
		IN_TAB = open_tab(++TABNUM, tabname_localized) # new notebook tab

		if(START_TAB == tabname_canonical || START_TAB == TABNUM)
			{ active_tab = TABNUM }
	}
}
# Start new tab`s layouts.
function open_tab(n, tabname_escaped,   tabname_spaced, cmd) {
	tabname_spaced = tabname_escaped; gsub(/(\\[rn])+/, " ", tabname_spaced)
	print "yad "YAD_ICONBOX" --tabnum="n" << \\EOF &" >> HEREDOC
	if(!MENU_ONE_LEVEL) { print "submenu=_"n" "tabname_spaced >> MENU }
	print "mkdir -p \""tabname_spaced"\"" >> APPLET_COPIER
	return 1
}
# Print parsed data to the current tab`s heredoc, submenu and desktop copier script.
function add_item_to_tab(tabname_escaped, name, tooltip, icon, command, use_term,   tabname_spaced) {
	tabname_spaced = tabname_escaped; gsub(/(\\[rn])+/, " ", tabname_spaced)
	printf "%s\n%s\n%s\n%s\n%s\n", name, tooltip, icon, command, use_term >> HEREDOC
	printf "item=%s\ntooltip=%s\nicon=%s\ncmd=%s\n", name, tooltip, icon, command >> MENU

	printf "p=\"%s\"; p=\"%s/${p##*/}\"; ", FILENAME, tabname_spaced >> APPLET_COPIER
	printf "grep -hv NoDisplay \"%s\" > \"$p\"\n", FILENAME >> APPLET_COPIER
	print  "mv \"$p\" \"${p%%/*}/"name"\" 2>/dev/null" >> APPLET_COPIER
}
# Close tab`s heredoc and submenu; track pids.
function close_tab(n) {
	printf "EOF\n%s\n", "TAB_COUNT="n" TAB"n"_PID=$! TAB_PIDS=\"$TAB_PIDS $!\"" >> HEREDOC
	if(!MENU_ONE_LEVEL) { print "endsubmenu" >> MENU }
}
# Parse each .desktop file into lines of a shell heredoc that feeds a yad iconbox.
# There`s one iconbox for each tab.
BEGINFILE { name = tooltip = icon = command = ""; use_term = "false" }
# localized $LR/$LL supersedes if match found
$1 == "Name"     { name = $2; next }
$1 == "Name['"${LR:-@}"']" || $1 == "Name['"${LL:-@}"']" { name = $2; next }
$1 == "Comment"  { tooltip = $2; next }
$1 == "Comment['"${LR:-@}"']" || $1 == "Comment['"${LL:-@}"']" { tooltip = $2; next }
$1 == "Icon"     { icon = find_icon($2); next }
$1 == "Exec"     {
	command = $2
	gsub(/[[:blank:]]*%[[:alpha:]][[:blank:]]*$/, "", command)
	# Apply workaround yad>g_spawn_command_line_async>fatdog-choose-locale.sh>localedef, email 20190205
	if(index(command, "\"")) { printf WARN"\n", FILENAME, FNR > "/dev/stderr" }
	command = sprintf("sh -c \"</dev/null %s\"", command)

	next
}
ENDFILE { add_item_to_tab(tabname_localized, name, tooltip, icon, command, use_term) }
END {
	if(IN_TAB) { close_tab(TABNUM) }

	# settle option --start-tab
	if(active_tab > 0) { printf "--active-tab=%d ", active_tab >> YAD_NOTEBOOK }

	print " & NOTEBOOK_PID=$!" >> YAD_NOTEBOOK # close the notebook
}

# Convert icon names to file path. This is necessary to ensure that gtk as used {{{
# by yad can process all icons. If this step is omitted for speed, yad will
# replace its own icon for the icons that gtk can`t process.
BEGIN {
	nD = split("'"$ICON_DIRS"'", D, " ")
	nE = split(".png .svg .xpm", E, " ")
}
function find_icon(name_or_path,   d,e, path, name, cmd) {
	if("/" == substr(name_or_path, 1, 1)) { return name_or_path }
	# cached?
	if(name_or_path in Cache) { return Cache[name_or_path] }
	name = name_or_path
	# quick search
	sub(/\.(png|svg|xpm)$/, "", name)
	for(e =1; e <= nE; e++) {
		for(d = 1; d <= nD; d++) {
			if(is_readable(path = D[d]"/"name E[e])) { return Cache[name_or_path] = path }
		}
	}
	# full search - this can be slow but it`s seldom needed in practice
	cmd = "find '"$ICON_DIRS"' -name \""name".*\""
	cmd | getline path # the first found only
	close(path)
	if(path) { return Cache[name_or_path] = path }
	return Cache[name_or_path] = name_or_path # not found
}
function is_readable(path,   s, ret) {
	if(ret = (0 < (getline s < path))) { close(path) }
	return ret
} # }}}
#gawk}}}
' START_TAB="$opt_start_tab" HEREDOC="$YAD_TABS" YAD_NOTEBOOK="$YAD_NOTEBOOK" \
	YAD_ICONBOX="$YAD_ICONBOX" MENU="$MENU" MENU_ONE_LEVEL="$opt_menu_one_level" \
	APPLET_COPIER="$APPLET_COPIER"

if [ 1 = "${opt_menu:-$CP_MENU}" ]; then
	case $opt_menu_export_to in
		-  ) cat "$MENU" ;;
		'' ) exec gtkmenuplus -i -i "$MENU" >/dev/null ;;
		*  ) cp "$MENU" "$opt_menu_export_to" ;;
	esac
elif [ "$opt_copy_applets" ]; then
	exec sh "$APPLET_COPIER"
else
	trap "notebook_ripper $KEY" HUP INT QUIT TERM ABRT 0
	# Start the tabs then the notebook that displays them.
	. "$YAD_TABS"
	. "$YAD_NOTEBOOK"
	wait # forward signals
fi

