#!/bin/sh

A2U__TERMINAL_HANDLER=xdg-terminal-exec
A2U__SELF_NAME=${0##*/}

# special characters
A2U__OIFS=$IFS
A2U__LF='
'
{
	read -r A2U__RSEP
	read -r A2U__USEP
	read -r A2U__CR
} <<- EOF
	$(printf '%b\n' '\036' '\037' '\r')
EOF

# Treat non-zero exit status from simple commands as an error
# Treat unset variables as errors when performing parameter expansion
# Disable pathname expansion
set -euf

shcat() {
	while IFS= read -r line; do
		printf '%s\n' "$line"
	done
}

usage() {
	shcat <<- EOF
		Usage:
		  $A2U__SELF_NAME \\
		    [-h | --help]
		    [-s a|b|s|custom.slice] \\$(
			case "$A2U__SELF_NAME" in
			*-scope | *-service) true ;;
			*)
				printf '\n'
				# shellcheck disable=SC1003
				printf '    %s\n' '[-t scope|service] \'
				;;
			esac
		)
		    [{-a app_name | -u unit_id}] \\
		    [-d description] \\
		    [-p Property=value] \\
		    [-S {out|err|both}] \\
		    [{-c|-C}] \\
		    [-T] \\$(
			case "$A2U__SELF_NAME" in
			*-open | *-open-scope | *-open-service) true ;;
			*)
				printf '\n'
				# shellcheck disable=SC1003
				printf '    %s\n' '[-O | --open ] \'
				;;
			esac
		)
		    [--fuzzel-compat] \\
		    [--test] \\
		    [--] $(
			case "$A2U__SELF_NAME" in
			*-open | *-open-scope | *-open-service) printf '%s\n' '{file|URL ...}' ;;
			*-term | *-terminal | *-term-scope | *-terminal-scope | *-term-service | *-terminal-service)
				printf '%s\n' '[entry-id.desktop | entry-id.desktop:action-id | command] [args ...]'
				;;
			*)
				printf '%s\n' '{entry-id.desktop | entry-id.desktop:action-id | command} [args ...]'
				;;
			esac
		)
	EOF
}

help() {
	shcat <<- EOF
		$A2U__SELF_NAME - Application launcher, file opener, default terminal launcher
		for systemd environments.

		Launches applications from Desktop Entries or arbitrary
		command lines, as systemd user scopes or services.

		$(usage)

		Options:

		  -s a|b|s|custom.slice
		    Select slice among short references:
		    a=app.slice b=background.slice s=session.slice
		    Or set slice explicitly.
		    Default and short references can be preset via APP2UNIT_SLICES env var in
		    the format above.

		  -t scope|service
		    Type of unit to launch. Can be preselected via APP2UNIT_TYPE env var and
		    if \$0 ends with '-scope' or '-service'.

		  -a app_name
		    Override substring of Unit ID representing application name.
		    Defaults to Entry ID without extension, or executable name.

		  -u unit_id
		    Override the whole Unit ID. Must match type. Defaults to recommended
		    templates:
		      app-\${desktop}-\${app_name}@\${random}.service
		      app-\${desktop}-\${app_name}-\${random}.scope

		  -d description
		    Set/override unit description. By default description is generated from
		    Entry's "Name=" and "GenericName=" keys.

		  -p Property=value
		    Set additional properties for unit.

	EOF
	case "$A2U__SELF_NAME" in
	*-term | *-terminal | *-term-scope | *-terminal-scope | *-term-service | *-terminal-service) true ;;
	*)
		shcat <<- EOF
			  -T
			    Force launch in terminal (${A2U__TERMINAL_HANDLER} is used). Any unknown option
			    starting with '-' after this will be passed to ${A2U__TERMINAL_HANDLER}.
			    Command may be omitted to just launch default terminal.
			    This mode can also be selected if \$0 ends with '-term' or '-terminal',
			    also optionally followed by '-scope' or '-service' unit type suffixes.

		EOF
		;;
	esac
	shcat <<- EOF
		  -S out|err|both
		    Silence stdout stderr or both.

		  -c
		    Do not add graphical-session.target dependency and ordering.
		    Also can be preset with APP2UNIT_PART_OF_GST=false.

		  -C
		    Add graphical-session.target dependency and ordering.
		    Also can be preset with APP2UNIT_PART_OF_GST=true.

	EOF
	case "$A2U__SELF_NAME" in
	*-open | *-open-scope | *-open-service) true ;;
	*)
		shcat <<- EOF
			  -O | --open (also selected by default if \$0 ends with '-open')
			    Opener mode: argument(s) are treated as file(s) or URL(s) to open.
			    Desktop Entry for them is found via xdg-mime. Only single association
			    is supported.
			    This mode can also be selected if \$0 ends with '-open', also optionally
			    followed by '-scope' or '-service' unit type suffixes.

			  --fuzzel-compat
			    For using in fuzzel like this:
			      fuzzel --launch-prefix='app2unit --fuzzel-compat --'

		EOF
		;;
	esac

	shcat <<- EOF
		  --test
		    Do not run anything, print command.

		  --
		    Disambiguate command from options.

	EOF

	case "$A2U__SELF_NAME" in
	*-open | *-open-scope | *-open-service)
		shcat <<- EOF
			File(s)|URL(s):

			  Objects to query xdg-mime for associations and open. The only
			  restriction is: all given objects should have the same association.
		EOF
		;;
	*)
		shcat <<- EOF
			Desktop Entry or Command:

			  Use Desktop Entry ID, optionally suffixed with Action ID:
			    entry-id.desktop
			    entry-id.desktop:action-id
			  Arguments should be supproted by Desktop Entry.

			  Or use a custom command, arguments will be passed as is.
		EOF
		;;
	esac
}

error() {
	# Print messages to stderr, send notification (only first arg) if stderr is not interactive
	printf '%s\n' "$@" >&2
	# if notify-send is installed and stderr is not a terminal, also send notification
	if [ ! -t 2 ] && command -v notify-send > /dev/null; then
		notify-send -u critical -i dialog-error -a "${A2U__SELF_NAME}" "Error" "$1"
	fi
}

warning() {
	# Print messages to stdout, send notification (only first arg) if stdout is not interactive
	printf '%s\n' "$@"
	# if notify-send is installed and stdout is not a terminal, also send notification
	if [ ! -t 1 ] && command -v notify-send > /dev/null; then
		notify-send -u normal -i dialog-warning -a "${A2U__SELF_NAME}" "Warning" "$1"
	fi
}

message() {
	# Print messages to stdout, send notification (only first arg) if stdout is not interactive
	printf '%s\n' "$@"
	# if notify-send is installed and stdout is not a terminal, also send notification
	if [ ! -t 1 ] && command -v notify-send > /dev/null; then
		notify-send -u normal -i dialog-information -a "${A2U__SELF_NAME}" "Info" "$1"
	fi
}

check_bool() {
	case "$1" in
	true | True | TRUE | yes | Yes | YES | 1) return 0 ;;
	false | False | FALSE | no | No | NO | 0) return 1 ;;
	*)
		error "Assuming '$1' means no"
		return 1
		;;
	esac
}

# Utility function to print debug messages to stderr (or not)
if check_bool "${APP2UNIT_DEBUG-${DEBUG-0}}"; then
	A2U__DEBUG=true
	debug() {
		# print each arg at new line, prefix each printed line with 'D: '
		while IFS= read -r debug_line; do
			printf 'D: %s\n' "$debug_line"
		done <<- EOF >&2
			$(printf '%s\n' "$@")
		EOF
	}
else
	A2U__DEBUG=
	debug() { :; }
fi

replace() {
	# takes $1, replaces $2 with $3
	# does it in large chunks
	# writes result to global A2U__REPLACED_STR to avoid $() newline issues

	# right part of string
	r_remainder=${1}
	A2U__REPLACED_STR=
	while [ -n "$r_remainder" ]; do
		# left part before first encounter of $2
		r_left=${r_remainder%%"$2"*}
		# append
		A2U__REPLACED_STR=${A2U__REPLACED_STR}$r_left
		case "$r_left" in
		# nothing left to cut
		"$r_remainder") break ;;
		esac
		# append replace substring
		A2U__REPLACED_STR=${A2U__REPLACED_STR}$3
		# cut remainder
		r_remainder=${r_remainder#*"$2"}
	done
}

