From 6653fcf668f243215ee117656c43cec4f8690e44 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sun, 24 May 2026 17:54:03 -0400 Subject: [PATCH] blepadd native messaging workaround --- docs/WebExtension.md | 24 ++++- scripts/install-native-host.sh | 185 +++++++++++++++++++++++++++++++++ src/install_native_host.go | 184 -------------------------------- src/main.go | 19 ---- 4 files changed, 207 insertions(+), 205 deletions(-) create mode 100755 scripts/install-native-host.sh delete mode 100644 src/install_native_host.go diff --git a/docs/WebExtension.md b/docs/WebExtension.md index 13214fe..0d25c0e 100644 --- a/docs/WebExtension.md +++ b/docs/WebExtension.md @@ -2,6 +2,26 @@ A companion browser extension is available that allows you to open any page in Switchyard with one click. For now, the extension uses our URI scheme under the hood. It redirects the current tab to a `switchyard://open` URL, handing off to Switchyard for browser selection. -The extension lives in the `webextension/` directory and ships as a standard WebExtension compatible with both Firefox and Chromium browsers. It is included in GitHub releases as `switchyard-webextension.zip`. +The extension lives in the `webextension/` directory and ships as a standard WebExtension compatible with both Firefox and Chromium browsers. It is included in GitHub releases as `switchyard-webextension.zip`. -The intent is to add deeper integration with the desktop app. Eventually, it will be submitted to the Chrome Web Store and Mozilla. \ No newline at end of file +The intent is to add deeper integration with the desktop app. Eventually, it will be submitted to the Chrome Web Store and Mozilla. + +## Native Messaging Support + +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. + +Please read [`scripts/install-native-host.sh`](../scripts/install-native-host.sh) before running this command: + +```bash +curl -fsSL https://raw.githubusercontent.com/alyraffauf/Switchyard/master/scripts/install-native-host.sh | bash +``` + +After installation, restart your browser(s). The extension popup will list your installed browsers. + +### Uninstall + +```bash +bash install-native-host.sh --uninstall +``` + +This removes all manifests and wrappers, but the `flatpak override` permissions granted during install are left in place. It's up to the user to decide what to do with them. diff --git a/scripts/install-native-host.sh b/scripts/install-native-host.sh new file mode 100755 index 0000000..a63aa21 --- /dev/null +++ b/scripts/install-native-host.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# Switchyard native messaging host installer — https://github.com/alyraffauf/Switchyard +# +# Installs manifests that let the browser extension list your installed browsers. +# +# For Flatpak browsers this grants --talk-name=org.freedesktop.Flatpak, which +# lets code inside the browser sandbox run arbitrary host commands via +# flatpak-spawn --host. This weakens the Flatpak sandbox for those browsers. +# Read and understand this before running it. +# +# Usage: bash install-native-host.sh [--uninstall] + +set -euo pipefail + +# ── Find Switchyard ──────────────────────────────────────────────────────────── + +CMD="" +for id in io.github.alyraffauf.Switchyard io.github.alyraffauf.Switchyard.Devel; do + if flatpak info "$id" &>/dev/null; then + CMD="/usr/bin/flatpak run --command=switchyard $id --native-host" + break + fi +done +if [[ -z $CMD ]]; then + bin=$(command -v switchyard 2>/dev/null) || { + echo "error: Switchyard not found" >&2 + exit 1 + } + CMD="$bin --native-host" +fi + +# ── Config ───────────────────────────────────────────────────────────────────── + +NAME="io.github.alyraffauf.switchyard" +WRAPPER="$HOME/.local/share/switchyard/native-host-wrapper.sh" +CHROME_ORIGIN="chrome-extension://gmdmmjfmpfbmddgphjbkbbmdolkifloi/" +FF_EXT="switchyard@alyraffauf.github.io" + +NATIVE_CHROMIUM=(net.imput.helium google-chrome chromium BraveSoftware/Brave-Browser microsoft-edge vivaldi) +NATIVE_FIREFOX=(.mozilla .librewolf .zen) + +# (appId:configSubdir) — configSubdir maps to ~/.config// inside the sandbox +FLATPAK_CHROMIUM=( + com.google.Chrome:google-chrome + com.google.ChromeDev:google-chrome-unstable + org.chromium.Chromium:chromium + com.brave.Browser:BraveSoftware/Brave-Browser + com.microsoft.Edge:microsoft-edge + com.vivaldi.Vivaldi:vivaldi + io.github.ungoogled_software.ungoogled_chromium:chromium + net.imput.helium:net.imput.helium +) + +# (appId:profileDir) — profileDir is persisted as ~// inside the sandbox +FLATPAK_FIREFOX=( + org.mozilla.firefox:.mozilla + io.gitlab.librewolf-community:.librewolf + io.github.zen_browser.zen:.zen +) + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +N=0 + +chromium_manifest() { + printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_origins":["%s"]}\n' \ + "$NAME" "$1" "$CHROME_ORIGIN" +} + +firefox_manifest() { + printf '{"name":"%s","description":"Switchyard browser selector","path":"%s","type":"stdio","allowed_extensions":["%s"]}\n' \ + "$NAME" "$1" "$FF_EXT" +} + +# ── Uninstall ────────────────────────────────────────────────────────────────── + +if [[ ${1-} == "--uninstall" ]]; then + echo "Removing Switchyard native messaging host..." + + for sub in "${NATIVE_CHROMIUM[@]}"; do + f="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json" + [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true + done + + for sub in "${NATIVE_FIREFOX[@]}"; do + f="$HOME/$sub/native-messaging-hosts/$NAME.json" + [[ -f $f ]] && rm -f "$f" && echo " removed $f" && N=$((N + 1)) || true + done + + for entry in "${FLATPAK_CHROMIUM[@]}"; do + appId="${entry%%:*}" + sub="${entry##*:}" + dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts" + f="$dir/$NAME.json" + [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true + done + + for entry in "${FLATPAK_FIREFOX[@]}"; do + appId="${entry%%:*}" + sub="${entry##*:}" + dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts" + f="$dir/$NAME.json" + [[ -f $f ]] && rm -f "$f" "$dir/switchyard-native-host.sh" && echo " removed $f" && N=$((N + 1)) || true + done + + [[ -f $WRAPPER ]] && rm -f "$WRAPPER" && echo " removed $WRAPPER" || true + + echo + echo "Removed $N manifest(s)." + exit 0 +fi + +# ── Install ──────────────────────────────────────────────────────────────────── + +echo "Installing Switchyard native messaging host..." + +mkdir -p "$(dirname "$WRAPPER")" +printf '#!/bin/sh\nexec %s "$@"\n' "$CMD" >"$WRAPPER" +chmod +x "$WRAPPER" +echo "Wrapper: $WRAPPER" + +# Flatpak browsers need a different wrapper: they detect the sandbox via +# $container and use flatpak-spawn --host to escape it. This requires the +# org.freedesktop.Flatpak talk-name, granted per-browser below. +FP_WRAPPER="#!/bin/sh +if [ \"\${container-}\" = flatpak ]; then + exec /usr/bin/flatpak-spawn --host $CMD +else + exec $CMD +fi" + +for sub in "${NATIVE_CHROMIUM[@]}"; do + [[ -d "$HOME/.config/$sub" ]] || continue + target="$HOME/.config/$sub/NativeMessagingHosts/$NAME.json" + mkdir -p "$(dirname "$target")" + chromium_manifest "$WRAPPER" >"$target" + echo " $target" + N=$((N + 1)) +done + +for sub in "${NATIVE_FIREFOX[@]}"; do + [[ -d "$HOME/$sub" ]] || continue + target="$HOME/$sub/native-messaging-hosts/$NAME.json" + mkdir -p "$(dirname "$target")" + firefox_manifest "$WRAPPER" >"$target" + echo " $target" + N=$((N + 1)) +done + +for entry in "${FLATPAK_CHROMIUM[@]}"; do + appId="${entry%%:*}" + sub="${entry##*:}" + [[ -d "$HOME/.var/app/$appId" ]] || continue + dir="$HOME/.var/app/$appId/config/$sub/NativeMessagingHosts" + wp="$dir/switchyard-native-host.sh" + mkdir -p "$dir" + printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp" + chromium_manifest "$wp" >"$dir/$NAME.json" + flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId" + echo " $dir/$NAME.json [Flatpak]" + N=$((N + 1)) +done + +for entry in "${FLATPAK_FIREFOX[@]}"; do + appId="${entry%%:*}" + sub="${entry##*:}" + [[ -d "$HOME/.var/app/$appId" ]] || continue + dir="$HOME/.var/app/$appId/$sub/native-messaging-hosts" + wp="$dir/switchyard-native-host.sh" + mkdir -p "$dir" + printf '%s\n' "$FP_WRAPPER" >"$wp" && chmod +x "$wp" + firefox_manifest "$wp" >"$dir/$NAME.json" + flatpak override --user --talk-name=org.freedesktop.Flatpak "$appId" + echo " $dir/$NAME.json [Flatpak]" + N=$((N + 1)) +done + +[[ $N -gt 0 ]] || { + echo + echo "No supported browsers found." + exit 0 +} +echo +echo "Installed $N manifest(s). Restart your browser to activate." diff --git a/src/install_native_host.go b/src/install_native_host.go deleted file mode 100644 index 1905180..0000000 --- a/src/install_native_host.go +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package main - -import ( - "bytes" - _ "embed" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" -) - -//go:embed embedded/native-messaging-host-chromium.json -var chromiumManifestTemplate string - -//go:embed embedded/native-messaging-host-firefox.json -var firefoxManifestTemplate string - -// Must match HOST in webextension/popup.js. -const nativeHostName = "io.github.alyraffauf.switchyard" - -var chromiumConfigSubdirs = []string{ - "net.imput.helium", - "google-chrome", - "chromium", - "BraveSoftware/Brave-Browser", - "microsoft-edge", - "vivaldi", -} - -var firefoxHomeSubdirs = []string{ - ".mozilla", - ".librewolf", - ".zen", -} - -func installNativeHost() error { - home, err := os.UserHomeDir() - if err != nil { - return err - } - wrapperPath, err := ensureNativeHostWrapper(home) - if err != nil { - return err - } - fmt.Println("Wrapper:", wrapperPath) - - chromiumJSON := []byte(strings.ReplaceAll(chromiumManifestTemplate, "@SWITCHYARD_NM@", wrapperPath)) - firefoxJSON := []byte(strings.ReplaceAll(firefoxManifestTemplate, "@SWITCHYARD_NM@", wrapperPath)) - - installed := 0 - for _, sub := range chromiumConfigSubdirs { - if installManifestTo(filepath.Join(home, ".config", sub), "NativeMessagingHosts", chromiumJSON) { - installed++ - } - } - for _, sub := range firefoxHomeSubdirs { - if installManifestTo(filepath.Join(home, sub), "native-messaging-hosts", firefoxJSON) { - installed++ - } - } - - if installed == 0 { - fmt.Println("\nNo supported browsers found. Install a Chromium- or Firefox-family browser first.") - return nil - } - fmt.Printf("\nInstalled %d manifest(s). Restart your browser to detect the native host.\n", installed) - return nil -} - -func uninstallNativeHost() error { - home, err := os.UserHomeDir() - if err != nil { - return err - } - - removed := 0 - for _, sub := range chromiumConfigSubdirs { - if uninstallManifestFrom(filepath.Join(home, ".config", sub), "NativeMessagingHosts") { - removed++ - } - } - for _, sub := range firefoxHomeSubdirs { - if uninstallManifestFrom(filepath.Join(home, sub), "native-messaging-hosts") { - removed++ - } - } - - wrappers := []string{ - nativeHostWrapperPath(home), - // Legacy locations from earlier installs. - filepath.Join(home, ".local/share/flatpak/exports/bin/io.github.alyraffauf.Switchyard-native-host"), - filepath.Join(home, ".local/share/flatpak/exports/bin/io.github.alyraffauf.Switchyard.Devel-native-host"), - } - for _, p := range wrappers { - if removeFileHost(p) { - fmt.Println(" removed " + p) - } - } - - fmt.Printf("\nRemoved %d manifest(s).\n", removed) - return nil -} - -func installManifestTo(profileDir, manifestSubdir string, content []byte) bool { - if _, err := os.Stat(profileDir); err != nil { - return false - } - target := filepath.Join(profileDir, manifestSubdir, nativeHostName+".json") - if err := writeFileHost(target, content, 0o644); err != nil { - fmt.Fprintln(os.Stderr, "warning:", target+":", err) - return false - } - fmt.Println(" " + target) - return true -} - -func uninstallManifestFrom(profileDir, manifestSubdir string) bool { - target := filepath.Join(profileDir, manifestSubdir, nativeHostName+".json") - if !removeFileHost(target) { - return false - } - fmt.Println(" removed " + target) - return true -} - -func nativeHostWrapperPath(home string) string { - return filepath.Join(home, ".local/share/switchyard/native-host-wrapper.sh") -} - -// ensureNativeHostWrapper writes the script the browser will exec as the -// manifest's "path" field. The manifest format forbids inline args, so we -// need a shim to add --native-host before re-entering switchyard. -func ensureNativeHostWrapper(home string) (string, error) { - wrapperPath := nativeHostWrapperPath(home) - var script string - if id := os.Getenv("FLATPAK_ID"); id != "" { - script = fmt.Sprintf("#!/bin/sh\nexec /usr/bin/flatpak run --command=switchyard %s --native-host \"$@\"\n", id) - } else { - self, err := os.Executable() - if err != nil { - return "", err - } - script = fmt.Sprintf("#!/bin/sh\nexec %s --native-host \"$@\"\n", shellQuote(self)) - } - if err := writeFileHost(wrapperPath, []byte(script), 0o755); err != nil { - return "", fmt.Errorf("create wrapper at %s: %w", wrapperPath, err) - } - return wrapperPath, nil -} - -// writeFileHost writes to a path on the host filesystem. Inside Flatpak the -// sandbox can read but not write most host paths, so it shells out via -// flatpak-spawn instead of writing directly. -func writeFileHost(path string, content []byte, mode os.FileMode) error { - if os.Getenv("FLATPAK_ID") != "" { - script := fmt.Sprintf("mkdir -p %s && cat > %s && chmod %o %s", - shellQuote(filepath.Dir(path)), shellQuote(path), mode, shellQuote(path)) - cmd := exec.Command("flatpak-spawn", "--host", "sh", "-c", script) - cmd.Stdin = bytes.NewReader(content) - cmd.Stderr = os.Stderr - return cmd.Run() - } - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - return os.WriteFile(path, content, mode) -} - -func removeFileHost(path string) bool { - if _, err := os.Stat(path); err != nil { - return false - } - if os.Getenv("FLATPAK_ID") != "" { - return exec.Command("flatpak-spawn", "--host", "rm", "-f", path).Run() == nil - } - return os.Remove(path) == nil -} - -func shellQuote(s string) string { - return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'" -} diff --git a/src/main.go b/src/main.go index 37150de..daf1530 100644 --- a/src/main.go +++ b/src/main.go @@ -4,7 +4,6 @@ package main import ( - "fmt" "net/url" "os" "strings" @@ -19,10 +18,6 @@ import ( func main() { app := adw.NewApplication(getAppID(), gio.ApplicationHandlesOpen) - app.AddMainOption("install-native-host", 0, glib.OptionFlagNone, glib.OptionArgNone, - "Install browser native-messaging host manifests", "") - app.AddMainOption("uninstall-native-host", 0, glib.OptionFlagNone, glib.OptionArgNone, - "Remove browser native-messaging host manifests", "") app.AddMainOption("native-host", 0, glib.OptionFlagNone, glib.OptionArgNone, "Run as native-messaging host (invoked by browsers)", "") @@ -31,20 +26,6 @@ func main() { runNativeMessagingHost() return 0 } - if options.Contains("install-native-host") { - if err := installNativeHost(); err != nil { - fmt.Fprintln(os.Stderr, "error:", err) - return 1 - } - return 0 - } - if options.Contains("uninstall-native-host") { - if err := uninstallNativeHost(); err != nil { - fmt.Fprintln(os.Stderr, "error:", err) - return 1 - } - return 0 - } return -1 }) -- 2.51.2