|
1 |
+ |
#!/bin/sh
|
|
2 |
+ |
# alloy-open — open a link, or say why it cannot.
|
|
3 |
+ |
#
|
|
4 |
+ |
# Alloy may ship without a browser. `BROWSER=none` is a supported build value
|
|
5 |
+ |
# and under [[alloy-byo-principle]] it may become the default, so "no browser
|
|
6 |
+ |
# installed" is a state the system has to handle rather than an error state.
|
|
7 |
+ |
# Measured on 2026-08-17, before this existed, that state looked like:
|
|
8 |
+ |
#
|
|
9 |
+ |
# $ xdg-open https://example.com ; echo $?
|
|
10 |
+ |
# /usr/sbin/xdg-open: line 1045: www-browser: command not found
|
|
11 |
+ |
# ...five more of those...
|
|
12 |
+ |
# xdg-open: no method available for opening 'https://example.com'
|
|
13 |
+ |
# 3
|
|
14 |
+ |
#
|
|
15 |
+ |
# All of it on stderr. Invoked from yazi's Open bind, from a mako action, or
|
|
16 |
+ |
# from a click in shop, the person sees NOTHING. That is the silent-failure
|
|
17 |
+ |
# class this repo hunts, and it is the reason this script exists: for the
|
|
18 |
+ |
# message far more than for the launching, the same way alloy-shot exists for
|
|
19 |
+ |
# the notification more than for the capture.
|
|
20 |
+ |
#
|
|
21 |
+ |
# Registered as the system default for http, https and text/html in
|
|
22 |
+ |
# /etc/xdg/mimeapps.list. That is the sysadmin level, which sits ABOVE the
|
|
23 |
+ |
# vendor file and BELOW the user's own, so `xdg-settings set
|
|
24 |
+ |
# default-web-browser` still wins and a person who has chosen a browser never
|
|
25 |
+ |
# comes through here again.
|
|
26 |
+ |
#
|
|
27 |
+ |
# Testing this in a container will make it look broken, and it is not. xdg-open
|
|
28 |
+ |
# consults the scheme-handler registration only inside `if has_display`
|
|
29 |
+ |
# (open_generic, /usr/sbin/xdg-open), so with no WAYLAND_DISPLAY or DISPLAY it
|
|
30 |
+ |
# skips straight to its hardcoded list of text browsers and prints the wall of
|
|
31 |
+ |
# "command not found" this script exists to prevent. Set WAYLAND_DISPLAY to
|
|
32 |
+ |
# test it, or read the wrong conclusion.
|
|
33 |
+ |
#
|
|
34 |
+ |
# It also fixes a smaller thing that worked by luck. The vendor mimeapps.list
|
|
35 |
+ |
# ships from shared-mime-info and names `org.mozilla.firefox.desktop`, which
|
|
36 |
+ |
# this image does not contain under any BROWSER value: Helium is
|
|
37 |
+ |
# helium.desktop and Fedora's Firefox rpm is firefox.desktop, while that id
|
|
38 |
+ |
# belongs to the Flatpak. It resolved correctly anyway, by falling through to
|
|
39 |
+ |
# mimeinfo.cache where exactly one candidate declared the scheme. One browser
|
|
40 |
+ |
# is not a thing to rely on when the point of the exercise is that the user
|
|
41 |
+ |
# brings their own.
|
|
42 |
+ |
|
|
43 |
+ |
set -eu
|
|
44 |
+ |
|
|
45 |
+ |
self=alloy-open.desktop
|
|
46 |
+ |
|
|
47 |
+ |
# The same directory list alloy-menu uses, and for the same reason: a browser
|
|
48 |
+ |
# the user installed as a Flatpak exports its desktop entry into the flatpak
|
|
49 |
+ |
# tree, not into /usr/share/applications. Looking only at the latter would
|
|
50 |
+ |
# make BYO-via-Flatpak invisible to the thing whose whole job is to find it.
|
|
51 |
+ |
dirs="/usr/share/applications
|
|
52 |
+ |
${XDG_DATA_HOME:-$HOME/.local/share}/applications
|
|
53 |
+ |
/var/lib/flatpak/exports/share/applications
|
|
54 |
+ |
${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications"
|
|
55 |
+ |
|
|
56 |
+ |
# Find a desktop entry that claims https, skipping this one.
|
|
57 |
+ |
#
|
|
58 |
+ |
# Excluding self is not defensive tidiness: without it this script is the
|
|
59 |
+ |
# handler that finds itself and execs itself, which is an unkillable loop
|
|
60 |
+ |
# behind a keybind. Matched on the filename rather than on Exec= because the
|
|
61 |
+ |
# filename is what the mimeapps entry names.
|
|
62 |
+ |
browser_entry() {
|
|
63 |
+ |
printf '%s\n' "$dirs" | while IFS= read -r dir; do
|
|
64 |
+ |
[ -d "$dir" ] || continue
|
|
65 |
+ |
for f in "$dir"/*.desktop; do
|
|
66 |
+ |
[ -f "$f" ] || continue
|
|
67 |
+ |
[ "${f##*/}" = "$self" ] && continue
|
|
68 |
+ |
grep -q '^MimeType=.*x-scheme-handler/https' "$f" || continue
|
|
69 |
+ |
grep -q '^NoDisplay=true' "$f" && continue
|
|
70 |
+ |
printf '%s\n' "$f"
|
|
71 |
+ |
return 0
|
|
72 |
+ |
done
|
|
73 |
+ |
done | head -n 1
|
|
74 |
+ |
}
|
|
75 |
+ |
|
|
76 |
+ |
# Exec= carries field codes (%u %U %f %F and friends) that are placeholders
|
|
77 |
+ |
# rather than arguments. Strip them all and append the URL ourselves, which is
|
|
78 |
+ |
# what a launcher is supposed to do and what alloy-menu already does for the
|
|
79 |
+ |
# same reason.
|
|
80 |
+ |
#
|
|
81 |
+ |
# Flatpak wraps its own markers around them, and they have to go too. A real
|
|
82 |
+ |
# exported entry reads:
|
|
83 |
+ |
#
|
|
84 |
+ |
# Exec=/usr/bin/flatpak run --branch=stable org.mozilla.firefox @@u %U @@
|
|
85 |
+ |
#
|
|
86 |
+ |
# `@@u` and the closing `@@` are flatpak's way of saying "the file or URI
|
|
87 |
+ |
# arguments belong here". Strip only the field code and the browser is
|
|
88 |
+ |
# launched with two literal arguments of `@@u` and `@@`, which it treats as
|
|
89 |
+ |
# URLs to open. Caught by testing against an exported entry rather than a
|
|
90 |
+ |
# hand-written one, which is the only reason it was visible.
|
|
91 |
+ |
exec_line() {
|
|
92 |
+ |
sed -n 's/^Exec=//p' "$1" | head -n 1 \
|
|
93 |
+ |
| sed 's/ *@@[uUfF]//g; s/ *@@//g; s/ *%[a-zA-Z]//g'
|
|
94 |
+ |
}
|
|
95 |
+ |
|
|
96 |
+ |
say() {
|
|
97 |
+ |
# Both channels on purpose. A link opened from the file manager or a
|
|
98 |
+ |
# notification has no terminal to read, and a link opened from a shell has
|
|
99 |
+ |
# no reason to raise a desktop notification.
|
|
100 |
+ |
printf '%s\n' "$1" >&2
|
|
101 |
+ |
if command -v notify-send >/dev/null 2>&1; then
|
|
102 |
+ |
notify-send -u normal -a Alloy "No browser installed" "$1" 2>/dev/null || true
|
|
103 |
+ |
fi
|
|
104 |
+ |
}
|
|
105 |
+ |
|
|
106 |
+ |
url="${1:-}"
|
|
107 |
+ |
if [ -z "$url" ]; then
|
|
108 |
+ |
echo "usage: alloy-open URL" >&2
|
|
109 |
+ |
exit 2
|
|
110 |
+ |
fi
|
|
111 |
+ |
|
|
112 |
+ |
entry="$(browser_entry || true)"
|
|
113 |
+ |
|
|
114 |
+ |
if [ -n "$entry" ]; then
|
|
115 |
+ |
cmd="$(exec_line "$entry")"
|
|
116 |
+ |
[ -n "$cmd" ] || { say "The browser entry ${entry##*/} has no Exec line, so it cannot be launched."; exit 1; }
|
|
117 |
+ |
# shellcheck disable=SC2086
|
|
118 |
+ |
exec $cmd "$url"
|
|
119 |
+ |
fi
|
|
120 |
+ |
|
|
121 |
+ |
# No browser. This is the branch the script is for.
|
|
122 |
+ |
#
|
|
123 |
+ |
# Both routes named are ones that work today. `alloy browser` is the intended
|
|
124 |
+ |
# front door and does not exist yet, so it is deliberately NOT mentioned:
|
|
125 |
+ |
# telling someone to run a command that is not there is the same silent
|
|
126 |
+ |
# failure this script was written to remove, one level up. When that verb
|
|
127 |
+ |
# ships it replaces the flatpak line here.
|
|
128 |
+ |
printf 'No browser is installed, so this link cannot be opened: %s\n' "$url" >&2
|
|
129 |
+ |
say "Install one with: flatpak install fedora org.mozilla.firefox
|
|
130 |
+ |
Or rebuild the image with BROWSER=helium to bake one in."
|
|
131 |
+ |
exit 3
|