#!/bin/sh
# vim:noexpandtab tabstop=8 shiftwidth=0:

# rox-open - ROX-Filer's MIME-database-aware file and URI opener.
# mimeopen - xdg-open sub when XDG_DEFAULT_DESKTOP=X-Generic.
# (C) The Fatdog64 team 2025.
# Released under the GNU GPL2 license.

# rox-open (C) step 2022-2026
# license: GNU GPL2
# Depends: awk(1), file(1), realpath(1), package xdg-utils

version=2026.06.13

export TEXTDOMAIN=fatdog
export OUTPUT_CHARSET=UTF-8

rox=/usr/bin/rox # absolute path!

# /etc/profile.d/default-xdg-open.sh may export ROX_OPEN_* variables
rox_open_fallback_cmd="${ROX_OPEN_FALLBACK_CMD:-"/usr/bin/xdg-open"}"
rox_open_file_no_cwd="$ROX_OPEN_FILE_NO_CWD"
rox_open_https="$ROX_OPEN_HTTPS"
rox_open_quiet="$ROX_OPEN_QUIET"

A0="$0"

### config

# described in help below - pattern evaluated by awk
host_ere='www[.]'
in_between_ere='[^.]+[.]'
tld_ere='(com|net|org|au|uk)'
default_ere="^${host_ere}${in_between_ere}${tld_ere}"
[ "$rox_open_https" = 'off' ] &&
	https_ere= ||
	https_ere="${rox_open_https:-"$default_ere"}"

### help

