1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const build_config = @import("../build_config.zig");
const apprt = @import("../apprt.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.@"os-open");
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
/// Output on stdout is ignored.
///
/// This function is purposely simple for the sake of providing some portable
/// way to open URLs. If you are implementing an apprt for Ghostty, you should
/// consider doing something special-cased for your platform.
pub fn open(
kind: apprt.action.OpenUrl.Kind,
url: []const u8,
) !void {
// On macOS, the apprt handles OSC 8 targets before this fallback. Ghostty's
// native apprt applies its allowlist, confirmation, and file safety policy.
// If a macOS embedder declines the action, fail closed rather than bypassing
// that policy by handing producer-controlled terminal output to `open`.
if (comptime builtin.os.tag == .macos) {
if (kind == .osc8) return error.UnsafeOSC8Link;
}
var spawn_opts: std.process.SpawnOptions = switch (builtin.os.tag) {
.linux, .freebsd => .{ .argv = &.{ "xdg-open", url } },
.windows => .{ .argv = &.{ "rundll32", "url.dll,FileProtocolHandler", url } },
.macos => switch (kind) {
.text => .{ .argv = &.{ "open", "-t", url } },
.html, .unknown => .{ .argv = &.{ "open", url } },
.osc8 => unreachable,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
// Ignore anything from stdout. This must be set before spawning the
// process.
spawn_opts.stdout = .ignore;
// Pipe stderr so we can log the stderr from the command. This must be set
// before spawning the process.
spawn_opts.stderr = .pipe;
const exe = if (comptime build_config.snap) local_env: {
// In the snap on Linux the launcher exports LD_LIBRARY_PATH
// pointing at the snap's bundled libraries. Leaking this into
// child process can can be problematic, so let's drop it from the
// env.
//
// Note that `spawn` copies the passed in `Environ.Map` into a
// fresh `Environ` block, so this is safe to release immediately
// after spawn.
var environ_map = try global.environMap();
defer environ_map.deinit();
_ = environ_map.orderedRemove("LD_LIBRARY_PATH");
spawn_opts.environ_map = &environ_map;
break :local_env try std.process.spawn(global.io(), spawn_opts);
} else
// Non-snap releases don't need to alter the env.
try std.process.spawn(global.io(), spawn_opts);
const thread = try std.Thread.spawn(.{}, openThread, .{ global.io(), exe });
thread.detach();
}
test "macOS OSC 8 links have no generic opener fallback" {
if (builtin.os.tag != .macos) return error.SkipZigTest;
try std.testing.expectError(
error.UnsafeOSC8Link,
open(.osc8, "file:///tmp/payload.command"),
);
}
fn openThread(io: std.Io, exe_: std.process.Child) void {
// Copy the exe so it is non-const. This is necessary because wait()
// requires a mutable reference and we can't have one as a thread
// param.
var exe = exe_;
if (exe.stderr) |stderr| {
var buffer: [256]u8 = undefined;
var stream = stderr.readerStreaming(io, &buffer);
const reader = &stream.interface;
while (true) {
const line = reader.takeDelimiterExclusive('\n') catch |outer| switch (outer) {
error.EndOfStream => break,
error.ReadFailed => break,
error.StreamTooLong => reader.take(buffer.len) catch |inner| switch (inner) {
error.ReadFailed => break,
error.EndOfStream => break,
},
};
log.warn("open stderr={s}", .{line});
}
}
_ = exe.wait(io) catch {};
}
|