From 160e510da7d2a220d3fa193850494d9123bf991e Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 20 Oct 2026 00:51:26 +0000 Subject: [PATCH 1/3] w3bs:// on every platform: OS handler registration for macOS, Windows, Linux A W3BS link from another app, or one typed in the address bar (Chromium hands any scheme it cannot load to the OS), now reaches TronBrowser everywhere: - macOS: tron-w3bs-register builds ~/Applications/TronBrowser Links.app, a faceless applet declaring CFBundleURLTypes w3bs that runs the launcher (re-signed ad hoc, registered with LaunchServices, default only if unclaimed) - Windows: tron-w3bs.ps1 registers HKCU\Software\Classes\w3bs and opens the URI; cmd.exe never parses a URI from a page. `tron w3bs://...` works too - Linux: the launcher sets the xdg default for installs that never ran install.sh (deb/rpm/AppImage/brew), unless another app handles w3bs The launcher and tronbrowser.cmd register in the background on every start, so every install method heals after upgrades. The launcher now encodes the URI like encodeURIComponent, so the address bar stays readable. w3bs-scheme.yml proves each OS's real dispatch (xdg-open, open, Start-Process) reaches the engine with the resolver URL. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 1 + .github/workflows/w3bs-scheme.yml | 46 ++++++++++++ apps/desktop/launcher/tron-w3bs-register | 92 +++++++++++++++++++++++ apps/desktop/launcher/tron-w3bs.ps1 | 48 ++++++++++++ apps/desktop/launcher/tronbrowser | 16 +++- apps/desktop/launcher/tronbrowser.cmd | 12 +++ apps/desktop/scripts/build-release.sh | 4 + apps/desktop/test/launcher.test.ts | 2 + apps/desktop/test/w3bs-scheme/posix.sh | 83 ++++++++++++++++++++ apps/desktop/test/w3bs-scheme/windows.ps1 | 66 ++++++++++++++++ 10 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/w3bs-scheme.yml create mode 100755 apps/desktop/launcher/tron-w3bs-register create mode 100644 apps/desktop/launcher/tron-w3bs.ps1 create mode 100644 apps/desktop/test/w3bs-scheme/posix.sh create mode 100644 apps/desktop/test/w3bs-scheme/windows.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60a5ef2..31740e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -120,6 +120,7 @@ jobs: Copy-Item apps/desktop/launcher/tronbrowser.cmd "$stage/tronbrowser.cmd" Copy-Item apps/desktop/launcher/tron-tor-helper "$stage/tron-tor-helper" Copy-Item apps/desktop/launcher/tron-windows.py "$stage/tron-windows.py" + Copy-Item apps/desktop/launcher/tron-w3bs.ps1 "$stage/tron-w3bs.ps1" Copy-Item -Recurse apps/desktop/extensions/ai-sidebar "$stage/extensions/ai-sidebar" # Same wholesale-copy problem build-release.sh has: the vitest files # next to the extension sources ride along into the zip. Chrome never diff --git a/.github/workflows/w3bs-scheme.yml b/.github/workflows/w3bs-scheme.yml new file mode 100644 index 0000000..7c76065 --- /dev/null +++ b/.github/workflows/w3bs-scheme.yml @@ -0,0 +1,46 @@ +name: w3bs:// handler + +# The OS-level half of w3bs:// support: each platform's real URL dispatch +# (xdg-open, LaunchServices, ShellExecute) has to reach the launcher, which +# opens the w3bs.org resolver. Nothing here can be checked from Linux alone. +on: + push: + branches: [main] + paths: + - 'apps/desktop/launcher/**' + - 'apps/desktop/test/w3bs-scheme/**' + - '.github/workflows/w3bs-scheme.yml' + pull_request: + paths: + - 'apps/desktop/launcher/**' + - 'apps/desktop/test/w3bs-scheme/**' + - '.github/workflows/w3bs-scheme.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + posix: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - name: Install xdg-utils + if: runner.os == 'Linux' + run: command -v xdg-open || (sudo apt-get update -qq && sudo apt-get install -y -qq xdg-utils) + - name: OS opens w3bs:// in TronBrowser + run: sh apps/desktop/test/w3bs-scheme/posix.sh + + windows: + runs-on: windows-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - name: Windows opens w3bs:// in TronBrowser + shell: pwsh + run: ./apps/desktop/test/w3bs-scheme/windows.ps1 diff --git a/apps/desktop/launcher/tron-w3bs-register b/apps/desktop/launcher/tron-w3bs-register new file mode 100755 index 0000000..f908f06 --- /dev/null +++ b/apps/desktop/launcher/tron-w3bs-register @@ -0,0 +1,92 @@ +#!/bin/sh +# Register TronBrowser as the handler for w3bs:// URIs with the OS, so a W3BS +# link from another app, or one typed in the address bar (Chromium hands any +# scheme it cannot load to the OS), comes back to `tron`, which opens it +# through the w3bs.org resolver. +# +# tron-w3bs-register +# +# is what the OS runs with the URI as its one argument: the launcher +# passes its own path. Idempotent, quiet and best-effort: the launcher runs it +# in the background on every start, so upgrades and moved installs heal +# themselves. A handler someone else registered (the w3bs.org inspector, say) is +# left alone. Windows registers from tron-w3bs.ps1 instead. +set -u + +TARGET="${TRONBROWSER_W3BS_HANDLER_TARGET:-${1:-}}" +[ -n "$TARGET" ] || { echo "usage: tron-w3bs-register " >&2; exit 2; } + +linux() { + command -v xdg-mime >/dev/null 2>&1 || return 0 + _cur="$(xdg-mime query default x-scheme-handler/w3bs 2>/dev/null || true)" + [ -z "$_cur" ] || return 0 + # Only claim the scheme through a desktop entry that declares it; install.sh, + # the .deb/.rpm and the AppImage all write one named tronbrowser.desktop. + for _d in "${XDG_DATA_HOME:-$HOME/.local/share}" $(printf '%s' "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" | tr ':' ' '); do + if grep -qs 'x-scheme-handler/w3bs' "$_d/applications/tronbrowser.desktop"; then + xdg-mime default tronbrowser.desktop x-scheme-handler/w3bs 2>/dev/null || true + return 0 + fi + done +} + +# macOS routes a URL scheme to an app bundle that declares it, never to a +# script, and Chromium.app cannot be the one: it would hand w3bs:// straight +# back to the OS. So build a small faceless applet that declares w3bs and runs +# $TARGET with the URI. +macos() { + APP="${TRONBROWSER_W3BS_APP:-$HOME/Applications/TronBrowser Links.app}" + ID="dev.tronbrowser.links" + STAMP="$APP/Contents/Resources/tron-target" + if [ -f "$STAMP" ] && [ "$(cat "$STAMP" 2>/dev/null)" = "$TARGET" ]; then + return 0 + fi + command -v osacompile >/dev/null 2>&1 || return 0 + PB=/usr/libexec/PlistBuddy + LSREG=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister + + _src="$(mktemp -t tron-w3bs)" || return 0 + # AppleScript string literal: escape backslashes and double quotes. + _t="$(printf '%s' "$TARGET" | sed 's/\\/\\\\/g; s/"/\\"/g')" + cat > "$_src" </dev/null 2>&1 &" +end open location +on run +end run +EOF + mkdir -p "$(dirname "$APP")" + rm -rf "$APP" + osacompile -o "$APP" "$_src" || { rm -f "$_src"; return 0; } + rm -f "$_src" + + _plist="$APP/Contents/Info.plist" + "$PB" -c "Set :CFBundleIdentifier $ID" "$_plist" 2>/dev/null || "$PB" -c "Add :CFBundleIdentifier string $ID" "$_plist" + "$PB" -c "Set :CFBundleName TronBrowser Links" "$_plist" 2>/dev/null || true + "$PB" -c "Add :LSUIElement bool true" "$_plist" 2>/dev/null || true + "$PB" -c "Delete :CFBundleURLTypes" "$_plist" 2>/dev/null || true + "$PB" -c "Add :CFBundleURLTypes array" "$_plist" + "$PB" -c "Add :CFBundleURLTypes:0 dict" "$_plist" + "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLName string W3BS resource" "$_plist" + "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$_plist" + "$PB" -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string w3bs" "$_plist" + printf '%s' "$TARGET" > "$STAMP" + # Editing Info.plist breaks the applet's ad-hoc signature, and Apple silicon + # refuses to run an app with a broken one. + codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true + [ -x "$LSREG" ] && "$LSREG" -f "$APP" >/dev/null 2>&1 || true + + # Become the default only when no other app has claimed w3bs. + osascript -l JavaScript >/dev/null 2>&1 < open a W3BS URI through the w3bs.org resolver +# +# The OS runs this script, not tronbrowser.cmd, for a w3bs:// link: a URI from +# a web page must never be parsed by cmd.exe, where & and % mean something. +# tronbrowser.cmd runs -Register in the background on every start, so scoop, +# choco, winget and the ZIP all register, and an upgrade that moved the files +# re-points the handler. A handler someone else registered is left alone. +param( + [Parameter(Position = 0)][string]$Uri = '', + [switch]$Register +) +$ErrorActionPreference = 'Stop' +$script = $MyInvocation.MyCommand.Path +$here = Split-Path -Parent $script +$key = 'HKCU:\Software\Classes\w3bs' + +if ($Register) { + $ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + $command = '"{0}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{1}" "%1"' -f $ps, $script + $current = $null + if (Test-Path "$key\shell\open\command") { + $current = (Get-Item "$key\shell\open\command").GetValue('') + } + if ($current -eq $command) { exit 0 } + if ($current -and $current -notmatch 'tron-w3bs\.ps1') { exit 0 } + New-Item -Path "$key\shell\open\command" -Force | Out-Null + Set-Item -Path $key -Value 'URL:W3BS resource' + New-ItemProperty -Path $key -Name 'URL Protocol' -Value '' -PropertyType String -Force | Out-Null + Set-Item -Path "$key\shell\open\command" -Value $command + exit 0 +} + +$gateway = if ($env:TRONBROWSER_W3BS_GATEWAY) { $env:TRONBROWSER_W3BS_GATEWAY } else { 'https://w3bs.org/browse?q=' } +$u = $Uri.Trim() +# W3BS-URI-1 identifiers are lowercase letters, digits, - . / @ and the scheme: +# nothing a query string or cmd.exe needs escaped, so a well-formed one goes +# through as written. Anything else opens the resolver without it rather than +# smuggling % or & into the command line below. +if ($u -match '^(?i)w3bs://[a-z0-9][a-z0-9./@-]{0,500}$') { + $url = $gateway + $u +} else { + $url = $gateway -replace '\?q=$', '' +} +& (Join-Path $here 'tronbrowser.cmd') $url +exit $LASTEXITCODE diff --git a/apps/desktop/launcher/tronbrowser b/apps/desktop/launcher/tronbrowser index b454b78..ddc4c07 100755 --- a/apps/desktop/launcher/tronbrowser +++ b/apps/desktop/launcher/tronbrowser @@ -50,7 +50,13 @@ 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')" ;; + # Percent-encode like encodeURIComponent: unreserved characters stay. + _a="$W3BS_GATEWAY$(printf '%s' "$_a" | LC_ALL=C awk ' + BEGIN { for (i = 1; i < 256; i++) ord[sprintf("%c", i)] = i } + { for (i = 1; i <= length($0); i++) { + c = substr($0, i, 1) + if (c ~ /[A-Za-z0-9._~!*()\047-]/) printf "%s", c; else printf "%%%02X", ord[c] + } }')" ;; esac set -- "$@" "$_a" done @@ -774,6 +780,14 @@ if [ "$TOR" != "1" ] && [ -z "$MAC_APP" ] && [ -f "$DIR/tron-pwa" ]; then fi fi +# w3bs:// from other apps and the address bar reaches the OS, which must hand it +# back to this launcher (see the w3bs:// rewrite above). On every start, in the +# background, so package-manager installs that never ran install.sh register +# too and an upgrade that moved us re-points the handler. +if [ -f "$DIR/tron-w3bs-register" ]; then + sh "$DIR/tron-w3bs-register" "$DIR/$(basename "$SELF")" >/dev/null 2>&1 { 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'); + // Encoded the way encodeURIComponent does it, so the address bar stays readable. + expect(opened[0]).toBe('https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401'); expect(opened[1]).toBe('https://tronbrowser.dev'); }); diff --git a/apps/desktop/test/w3bs-scheme/posix.sh b/apps/desktop/test/w3bs-scheme/posix.sh new file mode 100644 index 0000000..967aa88 --- /dev/null +++ b/apps/desktop/test/w3bs-scheme/posix.sh @@ -0,0 +1,83 @@ +#!/bin/sh +# End to end on Linux and macOS: register w3bs:// the way the launcher does, ask +# the OS to open a W3BS URI (xdg-open / open, the same path a link from another +# app or the address bar takes), and check the engine was handed the w3bs.org +# resolver URL. The engine is a stub that records its argv, and HOME and the XDG +# dirs are scratch, so nothing on the machine running this is touched except, +# on macOS, LaunchServices learning about the scratch applet. +set -eu + +REPO="$(CDPATH= cd -- "$(dirname -- "$0")/../../../.." && pwd)" +W="$(mktemp -d)" +export HOME="$W/home" +export XDG_DATA_HOME="$HOME/.local/share" XDG_CONFIG_HOME="$HOME/.config" +mkdir -p "$W/bin" "$XDG_DATA_HOME/applications" "$XDG_CONFIG_HOME" +cp "$REPO/apps/desktop/launcher/tronbrowser" "$REPO/apps/desktop/launcher/tron-w3bs-register" "$W/bin/" +ARGV="$W/argv" +URI='w3bs://prompt/w3bs/research@1' +WANT='https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401' +APP="$HOME/Applications/TronBrowser Links.app" + +# Named ungoogled-chromium so the launcher's de-googled check stays quiet. +cat > "$W/bin/ungoogled-chromium" < "$ARGV.tmp" && mv "$ARGV.tmp" "$ARGV" +EOF +# What the OS runs. LaunchServices and xdg-open start it with their own bare +# environment, so the stub engine and scratch profile are set here. +cat > "$W/bin/tron" <&2; exit 1; } + +case "$(uname -s)" in + Darwin) + TRONBROWSER_W3BS_APP="$APP" sh "$W/bin/tron-w3bs-register" "$W/bin/tron" + PLIST="$APP/Contents/Info.plist" + [ -f "$PLIST" ] || fail "no handler app at $APP" + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleURLTypes:0:CFBundleURLSchemes:0' "$PLIST")" = w3bs ] \ + || fail "handler app does not declare w3bs" + codesign --verify "$APP" || fail "handler app signature is broken" + open "$URI" + ;; + *) + command -v xdg-open >/dev/null 2>&1 || fail "xdg-open is not installed" + cat > "$XDG_DATA_HOME/applications/tronbrowser.desktop" < "$XDG_DATA_HOME/applications/other.desktop" + xdg-mime default other.desktop x-scheme-handler/w3bs + sh "$W/bin/tron-w3bs-register" "$W/bin/tron" + [ "$(xdg-mime query default x-scheme-handler/w3bs)" = other.desktop ] \ + || fail "replaced another app's w3bs handler" + # ...and with none set, TronBrowser claims the scheme. + rm -f "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/other.desktop" + sh "$W/bin/tron-w3bs-register" "$W/bin/tron" + [ "$(xdg-mime query default x-scheme-handler/w3bs)" = tronbrowser.desktop ] \ + || fail "tronbrowser.desktop is not the w3bs handler" + # xdg-open only consults scheme handlers when it sees a display, which a + # real desktop always has and a CI runner never does. It only checks that + # the variable is set; nothing connects to it. + DISPLAY="${DISPLAY:-:99}" xdg-open "$URI" + ;; +esac + +i=0 +while [ ! -s "$ARGV" ] && [ "$i" -lt 60 ]; do sleep 1; i=$((i + 1)); done +[ -s "$ARGV" ] || fail "the engine was never started for $URI" +grep -qxF "$WANT" "$ARGV" || { cat "$ARGV" >&2; fail "the engine was not handed $WANT"; } +echo "ok: $URI -> $WANT" +rm -rf "$W" diff --git a/apps/desktop/test/w3bs-scheme/windows.ps1 b/apps/desktop/test/w3bs-scheme/windows.ps1 new file mode 100644 index 0000000..28abd2d --- /dev/null +++ b/apps/desktop/test/w3bs-scheme/windows.ps1 @@ -0,0 +1,66 @@ +# End to end on Windows: register w3bs:// the way tronbrowser.cmd does, ask the +# shell to open a W3BS URI (Start-Process, the same ShellExecute path a link +# from another app or the address bar takes), and check the engine was handed +# the w3bs.org resolver URL. The engine is a stub .cmd that records its argv. +# Writes HKCU\Software\Classes\w3bs; meant for a disposable CI runner. +$ErrorActionPreference = 'Stop' +$repo = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path +$w = Join-Path ([IO.Path]::GetTempPath()) ("tron-w3bs-" + [guid]::NewGuid()) +New-Item -ItemType Directory -Force $w | Out-Null +Copy-Item (Join-Path $repo 'apps\desktop\launcher\tronbrowser.cmd') $w +Copy-Item (Join-Path $repo 'apps\desktop\launcher\tron-w3bs.ps1') $w +$argv = Join-Path $w 'argv.txt' +$stub = Join-Path $w 'stub-browser.cmd' +Set-Content -Encoding ascii $stub "@echo off`r`n>>`"%TB_ARGV%`" echo(%*`r`n" +$env:TRONBROWSER_BROWSER = $stub +$env:TRONBROWSER_DATA = Join-Path $w 'profile' +$env:TB_ARGV = $argv +$key = 'HKCU:\Software\Classes\w3bs' +$uri = 'w3bs://prompt/w3bs/research@1' +$want = 'https://w3bs.org/browse?q=w3bs://prompt/w3bs/research@1' +$ps = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + +function Wait-Argv([string]$expect, [string]$what) { + for ($i = 0; $i -lt 60 -and -not (Test-Path $argv); $i++) { Start-Sleep -Seconds 1 } + if (-not (Test-Path $argv)) { throw "FAIL ($what): the engine was never started" } + Start-Sleep -Milliseconds 500 + $got = Get-Content -Raw $argv + Remove-Item $argv + if (-not $got.Contains($expect)) { throw "FAIL ($what): wanted $expect, engine got: $got" } + Write-Host "ok ($what): $expect" +} +function Handler { if (Test-Path "$key\shell\open\command") { (Get-Item "$key\shell\open\command").GetValue('') } } + +Remove-Item -Recurse -Force $key -ErrorAction SilentlyContinue + +# A normal launch registers the scheme, in the background. +& cmd.exe /d /c "`"$w\tronbrowser.cmd`" https://example.invalid/" +Wait-Argv 'https://example.invalid/' 'plain launch' +for ($i = 0; $i -lt 60 -and -not (Handler); $i++) { Start-Sleep -Seconds 1 } +if ((Handler) -notlike "*$w\tron-w3bs.ps1*") { throw "FAIL: launch did not register w3bs (handler: $(Handler))" } +if ($null -eq (Get-Item $key).GetValue('URL Protocol')) { throw 'FAIL: no URL Protocol value' } +Write-Host "ok (register): $(Handler)" + +# The shell hands a w3bs:// URI to TronBrowser. +Start-Process $uri +Wait-Argv $want 'Start-Process' + +# `tron w3bs://...` from a terminal. +& cmd.exe /d /c "`"$w\tronbrowser.cmd`" $uri" +Wait-Argv $want 'tron w3bs://' + +# A malformed URI with cmd.exe metacharacters opens the resolver without it. +$marker = Join-Path $w 'pwned.txt' +& $ps -NoProfile -ExecutionPolicy Bypass -File "$w\tron-w3bs.ps1" "w3bs://x&echo pwned>`"$marker`"&" +Wait-Argv 'https://w3bs.org/browse' 'hostile URI' +if (Test-Path $marker) { throw 'FAIL: a w3bs:// URI ran a command' } + +# Someone else's handler is left alone. +Set-Item "$key\shell\open\command" -Value '"C:\Other\other.exe" "%1"' +& $ps -NoProfile -ExecutionPolicy Bypass -File "$w\tron-w3bs.ps1" -Register +if ((Handler) -ne '"C:\Other\other.exe" "%1"') { throw "FAIL: replaced another app's handler: $(Handler)" } +Write-Host 'ok (other handler kept)' + +Remove-Item -Recurse -Force $key +Remove-Item -Recurse -Force $w -ErrorAction SilentlyContinue +Write-Host 'all w3bs:// Windows checks passed' From ae4abe565c01b7a74d93559f039d4c4b90f7ff2d Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 20 Oct 2026 00:55:15 +0000 Subject: [PATCH 2/3] w3bs:// register: write the xdg default GIO needs; test the GNOME path, retry LaunchServices Co-Authored-By: Claude Opus 5.5 --- .github/workflows/w3bs-scheme.yml | 6 ++++-- apps/desktop/launcher/tron-w3bs-register | 5 ++++- apps/desktop/test/w3bs-scheme/posix.sh | 23 ++++++++++++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/w3bs-scheme.yml b/.github/workflows/w3bs-scheme.yml index 7c76065..5ff8c85 100644 --- a/.github/workflows/w3bs-scheme.yml +++ b/.github/workflows/w3bs-scheme.yml @@ -30,9 +30,11 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v5 - - name: Install xdg-utils + - name: Install xdg-utils and gio if: runner.os == 'Linux' - run: command -v xdg-open || (sudo apt-get update -qq && sudo apt-get install -y -qq xdg-utils) + run: | + command -v xdg-open && command -v gio \ + || (sudo apt-get update -qq && sudo apt-get install -y -qq xdg-utils libglib2.0-bin) - name: OS opens w3bs:// in TronBrowser run: sh apps/desktop/test/w3bs-scheme/posix.sh diff --git a/apps/desktop/launcher/tron-w3bs-register b/apps/desktop/launcher/tron-w3bs-register index f908f06..73ba732 100755 --- a/apps/desktop/launcher/tron-w3bs-register +++ b/apps/desktop/launcher/tron-w3bs-register @@ -19,7 +19,10 @@ TARGET="${TRONBROWSER_W3BS_HANDLER_TARGET:-${1:-}}" linux() { command -v xdg-mime >/dev/null 2>&1 || return 0 _cur="$(xdg-mime query default x-scheme-handler/w3bs 2>/dev/null || true)" - [ -z "$_cur" ] || return 0 + # xdg-mime reports tronbrowser.desktop merely because it declares the scheme, + # while GIO (GNOME, and what xdg-open defers to there) only honours an + # explicit default. So write ours unless another app holds it. + case "$_cur" in ''|tronbrowser.desktop) ;; *) return 0 ;; esac # Only claim the scheme through a desktop entry that declares it; install.sh, # the .deb/.rpm and the AppImage all write one named tronbrowser.desktop. for _d in "${XDG_DATA_HOME:-$HOME/.local/share}" $(printf '%s' "${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" | tr ':' ' '); do diff --git a/apps/desktop/test/w3bs-scheme/posix.sh b/apps/desktop/test/w3bs-scheme/posix.sh index 967aa88..f2c7de0 100644 --- a/apps/desktop/test/w3bs-scheme/posix.sh +++ b/apps/desktop/test/w3bs-scheme/posix.sh @@ -44,7 +44,18 @@ case "$(uname -s)" in [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleURLTypes:0:CFBundleURLSchemes:0' "$PLIST")" = w3bs ] \ || fail "handler app does not declare w3bs" codesign --verify "$APP" || fail "handler app signature is broken" - open "$URI" + # LaunchServices learns about the app asynchronously. + i=0 + until open "$URI" 2>/dev/null; do + i=$((i + 1)) + if [ "$i" -ge 20 ]; then + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -dump \ + | grep -i -B3 -A3 'w3bs\|tronbrowser.links' >&2 || true + open "$URI" || true + fail "LaunchServices has no handler for w3bs" + fi + sleep 1 + done ;; *) command -v xdg-open >/dev/null 2>&1 || fail "xdg-open is not installed" @@ -68,10 +79,12 @@ EOF sh "$W/bin/tron-w3bs-register" "$W/bin/tron" [ "$(xdg-mime query default x-scheme-handler/w3bs)" = tronbrowser.desktop ] \ || fail "tronbrowser.desktop is not the w3bs handler" - # xdg-open only consults scheme handlers when it sees a display, which a - # real desktop always has and a CI runner never does. It only checks that - # the variable is set; nothing connects to it. - DISPLAY="${DISPLAY:-:99}" xdg-open "$URI" + # As on a GNOME desktop: xdg-open hands the URI to `gio open`, which looks + # the handler up in mimeapps.list. Generic mode is not used because + # xdg-utils 1.1.x (Ubuntu 24.04) misreads any scheme with a digit, like + # w3bs, as a file path. DISPLAY only has to be set; nothing connects to it. + command -v gio >/dev/null 2>&1 || fail "gio is not installed" + DISPLAY="${DISPLAY:-:99}" XDG_CURRENT_DESKTOP=GNOME xdg-open "$URI" ;; esac From b4dc8919648b20292e18b42a5165ee1f2b4ef16b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 20 Oct 2026 00:56:33 +0000 Subject: [PATCH 3/3] w3bs:// macOS test: put the applet where LaunchServices routes URLs Co-Authored-By: Claude Opus 5.5 --- apps/desktop/test/w3bs-scheme/posix.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/desktop/test/w3bs-scheme/posix.sh b/apps/desktop/test/w3bs-scheme/posix.sh index f2c7de0..5d1de3a 100644 --- a/apps/desktop/test/w3bs-scheme/posix.sh +++ b/apps/desktop/test/w3bs-scheme/posix.sh @@ -9,6 +9,7 @@ set -eu REPO="$(CDPATH= cd -- "$(dirname -- "$0")/../../../.." && pwd)" W="$(mktemp -d)" +REAL_HOME="$HOME" export HOME="$W/home" export XDG_DATA_HOME="$HOME/.local/share" XDG_CONFIG_HOME="$HOME/.config" mkdir -p "$W/bin" "$XDG_DATA_HOME/applications" "$XDG_CONFIG_HOME" @@ -16,7 +17,10 @@ cp "$REPO/apps/desktop/launcher/tronbrowser" "$REPO/apps/desktop/launcher/tron-w ARGV="$W/argv" URI='w3bs://prompt/w3bs/research@1' WANT='https://w3bs.org/browse?q=w3bs%3A%2F%2Fprompt%2Fw3bs%2Fresearch%401' -APP="$HOME/Applications/TronBrowser Links.app" +# LaunchServices will not route a URL to an app in a temporary directory, so on +# macOS the applet goes where real installs put it, under a test-only name. +# Same bundle id as the real one: run this on a disposable machine (CI). +APP="$REAL_HOME/Applications/TronBrowser Links (test).app" # Named ungoogled-chromium so the launcher's de-googled check stays quiet. cat > "$W/bin/ungoogled-chromium" <&2; fail "the engine was not handed $WANT"; } echo "ok: $URI -> $WANT" +if [ -d "$APP" ]; then + /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u "$APP" 2>/dev/null || true + rm -rf "$APP" +fi rm -rf "$W"