Open a URI in the user's chosen application from Zig: the desktop portal on Linux, ShellExecuteExW on Windows, Launch Services on macOS.

README.md

zig-openuri #

Open a URI in the user's chosen application from Zig: a web page in their browser, a mailto: link in their mail client, a local file in whatever opens files of that type. It does what clicking a link in a document does.

const openuri = @import("openuri");

switch (try openuri.open(io, gpa, environ, "https://ziglang.org/", .{})) {
    .opened => {},
    .cancelled => {}, // the user dismissed the choice of application
}

The API reference is generated from the doc comments and published at https://jeff.jcollie.page/zig-openuri/.

How each platform opens a URI #

Platform Mechanism
Linux The org.freedesktop.portal.OpenURI desktop portal over the session bus, through mdbus. file: URIs go to the portal's OpenFile, which receives the file as an open descriptor.
Windows ShellExecuteExW with the open verb.
macOS LSOpenCFURLRef from Launch Services.

The portal is what makes this work from inside a Flatpak or Snap sandbox. It also lets the desktop ask the user which application to use, so on Linux the outcome can be that they cancelled. When there is no portal, open returns error.PortalUnavailable. It does not fall back to running xdg-open (why). A program that wants that fallback can catch the error and run xdg-open itself.

On Linux, open connects to the session bus for the length of the call. A program that already holds an mdbus connection can pass it to openWithConnection. Only the portal's own reply and signal are taken from the connection, and everything else stays queued for the caller.

Options #

Option Platforms Meaning
schemes all The only schemes that may be opened, or null for any.
parent_window Linux The caller's X11 window ID, or xdg-foreign handle on Wayland, so that a dialog from the portal appears over it.
activation_token Linux Lets the opened application's window take focus. A program started from a launcher finds one in XDG_ACTIVATION_TOKEN.
ask Linux Always ask which application to use.
writable Linux Open a file: URI for an application that edits it.
timeout_ms Linux How long to wait for the portal (default two minutes, null for no limit).

Opening URIs you did not write #

A URI from a chat message, a web page or text in a terminal is untrusted. Handing it to the operating system's opener can run a program. On Windows, C:\x\evil.exe is a valid argument to ShellExecuteExW, and a file: URI naming a program runs it on Windows and macOS. So zig-openuri:

  • always refuses single-letter schemes, which are Windows drive letters rather than schemes;
  • refuses URIs containing spaces, control characters, or invalid UTF-8;
  • takes an allowlist in Options.schemes, so that a program can say, for example, that it opens only http, https and mailto.

A program opening untrusted URIs should always pass an allowlist. The reasons are recorded in ADR 0001.

The openuri command #

$ openuri [--ask] [--timeout=SECONDS] [--scheme=NAME]... URI

It exits 0 when the URI was opened, 1 when the choice of application was cancelled, and 2 on any error. On Linux it passes on the activation token from XDG_ACTIVATION_TOKEN.

Using it #

Requires Zig 0.17.

$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-openuri.git
const openuri = b.dependency("openuri", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("openuri", openuri.module("openuri"));

The module links what each platform needs: shell32 on Windows, and CoreFoundation and CoreServices on macOS. Linking for macOS therefore needs Apple's SDK, either by building on a Mac or by passing --sysroot. On Windows, some protocol handlers need COM, so a program should initialize COM on the calling thread itself if it opens arbitrary schemes.

Building #

$ nix develop
$ zig build              # the openuri command, in zig-out/bin
$ zig build test         # unit tests, and the portal tests on Linux
$ zig build check        # compile for Linux, Windows and macOS on x86_64 and aarch64
$ zig build docs-serve   # read the API documentation at http://localhost:8000/

On Linux the portal tests start a private dbus-daemon with a fake portal on it. They skip themselves when dbus-daemon is not on PATH, which the devshell provides.

Where this lives #

The canonical repository is on my Forgejo instance, and Tangled carries a mirror:

$ git clone https://git.jcollie.dev/jeff/zig-openuri.git
$ git clone https://tangled.org/jcollie.dev/zig-openuri

Issues and pull requests #

Please report issues and send pull requests on the Tangled mirror: https://tangled.org/jcollie.dev/zig-openuri. See CONTRIBUTING.md for how to work on the code.

License #

MIT. The project follows the REUSE specification.

References cited #