Rules-based browser launcher for TUI + GNOME. switchyard.aly.codes
tui gome bowser go
0

Configure Feed

Select the types of activity you want to include in your feed.

rewrote install-desktop-integration in python with a bash wrapper

Aly Raffauf (May 26, 2026, 5:37 PM EDT) f3d355a7 6ab72597

+262 -175
+1 -1
docs/WebExtension.md
··· 14 14 15 15 The 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. 16 16 17 - Please read [`scripts/install-desktop-integration.sh`](../scripts/install-desktop-integration.sh) before running this command: 17 + Please read [`scripts/install-desktop-integration.sh`](../scripts/install-desktop-integration.py) before running this command: 18 18 19 19 ```bash 20 20 curl -fsSL https://raw.githubusercontent.com/alyraffauf/Switchyard/master/scripts/install-desktop-integration.sh | bash
+253
scripts/install-desktop-integration.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-3.0-or-later 3 + # Switchyard desktop integration installer — https://github.com/alyraffauf/Switchyard 4 + 5 + import argparse 6 + import json 7 + import shutil 8 + import subprocess 9 + import sys 10 + from dataclasses import asdict, dataclass 11 + from pathlib import Path 12 + 13 + FLATPAK_CHROMIUM: dict[str, str] = { 14 + "com.brave.Browser": "BraveSoftware/Brave-Browser", 15 + "com.google.Chrome": "google-chrome", 16 + "com.microsoft.Edge": "microsoft-edge", 17 + "com.vivaldi.Vivaldi": "vivaldi", 18 + "io.github.ungoogled_software.ungoogled_chromium": "chromium", 19 + "net.imput.helium": "net.imput.helium", 20 + "org.chromium.Chromium": "chromium", 21 + } 22 + 23 + NATIVE_CHROMIUM = [ 24 + "net.imput.helium", 25 + "google-chrome", 26 + "chromium", 27 + "BraveSoftware/Brave-Browser", 28 + "microsoft-edge", 29 + "vivaldi", 30 + ] 31 + 32 + 33 + FLATPAK_FIREFOX: dict[str, str] = { 34 + "org.mozilla.firefox": ".mozilla", 35 + "io.gitlab.librewolf-community": ".librewolf", 36 + } 37 + 38 + NATIVE_FIREFOX = [".mozilla", ".librewolf"] 39 + 40 + 41 + @dataclass 42 + class NativeMessagingManifest: 43 + path: str 44 + name: str = "io.github.alyraffauf.switchyard" 45 + description: str = "Switchyard browser selector" 46 + type: str = "stdio" 47 + allowed_origins: list[str] | None = None 48 + allowed_extensions: list[str] | None = None 49 + 50 + 51 + def run(cmd: list[str]) -> bool: 52 + return subprocess.run(cmd, capture_output=True).returncode == 0 53 + 54 + 55 + def flatpak_installed(flatpak: str | None, id: str) -> bool: 56 + if flatpak: 57 + return run([flatpak, "info", id]) 58 + return False 59 + 60 + 61 + def find_switchyard(flatpak: str | None) -> str | None: 62 + switchyard = shutil.which("switchyard") 63 + 64 + if flatpak and flatpak_installed(flatpak, "io.github.alyraffauf.Switchyard"): 65 + return f"{flatpak} run io.github.alyraffauf.Switchyard" 66 + 67 + return switchyard 68 + 69 + 70 + def write_wrapper(switchyard: str, path: Path): 71 + wrapper = f"""#!/bin/sh 72 + if [ "${{container-}}" = flatpak ]; then 73 + exec /usr/bin/flatpak-spawn --host {switchyard} --native-host "$@" 74 + else 75 + exec {switchyard} --native-host "$@" 76 + fi 77 + """ 78 + 79 + path.parent.mkdir(parents=True, exist_ok=True) 80 + path.write_text(wrapper) 81 + path.chmod(0o755) 82 + 83 + 84 + def install_manifests(switchyard: str, configs: list[Path], hosts_dir: str, **kwargs): 85 + for config_dir in configs: 86 + manifest_file = config_dir / hosts_dir / "io.github.alyraffauf.switchyard.json" 87 + manifest_file.parent.mkdir(parents=True, exist_ok=True) 88 + manifest = NativeMessagingManifest( 89 + path=str(manifest_file.parent / "switchyard-native-host-wrapper.sh"), 90 + **kwargs, 91 + ) 92 + manifest_file.write_text( 93 + json.dumps( 94 + {key: val for key, val in asdict(manifest).items() if val is not None} 95 + ) 96 + ) 97 + print(f" Installed {manifest_file}") 98 + write_wrapper( 99 + switchyard, manifest_file.parent / "switchyard-native-host-wrapper.sh" 100 + ) 101 + 102 + 103 + def install(yes: bool = False): 104 + print("Installing Switchyard native messaging host...") 105 + 106 + flatpak = shutil.which("flatpak") 107 + switchyard = find_switchyard(flatpak) 108 + 109 + if switchyard is None: 110 + print("Switchyard is not installed.") 111 + sys.exit(1) 112 + 113 + installed_chromium = [ 114 + app_id for app_id in FLATPAK_CHROMIUM if flatpak_installed(flatpak, app_id) 115 + ] 116 + installed_firefox = [ 117 + app_id for app_id in FLATPAK_FIREFOX if flatpak_installed(flatpak, app_id) 118 + ] 119 + to_override = installed_chromium + installed_firefox 120 + 121 + chromium = [ 122 + Path.home() / f".var/app/{app_id}/config/{FLATPAK_CHROMIUM[app_id]}" 123 + for app_id in installed_chromium 124 + ] + [Path.home() / f".config/{config_dir}" for config_dir in NATIVE_CHROMIUM] 125 + 126 + firefox = [ 127 + Path.home() / f".var/app/{app_id}/{FLATPAK_FIREFOX[app_id]}" 128 + for app_id in installed_firefox 129 + ] + [Path.home() / profile_dir for profile_dir in NATIVE_FIREFOX] 130 + 131 + install_manifests( 132 + switchyard, 133 + chromium, 134 + "NativeMessagingHosts", 135 + allowed_origins=[ 136 + "chrome-extension://ncehhpikkabfdcceimdhjjjodogflokc/", 137 + "chrome-extension://gmdmmjfmpfbmddgphjbkbbmdolkifloi/", 138 + ], 139 + ) 140 + 141 + install_manifests( 142 + switchyard, 143 + firefox, 144 + "native-messaging-hosts", 145 + allowed_extensions=["switchyard@alyraffauf.github.io"], 146 + ) 147 + 148 + if to_override: 149 + print( 150 + "\nThe following browsers are installed via Flatpak and run in a " 151 + "sandbox. To let them talk to Switchyard, we need to grant each one " 152 + "permission to reach outside the sandbox. This does weaken their " 153 + "isolation slightly, so it's worth knowing before we proceed.\n" 154 + ) 155 + for app_id in to_override: 156 + print(f" {app_id}") 157 + print() 158 + 159 + if yes: 160 + answer = "y" 161 + else: 162 + answer = input("Grant permission to these browsers? [y/N] ").strip().lower() 163 + 164 + if answer != "y": 165 + print("Skipping permission grants.") 166 + to_override = [] 167 + 168 + assert flatpak 169 + for app_id in to_override: 170 + granted = run( 171 + [ 172 + flatpak, 173 + "override", 174 + "--user", 175 + "--talk-name=org.freedesktop.Flatpak", 176 + app_id, 177 + ] 178 + ) 179 + if granted: 180 + print(f" Granted: {app_id}") 181 + else: 182 + print(f" Failed to grant permission for {app_id}") 183 + 184 + print("\nManifests installed. Please restart your browser.") 185 + 186 + 187 + def uninstall(): 188 + chromium = [ 189 + Path.home() / f".var/app/{app_id}/config/{FLATPAK_CHROMIUM[app_id]}" 190 + for app_id in FLATPAK_CHROMIUM 191 + ] + [Path.home() / f".config/{config_dir}" for config_dir in NATIVE_CHROMIUM] 192 + 193 + firefox = [ 194 + Path.home() / f".var/app/{app_id}/{FLATPAK_FIREFOX[app_id]}" 195 + for app_id in FLATPAK_FIREFOX 196 + ] + [Path.home() / profile_dir for profile_dir in NATIVE_FIREFOX] 197 + 198 + count = 0 199 + for config_dir in chromium: 200 + hosts = config_dir / "NativeMessagingHosts" 201 + for file in ( 202 + hosts / "io.github.alyraffauf.switchyard.json", 203 + hosts / "switchyard-native-host-wrapper.sh", 204 + ): 205 + if file.exists(): 206 + file.unlink() 207 + print(f" Removed {file}") 208 + count += 1 209 + for profile_dir in firefox: 210 + hosts = profile_dir / "native-messaging-hosts" 211 + for file in ( 212 + hosts / "io.github.alyraffauf.switchyard.json", 213 + hosts / "switchyard-native-host-wrapper.sh", 214 + ): 215 + if file.exists(): 216 + file.unlink() 217 + print(f" Removed {file}") 218 + count += 1 219 + print(f"\nRemoved {count} file(s).") 220 + 221 + 222 + if __name__ == "__main__": 223 + parser = argparse.ArgumentParser( 224 + prog="install-desktop-integration.py", 225 + description="Set up desktop integration between the Switchyard app and browser extension.", 226 + ) 227 + 228 + group = parser.add_mutually_exclusive_group() 229 + group.add_argument( 230 + "--install", action="store_true", help="install the native messaging manifest." 231 + ) 232 + 233 + parser.add_argument( 234 + "--yes", "-y", action="store_true", help="skip confirmation prompts." 235 + ) 236 + 237 + group.add_argument( 238 + "--uninstall", 239 + action="store_true", 240 + help="remove the native messaging host manifest.", 241 + ) 242 + 243 + args = parser.parse_args() 244 + 245 + if not args.install and not args.uninstall: 246 + parser.print_help() 247 + sys.exit(0) 248 + 249 + if args.install: 250 + install(yes=args.yes) 251 + 252 + if args.uninstall: 253 + uninstall()
+8 -174
scripts/install-desktop-integration.sh
··· 2 2 # SPDX-License-Identifier: GPL-3.0-or-later 3 3 # Switchyard desktop integration installer — https://github.com/alyraffauf/Switchyard 4 4 # 5 - # Installs the desktop integration that lets the browser extension list your installed browsers. 6 - # 7 - # For Flatpak browsers this grants --talk-name=org.freedesktop.Flatpak, which 8 - # lets code inside the browser sandbox run arbitrary host commands via 9 - # flatpak-spawn --host. This weakens the Flatpak sandbox for those browsers. 10 - # Read and understand this before running it. 5 + # Usage: curl -fsSL <url> | bash -s -- [--install | --uninstall] 11 6 # 12 - # Usage: bash install-desktop-integration.sh [--uninstall] 7 + # This is a thin entrypoint that downloads and runs the Python installer. 8 + # Arguments are forwarded directly to install-desktop-integration.py. 13 9 14 10 set -euo pipefail 15 11 16 - # ── Find Switchyard ──────────────────────────────────────────────────────────── 17 - 18 - CMD="" 19 - for id in io.github.alyraffauf.Switchyard io.github.alyraffauf.Switchyard.Devel; do 20 - if flatpak info "$id" &>/dev/null; then 21 - CMD="/usr/bin/flatpak run --command=switchyard $id --native-host" 22 - break 23 - fi 24 - done 25 - if [[ -z $CMD ]]; then 26 - bin=$(command -v switchyard 2>/dev/null) || { 27 - echo "error: Switchyard not found" >&2 28 - exit 1 29 - } 30 - CMD="$bin --native-host" 31 - fi 32 - 33 - # ── Config ───────────────────────────────────────────────────────────────────── 34 - 35 - NAME="io.github.alyraffauf.switchyard" 36 - WRAPPER="$HOME/.local/share/switchyard/native-host-wrapper.sh" 37 - CHROME_ORIGIN="chrome-extension://ncehhpikkabfdcceimdhjjjodogflokc/" 38 - FF_EXT="switchyard@alyraffauf.github.io" 39 - 40 - NATIVE_CHROMIUM=(net.imput.helium google-chrome chromium BraveSoftware/Brave-Browser microsoft-edge vivaldi) 41 - NATIVE_FIREFOX=(.mozilla .librewolf .zen) 42 - 43 - # (appId:configSubdir) — configSubdir maps to ~/.config/<sub>/ inside the sandbox 44 - FLATPAK_CHROMIUM=( 45 - com.google.Chrome:google-chrome 46 - com.google.ChromeDev:google-chrome-unstable 47 - org.chromium.Chromium:chromium 48 - com.brave.Browser:BraveSoftware/Brave-Browser 49 - com.microsoft.Edge:microsoft-edge 50 - com.vivaldi.Vivaldi:vivaldi 51 - io.github.ungoogled_software.ungoogled_chromium:chromium 52 - net.imput.helium:net.imput.helium 53 - ) 54 - 55 - # (appId:profileDir) — profileDir is persisted as ~/<sub>/ inside the sandbox 56 - FLATPAK_FIREFOX=( 57 - org.mozilla.firefox:.mozilla 58 - io.gitlab.librewolf-community:.librewolf 59 - io.github.zen_browser.zen:.zen 60 - ) 61 - 62 - # ── Helpers ──────────────────────────────────────────────────────────────────── 63 - 64 - N=0 65 - 66 - chromium_manifest() { 67 - printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_origins":["%s"]}\n' \ 68 - "$NAME" "$1" "$CHROME_ORIGIN" 69 - } 70 - 71 - firefox_manifest() { 72 - printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_extensions":["%s"]}\n' \ 73 - "$NAME" "$1" "$FF_EXT" 74 - } 75 - 76 - # ── Uninstall ────────────────────────────────────────────────────────────────── 77 - 78 - if [[ ${1-} == "--uninstall" ]]; then 79 - echo "Removing Switchyard native messaging host..." 80 - 81 - for sub in "${NATIVE_CHROMIUM[@]}"; do 82 - f="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json" 83 - [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true 84 - done 85 - 86 - for sub in "${NATIVE_FIREFOX[@]}"; do 87 - f="$HOME/$sub/native-messaging-hosts/$NAME.json" 88 - [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true 89 - done 90 - 91 - for entry in "${FLATPAK_CHROMIUM[@]}"; do 92 - appId="${entry%%:*}" 93 - sub="${entry##*:}" 94 - dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts" 95 - f="$dir/$NAME.json" 96 - [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true 97 - done 98 - 99 - for entry in "${FLATPAK_FIREFOX[@]}"; do 100 - appId="${entry%%:*}" 101 - sub="${entry##*:}" 102 - dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts" 103 - f="$dir/$NAME.json" 104 - [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true 105 - done 106 - 107 - [[ -f $WRAPPER ]] && rm -f "$WRAPPER" && echo " removed $WRAPPER" || true 108 - 109 - echo 110 - echo "Removed $N manifest(s)." 111 - exit 0 12 + if ! command -v python3 &>/dev/null; then 13 + echo "error: python3 is required but not installed" >&2 14 + exit 1 112 15 fi 113 16 114 - # ── Install ──────────────────────────────────────────────────────────────────── 115 - 116 - echo "Installing Switchyard native messaging host..." 117 - 118 - mkdir -p "$(dirname "$WRAPPER")" 119 - printf '#!/bin/sh\nexec %s "$@"\n' "$CMD" >"$WRAPPER" 120 - chmod +x "$WRAPPER" 121 - echo "Wrapper: $WRAPPER" 17 + PY_URL="https://raw.githubusercontent.com/alyraffauf/Switchyard/refs/heads/master/scripts/install-desktop-integration.py" 122 18 123 - # Flatpak browsers need a different wrapper: they detect the sandbox via 124 - # $container and use flatpak-spawn --host to escape it. This requires the 125 - # org.freedesktop.Flatpak talk-name, granted per-browser below. 126 - FP_WRAPPER="#!/bin/sh 127 - if [ \"\${container-}\" = flatpak ]; then 128 - exec /usr/bin/flatpak-spawn --host $CMD 129 - else 130 - exec $CMD 131 - fi" 132 - 133 - for sub in "${NATIVE_CHROMIUM[@]}"; do 134 - [[ -d "$HOME/.config/$sub" ]] || continue 135 - target="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json" 136 - mkdir -p "$(dirname "$target")" 137 - chromium_manifest "$WRAPPER" >"$target" 138 - echo " $target" 139 - N=$((N + 1)) 140 - done 141 - 142 - for sub in "${NATIVE_FIREFOX[@]}"; do 143 - [[ -d "$HOME/$sub" ]] || continue 144 - target="$HOME/$sub/native-messaging-hosts/$NAME.json" 145 - mkdir -p "$(dirname "$target")" 146 - firefox_manifest "$WRAPPER" >"$target" 147 - echo " $target" 148 - N=$((N + 1)) 149 - done 150 - 151 - for entry in "${FLATPAK_CHROMIUM[@]}"; do 152 - appId="${entry%%:*}" 153 - sub="${entry##*:}" 154 - [[ -d "$HOME/.var/app/$appId" ]] || continue 155 - dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts" 156 - wp="$dir/switchyard-native-host.sh" 157 - mkdir -p "$dir" 158 - printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp" 159 - chromium_manifest "$wp" >"$dir/$NAME.json" 160 - flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId" 161 - echo " $dir/$NAME.json [Flatpak]" 162 - N=$((N + 1)) 163 - done 164 - 165 - for entry in "${FLATPAK_FIREFOX[@]}"; do 166 - appId="${entry%%:*}" 167 - sub="${entry##*:}" 168 - [[ -d "$HOME/.var/app/$appId" ]] || continue 169 - dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts" 170 - wp="$dir/switchyard-native-host.sh" 171 - mkdir -p "$dir" 172 - printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp" 173 - firefox_manifest "$wp" >"$dir/$NAME.json" 174 - flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId" 175 - echo " $dir/$NAME.json [Flatpak]" 176 - N=$((N + 1)) 177 - done 178 - 179 - [[ $N -gt 0 ]] || { 180 - echo 181 - echo "No supported browsers found." 182 - exit 0 183 - } 184 - echo 185 - echo "Installed $N manifest(s). Restart your browser to activate." 19 + curl -fsSL "$PY_URL" | python3 - "$@"