normpath() {
	# lightly normalize paths for comparison purposes
	# write to A2U__NORMALIZED_PATH var
	case "$1" in
	/*) path_result= ;;
	*) path_result=${PWD} ;;
	esac
	IFS='/'
	for path_item in $1; do
		case "$path_item" in
		# ignore empty element or current dir
		'.' | '') true ;;
		# deal with parent dir
		'..')
			case "$path_result" in
			# nothing above root
			'/') true ;;
			# remove last component if more than one
			'/'*'/'*) path_result=${path_result%/*} ;;
			# last component, reduce to root
			/*) path_result='/' ;;
			esac
			;;
		*) path_result=${path_result}/${path_item} ;;
		esac
	done
	IFS=$A2U__OIFS
	debug "before normpath: $1" " after normpath: $path_result"
	A2U__NORMALIZED_PATH=$path_result
}

make_paths() {
	# constructs normalized A2U__APPLICATIONS_DIRS
	IFS=':'
	A2U__APPLICATIONS_DIRS=
	# Populate list of directories to search for entries in, in descending order of preference
	for dir in ${XDG_DATA_HOME:-${HOME}/.local/share}${IFS}${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do
		case "$dir" in
		/*) true ;;
		*)
			error "Non-absolute path in \$XDG_DATA_HOME:\$XDG_DATA_DIRS: $dir"
			exit 1
			;;
		esac
		# Normalise base path and append the data subdirectory with a trailing '/'
		normpath "${dir}"
		A2U__APPLICATIONS_DIRS=${A2U__APPLICATIONS_DIRS:+${A2U__APPLICATIONS_DIRS}:}${A2U__NORMALIZED_PATH}/applications/
	done
	IFS=$A2U__OIFS
}

find_entry() {
	# finds entry by ID
	# writes to A2U__FOUND_ENTRY_PATH var
	fe_find_entry_id=$1

	# start assembling find args
	set --

	# Append application directory paths to be searched
	IFS=':'
	for fe_directory in $A2U__APPLICATIONS_DIRS; do
		# Append '.' to delimit start of Entry ID
		set -- "$@" "$fe_directory".
	done

	# Find all files
	set -- "$@" -type f

	# Append path conditions per directory
	or_arg=
	for fe_directory in $A2U__APPLICATIONS_DIRS; do
		# Match full path with proper first character of Entry ID and .desktop extension
		# Reject paths with invalid characters in Entry ID
		set -- "$@" ${or_arg} '(' -path "$fe_directory"'./[a-zA-Z0-9_]*.desktop' ! -path "$fe_directory"'./*[^a-zA-Z0-9_./-]*' ')'
		or_arg='-o'
	done

	# iterate over found paths
	IFS=$A2U__OIFS
	while read -r fe_entry_path <&3; do
		# raw drop or parse and separate data dir path from entry
		case "$fe_entry_path" in
		# empties, just in case
		'' | */./) continue ;;
		# subdir, also replace / with -
		*/./*/*)
			replace "${fe_entry_path#*/./}" "/" "-"
			fe_entry_id=$A2U__REPLACED_STR
			;;
		# normal separation
		*/./*) fe_entry_id=${fe_entry_path#*/./} ;;
		esac
		# check ID
		case "$fe_entry_id" in
		"$fe_find_entry_id")
			A2U__FOUND_ENTRY_PATH=$fe_entry_path
			return 0
			;;
		esac
	done 3<<- EOP
		$(find -L "$@" 2> /dev/null)
	EOP

	error "Could not find entry '$fe_find_entry_id'!"
	return 1
}

de_expand_str() {
	# expands \s, \n, \t, \r, \\
	# https://specifications.freedesktop.org/desktop-entry-spec/latest/value-types.html
	# writes result to global $A2U__EXPANDED_STR in place to avoid $() expansion newline issues
	debug "expander received: $1"
	A2U__EXPANDED_STR=
	exp_remainder=$1
	while [ -n "$exp_remainder" ]; do
		# left is substring of remainder before the first encountered backslash
		exp_left=${exp_remainder%%\\*}

		# append left to A2U__EXPANDED_STR
		A2U__EXPANDED_STR=${A2U__EXPANDED_STR}${exp_left}
		debug "expander appended: $exp_left"

		case "$exp_left" in
		"$exp_remainder")
			debug "expander ended: $A2U__EXPANDED_STR"
			# no more backslashes left
			break
			;;
		esac

		# remove left substring and backslash from remainder
		exp_remainder=${exp_remainder#"$exp_left"\\}

		case "$exp_remainder" in
		# expand and append to A2U__EXPANDED_STR
		s*)
			A2U__EXPANDED_STR=${A2U__EXPANDED_STR}' '
			exp_remainder=${exp_remainder#?}
			debug "expander substituted space"
			;;
		n*)
			A2U__EXPANDED_STR=${A2U__EXPANDED_STR}$A2U__LF
			exp_remainder=${exp_remainder#?}
			debug "expander substituted newline"
			;;
		t*)
			A2U__EXPANDED_STR=${A2U__EXPANDED_STR}'	'
			exp_remainder=${exp_remainder#?}
			debug "expander substituted tab"
			;;
		r*)
			A2U__EXPANDED_STR=${A2U__EXPANDED_STR}${A2U__CR}
			exp_remainder=${exp_remainder#?}
			debug "expander substituted caret return"
			;;
		\\*)
			A2U__EXPANDED_STR=${A2U__EXPANDED_STR}\\
			exp_remainder=${exp_remainder#?}
			debug "expander substituted backslash"
			;;
		# unsupported sequence, reappend backslash
		#*)
		#	A2U__EXPANDED_STR=${A2U__EXPANDED_STR}\\
		#	debug 'expander reappended backslash'
		#	;;
		esac
	done
}

