Files
InstanceBrick--nightcap/scripts/register_url_handler.sh
T

218 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright 2026 Mocktail Project Authors
# SPDX-License-Identifier: Apache-2.0
set -Eeuo pipefail
umask 077
readonly DESKTOP_ID="space.bigrat.mocktail.desktop"
readonly ROBLOX_SCHEME="x-scheme-handler/roblox"
readonly ROBLOX_PLAYER_SCHEME="x-scheme-handler/roblox-player"
Die() {
printf 'mocktail-url-handler: %s\n' "$*" >&2
exit 1
}
Usage() {
cat <<'EOF'
Usage: register_url_handler.sh MODE [OPTIONS]
Install a per-user Mocktail desktop entry for Roblox links.
Modes:
--install-only Install the desktop entry without changing defaults.
--set-default Install it and explicitly select Mocktail for both
roblox and roblox-player URL schemes.
Options:
--executable PATH Mocktail ELF or AppImage to launch. By default the
script finds the matching build or installed binary.
--desktop-file PATH Mocktail desktop template. By default the source-tree
or installed template beside this script is used.
-h, --help Show this help.
Run this command as the desktop user, never through sudo.
EOF
}
RequireValue() {
local option="$1"
local value="${2:-}"
[[ -n "${value}" && "${value}" != --* ]] ||
Die "${option} requires a value"
}
ResolveRegularFile() {
local path="$1"
local label="$2"
[[ -f "${path}" && -r "${path}" ]] ||
Die "${label} is not a readable regular file: ${path}"
realpath -e -- "${path}" 2>/dev/null ||
Die "cannot resolve ${label}: ${path}"
}
ResolveExecutable() {
local path="$1"
[[ -f "${path}" && -x "${path}" ]] ||
Die "Mocktail executable is not a regular executable file: ${path}"
realpath -e -- "${path}" 2>/dev/null ||
Die "cannot resolve Mocktail executable: ${path}"
}
mode=""
mocktail_executable=""
desktop_source=""
while (( $# > 0 )); do
case "$1" in
--install-only|--set-default)
[[ -z "${mode}" ]] || Die "select exactly one registration mode"
mode="$1"
shift
;;
--executable)
RequireValue "$1" "${2:-}"
mocktail_executable="$2"
shift 2
;;
--desktop-file)
RequireValue "$1" "${2:-}"
desktop_source="$2"
shift 2
;;
-h|--help)
Usage
exit 0
;;
*) Die "unknown option: $1" ;;
esac
done
[[ -n "${mode}" ]] || Die "select --install-only or --set-default"
(( EUID != 0 )) || Die "run this command as the desktop user, not root"
script_dir="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
if [[ -z "${mocktail_executable}" ]]; then
for candidate in \
"${script_dir}/mocktail" \
"${script_dir}/../build/mocktail"; do
if [[ -f "${candidate}" && -x "${candidate}" ]]; then
mocktail_executable="${candidate}"
break
fi
done
if [[ -z "${mocktail_executable}" ]]; then
mocktail_executable="$(command -v mocktail 2>/dev/null || true)"
fi
fi
[[ -n "${mocktail_executable}" ]] ||
Die "build or install Mocktail, or pass --executable PATH"
mocktail_executable="$(ResolveExecutable "${mocktail_executable}")"
source_tree_root="$(cd -P -- "${script_dir}/.." && pwd)"
launch_working_directory=""
if [[ "${mocktail_executable}" == \
"${source_tree_root}/build/mocktail" ]]; then
launch_working_directory="${source_tree_root}"
fi
# Exec is emitted without a shell. Keep the executable path to a
# conservative Desktop Entry token set so an unusual path cannot create an
# additional argument or field code. Users can move an AppImage to such a path
# before registering it.
if [[ ! "${mocktail_executable}" =~ ^/[[:alnum:]_./+:,@=-]+$ ]]; then
Die "Mocktail executable path contains unsupported desktop-entry characters"
fi
if [[ -z "${desktop_source}" ]]; then
for candidate in \
"${script_dir}/../packaging/${DESKTOP_ID}" \
"${script_dir}/../share/applications/${DESKTOP_ID}"; do
if [[ -f "${candidate}" && -r "${candidate}" ]]; then
desktop_source="${candidate}"
break
fi
done
fi
[[ -n "${desktop_source}" ]] ||
Die "cannot find the Mocktail desktop template; pass --desktop-file PATH"
desktop_source="$(ResolveRegularFile "${desktop_source}" "desktop template")"
[[ "$(grep -Fxc '[Desktop Entry]' "${desktop_source}")" == 1 &&
"$(grep -Fxc 'Type=Application' "${desktop_source}")" == 1 &&
"$(grep -Fxc 'Icon=space.bigrat.mocktail' "${desktop_source}")" == 1 &&
"$(grep -Fxc 'Exec=env SDL_VIDEODRIVER=wayland,x11 MOCKTAIL_SMALL_TEXTURE_UPSCALE=4 mocktail %u' \
"${desktop_source}")" == 1 &&
"$(grep -Fxc 'MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;' \
"${desktop_source}")" == 1 &&
"$(grep -Fxc 'X-Mocktail-Managed=true' "${desktop_source}")" == 1 &&
"$(grep -c '^Exec=' "${desktop_source}")" == 1 ]] ||
Die "desktop template does not match the Mocktail URL-handler contract"
[[ -n "${HOME:-}" && "${HOME}" == /* ]] ||
Die "HOME must be an absolute desktop-user path"
data_home="${XDG_DATA_HOME:-${HOME}/.local/share}"
[[ "${data_home}" == /* ]] || Die "XDG_DATA_HOME must be absolute"
applications_dir="${data_home}/applications"
mkdir -p -- "${applications_dir}"
applications_dir="$(realpath -e -- "${applications_dir}")" ||
Die "cannot resolve the per-user applications directory"
[[ -d "${applications_dir}" && ! -L "${applications_dir}" ]] ||
Die "per-user applications path is not a regular directory"
[[ "$(stat -c '%u' -- "${applications_dir}")" == "$(id -u)" ]] ||
Die "per-user applications directory is not owned by the current user"
desktop_target="${applications_dir}/${DESKTOP_ID}"
if [[ -e "${desktop_target}" || -L "${desktop_target}" ]]; then
[[ -f "${desktop_target}" && ! -L "${desktop_target}" &&
"$(stat -c '%u' -- "${desktop_target}")" == "$(id -u)" ]] ||
Die "refusing to replace an unsafe desktop entry: ${desktop_target}"
grep -Fxq 'X-Mocktail-Managed=true' "${desktop_target}" ||
Die "refusing to replace an unmanaged desktop entry: ${desktop_target}"
fi
temporary="$(mktemp "${applications_dir}/.${DESKTOP_ID}.XXXXXX.desktop")"
trap 'rm -f -- "${temporary}"' EXIT
while IFS= read -r line || [[ -n "${line}" ]]; do
if [[ "${line}" == Exec=* ]]; then
printf 'Exec=env SDL_VIDEODRIVER=wayland,x11 MOCKTAIL_SMALL_TEXTURE_UPSCALE=4 %s %%u\n' \
"${mocktail_executable}"
if [[ -n "${launch_working_directory}" ]]; then
printf 'Path=%s\n' "${launch_working_directory}"
fi
else
printf '%s\n' "${line}"
fi
done <"${desktop_source}" >"${temporary}"
chmod 0644 "${temporary}"
if command -v desktop-file-validate >/dev/null 2>&1; then
desktop-file-validate "${temporary}" ||
Die "generated desktop entry failed validation"
fi
mv -f -- "${temporary}" "${desktop_target}"
trap - EXIT
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q "${applications_dir}" ||
Die "could not refresh the per-user desktop database"
else
printf 'mocktail-url-handler: warning: update-desktop-database is unavailable\n' \
>&2
fi
if [[ "${mode}" == --set-default ]]; then
command -v xdg-mime >/dev/null 2>&1 ||
Die "xdg-mime is required to select the URL handler"
xdg-mime default "${DESKTOP_ID}" \
"${ROBLOX_SCHEME}" "${ROBLOX_PLAYER_SCHEME}" ||
Die "could not select Mocktail as the Roblox URL handler"
for scheme in "${ROBLOX_SCHEME}" "${ROBLOX_PLAYER_SCHEME}"; do
selected="$(xdg-mime query default "${scheme}" 2>/dev/null || true)"
[[ "${selected}" == "${DESKTOP_ID}" ]] ||
Die "desktop environment did not retain Mocktail for ${scheme}"
done
printf 'Mocktail now handles roblox and roblox-player links.\n'
else
printf 'Installed %s without changing URL-handler defaults.\n' \
"${desktop_target}"
fi