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.
6.3 kB · 164 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! `openuri`: open a URI in the user's chosen handler from the command line.//!//! openuri [--ask] [--timeout=SECONDS] [--scheme=NAME]... URI//!//! Exits 0 when the URI was opened, 1 when the user cancelled the choice of//! handler, and 2 on any error, with the reason on stderr.
const std = @import("std");const builtin = @import("builtin");const Io = std.Io;
const openuri = @import("openuri");
const usage = \\usage: openuri [--ask] [--timeout=SECONDS] [--scheme=NAME]... URI \\ \\Open URI in the user's chosen application. \\ \\ --ask always ask which application to use (Linux) \\ --timeout=SECONDS how long to wait for the desktop portal (Linux), \\ 0 to wait as long as it takes; the default is 120 \\ --scheme=NAME only open URIs with this scheme; may be repeated \\ --help show this and exit \\ \\Exit status is 0 when the URI was opened, 1 when the choice of \\application was cancelled, and 2 on any error. \\;
pub fn main(init: std.process.Init) !u8 { const io = init.io; const arena = init.arena.allocator(); const args = try init.minimal.args.toSlice(arena);
var stderr_buffer: [1024]u8 = undefined; var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); const stderr = &stderr_writer.interface; defer stderr.flush() catch {};
const parsed = parseArgs(arena, if (args.len != 0) args[1..] else args) catch |err| { switch (err) { error.Help => { try stderr.writeAll(usage); return 0; }, error.OutOfMemory => return err, else => { try stderr.print("openuri: {s}\n\n{s}", .{ describeArgError(err), usage }); return 2; }, } };
var options = parsed.options; if (builtin.os.tag == .linux) { // A launcher passes the program its activation token here, and the // handler needs it to be allowed to take focus. options.activation_token = init.minimal.environ.getPosix("XDG_ACTIVATION_TOKEN"); }
const outcome = openuri.open(io, init.gpa, init.minimal.environ, parsed.uri, options) catch |err| { try stderr.print("openuri: cannot open {s}: {s}\n", .{ parsed.uri, describeError(err) }); return 2; }; return switch (outcome) { .opened => 0, .cancelled => 1, };}
const Parsed = struct { uri: []const u8, options: openuri.Options,};
const ArgError = error{ Help, MissingUri, ExtraArgument, UnknownOption, BadTimeout, OutOfMemory,};
fn parseArgs(arena: std.mem.Allocator, args: []const []const u8) ArgError!Parsed { var options: openuri.Options = .{}; var schemes: std.ArrayList([]const u8) = .empty; var uri: ?[]const u8 = null; var options_done = false;
for (args) |arg| { if (!options_done and std.mem.startsWith(u8, arg, "-")) { if (std.mem.eql(u8, arg, "--")) { options_done = true; } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { return error.Help; } else if (std.mem.eql(u8, arg, "--ask")) { options.ask = true; } else if (std.mem.startsWith(u8, arg, "--timeout=")) { const seconds = std.fmt.parseInt(u32, arg["--timeout=".len..], 10) catch return error.BadTimeout; options.timeout_ms = if (seconds == 0) null else std.math.mul(u32, seconds, std.time.ms_per_s) catch return error.BadTimeout; } else if (std.mem.startsWith(u8, arg, "--scheme=")) { try schemes.append(arena, arg["--scheme=".len..]); } else { return error.UnknownOption; } continue; } if (uri != null) return error.ExtraArgument; uri = arg; }
if (schemes.items.len != 0) options.schemes = schemes.items; return .{ .uri = uri orelse return error.MissingUri, .options = options };}
fn describeArgError(err: ArgError) []const u8 { return switch (err) { error.MissingUri => "no URI given", error.ExtraArgument => "only one URI may be given", error.UnknownOption => "unknown option", error.BadTimeout => "--timeout takes a whole number of seconds", error.Help, error.OutOfMemory => unreachable, };}
fn describeError(err: openuri.Error) []const u8 { return switch (err) { error.InvalidUri => "not a valid URI", error.SchemeNotAllowed => "that scheme is not allowed", error.FileNotFound => "no such file", error.PortalUnavailable => "the desktop portal is not available", error.NoHandler => "no application could open it", error.AccessDenied => "permission denied", error.Timeout => "no answer from the desktop portal in time", error.OutOfMemory => "out of memory", error.Unexpected => "unexpected error", };}
const testing = std.testing;
test "arguments are parsed" { var arena_state: std.heap.ArenaAllocator = .init(testing.allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
const parsed = try parseArgs(arena, &.{ "--ask", "--timeout=5", "--scheme=https", "--scheme=mailto", "https://x" }); try testing.expectEqualStrings("https://x", parsed.uri); try testing.expect(parsed.options.ask); try testing.expectEqual(5000, parsed.options.timeout_ms.?); try testing.expectEqual(2, parsed.options.schemes.?.len);
try testing.expectEqual(null, (try parseArgs(arena, &.{ "--timeout=0", "x:y" })).options.timeout_ms); try testing.expectEqualStrings("-odd:uri", (try parseArgs(arena, &.{ "--", "-odd:uri" })).uri); try testing.expectError(error.MissingUri, parseArgs(arena, &.{"--ask"})); try testing.expectError(error.ExtraArgument, parseArgs(arena, &.{ "a:b", "c:d" })); try testing.expectError(error.UnknownOption, parseArgs(arena, &.{"--nope"})); try testing.expectError(error.BadTimeout, parseArgs(arena, &.{"--timeout=soon"})); try testing.expectError(error.Help, parseArgs(arena, &.{"--help"}));}