de_tokenize_exec() {
	# Shell-based DE Exec string tokenizer.
	# https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
	# How hard can it be?
	# Fills global A2U__EXEC_USEP var with $A2U__USEP-separated command array in place to avoid $() expansion newline issues
	debug "tokenizer received: $1"
	A2U__EXEC_USEP=
	tok_remainder=$1
	tok_quoted=0
	tok_in_space=0
	while [ -n "$tok_remainder" ]; do
		# left is substring of remainder before the first encountered special char
		tok_left=${tok_remainder%%[[:space:]\"\`\$\\\'\>\<\~\|\&\;\*\?\#\(\)]*}

		# left should be safe to append right away
		A2U__EXEC_USEP=${A2U__EXEC_USEP}${tok_left}
		debug "tokenizer appended: >$tok_left<"

		# end of the line
		case "$tok_remainder" in
		"$tok_left")
			debug "tokenizer is out of special chars"
			break
			;;
		esac

		# isolate special char
		tok_remainder=${tok_remainder#"$tok_left"}
		cut=${tok_remainder#?}
		tok_char=${tok_remainder%"$cut"}
		unset cut
		# cut it from remainder
		tok_remainder=${tok_remainder#"$tok_char"}

		# check if still in space
		case "${tok_in_space}${tok_left}${tok_char}" in
		1[[:space:]])
			debug "tokenizer still in space :) skipping space character"
			continue
			;;
		1*)
			debug "tokenizer no longer in space :("
			tok_in_space=0
			;;
		esac

		## decide what to do with the character
		# doublequote while quoted
		case "${tok_quoted}${tok_char}" in
		'1"')
			tok_quoted=0
			debug "tokenizer closed double quotes"
			continue
			;;
		# doublequote while unquoted
		'0"')
			tok_quoted=1
			debug "tokenizer opened double quotes"
			continue
			;;
		# error out on unquoted special chars
		0[\`\$\\\'\>\<\~\|\&\;\*\?\#\(\)])
			error "${A2U__ENTRY_ID}: Encountered unquoted character: '$tok_char'"
			return 1
			;;
		# error out on quoted but unescaped chars
		1[\`\$])
			error "${A2U__ENTRY_ID}: Encountered unescaped quoted character: '$tok_char'"
			return 1
			;;
		# process quoted escapes
		1\\)
			case "$tok_remainder" in
			# if there is no next char, fail
			'')
				error "${A2U__ENTRY_ID}: Dangling backslash encountered!"
				return 1
				;;
			# cut and append the next char right away
			# or a half of multibyte char, the other half should go into the next
			# 'tok_left' hopefully...
			*)
				cut=${tok_remainder#?}
				tok_char=${tok_remainder%"$cut"}
				tok_remainder=${cut}
				unset cut
				A2U__EXEC_USEP=${A2U__EXEC_USEP}${tok_char}
				debug "tokenizer appended escaped: >$tok_char<"
				;;
			esac
			;;
		# Consider Cosmos
		0[[:space:]])
			case "${tok_remainder}" in
			# there is non-space to follow
			*[![:space:]]*)
				# append separator
				A2U__EXEC_USEP=${A2U__EXEC_USEP}${A2U__USEP}
				tok_in_space=1
				debug "tokenizer entered spaaaaaace!!!! separator appended"
				;;
			# ignore unquoted space at the end of string
			*)
				debug "tokenizer entered outer spaaaaaace!!!! separator skipped, this is the end"
				break
				;;
			esac
			;;
		# append quoted chars
		1[[:space:]\'\>\<\~\|\&\;\*\?\#\(\)])
			A2U__EXEC_USEP=${A2U__EXEC_USEP}${tok_char}
			debug "tokenizer appended quoted char: >$tok_char<"
			;;
		# this should not happen
		*)
			error "${A2U__ENTRY_ID}: parsing error at char '$tok_char', (quoted: $tok_quoted)"
			return 1
			;;
		esac
	done
	case "$tok_quoted" in
	1)
		error "${A2U__ENTRY_ID}: Double quote was not closed!"
		return 1
		;;
	esac

	[ -n "$A2U__DEBUG" ] || return 0
	# shellcheck disable=SC2086
	debug "tokenizer ended:" "$(
		IFS=$A2U__USEP
		printf '  >%s<\n' $A2U__EXEC_USEP
	)"
}

de_inject_fields() {
	# Operates on argument array and $A2U__EXEC_RSEP_USEP from entry
	# modifies $A2U__EXEC_RSEP_USEP according to args/fields
	# no arguments, erase fields from $A2U__EXEC_RSEP_USEP
	exec_usep=
	fu_found=false
	exec_iter_usep=
	IFS=$A2U__USEP
	for arg in $A2U__EXEC_RSEP_USEP; do
		case "$arg" in
		# remove deprecated fields
		*[!%]'%'[dDnNvm]* | '%'[dDnNvm]*) debug "injector removed deprecated '$arg'" ;;
		# treat file fields
		*[!%]'%'[fFuU]* | '%'[fFuU]*)
			case "$fu_found" in
			true)
				error "${A2U__ENTRY_ID}: Encountered more than one %[fFuU] field!"
				return 1
				;;
			esac
			fu_found=true
			if [ "$#" -eq "0" ]; then
				debug "injector removed '$arg'"
				continue
			fi
			case "$arg" in
			*[!%]'%F'* | *'%F'?* | *[!%]'%U'* | *'%U'?*)
				error "${A2U__ENTRY_ID}: Encountered non-standalone field '$arg'"
				return 1
				;;
			*[!%]'%f'* | '%f'*)
				for carg in "$@"; do
					replace "$arg" "%f" "$carg"
					carg=$A2U__REPLACED_STR
					debug "injector adding '$arg' iteration as '$carg'"
					exec_iter_usep=${exec_iter_usep}${exec_iter_usep:+$A2U__USEP}${carg}
				done
				# placeholder arg
				exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}%%__ITER__%%
				;;
			'%F')
				for carg in "$@"; do
					debug "injector extending '$arg' with '$carg'"
					exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${carg}
				done
				;;
			*[!%]'%u'* | '%u'*)
				for carg in "$@"; do
					urlencode "$carg"
					carg=$A2U__URLENCODED_STRING
					replace "$arg" "%u" "$carg"
					carg=$A2U__REPLACED_STR
					debug "injector adding '$arg' iteration as '$carg'"
					exec_iter_usep=${exec_iter_usep}${exec_iter_usep:+$A2U__USEP}${carg}
				done
				# placeholder arg
				exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}%%__ITER__%%
				;;
			'%U')
				for carg in "$@"; do
					urlencode "$carg"
					carg=$A2U__URLENCODED_STRING
					debug "injector extending '$arg' with '$carg'"
					exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${carg}
				done
				;;
			*) error "${A2U__ENTRY_ID}: not implemented '$arg'" ;;
			esac
			;;
		# icon field
		*[!%]'%i'* | '%i'*)
			if [ -n "$A2U__ENTRY_ICON" ]; then
				replace "$arg" "%i" "$A2U__ENTRY_ICON"
				rarg=$A2U__REPLACED_STR
				debug "injector replacing '%i': '$arg' -> '$rarg'"
				exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${rarg}
			else
				debug "injector removed '$rarg'"
			fi
			;;
		# name field
		*[!%]'%c'* | '%c'*)
			replace "$arg" "%c" "$A2U__ENTRY_NAME"
			rarg=$A2U__REPLACED_STR
			debug "injector replacing '%c': '$arg' -> '$rarg'"
			exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${rarg}
			;;
		# literal %
		*[!%]%%* | %%*)
			replace "$arg" "%%" "%"
			rarg=$A2U__REPLACED_STR
			debug "injector replacing '%%': '$arg' -> '$rarg'"
			exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${rarg}
			;;
		# invalid field
		*%?* | *[!%]%)
			error "${A2U__ENTRY_ID}: unknown % field in argument '${arg}'"
			return 1
			;;
		*)
			debug "injector keeped: '$arg'"
			exec_usep=${exec_usep}${exec_usep:+$A2U__USEP}${arg}
			;;
		esac
	done
	# fill A2U__EXEC_RSEP_USEP with argument iterations
	if [ -n "$exec_iter_usep" ]; then
		A2U__EXEC_RSEP_USEP=
		for arg in $exec_iter_usep; do
			replace "$exec_usep" "%%__ITER__%%" "$arg"
			cmd=$A2U__REPLACED_STR
			A2U__EXEC_RSEP_USEP=${A2U__EXEC_RSEP_USEP}${A2U__EXEC_RSEP_USEP:+$A2U__RSEP}${cmd}
		done
	else
		A2U__EXEC_RSEP_USEP=$exec_usep
	fi
	IFS=$A2U__OIFS
}

parse_entry_key() {
	# set global vars or fail entry
	key=$1
	value=$2
	action=$3
	read_exec=$4
	in_main=$5
	in_action=$6

	case "${in_action};${key}" in
	'false;'* | 'true;Name' | 'true;Name['*']' | 'true;Exec' | 'true;Icon') true ;;
	*)
		error "${A2U__ENTRY_ID}: Encountered '$key' key inside action!"
		return 1
		;;
	esac

	case "$key" in
	Type)
		debug "captured '$key' '$value'"
		case "$value" in
		Application | Link) A2U__ENTRY_TYPE=$value ;;
		*)
			error "${A2U__ENTRY_ID}: Unsupported type '$value'!"
			return 1
			;;
		esac
		;;
	Actions)
		# `It is not valid to have an action group for an action identifier not mentioned in the Actions key.
		# Such an action group must be ignored by implementors.`
		# ignore if no action requested
		[ -z "$action" ] && return 0
		debug "checking for '$action' in Actions '$value'"
		IFS=';'
		for check_action in $value; do
			case "$check_action" in
			"$action")
				IFS=$A2U__OIFS
				action_listed=true
				return 0
				;;
			esac
		done
		error "${A2U__ENTRY_ID}: Action '$action' is not listed in entry!"
		return 1
		;;
	TryExec)
		if [ -z "$value" ]; then
			debug "ignored empty '$key'"
			return 0
		fi
		debug "checking TryExec executable '$value'"
		de_expand_str "$value"
		value=$A2U__EXPANDED_STR
		if ! command -v "$value" > /dev/null 2>&1; then
			error "${A2U__ENTRY_ID}: TryExec '$value' failed!"
			return 1
		fi
		;;
	Hidden)
		debug "checking boolean Hidden '$value'"
		case "$value" in
		true)
			error "${A2U__ENTRY_ID}: Entry is Hidden"
			return 1
			;;
		esac
		;;
	Exec)
		case "$read_exec" in
		false)
			debug "ignored Exec from wrong section"
			return 0
			;;
		esac
		case "$in_action" in
		true) action_exec=true ;;
		esac
		debug "read Exec '$value'"
		# skip acutal reading if array is already filled
		if [ -n "$A2U__EXEC_RSEP_USEP" ]; then
			debug "skipping re-filling exec array"
			return 0
		fi
		# expand string-level escape sequences
		de_expand_str "$value"
		# Split Exec and save as string delimited by unit separator
		de_tokenize_exec "$A2U__EXPANDED_STR"
		A2U__EXEC_RSEP_USEP=$A2U__EXEC_USEP
		# get Exec[0]
		IFS=$A2U__USEP read -r exec0 _rest <<- EOCMD
			$A2U__EXEC_RSEP_USEP
		EOCMD
		case "$exec0" in
		'')
			error "${A2U__ENTRY_ID}: Could not extract Exec[0]!"
			return 1
			;;
		*/*)
			A2U__EXEC_NAME=${exec0##*/}
			A2U__EXEC_PATH=${exec0}
			;;
		*) A2U__EXEC_NAME=${exec0} ;;
		esac
		debug "checking Exec[0] executable '${A2U__EXEC_PATH:-$A2U__EXEC_NAME}'"
		if ! command -v "${A2U__EXEC_PATH:-$A2U__EXEC_NAME}" > /dev/null 2>&1; then
			error "${A2U__ENTRY_ID}: Exec command '${A2U__EXEC_PATH:-$A2U__EXEC_NAME}' not found"
			return 127
		fi
		;;
	URL)
		debug "captured '$key' '$value'"
		de_expand_str "$value"
		A2U__ENTRY_URL=$A2U__EXPANDED_STR
		;;
	"Name[${A2U__LCODE}]")
		case "${in_main}_${in_action}_${value}" in
		true_false_ | false_true_) debug "discarded empty '$key'" ;;
		true_false_*)
			debug "captured '$key' '$value'"
			de_expand_str "$value"
			A2U__ENTRY_LNAME=$A2U__EXPANDED_STR
			;;
		false_true_*)
			debug "captured '$key' '$value'"
			de_expand_str "$value"
			A2U__ENTRY_LNAME_ACTION=$A2U__EXPANDED_STR
			;;
		*) debug "discarded '$key' '$value'" ;;
		esac
		;;
	Name)
		case "${in_main}_${in_action}_${value}" in
		true_false_ | false_true_) debug "discarded empty '$key'" ;;
		true_false_*)
			debug "captured '$key' '$value'"
			de_expand_str "$value"
			A2U__ENTRY_NAME=$A2U__EXPANDED_STR
			;;
		false_true_*)
			debug "captured '$key' '$value'"
			de_expand_str "$value"
			A2U__ENTRY_NAME_ACTION=$A2U__EXPANDED_STR
			;;
		*) debug "discarded '$key' '$value'" ;;
		esac
		;;
	"GenericName[${A2U__LCODE}]")
		debug "captured '$key' '$value'"
		de_expand_str "$value"
		A2U__ENTRY_LCOMMENT=$A2U__EXPANDED_STR
		;;
	GenericName)
		debug "captured '$key' '$value'"
		de_expand_str "$value"
		A2U__ENTRY_COMMENT=$A2U__EXPANDED_STR
		;;
	Icon)
		if [ -n "$value" ] && { [ "$in_main" = "true" ] || [ "$in_action" = "true" ]; }; then
			debug "captured '$key' '$value'"
			de_expand_str "$value"
			A2U__ENTRY_ICON=$A2U__EXPANDED_STR
		else
			debug "discarded '$key' '$value'"
		fi
		;;
	Path)
		if [ -z "$value" ]; then
			debug "ignored empty '$key'"
			return 0
		fi
		debug "captured '$key' '$value'"
		de_expand_str "$value"
		A2U__ENTRY_WORKDIR=$A2U__EXPANDED_STR
		if [ ! -e "$A2U__ENTRY_WORKDIR" ]; then
			error "${A2U__ENTRY_ID}: Requested 'Path' '${A2U__ENTRY_WORKDIR}' does not exist!"
			return 1
		elif [ ! -d "$A2U__ENTRY_WORKDIR" ]; then
			error "${A2U__ENTRY_ID}: Requested 'Path' '${A2U__ENTRY_WORKDIR}' is not a directory!"
			return 1
		fi
		;;
	Terminal)
		debug "captured '$key' '$value'"
		case "$A2U__FUZZEL_COMPAT" in
		true)
			debug "ignoring Terminal in fuzzel compat mode"
			return 0
			;;
		esac
		case "$value" in
		true)
			# if terminal was not requested explicitly, check terminal handler
			case "$A2U__TERMINAL" in
			false) check_terminal_handler ;;
			esac
			A2U__TERMINAL=true
			;;
		esac
		;;
	esac
	# By default unrecognised keys, empty lines and comments get ignored
}

