// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // 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; }