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.

webextension: split manifests; introduce context menu placements

Aly Raffauf (Jun 20, 2026, 6:48 PM EDT) b5450643 2723e624

+98 -16
+13 -5
.github/workflows/ci.yml
··· 45 45 - uses: actions/setup-node@v4 46 46 with: 47 47 node-version: "lts/*" 48 - - name: Build extension 49 - working-directory: webextension 50 - run: npm ci && npm run build 48 + - uses: extractions/setup-just@v2 49 + - name: Bundle Firefox extension 50 + run: just bundle-extension 51 + - name: Validate Firefox manifest 52 + run: | 53 + unzip -p switchyard-webextension.zip manifest.json > /tmp/manifest.json 54 + jq -e '.background.scripts == ["build/background.js"] and (.background.service_worker | not)' /tmp/manifest.json 51 55 52 56 build-flatpak: 53 57 name: "Build Flatpak (${{ matrix.variant.arch }})" ··· 85 89 working-directory: webextension 86 90 run: npm ci && npm run build 87 91 - name: Submit to AMO 88 - working-directory: webextension 89 - run: npx web-ext sign --channel=listed --approval-timeout=0 --api-key=${{ secrets.AMO_JWT_ISSUER }} --api-secret=${{ secrets.AMO_JWT_SECRET }} 92 + run: | 93 + staging="$(mktemp -d)" 94 + cp -R webextension/build webextension/icons webextension/popup.html "${staging}/" 95 + rm -f "${staging}/build/manifest.firefox.json" 96 + cp webextension/manifest.firefox.json "${staging}/manifest.json" 97 + npx web-ext sign --source-dir="${staging}" --channel=listed --approval-timeout=0 --api-key=${{ secrets.AMO_JWT_ISSUER }} --api-secret=${{ secrets.AMO_JWT_SECRET }} 90 98 91 99 publish-cws: 92 100 name: "Publish to Chrome Web Store"
+18 -7
justfile
··· 82 82 root="{{justfile_directory()}}" 83 83 extdir="${root}/webextension" 84 84 outfile="${root}/switchyard-webextension.zip" 85 + staging="$(mktemp -d)" 86 + trap 'rm -rf "${staging}"' EXIT 87 + 85 88 rm -f "${outfile}" 86 - cd "${extdir}" && zip -r "${outfile}" . \ 87 - -x '.git*' 'node_modules/*' 'src/*' 'web-ext-artifacts/*' \ 88 - -x 'package.json' 'package-lock.json' 'tsconfig.json' 'esbuild.config.mjs' 89 + cp -R "${extdir}/build" "${extdir}/icons" "${extdir}/popup.html" "${staging}/" 90 + rm -f "${staging}/build/manifest.firefox.json" 91 + cp "${extdir}/manifest.firefox.json" "${staging}/manifest.json" 92 + cd "${staging}" && zip -r "${outfile}" . 89 93 echo "Extension bundled: switchyard-webextension.zip" 90 94 91 95 # Cut a new release: bump version, commit, tag, and push master + tag ··· 96 100 tag="v${version}" 97 101 metainfo="data/{{APPID}}.metainfo.xml" 98 102 manifest="webextension/manifest.json" 103 + firefox_manifest="webextension/manifest.firefox.json" 99 104 pkgjson="webextension/package.json" 100 105 101 106 if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then ··· 109 114 exit 1 110 115 fi 111 116 112 - # Allow src/app.go, metainfo, extension manifest, and package.json to be dirty; nothing else. 113 - dirty="$(git status --porcelain -- ':!src/app.go' ":!${metainfo}" ":!${manifest}" ":!${pkgjson}")" 117 + # Allow src/app.go, metainfo, extension manifests, and package.json to be dirty; nothing else. 118 + dirty="$(git status --porcelain -- ':!src/app.go' ":!${metainfo}" ":!${manifest}" ":!${firefox_manifest}" ":!${pkgjson}")" 114 119 if [ -n "$dirty" ]; then 115 - echo "error: working tree has changes outside src/app.go, ${metainfo}, ${manifest}:" >&2 120 + echo "error: working tree has changes outside src/app.go, ${metainfo}, ${manifest}, ${firefox_manifest}:" >&2 116 121 echo "$dirty" >&2 117 122 exit 1 118 123 fi ··· 140 145 exit 1 141 146 fi 142 147 148 + sed -i -E 's/^(\s*"version"\s*:\s*)"[^"]+"/\1"'"${version}"'"/' "${firefox_manifest}" 149 + if ! grep -qE '"version"\s*:\s*"'"${version}"'"' "${firefox_manifest}"; then 150 + echo "error: failed to update version in ${firefox_manifest}" >&2 151 + exit 1 152 + fi 153 + 143 154 sed -i -E 's/^(\s*"version"\s*:\s*)"[^"]+"/\1"'"${version}"'"/' "${pkgjson}" 144 155 if ! grep -qE '"version"\s*:\s*"'"${version}"'"' "${pkgjson}"; then 145 156 echo "error: failed to update version in ${pkgjson}" >&2 146 157 exit 1 147 158 fi 148 159 149 - git add src/app.go "${metainfo}" "${manifest}" "${pkgjson}" 160 + git add src/app.go "${metainfo}" "${manifest}" "${firefox_manifest}" "${pkgjson}" 150 161 git commit -m "update for ${tag}" 151 162 git tag -a "${tag}" -m "${tag}" 152 163 git push origin master
+1 -1
webextension/esbuild.config.mjs
··· 2 2 3 3 const watch = process.argv.includes("--watch"); 4 4 const ctx = await esbuild.context({ 5 - entryPoints: ["src/App.tsx"], 5 + entryPoints: ["src/App.tsx", "src/background.ts"], 6 6 bundle: true, 7 7 outdir: "build", 8 8 format: "esm",
+26
webextension/manifest.firefox.json
··· 1 + { 2 + "manifest_version": 3, 3 + "name": "Switchyard", 4 + "description": "Open the current page in Switchyard for browser selection", 5 + "version": "0.16.1", 6 + "homepage_url": "https://github.com/alyraffauf/Switchyard", 7 + "permissions": ["activeTab", "contextMenus", "nativeMessaging"], 8 + "background": { 9 + "scripts": ["build/background.js"] 10 + }, 11 + "icons": { 12 + "16": "icons/switchyard-16.png", 13 + "48": "icons/switchyard-48.png", 14 + "128": "icons/switchyard-128.png" 15 + }, 16 + "action": { 17 + "default_popup": "popup.html", 18 + "default_icon": { 19 + "16": "icons/switchyard-16.png", 20 + "48": "icons/switchyard-48.png" 21 + } 22 + }, 23 + "browser_specific_settings": { 24 + "gecko": { "id": "switchyard@alyraffauf.github.io" } 25 + } 26 + }
+4 -1
webextension/manifest.json
··· 4 4 "description": "Open the current page in Switchyard for browser selection", 5 5 "version": "0.16.1", 6 6 "homepage_url": "https://github.com/alyraffauf/Switchyard", 7 - "permissions": ["activeTab", "nativeMessaging"], 7 + "permissions": ["activeTab", "contextMenus", "nativeMessaging"], 8 + "background": { 9 + "service_worker": "build/background.js" 10 + }, 8 11 "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmV3D3go08Nhd2PfSt7t5IbK2dT8n01umhIEbExICCWhkO6qZx4E/TW+OXH9CyOIDaUnItt+ZV3VWD1TkLvHNLij5RQLxym6VSzNA4WPplVJvY/E1x3qiBVE+3KZK8tF3bWQ0d7VBFn8sUd4JkyKCs69rn+K8fuej30RCXwqtnFSP2H+nD69vsbGoy2YQ8v8RRN1TlIcSEJEfmcMXT20H7gVH1j/egULrzpey/blrCMRjxFAwbb1eNKa4OwVe4i/g72moo6RvOl1gbxem8RqoXhzWxIBfhpetf0fNqsgS3ZwKgVLr+teyX4MNELA5En1TItjeyYYFNn++1Y4QB90OpwIDAQAB", 9 12 "icons": { 10 13 "16": "icons/switchyard-16.png",
+2 -2
webextension/package-lock.json
··· 1 1 { 2 2 "name": "switchyard-webextension", 3 - "version": "0.14.0", 3 + "version": "0.16.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "switchyard-webextension", 9 - "version": "0.14.0", 9 + "version": "0.16.1", 10 10 "dependencies": { 11 11 "react": "^19.1.0", 12 12 "react-dom": "^19.1.0",
+34
webextension/src/background.ts
··· 1 + const ROUTABLE = /^https?:/; 2 + const ROUTABLE_PATTERNS = ["http://*/*", "https://*/*"]; 3 + 4 + function openInSwitchyard(url: string, tabId: number) { 5 + const params = new URLSearchParams({ url }); 6 + chrome.tabs.update(tabId, { url: `switchyard://open?${params}` }); 7 + } 8 + 9 + chrome.runtime.onInstalled.addListener(() => { 10 + chrome.contextMenus.removeAll(() => { 11 + chrome.contextMenus.create({ 12 + id: "open-link", 13 + title: "Open in Switchyard", 14 + contexts: ["link"], 15 + targetUrlPatterns: ROUTABLE_PATTERNS, 16 + }); 17 + chrome.contextMenus.create({ 18 + id: "open-page", 19 + title: "Open in Switchyard", 20 + contexts: ["page"], 21 + documentUrlPatterns: ROUTABLE_PATTERNS, 22 + }); 23 + }); 24 + }); 25 + 26 + chrome.contextMenus.onClicked.addListener((info, tab) => { 27 + const tabId = tab?.id; 28 + if (tabId == null) return; 29 + if (info.menuItemId === "open-link" && info.linkUrl && ROUTABLE.test(info.linkUrl)) { 30 + openInSwitchyard(info.linkUrl, tabId); 31 + } else if (info.menuItemId === "open-page" && tab?.url && ROUTABLE.test(tab.url)) { 32 + openInSwitchyard(tab.url, tabId); 33 + } 34 + });