usage() {
	( . gettext.sh
	ARG0=$0
	_CMD_="$rox_open_fallback_cmd"
	_ERE_="$default_ere"
	_PROG_="rox-open" # [sic]
	# help2man-friendly format, see `info help2man`
	eval_gettext 'Usage: [ENVIRONMENT] ${ARG0} PATH_OR_URI

${_PROG_} is a ROX-Filer-powered alternative to xdg-open.

Command-line options are ignored. Pass options via environment variables
using the env (1) command, or with settings in a shell profile file.

Environment:
  ROX_OPEN_FALLBACK_CMD=COMMAND  ~
    When ${_PROG_} does not know how to open PATH_OR_URI it delegates COMMAND.
    Current value: ${_CMD_}
  ROX_OPEN_FILE_NO_CWD=1         ~
    If PATH_OR_URI starts with "file://" but the next character is not "/",
    ${_PROG_} looks for a file in the current directory. This feature is
    non-standard but useful, especially within the st terminal. To disable
    this feature set ROX_OPEN_FILE_NO_CWD. Default unset.
  ROX_OPEN_HTTPS=ERE_PATTERN     ~
    If PATH_OR_URI looks like a web address but does not start with "https://",
    e.g. "www.google.com/maps", rox-open will not open it unless it matches
    the POSIX Extended Regular Expression ERE_PATTERN.
    Disable this feature altogether with ROX_OPEN_HTTPS="off".
    Current value: ${_ERE_}
  ROX_OPEN_QUIET=1               ~
    Do not display notifications. Default unset.
  XDG_DEFAULT_DESKTOP            ~
    Opt-in variable. Refer to the FILES section.

Files:
/etc/profile.d/default-xdg-open.sh
    Opt-in file. Also system-wide profile for ROX_OPEN_* variables.
')
}

can_rox_handle_scheme() { # $1-scheme $2-uri
	local scheme=$1 uri="$2"
	local p IFS

	# ROX handles scheme 'file' internally
	# cf. ROX git commit 07c22bda051720be7ac43f9ec5b092d07bae85d9
	[ "$scheme" = 'file' ] && return 0

	IFS=:
	for p in $XDG_CONFIG_DIRS $XDG_CONFIG_HOME; do
		[ -n "$p" -a -e "$p/rox.sourceforge.net/URI/$scheme" ] && return 0
	done
	return 1
}

default_app_from_rox_run_action() { # $1-mime_type         => $default_app
# Users can change the run action via ROX-Filer's right-click menu
	local typ sub p IFS

	IFS=/
	set -- $1
	typ=$1 sub=$2

	IFS=:
	for p in $XDG_CONFIG_HOME $XDG_CONFIG_DIRS; do
		default_app="$p/rox.sourceforge.net/MIME-types/${typ}_${sub}"
		[ -n "$p" -a -e "$default_app" ] && return 0
	done

	unset default_app
	return 1
}

infer_default_app() { # $1-path           => $default_app
	local path="$1"
	local bd=/usr/local/bin
	local app=

	# Method 1: use the file command; this is pretty much what
	# xdg-open does. file can return a MIME type/subtype different than
	# rox -m. Here we try to pair up the type a Fatdog default program.
	# https://www.iana.org/assignments/media-types/media-types.xhtml

	case "$(LANG=C command -p file -ib "$path")" in
	application/* ) : ;;
	audio/*       ) app="$bd/defaultmediaplayer" ;;
	example/*     ) : ;;
	font/*        ) : ;;
	haptics/*     ) : ;;
	image/*       ) app="$bd/defaultimageviewer" ;;
	message/*     ) : ;;
	model/*       ) : ;;
	multipart/*   ) : ;;
	text/*        ) app="$bd/defaulttexteditor" ;;
	video/*       ) app="$bd/defaultmediaplayer" ;;
	esac
	if [ -n "$app" ]; then
		default_app="$app"
		return 0
	fi
	unset default_app
	return 1
}

emit() { # $1-name $2-value
	[ "$rox_open_quiet" = 1 ] && return
	. gettext.sh
	set -- "$(export _CMD_="$rox_open_fallback_cmd"
		export _PROG_="$A0"
		export _NAME_="$1"
		export _VALUE_="$2"
		_WHAT_=
		case $1 in
		(mime-type)
			eval_gettext '$_PROG_: $_VALUE_: unknown MIME type bounced to $_CMD_.'
			export _WHAT_="$_VALUE_" ;;
		(scheme)
			eval_gettext '$_PROG_: $_VALUE_: unknown URI scheme bounced to $_CMD_.'
			export _WHAT_="${_VALUE_%%:*}" ;;
		(pass|*)
			eval_gettext '$_PROG_: $_VALUE_' ;;
		esac
		if [ -n "$_WHAT_" ]; then
			echo; eval_gettext \
			'Export ROX_OPEN_QUIET=1 to disable this message or teach ROX how to open "$_WHAT_".'
		fi)"
	[ -t 0 ] || env DISPLAY="${DISPLAY:-":0"}" notify-send "$*"
	printf '%s\n' "$@" >&2
}

exec_fallback() { # $1-path
	local path="$1"
	export ___ROX_OPEN_FAIL___=1
	[ "$XDG_DEFAULT_DESKTOP" = X-Generic ] && XDG_DEFAULT_DESKTOP=
	exec env "XDG_DEFAULT_DESKTOP=$XDG_DEFAULT_DESKTOP" \
		$rox_open_fallback_cmd "$path"
}

file_set_path() { # In: $path Out: $path Return: 1 if $path does not exist
	local p=${path#file://}
	case $p in (/*) [ -e "$p" ]; return $? ;; esac
	[ -z "$rox_open_file_no_cwd" ] || return 1
	[ -e "$PWD/$p" ] || return 1
	path="file://$PWD/$p"
}

follow_link() { # $1-linkname_path
	# Follow $1 stopping at the first match
	# of a /usr/local/bin/default* program.
	local h="${1:?}" p="$1" i=0
	[ "${h#/}" != "$h" ] || return 1
	while [ -L "$h" ] && [ "${h##*/usr/local/bin/default}" = "$h" ]; do
		: $((i=$i+1))
		[ $i -le 30 ] || return 2
		if ! p="$(readlink "$h")"; then return 3; fi
		[ "${p#/}" != "$p" ] && h="$p" || h="${h%/*}/$p"
	done
	echo "$h"
}

main() {

case "$1" in
	-h|--help|'')
		usage; exit ;;
	-v|--version)
		echo $version
		#### xdg-open is probing; we play along its expectations.

		#### (___ROX_OPEN_FAIL___ avoids infinite recursion).
		[ -n "$___ROX_OPEN_FAIL___" ] && exit 1
		#### X-Generic opt-in setting in /etc/profile.d/default-xdg-open.sh
		[ "$XDG_DEFAULT_DESKTOP" = X-Generic ] && exit 0
		#### This tells xdg-open to determine the default app on its own.
		exit 1
		;;
esac
unset ___ROX_OPEN_FAIL___

### Extract the path from command-line options.
# Remove the last positional parameter ($path) from
# the initial set of $n parameters $@, yielding $@'.
# throw out "$@" to keep compatibility with xdg-open.

n=$# i=1 path=
for path; do
	shift
	[ $i -eq $n ] && break
	set -- "$@" "$path"
	: $((i=$i+1))
