Links and documents
An app can handle its own URL schemes, such as my-notes://open/42, and open document types, such
as Markdown files. You declare both in Sabine.toml. Sabine registers them with the desktop, and
every link or file people open with the app, whether at launch, from another launch or from the
system, lands in one queue the app reads.
Declaring schemes and document types
Section titled “Declaring schemes and document types”List URL schemes as x-scheme-handler/<scheme> MIME types and document types by their MIME type
under [app], and give each document type its file extensions under [app.extensions]:
[app]id = "com.example.notes"name = "Notes"mime_types = ["text/markdown", "application/vnd.example.notes", "x-scheme-handler/my-notes"]
[app.extensions]"text/markdown" = ["md", "markdown"]"application/vnd.example.notes" = ["notes"]- URL schemes start with a letter and contain lowercase letters, digits,
+,.or-. - Extensions are lowercase letters and digits, plus
+,-or_, without the leading dot. - Every type under
[app.extensions]must be a document type listed inmime_types; URL schemes have no extensions.
The manifest is the only place URL schemes come from. The app registers exactly the schemes it declares, under its app id, so uninstalling it removes all of them. A manifest with URL schemes needs an app id.
Receiving links and documents
Section titled “Receiving links and documents”Sabine queues what the app should open: URLs with one of its schemes, and documents as file://
URLs. Read the queue from the page with app.takeOpenUrls(), and subscribe to
events.openUrlsAvailable to hear when more arrive:
import { app, events } from "@lantharos/sabine";
async function openPending() { for (const url of await app.takeOpenUrls()) { const link = new URL(url); if (link.protocol === "file:") { await openDocument(link); } else if (link.protocol === "my-notes:") { await openNoteLink(link); } }}
const unsubscribe = events.openUrlsAvailable(openPending);await openPending();Subscribe before the first takeOpenUrls(), as above, so nothing that arrives while the page
starts is missed. takeOpenUrls() empties the queue, including URLs that arrived before the page
loaded. Call unsubscribe() when the listener goes away.
When Sabine has to load the page again on its own, as when it switches the window to software painting or the page’s renderer crashed, the URLs the page took go back to the front of the queue, so the new page opens the same documents. Once the page navigates or reloads itself, or its window closes, what it took counts as handled.
Rust code reads the same queue with SabineProcess::take_open_urls(). Whichever side takes a URL
first consumes it.
The queue holds up to 128 URLs, each up to 32 KiB. URLs beyond that are dropped and recorded in the app’s diagnostics.
Command-line arguments
Section titled “Command-line arguments”When the app starts, Sabine looks at its arguments, and at the arguments a second launch hands to a single instance. An argument is queued when it is:
- a URL with one of the app’s schemes, or
- a file, named by path or by
file:URL, whose extension is listed under[app.extensions].
Relative paths are resolved against the working directory of the launch that received them.
Arguments that start with - and anything else are left to the app, so your own options keep
working:
notes --profile work ~/Notes/Ideas.md my-notes://open/42 config.tomlHere ~/Notes/Ideas.md and my-notes://open/42 are queued; --profile, work and config.toml
are not.
Keeping links in one window
Section titled “Keeping links in one window”On Linux and Windows the desktop opens a link or document by starting the app with it as an argument. Without a single-instance policy, every click starts another copy. Make later launches hand their arguments to the running app instead:
use sabine::prelude::*;use sabine::SingleInstancePolicy;
fn main() { SabineWindow::main(|window| { Ok(window .app() .single_instance(SingleInstancePolicy::FocusExisting)) });}macOS sends links and documents to the running app itself, through its application delegate, and Sabine brings the window forward when they arrive.
How each desktop registers them
Section titled “How each desktop registers them”Installed packages list the schemes and document types in the app’s desktop entry as
MimeType, and install a shared-mime-info file so desktops recognize the extensions.
Each launch also registers the URL schemes for the current user. When the installed desktop
entry already declares every scheme, Sabine makes it the handler. Otherwise it writes a hidden
handler entry, <app id>.sabine-url.desktop in ~/.local/share/applications, which starts
the running executable, or the AppImage file when the app runs from one. It then sets the app
as the default for each scheme in ~/.config/mimeapps.list, unless another installed app is
already the default. Other associations in the file are kept, and Sabine apps take turns
updating it, so they never overwrite each other’s changes.
Handler entries name the executable in TryExec as well as Exec, so once a package is
removed, desktops stop offering a handler whose program is gone.
Each scheme is registered for the current user under HKCU\Software\Classes\<scheme>, pointing
to the app’s executable, when the app is installed and again every time it runs.
Installers and sabine install register a <app id>.document ProgID and add it to each
extension’s OpenWithProgids, so the app appears under Open with for those files.
macOS reads schemes and document types from the bundle’s Info.plist. Bundles list URL
schemes in CFBundleURLTypes and document types in CFBundleDocumentTypes. Types macOS
already knows, such as public.plain-text for text/plain, are referred to directly; the
others are declared as imported types with their MIME type and extensions. The app is listed
as an alternate viewer for its documents, so it appears under Open With without taking over
files another app opens.
Schemes must be in the bundle before it is signed. At launch, the app checks that its bundle
declares every scheme in the manifest, and records each missing one in its diagnostics; the
app opens anyway, but macOS doesn’t hand it links with that scheme. Rebuild the bundle after
adding a scheme. An app started outside its bundle, as with cargo run, has no schemes.
Testing links
Section titled “Testing links”Development runs, such as sabine dev, register no URL schemes, so an installed build doesn’t lose
its handlers to a development copy. They still open the links and documents on their own command
line, so sabine dev -- my-notes://open/42 reaches the page’s open queue. Test what the desktop
hands the app with an installed build:
-
Install the app from your project:
Terminal window sabine install --bundle . -
Open a link from a terminal, or click one in a browser:
Terminal window xdg-open my-notes://open/42Terminal window start my-notes://open/42Terminal window open my-notes://open/42 -
Check that the page’s
openUrlsAvailablelistener received it.
Apps that only open from links
Section titled “Apps that only open from links”Some apps should only open through their schemes or documents, such as a sign-in window a
notification raises. Keep them out of launchers with listed = false:
[app]listed = falsemime_types = ["x-scheme-handler/my-app-signin"]The app is still installed and still handles its schemes and documents.
- Linux desktop entries get
NoDisplay=trueand keep theirMimeType. - Windows installers skip the Start menu shortcut and the finish page’s option to launch the app, while still registering the schemes and keeping the uninstall entry.
sabine installon macOS leaves the bundle out of~/Applicationsand registers it with Launch Services directly, so it handles its schemes without appearing in Launchpad. A bundle someone drags from a disk image into/Applicationsis listed like any other app, since noInfo.plistkey hides a usable app from Launchpad.
Uninstalling
Section titled “Uninstalling”Windows installers remove the scheme keys and the document ProgID when the app is uninstalled.
sabine uninstall removes what the app registered on every platform: the scheme keys and ProgID
on Windows; the hidden handler, the shared-mime-info file and the mimeapps.list entries naming
the app on Linux; and the bundle’s Launch Services record on macOS. A scheme or handler that has
since been taken over by a different executable is left alone.
Removing a deb or rpm package runs as root and can’t reach each user’s files, so after a package is
removed, or an AppImage deleted, the hidden handler and its mimeapps.list entries stay until the
app is installed and launched again. Because the handler names its program in TryExec, desktops
stop offering it in the meantime.
Related pages
Section titled “Related pages”- Single instance explains how later launches reach the running app.
- Manifest reference lists every
[app]key. - Bundling and Installing cover
packages and
sabine install.