read_entry_path() {
	# Read entry from given path
	entry_path="$1"
	entry_action="${2-}"
	read_exec=false
	action_listed=false
	in_main=false
	in_action=false
	action_exec=false
	break_on_next_section=false
	# shellcheck disable=SC2016
	debug "reading desktop entry '$entry_path'${entry_action:+ action '$entry_action'}"
	# Let `read` trim leading/trailing whitespace from the line
	while read -r line; do
		case $line in
		# `There should be nothing preceding [the Desktop Entry group] in the desktop entry file but [comments]`
		# if entry_action is not requested, allow reading Exec right away from the main group
		'[Desktop Entry]'*)
			debug "entered section: $line"
			in_main=true
			if [ -z "$entry_action" ]; then
				read_exec=true
				break_on_next_section=true
			fi
			;;
		# A `Key=Value` pair
		[a-zA-Z0-9-]*=*)
			# Split
			IFS='=' read -r key value <<- EOL
				$line
			EOL
			# Trim
			{ read -r key && read -r value; } <<- EOL
				$key
				$value
			EOL
			# Parse key, or abort
			parse_entry_key "$key" "$value" "$entry_action" "$read_exec" "$in_main" "$in_action" || return 1
			;;
		# found requested action, allow reading Exec
		"[Desktop Action ${entry_action}]"*)
			debug "entered section: $line"
			in_main=false
			break_on_next_section=true
			case "$action_listed" in
			true)
				read_exec=true
				in_action=true
				;;
			*)
				error "${A2U__ENTRY_ID}: Action '$entry_action' is not listed in Actions key!"
				return 1
				;;
			esac
			;;
		# Start of the next group header, stop if already read exec
		'['*)
			debug "entered section: $line"
			[ "$break_on_next_section" = "true" ] && break
			in_main=false
			in_action=false
			read_exec=false
			;;
		esac
		# By default empty lines and comments get ignored
	done < "$entry_path"

	# check for required things for action
	if [ -n "$entry_action" ]; then
		case "$action_listed" in
		true) true ;;
		*)
			error "${A2U__ENTRY_ID}: Action '$entry_action' is not listed in Actions key or does not exist!"
			return 1
			;;
		esac
		if [ "$action_exec" != "true" ] || [ -z "${A2U__ENTRY_LNAME_ACTION:-${A2U__ENTRY_NAME_ACTION:-}}" ]; then
			error "${A2U__ENTRY_ID}: Action '$entry_action' is incomplete"
			return 1
		fi
	fi

	# check for required things for types
	case "${A2U__ENTRY_TYPE};;${A2U__EXEC_RSEP_USEP};;${A2U__ENTRY_URL}" in
	'Application;;'?*';;' | 'Link;;;;'?*) true ;;
	';;'*)
		error "${A2U__ENTRY_ID}: type not specified!"
		return 1
		;;
	*)
		error "${A2U__ENTRY_ID}: type and keys mismatch: '$A2U__ENTRY_TYPE', Exec is$([ -z "${A2U__EXEC_RSEP_USEP}" ] && echo ' not') set, URL is$([ -z "${A2U__ENTRY_URL}" ] && echo ' not') set"
		return 1
		;;
	esac
}

random_string() {
	# gets random 8 hex characters
	LC_ALL=C tr -dc '0-9a-f' < /dev/urandom 2> /dev/null | head -c 8
}

validate_entry_id() {
	# validates Entry ID ($1)

	case "$1" in
	# invalid characters or degrees of emptiness
	*[!a-zA-Z0-9_.-]* | *[!a-zA-Z0-9_.-] | [!a-zA-Z0-9_.-]* | [!a-zA-Z0-9_.-] | '' | .desktop)
		debug "string not valid as Entry ID: '$1'"
		return 1
		;;
	# all that left with .desktop
	*.desktop) return 0 ;;
	# and without
	*)
		debug "string not valid as Entry ID '$1'"
		return 1
		;;
	esac
}

validate_action_id() {
	# validates action ID ($1)

	case "$1" in
	# empty is ok
	'') return 0 ;;
	# invalid characters
	*[!a-zA-Z0-9-]* | *[!a-zA-Z0-9-] | [!a-zA-Z0-9-]* | [!a-zA-Z0-9-])
		debug "string not valid as Action ID: '$1'"
		return 1
		;;
	# all that left
	*) return 0 ;;
	esac
}

