Something went wrong. Try again.
Open a URI in the user's chosen application from Zig: the desktop portal on Linux, ShellExecuteExW on Windows, Launch Services on macOS.
Something went wrong. Try again.
5.5 kB · 137 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Open a URI in the user's chosen handler -- a web page in their browser,//! a `mailto:` link in their mail client -- the way clicking a link in a//! document would.//!//! Each platform hands the URI to its own opener://!//! - **Linux**: the `org.freedesktop.portal.OpenURI` desktop portal over the//! session bus. `file:` URIs go to the portal's `OpenFile`, which takes the//! file as an open descriptor. The portal can ask the user which handler to//! use, so the outcome may be that they cancelled. There is no fallback to//! `xdg-open`; see docs/adr/0002.//! - **Windows**: `ShellExecuteExW`. Some protocol handlers need COM, and//! initializing it is the caller's job, since how COM is initialized on a//! thread is the application's decision.//! - **macOS**: `LSOpenCFURLRef` from Launch Services.//!//! ```zig//! const outcome = try openuri.open(io, gpa, environ, "https://ziglang.org/", .{});//! ```
const std = @import("std");const builtin = @import("builtin");const Io = std.Io;const Allocator = std.mem.Allocator;
const uri_check = @import("uri.zig");
const platform = switch (builtin.os.tag) { .linux => @import("linux.zig"), .windows => @import("windows.zig"), .macos => @import("macos.zig"), else => @compileError("openuri supports Linux, Windows and macOS"),};
/// What an open request ended in.pub const Outcome = enum { /// A handler was given the URI. opened, /// The user dismissed the choice of handler. Only the portal on Linux, /// and a cancelled elevation prompt on Windows, can report this. cancelled,};
pub const Error = error{ /// The URI is malformed, or a `file:` URI names a file on another host. InvalidUri, /// The scheme is a single letter (a Windows drive, never a scheme), or /// is not one of `Options.schemes`. SchemeNotAllowed, /// A `file:` URI names a file that does not exist. FileNotFound, /// Linux only: no session bus, or nothing on it answers for the portal. PortalUnavailable, /// No handler is registered for the URI, or the handler failed. NoHandler, /// The operating system refused to open the URI. AccessDenied, /// The portal did not answer in `Options.timeout_ms`. The outcome is /// unknown: the handler may still open the URI later. Timeout, OutOfMemory, Unexpected,};
/// The caller's own window, so that a dialog the portal shows -- the choice/// of handler -- is placed over it rather than wherever the desktop likes.pub const ParentWindow = union(enum) { /// An X11 window ID. x11: u32, /// A handle exported for the window with the `xdg-foreign` Wayland /// protocol. wayland: []const u8,};
pub const Options = struct { /// Linux only; ignored elsewhere. parent_window: ?ParentWindow = null, /// A token from the windowing system -- an XDG activation token on /// Wayland, a startup notification ID on X11 -- that lets the handler's /// window take focus rather than open behind the caller's. A program /// started by a launcher finds one in `XDG_ACTIVATION_TOKEN`. Linux only. activation_token: ?[]const u8 = null, /// Always ask the user which handler to use, even when there is a /// default. Linux only. ask: bool = false, /// Open a `file:` URI writable, for a handler that edits it. Linux only. writable: bool = false, /// The only schemes that may be opened, compared without regard to case, /// or null for any. A program opening URIs that came from somewhere it /// does not trust -- a chat message, text in a terminal -- should name /// exactly the schemes it means to open. Single-letter schemes are /// refused whatever this says. schemes: ?[]const []const u8 = null, /// How long to wait for the portal, in milliseconds, or null to wait as /// long as it takes. The portal may be showing the user a dialog, so this /// is generous. Linux only. timeout_ms: ?u32 = 2 * std.time.ms_per_min,};
/// Open `uri` in the user's chosen handler.////// On Linux this connects to the session bus, found through `environ`, for/// the length of the call; a program that already has a connection should/// use `openWithConnection` instead. `io` and `environ` are unused on Windows/// and macOS, and `gpa` only for converting the URI to UTF-16 on Windows.pub fn open( io: Io, gpa: Allocator, environ: std.process.Environ, uri: []const u8, options: Options,) Error!Outcome { const scheme = try uri_check.check(uri, options.schemes); return platform.open(io, gpa, environ, uri, uri_check.isFile(scheme), options);}
/// Open `uri` over a session bus connection the caller already holds. Only/// the portal's own reply and signal are taken from the connection; any/// other message that arrives meanwhile stays queued for the caller.pub const openWithConnection = if (builtin.os.tag == .linux) linuxOpenWithConnection else @compileError("openWithConnection is Linux only");
fn linuxOpenWithConnection( connection: *platform.Connection, uri: []const u8, options: Options,) Error!Outcome { const scheme = try uri_check.check(uri, options.schemes); return platform.openWithConnection(connection, uri, uri_check.isFile(scheme), options);}
test { _ = uri_check; _ = platform;}