Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ need their own validation.
Tauri's own `--port` is for its static-file server, not Vite. Without this flag,
the adapter derives a stable port from the worktree path (the same derivation
`just web` uses) and prints the chosen URL. Different paths can still collide.
All desktop builds use the `buzz` URL scheme. See [OS deep links](deep-links.md).
Ordinary desktop development runs do not claim the OS `buzz` URL scheme or the
packaged single-instance lock, so multiple worktrees can run at once. Packaged
and debug bundles still use `buzz`. See [OS deep links](deep-links.md).
Desktop requires the exact port to be free; an occupied port fails rather than
opening another copy's server. Other arguments, including runner/application arguments after `--`, pass
through unchanged. Port configuration is prepended so Tauri parses it even with
Expand Down
90 changes: 56 additions & 34 deletions docs/deep-links.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# OS deep links

Desktop builds register a URL scheme with the operating system, so a link opened
outside the app brings it to the front and navigates, at cold start too. Every build
uses `buzz://`, the same scheme in-app links and **Copy link** use. Windows/Linux
register on launch; macOS requires the registered bundle itself to run, not
`just desktop` or another copy with the same app identifier. Builds share this
scheme (see [current limits](#current-limits)). The browser build has no OS ingress
and keeps its `#buzz=` address form.
Packaged desktop builds register a URL scheme with the operating system, so a
link opened outside the app brings it to the front and navigates, at cold start
too. Every build uses `buzz://`, the same scheme in-app links and **Copy link**
use. Ordinary `just desktop` development runs do not register the scheme or
enforce a single native instance, so linked worktrees can run in parallel. macOS
requires the registered bundle itself to run, not `just desktop` or another copy
with the same app identifier. Packaged builds share this scheme (see
[current limits](#current-limits)). The browser build has no OS ingress and keeps
its `#buzz=` address form.

## Accepted links

Expand Down Expand Up @@ -64,11 +66,14 @@ context for in-app use and remain unsupported at the OS boundary.

## Testing locally

Write a link by hand, such as `buzz://channel/general` or
`buzz://message?channel=general&id=<64-hex event id>`. A warm app should come to the
front and open the conversation. A cold start should launch, show the start page, then open the
destination once the client is ready. A malformed link such as
`buzz://join?relay=example` must show the failure notice.
For OS ingress, use a packaged app or debug bundle. Write a link by hand, such as
`buzz://channel/general` or `buzz://message?channel=general&id=<64-hex event id>`.
A warm packaged app should come to the front and open the conversation. A cold
start should launch, show the start page, then open the destination once the
client is ready. A malformed link such as `buzz://join?relay=example` must show
the failure notice. Ordinary `just desktop` keeps in-app Buzz links and
**Copy link**, but external `buzz://` delivery is not a supported development
contract.

For entity destinations, use `just web` with the existing authenticated development
broker, select the owning community, and open Projects or a Buzz entity link in a
Expand All @@ -78,11 +83,13 @@ commit must show that exact revision and its diff. Back/Forward and Copy link
preserve the entity route, including the requested tab and commit.

**Before native testing:** coordinate with anyone using another Buzz copy on the
machine. Registration can change the installed app's handler, and the shared app
identifier permits only one active native instance across checkouts. Use a
disposable machine/profile or explicitly agree which app owns the handler and how
to restore it. Do not launch/register a test bundle over an active installation
without that agreement.
machine. Packaged/debug registration can change the installed app's handler, and
packaged apps with the shared app identifier permit only one active native
instance across checkouts. Use a disposable machine/profile or explicitly agree
which app owns the handler and how to restore it. If an older development launch
already claimed `buzz://`, launch the intended packaged app/debug bundle once, or
reinstall it, so that app claims the handler again. Do not launch/register a test
bundle over an active installation without agreement.

**macOS** only routes a scheme to a bundled app. `just desktop` binaries are never
registered, so build a debug bundle and launch it once to register it with Launch
Expand All @@ -99,38 +106,53 @@ open "buzz://channel/general"
Quit the app and run the last command again to test a cold start. The bundle lands
under the workspace `target/` directory because `src-tauri` is a workspace member.

**Windows** registers the launching binary under the current user on every start, so
a `just desktop` build works without an installer:
**Windows** packaged/debug builds register the launching binary under the current
user on every start. Ordinary `just desktop` does not register. `just desktop`
and `just desktop-bundle --no-bundle` share `target/debug`; if you ran
`just desktop` after the last debug build, rebuild before launching the executable
or the registered binary will still have development-mode behavior. Use the
installed app or build and run a debug binary, then open a link:

```powershell
just desktop-bundle --no-bundle
.\target\debug\buzz-foundation.exe
start buzz://channel/general
Comment thread
kalvinnchau marked this conversation as resolved.
```

The NSIS installer registers the installed app as well; whichever build launched
most recently owns the scheme.

**Linux** writes a `<binary>-handler.desktop` entry and calls `xdg-mime` and
`update-desktop-database` on every start, so both must be installed. Packaged `.deb`
and AppImage builds also declare the scheme in their desktop entry.
**Linux** packaged/debug builds write a `<binary>-handler.desktop` entry and call
`xdg-mime` and `update-desktop-database` on every start, so both must be
installed. Ordinary `just desktop` does not register. As on Windows, rerun
`just desktop-bundle --no-bundle` after any `just desktop` launch before testing
the shared `target/debug` executable. Packaged `.deb` and AppImage builds also
declare the scheme in their desktop entry.

```sh
just desktop-bundle --no-bundle
./target/debug/buzz-foundation
xdg-open "buzz://channel/general"
```

## Current limits

- Development builds register `buzz` like released ones, so on a machine with Buzz
installed the two compete for it: Windows and Linux route it to whichever binary
started last, and macOS to whichever registered bundle Launch Services picks.
- Every local bundle keeps the same application identifier, so they share app data
and single-instance identity. This is one active native Buzz per machine, not
per-worktree isolation; Launch Services can list several bundles under that identity.
On macOS, a registered bundle launched while a different copy (including an
unbundled dev process) is running can exit before receiving the OS URL. The running
copy may gain focus without opening the link. Cross-copy URL handoff is unsupported:
quit the other copy and open the link with the intended registered bundle. Cold-start
acceptance requires no native copy running; warm acceptance requires that exact
registered bundle already running. Windows/Linux use the plugin's argv handoff.
- Ordinary development runs do not register `buzz` or enforce the packaged
single-instance policy. Several worktree apps can run together, but external
`buzz://` links have no guaranteed development target.
- Every local package keeps the same application identifier, so packaged/debug
apps share app data and single-instance identity. This is one active packaged
native Buzz per machine, not per-worktree isolation; Launch Services can list
several bundles under that identity. On macOS, a registered bundle launched
while a different copy (including an unbundled dev process) is running can exit
before receiving the OS URL. The running copy may gain focus without opening
the link. Cross-copy URL handoff is unsupported: quit the other copy and open
the link with the intended registered bundle. Cold-start acceptance requires no
native copy running; warm acceptance requires that exact registered bundle
already running. Windows/Linux use the plugin's argv handoff.
- Ordinary development worktrees still share app data. The native agent store
permits one owner at a time; a second worktree may report owned storage while
the rest of the app remains usable.
- Invite links (`buzz://join`, `https://<relay>/invite/<code>`) and remote push
are not handled; they end in the
failure notice or, for HTTPS, never reach the app.
Expand Down
5 changes: 3 additions & 2 deletions scripts/desktop-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { desktopOverlay, options } from "./desktop-config.mjs";

// A debug bundle for the one thing `just desktop` cannot do: macOS routes a URL
// scheme only to a bundled application, so testing OS deep links there needs a
// bundle registered with Launch Services. Windows and Linux development binaries
// register themselves at every start and need no bundle. All builds use `buzz`.
// bundle registered with Launch Services. Windows and Linux can also use a debug
// build for packaged deep-link behavior; ordinary `just desktop` deliberately
// leaves the OS handler alone. All packaged builds use `buzz`.
// Release bundling stays a plain `pnpm tauri build`.
const args = process.argv.slice(2);
const { forwarded, rest } = options(args, []);
Expand Down
9 changes: 6 additions & 3 deletions src-tauri/src/deep_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,12 @@ pub(crate) fn setup<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
Ok(None) => {}
Err(error) => eprintln!("Could not read the launch deep link: {error}"),
}
// Installers register the scheme; portable and development binaries do not.
if let Err(error) = app.deep_link().register_all() {
eprintln!("Could not register {SCHEME}:// for this binary: {error}");
// Installers register the scheme; packaged portable/debug binaries can
// repair it on launch. Development runs leave any installed handler alone.
if !tauri::is_dev() {
if let Err(error) = app.deep_link().register_all() {
eprintln!("Could not register {SCHEME}:// for this binary: {error}");
}
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,20 @@ fn commands<R: tauri::Runtime>() -> impl Fn(tauri::ipc::Invoke<R>) -> bool + Sen
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
let builder = tauri::Builder::default();
let builder = if !tauri::is_dev() {
// Single instance comes first, as its documentation requires. Its deep-link
// feature forwards deep-link argv on Windows/Linux. macOS OS URLs reach
// the registered bundle directly; cross-copy URL handoff is unsupported.
// This callback only foregrounds the running window.
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
// This callback only foregrounds the running window. Development launches
// skip this so parallel worktrees can run side by side.
builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
deep_links::focus_main(app);
}))
} else {
builder
};
let builder = builder
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
Expand Down
19 changes: 16 additions & 3 deletions src/app/deep-links.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { isBuzzLink } from "../features/navigation/buzz-links";

const read = (path) => readFileSync(new URL(path, import.meta.url), "utf8");

it("registers single-instance ahead of deep-link in the native builder, with argv forwarding enabled", () => {
it("keeps packaged single-instance ahead of deep-link while development can run in parallel", () => {
const lib = read("../../src-tauri/src/lib.rs");
const guard = lib.indexOf("if !tauri::is_dev()");
const singleInstance = lib.indexOf(
".plugin(tauri_plugin_single_instance::init(",
guard,
);
const deepLink = lib.indexOf(".plugin(tauri_plugin_deep_link::init())");
expect(guard).toBeGreaterThan(-1);
expect(singleInstance).toBeGreaterThan(-1);
expect(deepLink).toBeGreaterThan(singleInstance);
expect(lib).not.toMatch(/debug_assertions/);
expect(lib).toMatch(/deep_links::setup\(app\.handle\(\)\)/);
const cargo = read("../../src-tauri/Cargo.toml");
expect(cargo).toMatch(/^tauri-plugin-deep-link = "2"$/m);
Expand All @@ -20,8 +24,17 @@ it("registers single-instance ahead of deep-link in the native builder, with arg
);
});

it("registers exactly the in-app link scheme with the OS, and bundles so installers claim it", () => {
// Development, bundled and released apps use the scheme in-app links produce.
it("leaves OS scheme registration to packaged launches", () => {
const deepLinks = read("../../src-tauri/src/deep_links.rs");
const guard = deepLinks.indexOf("if !tauri::is_dev()");
const registerAll = deepLinks.indexOf("app.deep_link().register_all()");
expect(guard).toBeGreaterThan(-1);
expect(registerAll).toBeGreaterThan(guard);
expect(deepLinks).not.toMatch(/unregister/);
});

it("declares exactly the in-app link scheme so packages can claim it", () => {
// Bundled and released apps use the scheme in-app links produce.
const config = JSON.parse(read("../../src-tauri/tauri.conf.json"));
const schemes = config.plugins["deep-link"].desktop.schemes;
expect(config.plugins["deep-link"]).toEqual({ desktop: { schemes } });
Expand Down
Loading