urlencode() {
	# pretty dumb urlencode
	# writes to A2U__URLENCODED_STRING var
	ue_string=$1
	case "$ue_string" in
	# assuming already url
	*[a-zA-Z0-9_-]://*)
		A2U__URLENCODED_STRING=$ue_string
		return
		;;
	# assuming absolute path
	/*) true ;;
	# assuming relative path
	*) ue_string=${PWD}/$ue_string ;;
	esac

	A2U__URLENCODED_STRING='file://'

	case "$ue_string" in
	# if contains extra chars, encode
	*[!._~0-9A-Za-z/-]*)
		while [ -n "$ue_string" ]; do
			ue_right=${ue_string#?}
			ue_char=${ue_string%"$ue_right"}
			debug "urlencode string $ue_string" "urlencode right $ue_right" "urlencode char $ue_char"
			case $ue_char in
			[._~0-9A-Za-z/-]) A2U__URLENCODED_STRING=${A2U__URLENCODED_STRING}$ue_char ;;
			*) A2U__URLENCODED_STRING=${A2U__URLENCODED_STRING}$(printf '%%%02x' "'$ue_char") ;;
			esac
			ue_string=$ue_right
		done
		;;
	# no extra chars, append as is
	*) A2U__URLENCODED_STRING=${A2U__URLENCODED_STRING}$ue_string ;;
	esac
}

gen_unit_id() {
	# generate Unit ID based on Entry ID or exec name if A2U__UNIT_ID is not already set
	# sets A2U__UNIT_ID

	if [ -z "$A2U__UNIT_ID" ]; then
		if [ -z "$A2U__UNIT_APP_SUBSTRING" ] && [ -n "${A2U__ENTRY_ID}" ]; then
			A2U__UNIT_APP_SUBSTRING=${A2U__ENTRY_ID%.desktop}
		elif [ -z "$A2U__UNIT_APP_SUBSTRING" ]; then
			A2U__UNIT_APP_SUBSTRING=${A2U__EXEC_NAME}
		fi
		if [ -n "${XDG_SESSION_DESKTOP:-}" ]; then
			A2U__UNIT_DESKTOP_SUBSTRING=${XDG_SESSION_DESKTOP}
		elif [ -n "${XDG_CURRENT_DESKTOP:-}" ]; then
			A2U__UNIT_DESKTOP_SUBSTRING=${XDG_CURRENT_DESKTOP%%:*}
		else
			A2U__UNIT_DESKTOP_SUBSTRING=NoDesktop
		fi
		# escape substrings if needed
		case "${A2U__UNIT_DESKTOP_SUBSTRING}${A2U__UNIT_APP_SUBSTRING}" in
		*[!a-zA-Z:_.]*)
			# prepend a character to shield potential . from being first
			read -r A2U__UNIT_DESKTOP_SUBSTRING A2U__UNIT_APP_SUBSTRING <<- EOL
				$(systemd-escape "A$A2U__UNIT_DESKTOP_SUBSTRING" "A$A2U__UNIT_APP_SUBSTRING")
			EOL
			# remove character
			A2U__UNIT_DESKTOP_SUBSTRING=${A2U__UNIT_DESKTOP_SUBSTRING#A}
			A2U__UNIT_APP_SUBSTRING=${A2U__UNIT_APP_SUBSTRING#A}
			;;
		esac

		A2U__RANDOM_STRING=$(random_string)
		case "$A2U__UNIT_TYPE" in
		service)
			A2U__UNIT_ID="app-${A2U__UNIT_DESKTOP_SUBSTRING}-${A2U__UNIT_APP_SUBSTRING}@${A2U__RANDOM_STRING}.service"
			;;
		scope)
			A2U__UNIT_ID="app-${A2U__UNIT_DESKTOP_SUBSTRING}-${A2U__UNIT_APP_SUBSTRING}-${A2U__RANDOM_STRING}.scope"
			;;
		*)
			error "Unsupported unit type '$A2U__UNIT_TYPE'!"
			return 1
			;;
		esac
	else
		case "$A2U__UNIT_ID" in
		*?".$A2U__UNIT_TYPE") true ;;
		*)
			error "Unit ID '$A2U__UNIT_ID' is not of type '$A2U__UNIT_TYPE'"
			return 1
			;;
		esac
	fi
	if [ "${#A2U__UNIT_ID}" -gt "254" ]; then
		error "Unit ID too long (${#A2U__UNIT_ID})!: $A2U__UNIT_ID"
		return 1
	fi
	case "$A2U__UNIT_ID" in
	.service | .scope | '')
		error "Unit ID is empty!"
		return 1
		;;
	*.service | *.scope) true ;;
	*)
		error "Invalid Unit ID '$A2U__UNIT_ID'!"
		return 1
		;;
	esac
}

randomize_unit_id() {
	# updates random string in existing A2U__UNIT_ID

	if [ -z "$A2U__RANDOM_STRING" ]; then
		debug "refusing to randomize unit ID"
		return 0
	fi
	A2U__NEW_RANDOM_STRING=$(random_string)
	debug "new random string: $A2U__NEW_RANDOM_STRING"
	A2U__UNIT_ID=${A2U__UNIT_ID%"${A2U__RANDOM_STRING}.${A2U__UNIT_TYPE}"}${A2U__NEW_RANDOM_STRING}.${A2U__UNIT_TYPE}
	#"
	A2U__RANDOM_STRING=${A2U__NEW_RANDOM_STRING}
}

systemd_run() {
	# wrapper for systemd-run
	# prepend common args
	A2U__UNIT_SLICE_ID=${A2U__UNIT_SLICE_ID:-app-graphical.slice}
	if [ -z "$A2U__UNIT_DESCRIPTION" ] && [ -n "${A2U__ENTRY_LNAME:-$A2U__ENTRY_NAME}" ] && [ -n "${A2U__ENTRY_LCOMMENT:-$A2U__ENTRY_COMMENT}" ]; then
		A2U__UNIT_DESCRIPTION="${A2U__ENTRY_LNAME:-$A2U__ENTRY_NAME} - ${A2U__ENTRY_LCOMMENT:-$A2U__ENTRY_COMMENT}"
	elif [ -z "$A2U__UNIT_DESCRIPTION" ] && [ -n "${A2U__ENTRY_LNAME:-$A2U__ENTRY_NAME}" ]; then
		A2U__UNIT_DESCRIPTION="${A2U__ENTRY_LNAME:-$A2U__ENTRY_NAME}"
	elif [ -z "$A2U__UNIT_DESCRIPTION" ] && [ -n "$A2U__EXEC_NAME" ]; then
		A2U__UNIT_DESCRIPTION=${A2U__EXEC_NAME}
	fi

	set -- \
		--slice="$A2U__UNIT_SLICE_ID" \
		--unit="$A2U__UNIT_ID" \
		--description="$A2U__UNIT_DESCRIPTION" \
		--quiet \
		--collect \
		-- "$@"

	# prepend extra properties
	IFS=${A2U__USEP}
	for prop in $A2U__UNIT_PROPERTIES; do
		set -- "--property=${prop}" "$@"
	done
	IFS=${A2U__OIFS}

	if [ "$A2U__PART_OF_GST" = "true" ]; then
		# prepend graphical session dependency/ordering args
		set -- \
			--property=After=graphical-session.target \
			--property=PartOf=graphical-session.target \
			"$@"
	fi

	if [ -n "$A2U__ENTRY_WORKDIR" ]; then
		# prepend requested Path or samedir
		set -- "--working-directory=${A2U__ENTRY_WORKDIR}" "$@"
	else
		set -- --same-dir "$@"
	fi

	# prepend unit type-dependent args
	case "$A2U__UNIT_TYPE" in
	scope) set -- --scope "$@" ;;
	service)
		set -- --property=Type=exec --property=ExitType=cgroup "$@"
		# silence service
		case "$A2U__SILENT" in
		# silence out
		out)
			set -- --property=StandardOutput=null "$@"
			# unsilence stderr if it is inheriting
			dso=
			dse=
			while IFS='=' read -r key value; do
				case "$key" in
				DefaultStandardOutput) dso=$value ;;
				DefaultStandardError) dse=$value ;;
				esac
			done <<- EOF
				$(systemctl --user show --property DefaultStandardOutput --property DefaultStandardError)
			EOF
			case "$dse" in
			inherit) set -- --property=StandardError="$dso" "$@" ;;
			esac
			;;
		# silence err
		err) set -- --property=StandardError=null "$@" ;;
		# silence both
		both) set -- --property=StandardOutput=null --property=StandardError=null "$@" ;;
		esac
		;;
	esac

	[ -z "$A2U__DEBUG" ] || debug "systemd run" "$(printf '  >%s<\n' systemd-run "$@")"

	# print args in test mode
	case "$A2U__TEST_MODE" in
	true)
		printf '%s\n' 'Command and arguments:'
		printf '  >%s<\n' systemd-run --user "$@"
		exit 0
		;;
	esac

	# silence scope output
	case "${A2U__UNIT_TYPE}_${A2U__SILENT}" in
	scope_out) exec > /dev/null ;;
	scope_err) exec 2> /dev/null ;;
	scope_both) exec > /dev/null 2>&1 ;;
	esac

	# exec
	exec systemd-run --user "$@"
}

parse_main_arg() {
	# fills some of global variables depending on main arg $1
	A2U__MAIN_ARG=$1

	A2U__ENTRY_ID=
	A2U__ENTRY_ACTION=
	A2U__ENTRY_PATH=
	A2U__EXEC_NAME=
	A2U__EXEC_PATH=

	case "$A2U__MAIN_ARG" in
	'')
		error "Empty main argument"
		return 1
		;;
	*.desktop:*)
		IFS=':' read -r A2U__ENTRY_ID A2U__ENTRY_ACTION <<- EOA
			$A2U__MAIN_ARG
		EOA
		;;
	*.desktop)
		A2U__ENTRY_ID=$A2U__MAIN_ARG
		A2U__ENTRY_ACTION=
		;;
	esac
	debug "A2U__ENTRY_ID: $A2U__ENTRY_ID" "A2U__ENTRY_ACTION: $A2U__ENTRY_ACTION"

	if [ -n "$A2U__ENTRY_ID" ]; then
		case "$A2U__ENTRY_ID" in
		*/*)
			# this is a path
			A2U__ENTRY_PATH=$A2U__ENTRY_ID
			A2U__ENTRY_ID=${A2U__ENTRY_ID##*/}
			if [ ! -f "$A2U__ENTRY_PATH" ]; then
				error "File not found: '$A2U__ENTRY_PATH'"
				return 127
			fi
			;;
		esac

		if ! validate_entry_id "$A2U__ENTRY_ID"; then
			if [ -z "$A2U__ENTRY_PATH" ]; then
				error "Invalid Entry ID '$A2U__ENTRY_ID'!"
				return 1
			else
				warning "Invalid Entry ID '$A2U__ENTRY_ID'!"
			fi
		fi
		if ! validate_action_id "$A2U__ENTRY_ACTION"; then
			error "Invalid Entry Action ID '$A2U__ENTRY_ACTION'!"
			return 1
		fi
		return 0
	fi

	# what's left is executable
	case "$A2U__MAIN_ARG" in
	*/*)
		A2U__EXEC_PATH=$A2U__MAIN_ARG
		A2U__EXEC_NAME=${A2U__EXEC_PATH##*/}
		debug "A2U__EXEC_PATH: $A2U__EXEC_PATH" "A2U__EXEC_NAME: $A2U__EXEC_NAME"
		if [ ! -f "$A2U__EXEC_PATH" ]; then
			error "File not found: '$A2U__EXEC_PATH'"
			return 127
		fi
		if [ ! -x "$A2U__EXEC_PATH" ]; then
			error "File is not executable: '$A2U__EXEC_PATH'"
			return 1
		fi
		return
		;;
	esac

	A2U__EXEC_NAME=$A2U__MAIN_ARG
	debug "A2U__EXEC_NAME: $A2U__EXEC_NAME"
	if ! command -v "$A2U__EXEC_NAME" > /dev/null 2>&1; then
		error "Executable not found: '$A2U__EXEC_NAME'"
		return 127
	fi
}

