#!/bin/dash

# Copyright (C) step, 2019
# Fatdog Control Panel and Menu
#
# License: GNU GPL Version 3 or later
# Status: 100% compatible with original fatdog-control-panel.sh
# Bonus: options: --menu shows menu instead of tabbed dialog, --menu-icon-size
# Depends: gawk, patched yad, gtkmenuplus
# version 1.0 20190228

# Pros: fast, resizable window, menu option
# Cons: one process per tab + overhead (2)

### 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="510 300"       # in pixels, startup window width x height
HIDPI_WINDOW_SIZE="1000 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_ONE_TAB opt_menu opt_menu_icon_size
opt_menu_one_level=0
while ! [ "${1#-}" = "$1" ]; do
	case "$1" in
		--start-tab=* ) # pass either 1-based index or uppercase English name
			opt_START_TAB=${1#*=} ;;
		--one-tab=* ) # show this tab only; pass same as --start-tab
			opt_ONE_TAB=${1#*=}; opt_START_TAB=$opt_ONE_TAB ;;
		--tabs=* ) # comma-separated list of tabs as in --start-tab
			opt_TABS=${1#*=} ;; # include only these tabs

		# --one-tab and --tabs also apply to menu
		--menu ) opt_menu=1 ;; # show menu instead of tabbed dialog
		--menu-icon-size=* ) opt_menu_icon_size=${1#*=} ;;
		--menu-one-level ) opt_menu_one_level=1 ;; # embed all submenus in level 1
		--help|--help=*|-h|-h=*)
			printf "$(gettext "%s [OPTIONS]\nOPTIONS:")\n" "${0##*/}"
			sed -n '/^##* Parse options/,/^done/ s/^[[:space:]]*\(--[^)]\+\).*/\t\1/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" \
	"--width=${WINDOW_SIZE% *}" \
	"--height=${WINDOW_SIZE#* }" \
	"--center" \
	"--buttons-layout=center" \
	"--no-buttons" \
	"--title=$APPTITLE" \
	"--window-icon=$APPICON" \
	;
YAD_ICONBOX="\
--plug=$KEY \
--icons \
--listen \
--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}
"
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
{
	set |
		gawk -v TABS="$opt_TABS" '#{{{gawk
BEGIN {
	FS = "[= |\x22\x27]"
	# Prepare dictionary for "in" lookup
	for(i = split(TABS, Tabs, /,/); i > 0; i--) {
		Tabs[":"Tabs[i]] = 1
		delete Tabs[i]
	}
}
/^TAB[0-9]+/ {
	p = index($1, "_")
	tabnum = substr($1, 4, p - 4)
	English_tabname = substr($1, p + 1)
	localized_tabname = $3
	# convert START_TAB`s matching <uppercase English name> to <integer>
	if("" != START_TAB && START_TAB == English_tabname) { START_TAB = tabnum }
	# skip all other tabs but the --one-tab, if defined
	if("" != ONE_TAB && ONE_TAB != tabnum && ONE_TAB != English_tabname)
		{ next }
	# skip all other tabs but the --tabs, if defined
	if(length(Tabs) && ! ((":"tabnum) in Tabs || (":"English_tabname) in Tabs))
		{ next }
	print "TABNAME="localized_tabname
	# list tab applet items, e.g. "hwclock"
	for(i = 4; i < NF; i++) { print $i }
}
# set notebook start tab
END { if(START_TAB > 0) { printf "--active-tab=%d ", START_TAB >> YAD_NOTEBOOK } }
#gawk}}}' ONE_TAB="${opt_ONE_TAB}" START_TAB="$opt_START_TAB" YAD_NOTEBOOK="$YAD_NOTEBOOK"
}

### 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

list_applet_items |
	# pipe: Convert items into .desktop file paths.
	gawk '#{{{gawk
BEGIN {
	nD = split("'"$DESKTOP_FILES_DIRS"'", D, " ")
	ORS = "\0"
}
# this will set variable TABNAME in the next gawk
/^TABNAME=/ { 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}}}' |

	# pipe: Process .desktop files.
	xargs -0 gawk -F= -v WARN="$MSG1" '#{{{gawk
BEGINFILE { # this marks the beginning of a new tab
	if(TABNAME_PREV != TABNAME) { # TABNAME set via ARGV[]
		printf "\x27%s\x27 ", "--tab="TABNAME >> YAD_NOTEBOOK
		TABNAME_PREV = TABNAME # new tab name
		if(IN_TAB) { close_tab(TABNUM, TABNAME) }
		IN_TAB = open_tab(++TABNUM, TABNAME)
	}
}
# Start new tab`s heredoc and submenu.
function open_tab(n, tabname,   cmd) {
	print "yad "YAD_ICONBOX" --tabnum="n" << \\EOF &" >> HEREDOC
	if(!MENU_ONE_LEVEL) { print "submenu=_"n" "tabname >> MENU }
	return 1
}
# Print parsed data to the current tab`s heredoc and submenu.
function add_item_to_tab(name, tooltip, icon, command, use_term) {
	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
}
# Close tab`s heredoc and submenu; track pids.
function close_tab(n, name) {
	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(name, tooltip, icon, command, use_term) }
END {
	if(IN_TAB) { close_tab(TABNUM) }
	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}}}' HEREDOC="$YAD_TABS" YAD_NOTEBOOK="$YAD_NOTEBOOK" YAD_ICONBOX="$YAD_ICONBOX" MENU="$MENU" MENU_ONE_LEVEL="$opt_menu_one_level"

case ${opt_menu:-$CP_MENU} in
	1 ) exec gtkmenuplus -i -i "$MENU" >/dev/null ;;
	* )
		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
esac

# vim:ts=4:

