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

Configure Feed

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

move desktop integration to its own package and package with nix

+283 -3
+1
browser-setup/.python-version
··· 1 + 3.14
+257
browser-setup/main.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 + def main(): 223 + parser = argparse.ArgumentParser( 224 + prog="switchyard-browser-setup", 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() 254 + 255 + 256 + if __name__ == "__main__": 257 + main()
+14
browser-setup/pyproject.toml
··· 1 + [build-system] 2 + requires = ["setuptools"] 3 + build-backend = "setuptools.build_meta" 4 + 5 + [project] 6 + name = "switchyard-browser-setup" 7 + version = "0.1.0" 8 + description = "Desktop integration installer for Switchyard" 9 + readme = "README.md" 10 + requires-python = ">=3.11" 11 + dependencies = [] 12 + 13 + [project.scripts] 14 + browser-setup = "main:main"
+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.py`](../scripts/install-desktop-integration.py) before running this command: 17 + Please read [`browser-setup/main.py`](../browser-setup/main.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 -s -- --install --yes
+8
flake/packages.nix
··· 7 7 packages = { 8 8 default = self'.packages.switchyard; 9 9 10 + browser-setup = pkgs.python3Packages.buildPythonApplication { 11 + pname = "browser-setup"; 12 + version = "dev"; 13 + src = ../browser-setup; 14 + pyproject = true; 15 + build-system = with pkgs.python3.pkgs; [ setuptools ]; 16 + }; 17 + 10 18 switchyard = pkgs.buildGoModule { 11 19 pname = "switchyard"; 12 20 version = "dev";
+2 -2
scripts/install-desktop-integration.sh
··· 5 5 # Usage: curl -fsSL <url> | bash -s -- [--install | --uninstall] 6 6 # 7 7 # This is a thin entrypoint that downloads and runs the Python installer. 8 - # Arguments are forwarded directly to install-desktop-integration.py. 8 + # Arguments are forwarded directly to browser-setup/main.py. 9 9 10 10 set -euo pipefail 11 11 ··· 14 14 exit 1 15 15 fi 16 16 17 - PY_URL="https://raw.githubusercontent.com/alyraffauf/Switchyard/refs/heads/master/scripts/install-desktop-integration.py" 17 + PY_URL="https://raw.githubusercontent.com/alyraffauf/Switchyard/refs/heads/master/browser-setup/main.py" 18 18 19 19 curl -fsSL "$PY_URL" | python3 - "$@"