check_terminal_handler() {
	# checks terminal handler availability
	if ! command -v "$A2U__TERMINAL_HANDLER" > /dev/null; then
		error "Terminal launch requested but '$A2U__TERMINAL_HANDLER' is unavailable!"
		exit 1
	fi
}

get_mime() {
	# gets mime type of file or url
	# writes to A2U__MIME var
	f_mime=
	case "$1" in
	[a-zA-Z]*:*)
		IFS=':' read -r scheme _rest <<- EOF
			$1
		EOF
		debug "potential scheme '$scheme'"
		case "$scheme" in
		*[!a-zA-Z0-9+.-]*)
			debug "not a valid scheme '$scheme', assuming file"
			f_mime=$(xdg-mime query filetype "$1")
			;;
		*) f_mime=x-scheme-handler/$scheme ;;
		esac
		;;
	*) f_mime=$(xdg-mime query filetype "$1") ;;
	esac

	case "$f_mime" in
	'' | 'x-scheme-handler/')
		error "Could not query mime type for '$1'"
		return 1
		;;
	*)
		debug "got mime '$f_mime' for '$1'"
		A2U__MIME=$f_mime
		;;
	esac
}

get_assoc() {
	# gets file association for mime type
	# writes to A2U__ASSOC var
	f_assoc=$(xdg-mime query default "$1")
	case "$f_assoc" in
	?*.desktop)
		debug "got association '$f_assoc' for mime '$1'"
		A2U__ASSOC=$f_assoc
		;;
	*)
		error "Could not query association for mime '$1'"
		return 1
		;;
	esac
}

########################

[ -z "$A2U__DEBUG" ] || debug "initial args:" "$(printf '  >%s<\n' "$@")"

A2U__EXEC_NAME=
A2U__EXEC_PATH=
A2U__EXEC_RSEP_USEP=
A2U__ENTRY_PATH=
A2U__ENTRY_ID=
A2U__ENTRY_TYPE=
A2U__ENTRY_URL=
A2U__ENTRY_COMMENT=
A2U__ENTRY_NAME=
A2U__ENTRY_ICON=
A2U__ENTRY_WORKDIR=
A2U__UNIT_DESCRIPTION=
A2U__UNIT_ID=
A2U__UNIT_APP_SUBSTRING=
A2U__UNIT_PROPERTIES=
A2U__NORMALIZED_PATH=

A2U__SILENT=
A2U__TEST_MODE=false

# vars for expander, tokenizer, injector output
A2U__EXPANDED_STR=
A2U__EXEC_USEP=
A2U__REPLACED_STR=

A2U__UNIT_TYPE=${APP2UNIT_TYPE:-scope}
case "$A2U__UNIT_TYPE" in
service | scope) true ;;
*)
	error "Unsupported unit type '$A2U__UNIT_TYPE'!"
	exit 1
	;;
esac

# deal with unit slice choices and default
A2U__UNIT_SLICE_ID=
A2U__UNIT_SLICE_CHOICES=${APP2UNIT_SLICES:-"a=app.slice b=background.slice s=session.slice"}
for choice in $A2U__UNIT_SLICE_CHOICES; do
	debug "evaluating slice choice '$choice'"
	slice_abbr=
	slice_id=
	case "$choice" in
	*[!a-zA-Z0-9=._-]* | *=*=* | *[!a-z]*=* | *=[!a-zA-Z0-9._-]* | *[!.][!s][!l][!i][!c][!e])
		error "Invalid slice choice '$choice', ignoring."
		continue
		;;
	[a-z]*=[a-zA-Z0-9_.-]*.slice)
		IFS='=' read -r slice_abbr slice_id <<- EOF
			$choice
		EOF
		;;
	*)
		error "Invalid slice choice '$choice', ignoring."
		continue
		;;
	esac
	if [ -z "$A2U__UNIT_SLICE_ID" ]; then
		A2U__UNIT_SLICE_CHOICES=
		A2U__UNIT_SLICE_ID="${slice_id}"
		debug "reset default slice as '${slice_id}'"
	fi
	debug "adding choice ${slice_abbr}=${slice_id}"
	A2U__UNIT_SLICE_CHOICES=${A2U__UNIT_SLICE_CHOICES}${A2U__UNIT_SLICE_CHOICES:+ }${slice_abbr}=${slice_id}
done
if [ -z "$A2U__UNIT_SLICE_ID" ]; then
	A2U__UNIT_SLICE_ID=app.slice
	debug "falling back to default slice 'app.slice'"
fi

A2U__PART_OF_GST=true
if [ -z "${APP2UNIT_PART_OF_GST:-}" ]; then
	A2U__PART_OF_GST=true
else
	if check_bool "$APP2UNIT_PART_OF_GST"; then
		A2U__PART_OF_GST=true
	else
		A2U__PART_OF_GST=false
	fi
