#!/bin/sh
# Proposal for XDG terminal execution utility
#
# by Vladimir Kudrya
# https://github.com/Vladimir-csp/
#
# This script is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. See <http://www.gnu.org/licenses/>.
#
# Contributors:
# Roman Chistokhodov https://github.com/FreeSlave/

# some transitional variables for desktop entries used here:
# ENTRY_FILE - desktop entry path relative to it's data directory
# ENTRY_ID - ENTRY_FILE with '/' swapped for '-' (see section E of Desktop Entry Spec)
# ENTRY_PATH - full path of specific desktop entry file


DATA_PREFIX_DIR=xdg-terminals
CONFIG_NAME=xdg-terminals.list

DATA_HIERARCHY="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
CONF_HIERARCHY="${XDG_CONFIG_HOME:-$HOME/.config}:${XDG_CONFIG_DIRS:-/etc/xdg:/usr/etc/xdg}"

DATA=''
BLACKLIST=''
ENTRY=''
EXEC=''
EXECARG=''

OIFS="$IFS"

debug(){
	[ "$DEBUG" = "1" ] && printf "%s\n" "$1" >&2
}

trim_spaces(){
	local TRIMVAR
	TRIMVAR="$*"
	TRIMVAR="${TRIMVAR#"${TRIMVAR%%[![:space:]]*}"}"
	TRIMVAR="${TRIMVAR%"${TRIMVAR##*[![:space:]]}"}"
	printf "%s" "$TRIMVAR"
}

trim_comment(){
	local TRIMVAR
	TRIMVAR="$*"
	TRIMVAR="${TRIMVAR%%#*}"
	printf "%s" "$TRIMVAR"
}

generate_config_list(){
	# Generate list of possible config files for current DE
	local DESKTOPS_LC="$(printf "%s" "$XDG_CURRENT_DESKTOP" | tr '[:upper:]' '[:lower:]')"
	IFS=":"
	debug "lower case desktops are: \"${DESKTOPS_LC}\""
	for CONFIG_DIR in $CONF_HIERARCHY
	do
		for DESKTOP in $DESKTOPS_LC
		do
			printf "%s\n" "${CONFIG_DIR}/${DESKTOP}${DESKTOP:+-}${CONFIG_NAME}"
		done
		printf "%s\n" "${CONFIG_DIR}/${CONFIG_NAME}"
	done | uniq
	IFS="$OIFS"
}


find_preferred_entry(){
	# Read entries listed in config files until a valid one is found
	local ENTRY_ID=
	local SKIP=
	local CONFIG_LIST=

	CONFIG_LIST="$(generate_config_list)"

	debug "finding preferred entry in configs"

	while read CONFIG_FILE
	do
		debug "looking for config \"${CONFIG_FILE}\""
		if [ -f "${CONFIG_FILE}" ]
		then
			debug "reading config \"${CONFIG_FILE}\""
			while read LINE
			do
				debug "parsing line \"$LINE\""
				LINE="$(trim_comment "$LINE")"
				LINE="$(trim_spaces "$LINE")"
				[ -z "$LINE" ] && continue || ENTRY_ID="$LINE"
				debug "retrieved entry ID \"$ENTRY_ID\""
				check_entry_id "$ENTRY_ID" || continue

				debug "finding path for entry ID \"$ENTRY_ID\""
				ENTRY_PATH="$(find_entry_path "$ENTRY_ID")"
				if [ -n "$ENTRY_PATH" ] && check_entry_path "$ENTRY_PATH"
				then
					debug "file at path \"$ENTRY_PATH\" checks out"
					printf "%s" "$ENTRY_PATH"
					return 0
				else
					debug "entry ID \"$ENTRY_ID\" failed check at path \"$ENTRY_PATH\", blacklisting"
					BLACKLIST="${BLACKLIST}${BLACKLIST:+;}$ENTRY_ID"
					continue
				fi
			done < "${CONFIG_FILE}"
		fi
	done << EOF
$CONFIG_LIST
EOF
}

find_any_entry(){
	# Read entries in data dirs until a valid one is found
	local ENTRY_ID=
	local ENTRY_PATH=
	local ENTRY_FILES=
	debug "looking for first available entry in data hierarchy"
	IFS=":"
	for DATA_DIR in $DATA_HIERARCHY
	do
		IFS="$OIFS"
		if [ -d "${DATA_DIR}/${DATA_PREFIX_DIR}" ]
		then
			debug "searching in \"${DATA_DIR}/${DATA_PREFIX_DIR}\""
			ENTRY_FILES="$(find "${DATA_DIR}/${DATA_PREFIX_DIR}" -type f -iname "*.desktop" -printf "%P\n")"
			while read ENTRY_FILE
			do
				[ -z "$ENTRY_FILE" ] && continue
				ENTRY_ID="$(printf "%s" "$ENTRY_FILE" | tr '/' '-')"
				ENTRY_PATH="${DATA_DIR}/${DATA_PREFIX_DIR}/$ENTRY_FILE"
				check_entry_id "$ENTRY_ID" || continue
				if check_entry_path "$ENTRY_PATH"
				then
					debug "file at path \"$ENTRY_PATH\" checks out"
					printf "%s" "$ENTRY_PATH"
					return 0
				else
					debug "entry ID \"$ENTRY_ID\" failed check at path \"$ENTRY_PATH\", blacklisting"
					BLACKLIST="${BLACKLIST}${BLACKLIST:+;}$ENTRY_ID"
					debug "current blacklist: \"$BLACKLIST\""
				fi
			done << EOF
$ENTRY_FILES
EOF
		fi
	done
	IFS="$OIFS"
}

find_entry_path(){
	# For given desktop entry ID find an actual file path
	local ENTRY_ID=
	local ENTRY_FILE=
	local ENTRY_PATH=
	local DATA_DIR=
	local FILE_LIST=
	local ID_LIST=
	local ID_NUM=

	ENTRY_ID="$1"

	IFS=":"
	for DATA_DIR in $DATA_HIERARCHY
	do
		IFS="$OIFS"
		if [ -d "${DATA_DIR}/${DATA_PREFIX_DIR}" ]
		then
			debug "looking in \"${DATA_DIR}/${DATA_PREFIX_DIR}\""
			FILE_LIST="$(find -L "${DATA_DIR}/${DATA_PREFIX_DIR}" -type f -iname "*.desktop" -printf "%P\n")"
			ID_LIST="$(printf "%s" "$FILE_LIST" | tr '/' '-')"
			ID_NUM="$(printf "%s" "$ID_LIST" | grep -n "^${ENTRY_ID}$" | head -n 1 | cut -d : -f 1)"
			ENTRY_FILE="$(printf "%s" "$FILE_LIST" | head -n "${ID_NUM:-0}" | tail -n 1)"
			[ -n "$ENTRY_FILE" ] && ENTRY_PATH="${DATA_DIR}/${DATA_PREFIX_DIR}/${ENTRY_FILE}" || continue
			debug "got entry path \"$ENTRY_PATH\""
			printf "%s" "$ENTRY_PATH"
			return 0
		fi
	done
	IFS="$OIFS"
	debug "path not found for entry ID \"$ENTRY_ID\""
	return 1
}

check_entry_id(){
	# Check if entry ID is blacklisted by previous checks
	local ENTRY_ID
	ENTRY_ID="$1"

	debug "checking if ID \"$ENTRY_ID\" is in blacklist \"$BLACKLIST\""
	IFS=';'
	for ITEM in $BLACKLIST
	do
		IFS="$OIFS"
		if [ "$ENTRY_ID" = "$ITEM" ]
		then
			debug "blacklist positive"
			return 1
		fi
	done
	IFS="$OIFS"
	return 0
}

check_entry_path(){
	# Determine if entry in given path is valid for execution in current DE
	local ENTRY_PATH=
	local DATA=
	local TRYEXEC=
	local EXEC0=
	local NOTSHOWIN=
	local ONLYSHOWIN=
	local HIDDEN=
	local FAIL=0

	ENTRY_PATH="$1"
	DATA="$(read_entry_path "$ENTRY_PATH")"

	debug "checking TryExec"
	TRYEXEC="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*TryExec[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	TRYEXEC="$(trim_spaces "$TRYEXEC")"
	if [ -n "$TRYEXEC" ]
	then
		debug "TryExec is defined"
		if command -v "$TRYEXEC" > /dev/null
		then
			debug "TryExec command $TRYEXEC exists"
		else
			debug "TryExec command $TRYEXEC not found"
			return 1
		fi
	else
		debug "TryExec not defined"
	fi

	debug "checking Exec[0]"
	EXEC0="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*Exec[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	EXEC0="$(trim_spaces "$EXEC0")"
	EXEC0="$(printf "%s" "$EXEC0" | execstring2zero 2>/dev/null | head -zn 1 | tr -d '\0')"
	if [ -n "$EXEC0" ] && command -v "$EXEC0" > /dev/null
	then
		debug "Exec[0] command $EXEC0 exists"
	else
		debug "Exec[0] command $EXEC0 not found"
		return 1
	fi

	debug "checking Hidden"
	HIDDEN="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*Hidden[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	HIDDEN="$(trim_spaces "$HIDDEN")"
	[ "$HIDDEN" = "true" ] && return 1

	debug "checking NotShowIn"
	NOTSHOWIN="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*NotShowIn[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	NOTSHOWIN="$(trim_spaces "$NOTSHOWIN")"
	IFS=';'
	for ITEM in $NOTSHOWIN
	do
		IFS=:
		for DESKTOP in $XDG_CURRENT_DESKTOP
		do
			IFS="$OIFS"
			debug "checking NotShowIn against \"$DESKTOP\"=\"$ITEM\""
			[ "$DESKTOP" = "$ITEM" ] && return 1
		done
	done
	IFS="$OIFS"

	debug "checking OnlyShowIn"
	ONLYSHOWIN="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*OnlyShowIn[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	ONLYSHOWIN="$(trim_spaces "$ONLYSHOWIN")"
	IFS=';'
	for ITEM in $ONLYSHOWIN
	do
		FAIL=1
		IFS=:
		for DESKTOP in $XDG_CURRENT_DESKTOP
		do
			IFS="$OIFS"
			debug "checking OnlyShowIn against \"$DESKTOP\"=\"$ITEM\""
			[ "$DESKTOP" = "$ITEM" ] && { FAIL=0 ; break ; }
		done
		[ "$FAIL" = "0" ] && break
	done
	IFS="$OIFS"
	[ "$FAIL" = "1" ] && return 1

	return 0
}

read_entry_path(){
	# read entry, only "Desktop Entry" section
	local ENTRY_PATH=
	local DE="0"
	ENTRY_PATH="$1"
	while read -r LINE
	do
		if printf "%s" "$LINE" | grep -q "^[[:space:]]*\["
		then
			printf "%s" "$LINE" | grep -qi "^[[:space:]]*\[Desktop Entry\]" && DE=1 || DE=0
		fi
		[ "$DE" = "1" ] &&  printf "%s\n" "$LINE"
	done < "${ENTRY_PATH}"
}

execstring2zero() {
	# converts argument string from stdin to zero-delimited list
	xargs sh -c 'NOT_FIRST_ARG=false ; for ARG in "$@" ; do [ "$NOT_FIRST_ARG" = "true" ] && printf "%b" "\0" ; printf "%s" "$ARG" ; NOT_FIRST_ARG=true ; done' arg-splitter
}

args2zero() {
	# converts argument array to zero-delimited list
	NOT_FIRST_ARG=false
	for ARG in "$@"
	do
		[ "$NOT_FIRST_ARG" = "true" ] && printf '%b' '\0'
		printf '%s' "$ARG"
		NOT_FIRST_ARG=true
	done
	unset NOT_FIRST_ARG
}

construct_argv() {
	# outputs zero-delimited full final argv list
	if [ "$#" = "0" ]
	then
		printf '%s' "$EXEC" | execstring2zero
	else
		printf '%s' "$EXEC" | execstring2zero
		if [ -n "$EXECARG" ]
		then
			printf '%b' '\0'
			printf '%s' "$EXECARG"
		fi
		printf '%b' '\0'
		args2zero "$@"
	fi
}

ENTRY_PATH="$(find_preferred_entry)"
[ -z "$ENTRY_PATH" ] && ENTRY_PATH="$(find_any_entry)"

if [ -n "$ENTRY_PATH" ]
then
	DATA="$(read_entry_path "$ENTRY_PATH")"
	EXEC="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*Exec[[:space:]]*=' | head -n 1 | cut -d = -f 2-)"
	EXEC="$(trim_spaces "$EXEC")"
	EXECARG="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*ExecArg[[:space:]]*=' | head -n 1)"
	EXECARG="$(trim_spaces "$EXECARG")"
	if [ -z "$EXECARG" ]
	then
		EXECARG="$(printf "%s\n" "$DATA" | grep '^[[:space:]]*X-ExecArg[[:space:]]*=' | head -n 1)"
		EXECARG="$(trim_spaces "$EXECARG")"
	fi
	[ -z "$EXECARG" ] && EXECARG="ExecArg=-e"
	EXECARG="$(printf "%s\n" "$EXECARG" | cut -d = -f 2-)"
	EXECARG="$(trim_spaces "$EXECARG")"
else
	EXEC="xterm"
	EXECARG="-e"
fi

debug "EXEC=$EXEC
EXECARG=$EXECARG
ARGV=$*
Final argv (per line):
$(construct_argv "$@" | tr '\0' "\n")
End of final argv"

# this variant is robust, but spawns xargs process instead of clean exec
#{
#	if [ "$#" = "0" ]
#	then
#		# prints out only arguments from Exec if any
#		printf '%s' "$EXEC" | execstring2zero | tail -zn +2
#	else
#		# grep adds zero byte at the end if anything was printed
#		printf '%s' "$EXEC" | execstring2zero | tail -zn +2 | grep -z .
#		if [ -n "$EXECARG" ]
#		then
#			printf '%s' "$EXECARG"
#			printf '%b' '\0'
#		fi
#		args2zero "$@"
#	fi
#} | exec xargs -o0 "$(printf '%s' "$EXEC" | execstring2zero | head -zn 1)"

# choose a character for IFS that is not present in command line
# use pseudographics to increase chances
for CHAR in '│' '─' '■' '▒' '○' '┼' '║' '╟' '╢'
do
	if [ "$(printf '%s' "$EXEC $EXECARG $*")" = "$(printf '%s' "$EXEC $EXECARG $*" | tr -d "$CHAR")" ]
	then
		debug "IFS=$CHAR"
		IFS="$CHAR"
		break
	fi
done

# feed exec a string, separated by chosen $IFS
exec $(
	construct_argv "$@" | tr '\0' "$IFS"
)
