From 85534419678faec833979b2c663642a65053dcd9 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 20 Oct 2026 00:33:42 +0000 Subject: [PATCH] Open w3bs:// URIs through the w3bs.org resolver Chromium has no handler for w3bs://, so W3BS URIs now route to https://w3bs.org/browse?q= from every entry point: - launcher: `tron w3bs://...` args are rewritten to the gateway (TRONBROWSER_W3BS_GATEWAY overrides it) - desktop entries claim x-scheme-handler/w3bs; install.sh makes TronBrowser the default only when no other handler is set - bundled extension: w3bs-links.js turns clicks on w3bs: links into in-tab navigations (middle/modifier click opens a new tab), and the new-tab box treats a W3BS URI as an address in web and AI modes Co-Authored-By: Claude Opus 5.5 --- .../extensions/ai-sidebar/manifest.json | 12 +++++++ apps/desktop/extensions/ai-sidebar/newtab.js | 7 ++++ .../extensions/ai-sidebar/w3bs-links.js | 35 +++++++++++++++++++ apps/desktop/extensions/ai-sidebar/w3bs.js | 10 ++++++ .../extensions/ai-sidebar/w3bs.test.js | 22 ++++++++++++ apps/desktop/launcher/tronbrowser | 17 +++++++++ apps/desktop/scripts/build-release.sh | 2 +- apps/desktop/test/launcher.test.ts | 18 ++++++++++ apps/web/public/install.sh | 10 +++++- distribution/appimage/tronbrowser.desktop | 2 +- 10 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/extensions/ai-sidebar/w3bs-links.js create mode 100644 apps/desktop/extensions/ai-sidebar/w3bs.js create mode 100644 apps/desktop/extensions/ai-sidebar/w3bs.test.js diff --git a/apps/desktop/extensions/ai-sidebar/manifest.json b/apps/desktop/extensions/ai-sidebar/manifest.json index 7557721e..3d011d94 100644 --- a/apps/desktop/extensions/ai-sidebar/manifest.json +++ b/apps/desktop/extensions/ai-sidebar/manifest.json @@ -65,6 +65,18 @@ ], "run_at": "document_start" }, + { + "matches": [ + "https://*/*", + "http://localhost/*", + "http://127.0.0.1/*" + ], + "js": [ + "w3bs-links.js" + ], + "run_at": "document_start", + "all_frames": true + }, { "matches": [ "https://chromewebstore.google.com/detail/*", diff --git a/apps/desktop/extensions/ai-sidebar/newtab.js b/apps/desktop/extensions/ai-sidebar/newtab.js index b1ec393e..15babc6f 100644 --- a/apps/desktop/extensions/ai-sidebar/newtab.js +++ b/apps/desktop/extensions/ai-sidebar/newtab.js @@ -4,6 +4,7 @@ import { fetchQuotes, fetchAllScores } from './markets.js'; import { fetchMotd } from './motd.js'; import { getToken as btrToken, BTR_BASE } from './bittorrented.js'; import { fetchWithTimeout, storageGet } from './net.js'; +import { w3bsUrl } from './w3bs.js'; const el = (id) => document.getElementById(id); @@ -93,6 +94,12 @@ el('search').addEventListener('submit', async (e) => { e.preventDefault(); const q = el('q').value.trim(); if (!q) return; + // A W3BS URI is an address, not a question, in either mode. + const w3bs = w3bsUrl(q); + if (w3bs) { + location.href = w3bs; + return; + } if (searchMode === 'ai') { // Hand the question to the AI sidebar (privacy-first; uses your provider/key). await chrome.storage.local.set({ pendingAiQuery: { text: q, at: Date.now() } }); diff --git a/apps/desktop/extensions/ai-sidebar/w3bs-links.js b/apps/desktop/extensions/ai-sidebar/w3bs-links.js new file mode 100644 index 00000000..a53efc4d --- /dev/null +++ b/apps/desktop/extensions/ai-sidebar/w3bs-links.js @@ -0,0 +1,35 @@ +// w3bs:// links. Chromium has no handler for the scheme, so a click on one +// would ask to open xdg-open (or do nothing). Send it to the w3bs.org resolver +// page in the same tab instead; middle and modifier clicks open a new tab, the +// way an ordinary link would. Keep W3BS_GATEWAY in step with w3bs.js. +(() => { + const W3BS_GATEWAY = 'https://w3bs.org/browse?q='; + + const w3bsLink = (event) => { + const path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target]; + for (const node of path) { + if (node instanceof HTMLAnchorElement || node instanceof SVGAElement) { + const href = node instanceof SVGAElement ? node.href.baseVal : node.getAttribute('href'); + return href && /^\s*w3bs:/i.test(href) ? href.trim() : null; + } + } + return null; + }; + + const open = (event) => { + if (event.defaultPrevented || (event.type === 'auxclick' && event.button !== 1)) return; + const uri = w3bsLink(event); + if (!uri) return; + event.preventDefault(); + event.stopImmediatePropagation(); + const url = W3BS_GATEWAY + encodeURIComponent(uri); + if (event.type === 'auxclick' || event.ctrlKey || event.metaKey || event.shiftKey) { + window.open(url, '_blank', 'noopener'); + } else { + location.assign(url); + } + }; + + document.addEventListener('click', open, true); + document.addEventListener('auxclick', open, true); +})(); diff --git a/apps/desktop/extensions/ai-sidebar/w3bs.js b/apps/desktop/extensions/ai-sidebar/w3bs.js new file mode 100644 index 00000000..33a2dbb8 --- /dev/null +++ b/apps/desktop/extensions/ai-sidebar/w3bs.js @@ -0,0 +1,10 @@ +// W3BS URIs (w3bs://type/namespace/name@version) open through the w3bs.org +// resolver page: Chromium has no handler for the scheme. The launcher and the +// w3bs-links.js content script use the same gateway. +export const W3BS_GATEWAY = 'https://w3bs.org/browse?q='; + +/** The resolver URL for a w3bs: URI, or null when `input` is not one. */ +export function w3bsUrl(input) { + const uri = String(input ?? '').trim(); + return /^w3bs:/i.test(uri) ? W3BS_GATEWAY + encodeURIComponent(uri) : null; +} diff --git a/apps/desktop/extensions/ai-sidebar/w3bs.test.js b/apps/desktop/extensions/ai-sidebar/w3bs.test.js new file mode 100644 index 00000000..71c3a732 --- /dev/null +++ b/apps/desktop/extensions/ai-sidebar/w3bs.test.js @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { w3bsUrl } from './w3bs.js'; + +describe('w3bsUrl', () => { + it('sends a W3BS URI to the w3bs.org resolver', () => { + expect(w3bsUrl('w3bs://prompt/w3bs/research@1')).toBe( + 'https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401', + ); + }); + + it('accepts any case and surrounding space', () => { + expect(w3bsUrl(' W3BS://prompt/w3bs/research ')).toBe( + 'https://w3bs.org/browse?q=W3BS%3A%2F%2Fprompt%2Fw3bs%2Fresearch', + ); + }); + + it('leaves everything else alone', () => { + expect(w3bsUrl('https://w3bs.org')).toBeNull(); + expect(w3bsUrl('what is w3bs://')).toBeNull(); + expect(w3bsUrl('')).toBeNull(); + }); +}); diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index a0ae2eae..b454b780 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -38,6 +38,23 @@ while [ "$_i" -lt "$_argc" ]; do set -- "$@" "$_a" done +# --- w3bs:// URIs --------------------------------------------------------- +# Chromium has no handler for w3bs://, so a W3BS URI handed to us (`tron +# w3bs://prompt/w3bs/research@1`, or the desktop entry's x-scheme-handler/w3bs) +# opens through the w3bs.org resolver page instead. Override the gateway with +# TRONBROWSER_W3BS_GATEWAY; the URI is appended percent-encoded. +W3BS_GATEWAY="${TRONBROWSER_W3BS_GATEWAY:-https://w3bs.org/browse?q=}" +_argc=$# +_i=0 +while [ "$_i" -lt "$_argc" ]; do + _a="$1"; shift; _i=$((_i + 1)) + case "$_a" in + w3bs:*|W3BS:*) + _a="$W3BS_GATEWAY$(printf '%s' "$_a" | od -An -v -tx1 | tr -d ' \n' | sed 's/\(..\)/%\1/g')" ;; + esac + set -- "$@" "$_a" +done + BROWSER="" # a binary path, or "flatpak" FLATPAK_APP="" IS_SNAP=0 diff --git a/apps/desktop/scripts/build-release.sh b/apps/desktop/scripts/build-release.sh index a0cd6ae1..b5cb41b1 100755 --- a/apps/desktop/scripts/build-release.sh +++ b/apps/desktop/scripts/build-release.sh @@ -160,7 +160,7 @@ TryExec=tron Icon=tronbrowser Terminal=false Categories=Network;WebBrowser; -MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https; +MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/w3bs; Keywords=web;browser;privacy;tor;ai; StartupNotify=true StartupWMClass=tronbrowser diff --git a/apps/desktop/test/launcher.test.ts b/apps/desktop/test/launcher.test.ts index 2be31366..5fd12f72 100644 --- a/apps/desktop/test/launcher.test.ts +++ b/apps/desktop/test/launcher.test.ts @@ -115,6 +115,24 @@ describe('launcher flags', () => { expect(argv).toContain('https://tronbrowser.dev'); expect(argv).toContain('--incognito'); }); + + it('opens a w3bs:// URI through the w3bs.org resolver', () => { + const { argv } = run(['w3bs://prompt/w3bs/research@1', 'https://tronbrowser.dev']); + const opened = argv.filter((a) => !a.startsWith('--')); + expect(opened).toHaveLength(2); + const url = new URL(opened[0]!); + expect(url.origin + url.pathname).toBe('https://w3bs.org/browse'); + expect(url.searchParams.get('q')).toBe('w3bs://prompt/w3bs/research@1'); + expect(opened[1]).toBe('https://tronbrowser.dev'); + }); + + it('honours TRONBROWSER_W3BS_GATEWAY', () => { + const { argv } = run(['w3bs://prompt/w3bs/research'], { + env: { TRONBROWSER_W3BS_GATEWAY: 'http://127.0.0.1:8080/browse?q=' }, + }); + const url = new URL(argv.find((a) => a.startsWith('http://127.0.0.1:8080/'))!); + expect(url.searchParams.get('q')).toBe('w3bs://prompt/w3bs/research'); + }); }); // --- Omnibox search engine --------------------------------------------------- diff --git a/apps/web/public/install.sh b/apps/web/public/install.sh index b68d16ba..4aaf6087 100755 --- a/apps/web/public/install.sh +++ b/apps/web/public/install.sh @@ -1182,11 +1182,19 @@ Exec=$TRON_CLI %U Icon=$icon Terminal=false Categories=Network;WebBrowser; -MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https; +MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/w3bs; StartupWMClass=TronBrowser StartupNotify=true DESKTOP command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps_dir" 2>/dev/null || true + # Open w3bs:// links through TronBrowser (the launcher hands them to the + # w3bs.org resolver), unless the user already picked another handler. + if command -v xdg-mime >/dev/null 2>&1; then + _w3bs="$(xdg-mime query default x-scheme-handler/w3bs 2>/dev/null || true)" + if [ -z "$_w3bs" ] || [ "$_w3bs" = "tronbrowser.desktop" ]; then + xdg-mime default tronbrowser.desktop x-scheme-handler/w3bs 2>/dev/null || true + fi + fi ensure_engine || true # TronBrowser's own engine; ensure_browser is the fallback ensure_browser diff --git a/distribution/appimage/tronbrowser.desktop b/distribution/appimage/tronbrowser.desktop index 56b1bacf..b9487cfc 100644 --- a/distribution/appimage/tronbrowser.desktop +++ b/distribution/appimage/tronbrowser.desktop @@ -7,4 +7,4 @@ Exec=tron %u Icon=tronbrowser Terminal=false Categories=Network;WebBrowser; -MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https; +MimeType=text/html;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/w3bs;