fi

A2U__TERMINAL=false
A2U__FUZZEL_COMPAT=false
A2U__OPENER_MODE=false

capture_terminal_args=false
case "$A2U__SELF_NAME" in
*-open | *-open-scope | *-open-service)
	A2U__OPENER_MODE=true
	case "$A2U__SELF_NAME" in
	*-scope) A2U__UNIT_TYPE=scope ;;
	*-service) A2U__UNIT_TYPE=service ;;
	esac
	;;
*-term | *-terminal | *-term-scope | *-terminal-scope | *-term-service | *-terminal-service)
	A2U__TERMINAL=true
	capture_terminal_args=true
	case "$A2U__SELF_NAME" in
	*-scope) A2U__UNIT_TYPE=scope ;;
	*-service) A2U__UNIT_TYPE=service ;;
	esac
	;;
esac

# will be set where needed
A2U__RANDOM_STRING=

A2U__LCODE=${LANGUAGE:-"$LANG"}
A2U__LCODE=${A2U__LCODE%_*}
A2U__LCODE=${A2U__LCODE:-NOLCODE}

# expand short args
first=true
found_delim=false
for arg in "$@"; do
	case "$first" in
	true)
		set --
		first=false
		;;
	esac
	case "$found_delim" in
	true)
		set -- "$@" "$arg"
		continue
		;;
	esac
	case "$arg" in
	--)
		found_delim=true
		set -- "$@" "$arg"
		;;
	-[a-zA-Z][a-zA-Z]*)
		arg=${arg#-}
		while [ -n "$arg" ]; do
			cut=${arg#?}
			char=${arg%"$cut"}
			set -- "$@" "-$char"
			arg=$cut
		done
		;;
	*) set -- "$@" "$arg" ;;
	esac
done

part_of_gst_set=false
# parse args
A2U__TERMINAL_ARGS_USEP=
while [ "$#" -gt "0" ]; do
	case "$1" in
	-h | --help)
		help
		exit 0
		;;
	-s)
		debug "arg '$1' '${2:-}'"
		case "${2:-}" in
		.slice | '')
			error "Empty slice id '${2:-}'" "$(usage)"
			exit 1
			;;
		*[!a-zA-Z0-9_.-]*)
			error "Invalid slice id '$2'" "$(usage)"
			exit 1
			;;
		*.slice)
			A2U__UNIT_SLICE_ID=$2
			shift 2
			continue
			;;
		*)
			for choice in $A2U__UNIT_SLICE_CHOICES; do
				IFS='=' read -r slice_abbr slice_id <<- EOF
					$choice
				EOF
				case "$slice_abbr" in
				"$2")
					A2U__UNIT_SLICE_ID=$slice_id
					shift 2
					continue 2
					;;
				esac
			done
			error "'$2' does not point to a slice choice!" "Choices: $A2U__UNIT_SLICE_CHOICES" "$(usage)"
			exit 1
			;;
		esac
		error "Failed to parse '-s' argument" "$(usage)"
		exit 1
		;;
	-t)
		debug "arg '$1' '${2:-}'"
		case "${2:-}" in
		scope | service) A2U__UNIT_TYPE=$2 ;;
		*)
			error "Expected unit type scope|service for -t, got '${2:-}'!" "$(usage)"
			exit 1
			;;
		esac
		shift 2
		;;
	-a)
		debug "arg '$1' '${2:-}'"
		if [ -z "${2:-}" ]; then
			error "Expected app name for -a!" "$(usage)"
			exit 1
		elif [ -n "$A2U__UNIT_ID" ]; then
			error "Conflicting options: -a, -u!" "$(usage)"
			exit 1
		else
			A2U__UNIT_APP_SUBSTRING=$2
		fi
		shift 2
		;;
	-u)
		debug "arg '$1' '${2:-}'"
		if [ -z "${2:-}" ]; then
			error "Expected Unit ID for -u!" "$(usage)"
			exit 1
		elif [ -n "$A2U__UNIT_APP_SUBSTRING" ]; then
			error "Conflicting options: -u, -a!" "$(usage)"
			exit 1
		else
			A2U__UNIT_ID=$2
		fi
		shift 2
		;;
	-d)
		debug "arg '$1' '${2:-}'"
		if [ -z "${2:-}" ]; then
			error "Expected unit description for -d!" "$(usage)"
			exit 1
		else
			A2U__UNIT_DESCRIPTION="$2"
		fi
		shift 2
		;;
	-c)
		case "$part_of_gst_set" in
		true)
			error "-c conflicts with -C" "$(usage)"
			exit 1
			;;
		esac
		debug "arg '$1'"
		A2U__PART_OF_GST=false
		part_of_gst_set=true
		shift
		;;
	-C)
		case "$part_of_gst_set" in
		true)
			error "-C conflicts with -c" "$(usage)"
			exit 1
			;;
		esac
		debug "arg '$1'"
		A2U__PART_OF_GST=true
		part_of_gst_set=true
		shift
		;;
	-S)
		debug "arg '$1' '${2:-}'"
		case "${2:-}" in
		out | err | both) A2U__SILENT=$2 ;;
		*)
			error "Expected silent mode out|err|both for -S, got '${2:-}'!" "$(usage)"
			exit 1
			;;
		esac
		shift 2
		;;
	-T)
		A2U__TERMINAL=true
		capture_terminal_args=true
		debug "arg '$1'"
		check_terminal_handler
		shift
		;;
	-O | --open)
		A2U__OPENER_MODE=true
		debug "arg '$1'"
		shift
		;;
	-p)
		debug "arg '$1' '${2:-}'"
		case "${2:-}" in
		'='*)
			error "Expected unit property assignment for -p, got '${2:-}'!" "$(usage)"
			;;
		*'='*)
			A2U__UNIT_PROPERTIES=${A2U__UNIT_PROPERTIES}${A2U__UNIT_PROPERTIES:+${A2U__USEP}}${2}
			;;
		*)
			error "Expected unit property assignment for -p, got '${2:-}'!" "$(usage)"
			exit 1
			;;
		esac
		shift 2
		;;
	--fuzzel-compat)
		A2U__FUZZEL_COMPAT=true
		debug "arg '$1'"
		shift
		;;
	--test)
		A2U__TEST_MODE=true
		debug "arg '$1'"
		shift
		;;
	--)
		debug "arg '$1', breaking"
		shift
		break
		;;
	-*)
		case "$capture_terminal_args" in
		false)
			error "Unknown option '$1'!" "$(usage)"
			exit 1
			;;
		true)
			debug "storing unknown opt '$1' for terminal"
			A2U__TERMINAL_ARGS_USEP=${A2U__TERMINAL_ARGS_USEP}${A2U__TERMINAL_ARGS_USEP:+$A2U__USEP}${1}
			shift
			;;
		esac
		;;
	*)
		debug "arg '$1', breaking"
		break
		;;
	esac
done

if [ "$#" -eq "0" ] && [ "$A2U__TERMINAL" = "false" ]; then
	error "Arguments expected" "$(usage)"
	exit 1
fi

if [ "$A2U__FUZZEL_COMPAT" = "true" ]; then
	if [ -z "${FUZZEL_DESKTOP_FILE_ID:-}" ]; then
		debug "no FUZZEL_DESKTOP_FILE_ID, cancelling A2U__FUZZEL_COMPAT"
		A2U__FUZZEL_COMPAT=false
	fi
	if [ "$A2U__OPENER_MODE" = "true" ]; then
		debug "opener mode, cancelling A2U__FUZZEL_COMPAT"
		A2U__FUZZEL_COMPAT=false
	fi
fi

if [ "$A2U__OPENER_MODE" = "true" ]; then
	if [ "$#" = "0" ]; then
		error "File(s) or URL(s) expected for open mode."
		exit 1
	fi
	A2U__MAIN_ARG=
	# determine if file or URL, get associations for A2U__MAIN_ARG
	for arg in "$@"; do
		get_mime "$arg"
		mime=$A2U__MIME
		get_assoc "$mime"
		assoc=$A2U__ASSOC
		if [ -z "$A2U__MAIN_ARG" ]; then
			debug "setting A2U__MAIN_ARG from association for '$arg': '$assoc'"
			A2U__MAIN_ARG=$assoc
		elif [ "$A2U__MAIN_ARG" = "$assoc" ]; then
			debug "arg '$arg' has the same association"
			true
		else
			error "Can not open multiple files/URLs with different associations"
			exit 1
		fi
	done
