Skip to main content

max / alloy

5.9 KB · 137 lines History Blame Raw
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: Fedora's Firefox rpm is
37 # firefox.desktop, while that id belongs to the Flatpak. It resolved correctly
38 # anyway, by falling through to mimeinfo.cache where exactly one candidate
39 # declared the scheme. One browser is not a thing to rely on: a `BROWSER=none`
40 # image has none, and a user's own Flatpak is a second.
41
42 set -eu
43
44 self=alloy-open.desktop
45
46 # The same directory list alloy-menu uses, and for the same reason: a browser
47 # the user installed as a Flatpak exports its desktop entry into the flatpak
48 # tree, not into /usr/share/applications. Looking only at the latter would
49 # make BYO-via-Flatpak invisible to the thing whose whole job is to find it.
50 dirs="/usr/share/applications
51 ${XDG_DATA_HOME:-$HOME/.local/share}/applications
52 /var/lib/flatpak/exports/share/applications
53 ${XDG_DATA_HOME:-$HOME/.local/share}/flatpak/exports/share/applications"
54
55 # Find a desktop entry that claims https, skipping this one.
56 #
57 # Excluding self is not defensive tidiness: without it this script is the
58 # handler that finds itself and execs itself, which is an unkillable loop
59 # behind a keybind. Matched on the filename rather than on Exec= because the
60 # filename is what the mimeapps entry names.
61 browser_entry() {
62 printf '%s\n' "$dirs" | while IFS= read -r dir; do
63 [ -d "$dir" ] || continue
64 for f in "$dir"/*.desktop; do
65 [ -f "$f" ] || continue
66 [ "${f##*/}" = "$self" ] && continue
67 grep -q '^MimeType=.*x-scheme-handler/https' "$f" || continue
68 grep -q '^NoDisplay=true' "$f" && continue
69 printf '%s\n' "$f"
70 return 0
71 done
72 done | head -n 1
73 }
74
75 # Exec= carries field codes (%u %U %f %F and friends) that are placeholders
76 # rather than arguments. Strip them all and append the URL ourselves, which is
77 # what a launcher is supposed to do and what alloy-menu already does for the
78 # same reason.
79 #
80 # Flatpak wraps its own markers around them, and they have to go too. A real
81 # exported entry reads:
82 #
83 # Exec=/usr/bin/flatpak run --branch=stable org.mozilla.firefox @@u %U @@
84 #
85 # `@@u` and the closing `@@` are flatpak's way of saying "the file or URI
86 # arguments belong here". Strip only the field code and the browser is
87 # launched with two literal arguments of `@@u` and `@@`, which it treats as
88 # URLs to open. Caught by testing against an exported entry rather than a
89 # hand-written one, which is the only reason it was visible.
90 exec_line() {
91 sed -n 's/^Exec=//p' "$1" | head -n 1 \
92 | sed 's/ *@@[uUfF]//g; s/ *@@//g; s/ *%[a-zA-Z]//g'
93 }
94
95 say() {
96 # Both channels on purpose. A link opened from the file manager or a
97 # notification has no terminal to read, and a link opened from a shell has
98 # no reason to raise a desktop notification.
99 printf '%s\n' "$1" >&2
100 if command -v notify-send >/dev/null 2>&1; then
101 notify-send -u normal -a Alloy "No browser installed" "$1" 2>/dev/null || true
102 fi
103 }
104
105 url="${1:-}"
106 if [ -z "$url" ]; then
107 echo "usage: alloy-open URL" >&2
108 exit 2
109 fi
110
111 entry="$(browser_entry || true)"
112
113 if [ -n "$entry" ]; then
114 cmd="$(exec_line "$entry")"
115 [ -n "$cmd" ] || { say "The browser entry ${entry##*/} has no Exec line, so it cannot be launched."; exit 1; }
116 # shellcheck disable=SC2086
117 exec $cmd "$url"
118 fi
119
120 # No browser. This is the branch the script is for.
121 #
122 # Both routes named are ones that work today. `alloy browser` is the intended
123 # front door and does not exist yet, so it is deliberately NOT mentioned:
124 # telling someone to run a command that is not there is the same silent
125 # failure this script was written to remove, one level up. When that verb
126 # ships it replaces the flatpak line here.
127 #
128 # The layering route is named because it works: Firefox is in Fedora's own
129 # repos, and those stay enabled after install. That is the whole reason it is
130 # Alloy's pick (wiki `alloy-byo-principle`), so a machine that chose `none` is
131 # one command from the browser the image would have carried.
132 printf 'No browser is installed, so this link cannot be opened: %s\n' "$url" >&2
133 say "Install one with: rpm-ostree install firefox
134 Or, sandboxed: flatpak install org.mozilla.firefox, from a remote you have added
135 Or rebuild the image with BROWSER=firefox to bake one in."
136 exit 3
137