···14141515The Switchyard extension can also show your installed browsers directly in the popup, letting you send the current tab to a specific browser in one click. This requires installing a native messaging host on your system.
16161717-Please read [`scripts/install-desktop-integration.sh`](../scripts/install-desktop-integration.sh) before running this command:
1717+Please read [`scripts/install-desktop-integration.sh`](../scripts/install-desktop-integration.py) before running this command:
18181919```bash
2020curl -fsSL https://raw.githubusercontent.com/alyraffauf/Switchyard/master/scripts/install-desktop-integration.sh | bash
+253
scripts/install-desktop-integration.py
···11+#!/usr/bin/env python3
22+# SPDX-License-Identifier: GPL-3.0-or-later
33+# Switchyard desktop integration installer — https://github.com/alyraffauf/Switchyard
44+55+import argparse
66+import json
77+import shutil
88+import subprocess
99+import sys
1010+from dataclasses import asdict, dataclass
1111+from pathlib import Path
1212+1313+FLATPAK_CHROMIUM: dict[str, str] = {
1414+ "com.brave.Browser": "BraveSoftware/Brave-Browser",
1515+ "com.google.Chrome": "google-chrome",
1616+ "com.microsoft.Edge": "microsoft-edge",
1717+ "com.vivaldi.Vivaldi": "vivaldi",
1818+ "io.github.ungoogled_software.ungoogled_chromium": "chromium",
1919+ "net.imput.helium": "net.imput.helium",
2020+ "org.chromium.Chromium": "chromium",
2121+}
2222+2323+NATIVE_CHROMIUM = [
2424+ "net.imput.helium",
2525+ "google-chrome",
2626+ "chromium",
2727+ "BraveSoftware/Brave-Browser",
2828+ "microsoft-edge",
2929+ "vivaldi",
3030+]
3131+3232+3333+FLATPAK_FIREFOX: dict[str, str] = {
3434+ "org.mozilla.firefox": ".mozilla",
3535+ "io.gitlab.librewolf-community": ".librewolf",
3636+}
3737+3838+NATIVE_FIREFOX = [".mozilla", ".librewolf"]
3939+4040+4141+@dataclass
4242+class NativeMessagingManifest:
4343+ path: str
4444+ name: str = "io.github.alyraffauf.switchyard"
4545+ description: str = "Switchyard browser selector"
4646+ type: str = "stdio"
4747+ allowed_origins: list[str] | None = None
4848+ allowed_extensions: list[str] | None = None
4949+5050+5151+def run(cmd: list[str]) -> bool:
5252+ return subprocess.run(cmd, capture_output=True).returncode == 0
5353+5454+5555+def flatpak_installed(flatpak: str | None, id: str) -> bool:
5656+ if flatpak:
5757+ return run([flatpak, "info", id])
5858+ return False
5959+6060+6161+def find_switchyard(flatpak: str | None) -> str | None:
6262+ switchyard = shutil.which("switchyard")
6363+6464+ if flatpak and flatpak_installed(flatpak, "io.github.alyraffauf.Switchyard"):
6565+ return f"{flatpak} run io.github.alyraffauf.Switchyard"
6666+6767+ return switchyard
6868+6969+7070+def write_wrapper(switchyard: str, path: Path):
7171+ wrapper = f"""#!/bin/sh
7272+if [ "${{container-}}" = flatpak ]; then
7373+ exec /usr/bin/flatpak-spawn --host {switchyard} --native-host "$@"
7474+else
7575+ exec {switchyard} --native-host "$@"
7676+fi
7777+"""
7878+7979+ path.parent.mkdir(parents=True, exist_ok=True)
8080+ path.write_text(wrapper)
8181+ path.chmod(0o755)
8282+8383+8484+def install_manifests(switchyard: str, configs: list[Path], hosts_dir: str, **kwargs):
8585+ for config_dir in configs:
8686+ manifest_file = config_dir / hosts_dir / "io.github.alyraffauf.switchyard.json"
8787+ manifest_file.parent.mkdir(parents=True, exist_ok=True)
8888+ manifest = NativeMessagingManifest(
8989+ path=str(manifest_file.parent / "switchyard-native-host-wrapper.sh"),
9090+ **kwargs,
9191+ )
9292+ manifest_file.write_text(
9393+ json.dumps(
9494+ {key: val for key, val in asdict(manifest).items() if val is not None}
9595+ )
9696+ )
9797+ print(f" Installed {manifest_file}")
9898+ write_wrapper(
9999+ switchyard, manifest_file.parent / "switchyard-native-host-wrapper.sh"
100100+ )
101101+102102+103103+def install(yes: bool = False):
104104+ print("Installing Switchyard native messaging host...")
105105+106106+ flatpak = shutil.which("flatpak")
107107+ switchyard = find_switchyard(flatpak)
108108+109109+ if switchyard is None:
110110+ print("Switchyard is not installed.")
111111+ sys.exit(1)
112112+113113+ installed_chromium = [
114114+ app_id for app_id in FLATPAK_CHROMIUM if flatpak_installed(flatpak, app_id)
115115+ ]
116116+ installed_firefox = [
117117+ app_id for app_id in FLATPAK_FIREFOX if flatpak_installed(flatpak, app_id)
118118+ ]
119119+ to_override = installed_chromium + installed_firefox
120120+121121+ chromium = [
122122+ Path.home() / f".var/app/{app_id}/config/{FLATPAK_CHROMIUM[app_id]}"
123123+ for app_id in installed_chromium
124124+ ] + [Path.home() / f".config/{config_dir}" for config_dir in NATIVE_CHROMIUM]
125125+126126+ firefox = [
127127+ Path.home() / f".var/app/{app_id}/{FLATPAK_FIREFOX[app_id]}"
128128+ for app_id in installed_firefox
129129+ ] + [Path.home() / profile_dir for profile_dir in NATIVE_FIREFOX]
130130+131131+ install_manifests(
132132+ switchyard,
133133+ chromium,
134134+ "NativeMessagingHosts",
135135+ allowed_origins=[
136136+ "chrome-extension://ncehhpikkabfdcceimdhjjjodogflokc/",
137137+ "chrome-extension://gmdmmjfmpfbmddgphjbkbbmdolkifloi/",
138138+ ],
139139+ )
140140+141141+ install_manifests(
142142+ switchyard,
143143+ firefox,
144144+ "native-messaging-hosts",
145145+ allowed_extensions=["switchyard@alyraffauf.github.io"],
146146+ )
147147+148148+ if to_override:
149149+ print(
150150+ "\nThe following browsers are installed via Flatpak and run in a "
151151+ "sandbox. To let them talk to Switchyard, we need to grant each one "
152152+ "permission to reach outside the sandbox. This does weaken their "
153153+ "isolation slightly, so it's worth knowing before we proceed.\n"
154154+ )
155155+ for app_id in to_override:
156156+ print(f" {app_id}")
157157+ print()
158158+159159+ if yes:
160160+ answer = "y"
161161+ else:
162162+ answer = input("Grant permission to these browsers? [y/N] ").strip().lower()
163163+164164+ if answer != "y":
165165+ print("Skipping permission grants.")
166166+ to_override = []
167167+168168+ assert flatpak
169169+ for app_id in to_override:
170170+ granted = run(
171171+ [
172172+ flatpak,
173173+ "override",
174174+ "--user",
175175+ "--talk-name=org.freedesktop.Flatpak",
176176+ app_id,
177177+ ]
178178+ )
179179+ if granted:
180180+ print(f" Granted: {app_id}")
181181+ else:
182182+ print(f" Failed to grant permission for {app_id}")
183183+184184+ print("\nManifests installed. Please restart your browser.")
185185+186186+187187+def uninstall():
188188+ chromium = [
189189+ Path.home() / f".var/app/{app_id}/config/{FLATPAK_CHROMIUM[app_id]}"
190190+ for app_id in FLATPAK_CHROMIUM
191191+ ] + [Path.home() / f".config/{config_dir}" for config_dir in NATIVE_CHROMIUM]
192192+193193+ firefox = [
194194+ Path.home() / f".var/app/{app_id}/{FLATPAK_FIREFOX[app_id]}"
195195+ for app_id in FLATPAK_FIREFOX
196196+ ] + [Path.home() / profile_dir for profile_dir in NATIVE_FIREFOX]
197197+198198+ count = 0
199199+ for config_dir in chromium:
200200+ hosts = config_dir / "NativeMessagingHosts"
201201+ for file in (
202202+ hosts / "io.github.alyraffauf.switchyard.json",
203203+ hosts / "switchyard-native-host-wrapper.sh",
204204+ ):
205205+ if file.exists():
206206+ file.unlink()
207207+ print(f" Removed {file}")
208208+ count += 1
209209+ for profile_dir in firefox:
210210+ hosts = profile_dir / "native-messaging-hosts"
211211+ for file in (
212212+ hosts / "io.github.alyraffauf.switchyard.json",
213213+ hosts / "switchyard-native-host-wrapper.sh",
214214+ ):
215215+ if file.exists():
216216+ file.unlink()
217217+ print(f" Removed {file}")
218218+ count += 1
219219+ print(f"\nRemoved {count} file(s).")
220220+221221+222222+if __name__ == "__main__":
223223+ parser = argparse.ArgumentParser(
224224+ prog="install-desktop-integration.py",
225225+ description="Set up desktop integration between the Switchyard app and browser extension.",
226226+ )
227227+228228+ group = parser.add_mutually_exclusive_group()
229229+ group.add_argument(
230230+ "--install", action="store_true", help="install the native messaging manifest."
231231+ )
232232+233233+ parser.add_argument(
234234+ "--yes", "-y", action="store_true", help="skip confirmation prompts."
235235+ )
236236+237237+ group.add_argument(
238238+ "--uninstall",
239239+ action="store_true",
240240+ help="remove the native messaging host manifest.",
241241+ )
242242+243243+ args = parser.parse_args()
244244+245245+ if not args.install and not args.uninstall:
246246+ parser.print_help()
247247+ sys.exit(0)
248248+249249+ if args.install:
250250+ install(yes=args.yes)
251251+252252+ if args.uninstall:
253253+ uninstall()
+8-174
scripts/install-desktop-integration.sh
···22# SPDX-License-Identifier: GPL-3.0-or-later
33# Switchyard desktop integration installer — https://github.com/alyraffauf/Switchyard
44#
55-# Installs the desktop integration that lets the browser extension list your installed browsers.
66-#
77-# For Flatpak browsers this grants --talk-name=org.freedesktop.Flatpak, which
88-# lets code inside the browser sandbox run arbitrary host commands via
99-# flatpak-spawn --host. This weakens the Flatpak sandbox for those browsers.
1010-# Read and understand this before running it.
55+# Usage: curl -fsSL <url> | bash -s -- [--install | --uninstall]
116#
1212-# Usage: bash install-desktop-integration.sh [--uninstall]
77+# This is a thin entrypoint that downloads and runs the Python installer.
88+# Arguments are forwarded directly to install-desktop-integration.py.
1391410set -euo pipefail
15111616-# ── Find Switchyard ────────────────────────────────────────────────────────────
1717-1818-CMD=""
1919-for id in io.github.alyraffauf.Switchyard io.github.alyraffauf.Switchyard.Devel; do
2020- if flatpak info "$id" &>/dev/null; then
2121- CMD="/usr/bin/flatpak run --command=switchyard $id --native-host"
2222- break
2323- fi
2424-done
2525-if [[ -z $CMD ]]; then
2626- bin=$(command -v switchyard 2>/dev/null) || {
2727- echo "error: Switchyard not found" >&2
2828- exit 1
2929- }
3030- CMD="$bin --native-host"
3131-fi
3232-3333-# ── Config ─────────────────────────────────────────────────────────────────────
3434-3535-NAME="io.github.alyraffauf.switchyard"
3636-WRAPPER="$HOME/.local/share/switchyard/native-host-wrapper.sh"
3737-CHROME_ORIGIN="chrome-extension://ncehhpikkabfdcceimdhjjjodogflokc/"
3838-FF_EXT="switchyard@alyraffauf.github.io"
3939-4040-NATIVE_CHROMIUM=(net.imput.helium google-chrome chromium BraveSoftware/Brave-Browser microsoft-edge vivaldi)
4141-NATIVE_FIREFOX=(.mozilla .librewolf .zen)
4242-4343-# (appId:configSubdir) — configSubdir maps to ~/.config/<sub>/ inside the sandbox
4444-FLATPAK_CHROMIUM=(
4545- com.google.Chrome:google-chrome
4646- com.google.ChromeDev:google-chrome-unstable
4747- org.chromium.Chromium:chromium
4848- com.brave.Browser:BraveSoftware/Brave-Browser
4949- com.microsoft.Edge:microsoft-edge
5050- com.vivaldi.Vivaldi:vivaldi
5151- io.github.ungoogled_software.ungoogled_chromium:chromium
5252- net.imput.helium:net.imput.helium
5353-)
5454-5555-# (appId:profileDir) — profileDir is persisted as ~/<sub>/ inside the sandbox
5656-FLATPAK_FIREFOX=(
5757- org.mozilla.firefox:.mozilla
5858- io.gitlab.librewolf-community:.librewolf
5959- io.github.zen_browser.zen:.zen
6060-)
6161-6262-# ── Helpers ────────────────────────────────────────────────────────────────────
6363-6464-N=0
6565-6666-chromium_manifest() {
6767- printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_origins":["%s"]}\n' \
6868- "$NAME" "$1" "$CHROME_ORIGIN"
6969-}
7070-7171-firefox_manifest() {
7272- printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_extensions":["%s"]}\n' \
7373- "$NAME" "$1" "$FF_EXT"
7474-}
7575-7676-# ── Uninstall ──────────────────────────────────────────────────────────────────
7777-7878-if [[ ${1-} == "--uninstall" ]]; then
7979- echo "Removing Switchyard native messaging host..."
8080-8181- for sub in "${NATIVE_CHROMIUM[@]}"; do
8282- f="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json"
8383- [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true
8484- done
8585-8686- for sub in "${NATIVE_FIREFOX[@]}"; do
8787- f="$HOME/$sub/native-messaging-hosts/$NAME.json"
8888- [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true
8989- done
9090-9191- for entry in "${FLATPAK_CHROMIUM[@]}"; do
9292- appId="${entry%%:*}"
9393- sub="${entry##*:}"
9494- dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts"
9595- f="$dir/$NAME.json"
9696- [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true
9797- done
9898-9999- for entry in "${FLATPAK_FIREFOX[@]}"; do
100100- appId="${entry%%:*}"
101101- sub="${entry##*:}"
102102- dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts"
103103- f="$dir/$NAME.json"
104104- [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true
105105- done
106106-107107- [[ -f $WRAPPER ]] && rm -f "$WRAPPER" && echo " removed $WRAPPER" || true
108108-109109- echo
110110- echo "Removed $N manifest(s)."
111111- exit 0
1212+if ! command -v python3 &>/dev/null; then
1313+ echo "error: python3 is required but not installed" >&2
1414+ exit 1
11215fi
11316114114-# ── Install ────────────────────────────────────────────────────────────────────
115115-116116-echo "Installing Switchyard native messaging host..."
117117-118118-mkdir -p "$(dirname "$WRAPPER")"
119119-printf '#!/bin/sh\nexec %s "$@"\n' "$CMD" >"$WRAPPER"
120120-chmod +x "$WRAPPER"
121121-echo "Wrapper: $WRAPPER"
1717+PY_URL="https://raw.githubusercontent.com/alyraffauf/Switchyard/refs/heads/master/scripts/install-desktop-integration.py"
12218123123-# Flatpak browsers need a different wrapper: they detect the sandbox via
124124-# $container and use flatpak-spawn --host to escape it. This requires the
125125-# org.freedesktop.Flatpak talk-name, granted per-browser below.
126126-FP_WRAPPER="#!/bin/sh
127127-if [ \"\${container-}\" = flatpak ]; then
128128- exec /usr/bin/flatpak-spawn --host $CMD
129129-else
130130- exec $CMD
131131-fi"
132132-133133-for sub in "${NATIVE_CHROMIUM[@]}"; do
134134- [[ -d "$HOME/.config/$sub" ]] || continue
135135- target="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json"
136136- mkdir -p "$(dirname "$target")"
137137- chromium_manifest "$WRAPPER" >"$target"
138138- echo " $target"
139139- N=$((N + 1))
140140-done
141141-142142-for sub in "${NATIVE_FIREFOX[@]}"; do
143143- [[ -d "$HOME/$sub" ]] || continue
144144- target="$HOME/$sub/native-messaging-hosts/$NAME.json"
145145- mkdir -p "$(dirname "$target")"
146146- firefox_manifest "$WRAPPER" >"$target"
147147- echo " $target"
148148- N=$((N + 1))
149149-done
150150-151151-for entry in "${FLATPAK_CHROMIUM[@]}"; do
152152- appId="${entry%%:*}"
153153- sub="${entry##*:}"
154154- [[ -d "$HOME/.var/app/$appId" ]] || continue
155155- dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts"
156156- wp="$dir/switchyard-native-host.sh"
157157- mkdir -p "$dir"
158158- printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp"
159159- chromium_manifest "$wp" >"$dir/$NAME.json"
160160- flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId"
161161- echo " $dir/$NAME.json [Flatpak]"
162162- N=$((N + 1))
163163-done
164164-165165-for entry in "${FLATPAK_FIREFOX[@]}"; do
166166- appId="${entry%%:*}"
167167- sub="${entry##*:}"
168168- [[ -d "$HOME/.var/app/$appId" ]] || continue
169169- dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts"
170170- wp="$dir/switchyard-native-host.sh"
171171- mkdir -p "$dir"
172172- printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp"
173173- firefox_manifest "$wp" >"$dir/$NAME.json"
174174- flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId"
175175- echo " $dir/$NAME.json [Flatpak]"
176176- N=$((N + 1))
177177-done
178178-179179-[[ $N -gt 0 ]] || {
180180- echo
181181- echo "No supported browsers found."
182182- exit 0
183183-}
184184-echo
185185-echo "Installed $N manifest(s). Restart your browser to activate."
1919+curl -fsSL "$PY_URL" | python3 - "$@"