elif [ "$A2U__FUZZEL_COMPAT" = "true" ]; then
	debug "setting A2U__MAIN_ARG from FUZZEL_DESKTOP_FILE_ID: '$FUZZEL_DESKTOP_FILE_ID'" "passing arguments to exec array"
	A2U__MAIN_ARG=$FUZZEL_DESKTOP_FILE_ID
	# Fuzzel compat mode, awaiting for https://codeberg.org/dnkl/fuzzel/issues/292
	# ignore command from entry, take only metadata, use arg array as is.
	if ! command -v "$1" > /dev/null 2>&1; then
		error "Executable not found: '$1'"
		exit 127
	fi
	for item in "$@"; do
		A2U__EXEC_RSEP_USEP=${A2U__EXEC_RSEP_USEP}${item}${A2U__USEP}
	done
	A2U__EXEC_RSEP_USEP=${A2U__EXEC_RSEP_USEP%"$A2U__USEP"}
elif [ "$#" -eq "0" ] && [ "$A2U__TERMINAL" = "true" ]; then
	# special case for launching just terminal

	# get entry path and cmdline from terminal handler
	if path_and_cmd=$(
		IFS=$A2U__USEP
		# shellcheck disable=SC2086
		set -- $A2U__TERMINAL_ARGS_USEP
		IFS=$A2U__OIFS
		# prevent old xdg-terminal-exec from running anything
		unset DISPLAY WAYLAND_DISPLAY
		"$A2U__TERMINAL_HANDLER" --print-path --print-cmd='\037' "$@"
	) && case "$path_and_cmd" in '/'*".desktop$A2U__LF"* | '/'*'.desktop:'*"$A2U__LF"*) true ;; *) false ;; esac then
		# entry path and action before newline
		A2U__MAIN_ARG=${path_and_cmd%%"$A2U__LF"*}
		# cmd after newline, fill exec array right away
		A2U__EXEC_RSEP_USEP=${path_and_cmd#*"$A2U__LF"}
		# shellcheck disable=SC2086
		[ -z "$A2U__DEBUG" ] || debug "initial args:" "$(printf '  >%s<\n' "$@")" \
			"replaced A2U__MAIN_ARG with '$A2U__MAIN_ARG'" \
			"populated A2U__EXEC_RSEP_USEP with:" \
			"$(
				IFS=$A2U__USEP
				printf '  > %s\n' $A2U__EXEC_RSEP_USEP
			)"
	else
		# issue a warning to stderr
		{
			# shellcheck disable=SC2028
			echo "Could not determine default terminal entry via '$A2U__TERMINAL_HANDLER --print-path --print-cmd=\037'!"
			echo "Falling back to injecting '$A2U__TERMINAL_HANDLER' as the main argument."
		} >&2
		A2U__MAIN_ARG="$A2U__TERMINAL_HANDLER"
	fi
	A2U__TERMINAL=false
else
	A2U__MAIN_ARG=$1
	shift
fi
parse_main_arg "$A2U__MAIN_ARG"

if [ -n "$A2U__ENTRY_PATH" ]; then
	# reverse-deduce and correct Entry ID against applications dirs
	make_paths
	normpath "$A2U__ENTRY_PATH"
	A2U__ENTRY_PATH=${A2U__NORMALIZED_PATH}
	IFS=':'
	for dir in $A2U__APPLICATIONS_DIRS; do
		if [ "$A2U__ENTRY_PATH" != "${A2U__ENTRY_PATH#"$dir"}" ]; then
			A2U__ENTRY_ID_PRE=${A2U__ENTRY_PATH#"$dir"}
			debug "Processing Entry ID '$A2U__ENTRY_ID_PRE' as deduced in '$dir'"
			case "$A2U__ENTRY_ID_PRE" in
			*/*)
				replace "$A2U__ENTRY_ID_PRE" "/" "-"
				A2U__ENTRY_ID_PRE=$A2U__REPLACED_STR
				;;
			esac
			if validate_entry_id "$A2U__ENTRY_ID_PRE"; then
				A2U__ENTRY_ID=$A2U__ENTRY_ID_PRE
				debug "deduced Entry ID '$A2U__ENTRY_ID_PRE'"
			else
				error "Deduced Entry ID '$A2U__ENTRY_ID_PRE' is invalid!"
			fi
			break
		fi
	done
	IFS=$A2U__OIFS
elif [ -n "$A2U__ENTRY_ID" ]; then
	make_paths
	find_entry "$A2U__ENTRY_ID"
	A2U__ENTRY_PATH=$A2U__FOUND_ENTRY_PATH
fi

# read and parse entry, fill ENTRY_* vars and A2U__EXEC_RSEP_USEP
if [ -n "$A2U__ENTRY_PATH" ]; then
	read_entry_path "$A2U__ENTRY_PATH" "$A2U__ENTRY_ACTION"
fi

# handle Link type URL
if [ -n "$A2U__ENTRY_URL" ]; then
	debug "re-parsing for Link entry URL: $A2U__ENTRY_URL"
	get_mime "$A2U__ENTRY_URL"
	mime=$A2U__MIME
	get_assoc "$mime"
	assoc=$A2U__ASSOC
	# replace initial vars and arg
	A2U__ENTRY_ID=$assoc
	set -- "$A2U__ENTRY_URL"
	A2U__ENTRY_URL=
	# re-parse new entry
	find_entry "$A2U__ENTRY_ID"
	A2U__ENTRY_PATH=$A2U__FOUND_ENTRY_PATH
	read_entry_path "$A2U__ENTRY_PATH"
fi

# generate Unit ID as A2U__UNIT_ID
gen_unit_id

# compose and execute arguments
if [ -n "$A2U__ENTRY_ID" ]; then

	# do not bother with fields in fuzzel compat, since there are no open args
	case "$A2U__FUZZEL_COMPAT" in
	false) de_inject_fields "$@" ;;
	esac

	# deal with potential multiple iterations
	case "$A2U__EXEC_RSEP_USEP" in
	*"$A2U__RSEP"*)
		IFS=$A2U__RSEP
		first=true
		pids=
		for cmd in $A2U__EXEC_RSEP_USEP; do
			IFS=$A2U__USEP
			# shellcheck disable=SC2086
			set -- $cmd
			IFS=$A2U__OIFS
			case "$A2U__TERMINAL" in
			true)
				# inject terminal handler
				debug "injected $A2U__TERMINAL_HANDLER"
				IFS=$A2U__USEP
				# shellcheck disable=SC2086
				set -- "$A2U__TERMINAL_HANDLER" $A2U__TERMINAL_ARGS_USEP "$@"
				IFS=$A2U__OIFS
				;;
			esac
			[ -z "$A2U__DEBUG" ] || debug "entry iteration" "$(printf '  >%s<\n' "$@")"
			if [ "$first" = "false" ]; then
				randomize_unit_id
			fi
			systemd_run "$@" &
			pids=${pids}${pids:+ }$!
			first=false
		done
		ec=0
		# shellcheck disable=SC2086
		for pid in $pids; do
			wait $pid || ec=1
		done
		exit $ec
		;;
	*)
		IFS=$A2U__USEP
		# shellcheck disable=SC2086
		set -- $A2U__EXEC_RSEP_USEP
		IFS=$A2U__OIFS
		case "$A2U__TERMINAL" in
		true)
			# inject terminal handler
			debug "injected $A2U__TERMINAL_HANDLER"
			IFS=$A2U__USEP
			# shellcheck disable=SC2086
			set -- "$A2U__TERMINAL_HANDLER" $A2U__TERMINAL_ARGS_USEP "$@"
			IFS=$A2U__OIFS
			;;
		esac
		[ -z "$A2U__DEBUG" ] || debug "entry single" "$(printf '  >%s<\n' "$@")"
		systemd_run "$@"
		;;
	esac
else
	set -- "${A2U__EXEC_PATH:-$A2U__EXEC_NAME}" "$@"
	IFS=$A2U__OIFS
	case "$A2U__TERMINAL" in
	true)
		# inject terminal handler
		debug "injected $A2U__TERMINAL_HANDLER"
		IFS=$A2U__USEP
		# shellcheck disable=SC2086
		set -- "$A2U__TERMINAL_HANDLER" $A2U__TERMINAL_ARGS_USEP "$@"
		IFS=$A2U__OIFS
		;;
	esac
	[ -z "$A2U__DEBUG" ] || debug "command" "$(printf '  >%s<\n' "$@")"
	systemd_run "$@"
fi