done

### Got URI?

# $scheme will be non-empty if $path starts with a URI scheme
# https://www.rfc-editor.org/rfc/rfc3986#section-3.1 (scheme)
# scheme      = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
u="$path" scheme=
while [ "${u#[-+.[:alnum:]]}" != "$u" ]; do u="${u#?}"; done
[ "${u#"://"}" = "$u" ] || scheme="${path%"$u"}"
[ "${scheme#[[:alpha:]]}" != "$scheme" ] || unset scheme

if [ -n "$scheme" ]; then
	## Open URI schemes known to ROX.

	if can_rox_handle_scheme "$scheme" "$path"; then
		if [ "$scheme" = file ]; then
			if ! file_set_path; then
				emit pass "${path#file://}: $(gettext 'no such file')"
				exit 2
			fi
		fi
		exec $rox -U "$path"
	fi

	## Schemes unknown to ROX fall back to the standard xdg-open.

	emit scheme "$path"
	exec_fallback "$path"
fi

### Open custom URL pattern.

if [ -n "$https_ere" ] && [ ! -e "$path" ] &&
	awk -v "R=$https_ere" -v "P=$path" 'BEGIN{exit(P!~R)}';
then
	exec $rox -U "https://$path"
fi

### Open directory.

if [ -d "$path" ]; then
	## Open RoxApps

	if [ -e "$path/AppRun" ]; then
		exec $rox "$path"
	else
		# -d ensures that a link to a directory is opened with the link
		# name's path rather than the target's path
		exec $rox -d "$path"
	fi
fi

### Anything else is a (link to a) file.

if [ -n "$path" ]; then
	[ "${path#/}" != "$path" ] || path="$PWD/$path"
fi
if ! [ -e "$path" ]; then
	emit pass "$path: $(gettext 'no such file')"
	exit 2
fi

mime_type="$($rox -m "$path")"

if [ "$mime_type" = 'application/x_executable' -o -x "$path" ]; then
	exec "$path" 0< /dev/null
fi

if [ -n "$mime_type" ]; then

	# Why unsetting the XDG_DEFAULT_DESKTOP environment variable? Because
	# if - for any reason - the rox $mime_type handler ended up invoking
	# xdg-open, an endless loop of invocations would result. Some cases:
	#   1) handler is /usr/local/bin/defaultprogram itself (not a symlink to it).
	#   2) handler is any program that calls xdg-open.
	#   3) exec_fallback is called and ROX_OPEN_FALLBACK_CMD is xdg-open.

	## Plan A: ROX knowns the MIME type.

	if default_app_from_rox_run_action "$mime_type" ||
		infer_default_app "$path"
	then
		# Follow $default_app link stopping at the first
		# match of a /usr/local/bin/default* program.
		h="$(follow_link "$default_app")" || exit 1

		# 'defaultprogram' leads to the aforesaid invocation
		# loop because defaultprogram sets DEF_PROG=xdg-open.
		if [ "${h##*/}" = defaultprogram ]; then             # case 1)
			emit pass "$(gettext 'defaultprogram: invalid default program')"
			exit 1
		fi

		if [ "${h##*/usr/local/bin/default}" = "$h" ]; then  # case 2)
			[ "$XDG_DEFAULT_DESKTOP" = X-Generic ] && XDG_DEFAULT_DESKTOP=
		fi

		exec env "XDG_DEFAULT_DESKTOP=$XDG_DEFAULT_DESKTOP" \
			"$h" "$path" 0< /dev/null
	fi

	## Plan B: Fall back to the standard xdg-open otherwise.

	emit mime-type "$mime_type"

	if xdg_default="$(xdg-mime query default $mime_type)" &&
		[ -n "$xdg_default" ]
	then
		exec_fallback "$path"
	fi
fi

# Reaching this point is unlikely. ROX and xdg-mime would
# both need not to return a mime type at all, which I suspect
# they can only do if the system is severely misbehaving.

# 20250110 step: When xdg-open does not know how to open a local path it
# throws it to the browser. So do we. Modern browsers receiving a local
# path engage the default search engine, which isn't useful. However, if
# the path starts with "file://" the browser opens the local file.

[ "${path#/}" != "$path" ] || path="$PWD/$path"
uri="file://$path"

# Fatdog64 note: if the browser runs as spot, path access could
# be denied. This script cannot reliably detect that case so be it.

exec_fallback "$uri"

} # main()

main "$@"


