# 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. ```zig 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 ****. ## How each platform opens a URI | Platform | Mechanism | | --- | --- | | Linux | The `org.freedesktop.portal.OpenURI` desktop portal over the session bus, through [mdbus](https://git.jcollie.dev/jeff/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](docs/adr/0002-no-xdg-open-fallback.md)). 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](docs/adr/0001-refuse-single-letter-schemes-and-offer-an-allowlist.md). ## The `openuri` command ```console $ 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. ```console $ zig fetch --save git+https://git.jcollie.dev/jeff/zig-openuri.git ``` ```zig 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 ```console $ 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](https://tangled.org/jcollie.dev/zig-openuri) carries a mirror: ```console $ 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: . See [CONTRIBUTING.md](CONTRIBUTING.md) for how to work on the code. ## License MIT. The project follows the [REUSE](https://reuse.software/) specification. ## References cited - Apple Inc. (n.d.). *LSOpenCFURLRef(\_:\_:)*. Apple Developer Documentation. - Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource Identifier (URI): Generic Syntax* (RFC 3986). RFC Editor. - freedesktop.org. (n.d.). *D-Bus Specification*. - Kerwin, M. (2017). *The "file" URI Scheme* (RFC 8089). RFC Editor. - Microsoft. (n.d.). *ShellExecuteExW function (shellapi.h)*. Microsoft Learn. - Microsoft. (n.d.). *SHELLEXECUTEINFOW structure (shellapi.h)*. Microsoft Learn. - Wayland Protocols contributors. (n.d.). *XDG activation protocol (xdg-activation-v1)*. Wayland Explorer. - XDG Desktop Portal contributors. (n.d.). *org.freedesktop.portal.OpenURI*. XDG Desktop Portal documentation. - XDG Desktop Portal contributors. (n.d.). *org.freedesktop.portal.Request*. XDG Desktop Portal documentation. - XDG Desktop Portal contributors. (n.d.). *Window Identifiers*. XDG Desktop Portal documentation.