#!/bin/sh
# alloy-open — open a link, or say why it cannot.
#
# Alloy may ship without a browser. `BROWSER=none` is a supported build value
# and under [[alloy-byo-principle]] it may become the default, so "no browser
# installed" is a state the system has to handle rather than an error state.
# Measured on 2026-08-17, before this existed, that state looked like:
#
#   $ xdg-open https://example.com ; echo $?
#   /usr/sbin/xdg-open: line 1045: www-browser: command not found
#   ...five more of those...
#   xdg-open: no method available for opening 'https://example.com'
#   3
#
# All of it on stderr. Invoked from yazi's Open bind, from a mako action, or
# from a click in shop, the person sees NOTHING. That is the silent-failure
# class this repo hunts, and it is the reason this script exists: for the
# message far more than for the launching, the same way alloy-shot exists for
# the notification more than for the capture.
#
# Registered as the system default for http, https and text/html in
# /etc/xdg/mimeapps.list. That is the sysadmin level, which sits ABOVE the
# vendor file and BELOW the user's own, so `xdg-settings set
# default-web-browser` still wins and a person who has chosen a browser never
# comes through here again.
#
# Testing this in a container will make it look broken, and it is not. xdg-open
# consults the scheme-handler registration only inside `if has_display`
# (open_generic, /usr/sbin/xdg-open), so with no WAYLAND_DISPLAY or DISPLAY it
# skips straight to its hardcoded list of text browsers and prints the wall of
# "command not found" this script exists to prevent. Set WAYLAND_DISPLAY to
# test it, or read the wrong conclusion.
#
# It also fixes a smaller thing that worked by luck. The vendor mimeapps.list
# ships from shared-mime-info and names `org.mozilla.firefox.desktop`, which
# this image does not contain under any BROWSER value: Fedora's Firefox rpm is
# firefox.desktop, while that id belongs to the Flatpak. It resolved correctly
# anyway, by falling through to mimeinfo.cache where exactly one candidate
# declared the scheme. One browser is not a thing to rely on: a `BROWSER=none`
# image has none, and a user's own Flatpak is a second.

set -eu

self=alloy-open.desktop

# The same directory list alloy-menu uses, and for the same reason: a browser
# the user installed as a Flatpak exports its desktop entry into the flatpak
# tree, not into /usr/share/applications. Looking only at the latter would
# make BYO-via-Flatpak invisible to the thing whose whole job is to find it.
dirs="/usr/share/applications
${XDG_DATA_HOME:-$HOME/.local/share}/applications
/var/lib/flatpak/exports/share/applications
${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications"

# Find a desktop entry that claims https, skipping this one.
#
# Excluding self is not defensive tidiness: without it this script is the
# handler that finds itself and execs itself, which is an unkillable loop
# behind a keybind. Matched on the filename rather than on Exec= because the
# filename is what the mimeapps entry names.
browser_entry() {
  printf '%s\n' "$dirs" | while IFS= read -r dir; do
    [ -d "$dir" ] || continue
    for f in "$dir"/*.desktop; do
      [ -f "$f" ] || continue
      [ "${f##*/}" = "$self" ] && continue
      grep -q '^MimeType=.*x-scheme-handler/https' "$f" || continue
      grep -q '^NoDisplay=true' "$f" && continue
      printf '%s\n' "$f"
      return 0
    done
  done | head -n 1
}

# Exec= carries field codes (%u %U %f %F and friends) that are placeholders
# rather than arguments. Strip them all and append the URL ourselves, which is
# what a launcher is supposed to do and what alloy-menu already does for the
# same reason.
#
# Flatpak wraps its own markers around them, and they have to go too. A real
# exported entry reads:
#
#   Exec=/usr/bin/flatpak run --branch=stable org.mozilla.firefox @@u %U @@
#
# `@@u` and the closing `@@` are flatpak's way of saying "the file or URI
# arguments belong here". Strip only the field code and the browser is
# launched with two literal arguments of `@@u` and `@@`, which it treats as
# URLs to open. Caught by testing against an exported entry rather than a
# hand-written one, which is the only reason it was visible.
exec_line() {
  sed -n 's/^Exec=//p' "$1" | head -n 1 \
    | sed 's/ *@@[uUfF]//g; s/ *@@//g; s/ *%[a-zA-Z]//g'
}

say() {
  # Both channels on purpose. A link opened from the file manager or a
  # notification has no terminal to read, and a link opened from a shell has
  # no reason to raise a desktop notification.
  printf '%s\n' "$1" >&2
  if command -v notify-send >/dev/null 2>&1; then
    notify-send -u normal -a Alloy "No browser installed" "$1" 2>/dev/null || true
  fi
}

url="${1:-}"
if [ -z "$url" ]; then
  echo "usage: alloy-open URL" >&2
  exit 2
fi

entry="$(browser_entry || true)"

if [ -n "$entry" ]; then
  cmd="$(exec_line "$entry")"
  [ -n "$cmd" ] || { say "The browser entry ${entry##*/} has no Exec line, so it cannot be launched."; exit 1; }
  # shellcheck disable=SC2086
  exec $cmd "$url"
fi

# No browser. This is the branch the script is for.
#
# Both routes named are ones that work today. `alloy browser` is the intended
# front door and does not exist yet, so it is deliberately NOT mentioned:
# telling someone to run a command that is not there is the same silent
# failure this script was written to remove, one level up. When that verb
# ships it replaces the flatpak line here.
#
# The layering route is named because it works: Firefox is in Fedora's own
# repos, and those stay enabled after install. That is the whole reason it is
# Alloy's pick (wiki `alloy-byo-principle`), so a machine that chose `none` is
# one command from the browser the image would have carried.
printf 'No browser is installed, so this link cannot be opened: %s\n' "$url" >&2
say "Install one with: rpm-ostree install firefox
Or, sandboxed: flatpak install org.mozilla.firefox, from a remote you have added
Or rebuild the image with BROWSER=firefox to bake one in."
exit 3
