# 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.