Skip to main content

max / alloy

Say so when there is no browser, instead of failing to stderr BROWSER=none is already a supported build value, so an image with no browser can already be built, and until now opening a link there printed six "command not found" lines to stderr and nothing anywhere else. From yazi's Open bind, a mako action or a click in shop, that is nothing at all. The silent-failure class this repo hunts, reachable from a supported configuration. alloy-open is registered in /etc/xdg/mimeapps.list for http, https and text/html. It finds whatever browser is installed and, when there is none, says so through notify-send and stderr both. The sysadmin level is deliberate: it sits above the vendor file and below the user's own, so an explicit choice still wins and never comes through here again. It searches the flatpak export directories as well as /usr/share/applications, which is the point rather than thoroughness: a browser the user brings arrives as a flatpak export, so looking only at /usr/share would make the bring-your-own case invisible to the thing whose job is to find it. Fixes a second thing that worked by luck. The vendor mimeapps.list from shared-mime-info names org.mozilla.firefox.desktop, which this image has never contained under any BROWSER value; it resolved anyway by falling through to mimeinfo.cache where exactly one candidate declared the scheme. A default that is wrong and works because there is only one alternative stops working the day the browser becomes the user's choice. Two loop guards, because the handler that scans for browsers can find itself: it skips its own filename, and the build asserts its desktop entry carries NoDisplay=true. The build also asserts every type in mimeapps.list names a desktop file the image actually contains. All four failure modes tested. Flatpak's @@u/@@ placeholder markers are stripped alongside the standard field codes; without that a flatpak browser is launched with two literal arguments it treats as URLs. Found by testing against a real exported entry rather than a hand-written one.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-17 21:25 UTC
Signed with PGP, not checked
Commit: ffe25b4eb287bd230dbaedca8267d332b63c0374
Parent: 24f81ba
4 files changed, +190 insertions, -0 deletions
@@ -1733,6 +1733,43 @@
1733 1733 echo "cursor: $(echo "$names" | tr '\n' ' ')kept, every other Bibata theme pruned"; \
1734 1734 fi
1735 1735
1736 + # =====================================================================
1737 + # The default web handler has to name something the image contains.
1738 + # =====================================================================
1739 + # /etc/xdg/mimeapps.list points http, https and text/html at alloy-open,
1740 + # which finds whatever browser is installed and, when there is none, says so
1741 + # rather than failing to stderr where nobody is looking. Three ways that can
1742 + # rot silently, so all three are checked.
1743 + #
1744 + # The vendor file is the cautionary tale this replaces: shared-mime-info ships
1745 + # /usr/share/applications/mimeapps.list naming `org.mozilla.firefox.desktop`,
1746 + # which this image has never contained under any BROWSER value. It resolved
1747 + # correctly anyway by falling through to mimeinfo.cache, where exactly one
1748 + # candidate declared the scheme. A default that is wrong and works because
1749 + # only one alternative exists is precisely what stops working the day the
1750 + # browser becomes the user's choice.
1751 + #
1752 + # NoDisplay is asserted rather than assumed: without it alloy-open.desktop is
1753 + # a browser candidate to alloy-open's own scan, and the script would exec
1754 + # itself forever behind a keybind. The script excludes itself by filename too,
1755 + # so this is the second of two guards rather than the only one.
1756 + RUN set -eu; \
1757 + test -x /usr/bin/alloy-open \
1758 + || { echo "/etc/xdg/mimeapps.list points at alloy-open and it is not executable in the image" >&2; exit 1; }; \
1759 + entry=/usr/share/applications/alloy-open.desktop; \
1760 + test -f "$entry" \
1761 + || { echo "alloy-open has no desktop entry, so nothing can be registered against it" >&2; exit 1; }; \
1762 + grep -q '^NoDisplay=true' "$entry" \
1763 + || { echo "alloy-open.desktop lacks NoDisplay=true; it would advertise itself as a browser to its own scan and to alloy-menu" >&2; exit 1; }; \
1764 + for t in x-scheme-handler/http x-scheme-handler/https text/html; do \
1765 + named="$(sed -n "s|^$t=||p" /etc/xdg/mimeapps.list | head -n 1)"; \
1766 + [ -n "$named" ] \
1767 + || { echo "/etc/xdg/mimeapps.list has no default for $t" >&2; exit 1; }; \
1768 + test -f "/usr/share/applications/$named" \
1769 + || { echo "/etc/xdg/mimeapps.list points $t at $named, which is not in the image" >&2; exit 1; }; \
1770 + done; \
1771 + echo "web handler: $(sed -n 's|^x-scheme-handler/https=||p' /etc/xdg/mimeapps.list | head -n 1), registered for http, https and text/html"
1772 +
1736 1773 # =====================================================================
1737 1774 # The installer's ssh door — assert both halves of the gate are intact.
1738 1775 # =====================================================================
@@ -1,0 +1,14 @@
1 + # Alloy's system-wide default handlers.
2 + #
3 + # The sysadmin level: above the vendor file that shared-mime-info ships, below
4 + # the user's own ~/.config/mimeapps.list. So `xdg-settings set
5 + # default-web-browser` still wins, which is what `alloy browser` will use.
6 + #
7 + # Web types point at alloy-open rather than at a browser directly, because
8 + # Alloy may ship without one. alloy-open finds whatever browser is installed
9 + # (rpm or Flatpak) and, when there is none, says so instead of failing to
10 + # stderr where nobody is looking.
11 + [Default Applications]
12 + x-scheme-handler/http=alloy-open.desktop
13 + x-scheme-handler/https=alloy-open.desktop
14 + text/html=alloy-open.desktop
@@ -1,0 +1,131 @@
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
@@ -1,0 +1,8 @@
1 + [Desktop Entry]
2 + Type=Application
3 + Name=Open link
4 + Comment=Opens a link in the installed browser, or explains that there is not one
5 + Exec=alloy-open %u
6 + Terminal=false
7 + NoDisplay=true
8 + MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;