Skip to content

Commit 70af7a1

Browse files
committed
core/gtk/glfw: open urls using an apprt action instead of doing it directly
Partial implementation of #5256 This implements the core changes necessary to open urls using an apprt action rather than doing it directly from the core. Implements the open_url action in the GTK and GLFW apprts. Note that this should not be merged until a macOS-savvy developer can add an implementation of the open_url action for the macOS apprt.
1 parent bd7c5cc commit 70af7a1

File tree

7 files changed

+222
-11
lines changed

7 files changed

+222
-11
lines changed

Diff for: include/ghostty.h

+15
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,19 @@ typedef struct {
558558
bool soft;
559559
} ghostty_action_reload_config_s;
560560

561+
// apprt.action.OpenUrlKind
562+
typedef enum {
563+
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
564+
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
565+
} ghostty_action_open_url_kind_e;
566+
567+
// apprt.action.OpenUrl.C
568+
typedef struct {
569+
ghostty_action_open_url_kind_e kind;
570+
const char* url;
571+
uintptr_t len;
572+
} ghostty_action_open_url_s;
573+
561574
// apprt.Action.Key
562575
typedef enum {
563576
GHOSTTY_ACTION_QUIT,
@@ -601,6 +614,7 @@ typedef enum {
601614
GHOSTTY_ACTION_RELOAD_CONFIG,
602615
GHOSTTY_ACTION_CONFIG_CHANGE,
603616
GHOSTTY_ACTION_CLOSE_WINDOW,
617+
GHOSTTY_ACTION_OPEN_URL,
604618
} ghostty_action_tag_e;
605619

606620
typedef union {
@@ -627,6 +641,7 @@ typedef union {
627641
ghostty_action_color_change_s color_change;
628642
ghostty_action_reload_config_s reload_config;
629643
ghostty_action_config_change_s config_change;
644+
ghostty_action_open_url_s open_url;
630645
} ghostty_action_u;
631646

632647
typedef struct {

Diff for: src/Surface.zig

+17-3
Original file line numberDiff line numberDiff line change
@@ -3290,15 +3290,23 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
32903290
.trim = false,
32913291
});
32923292
defer self.alloc.free(str);
3293-
try internal_os.open(self.alloc, .unknown, str);
3293+
_ = try self.rt_app.performAction(
3294+
.{ .surface = self },
3295+
.open_url,
3296+
.{ .kind = .unknown, .url = str },
3297+
);
32943298
},
32953299

32963300
._open_osc8 => {
32973301
const uri = self.osc8URI(sel.start()) orelse {
32983302
log.warn("failed to get URI for OSC8 hyperlink", .{});
32993303
return false;
33003304
};
3301-
try internal_os.open(self.alloc, .unknown, uri);
3305+
_ = try self.rt_app.performAction(
3306+
.{ .surface = self },
3307+
.open_url,
3308+
.{ .kind = .unknown, .url = uri },
3309+
);
33023310
},
33033311
}
33043312

@@ -4454,7 +4462,13 @@ fn writeScreenFile(
44544462
const path = try tmp_dir.dir.realpath(filename, &path_buf);
44554463

44564464
switch (write_action) {
4457-
.open => try internal_os.open(self.alloc, .text, path),
4465+
.open => {
4466+
_ = try self.rt_app.performAction(
4467+
.{ .surface = self },
4468+
.open_url,
4469+
.{ .kind = .text, .url = path },
4470+
);
4471+
},
44584472
.paste => self.io.queueMessage(try termio.Message.writeReq(
44594473
self.alloc,
44604474
path,

Diff for: src/apprt/action.zig

+40
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,11 @@ pub const Action = union(Key) {
244244
/// Closes the currently focused window.
245245
close_window,
246246

247+
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
248+
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
249+
/// apprt.
250+
open_url: OpenUrl,
251+
247252
/// Sync with: ghostty_action_tag_e
248253
pub const Key = enum(c_int) {
249254
quit,
@@ -287,6 +292,7 @@ pub const Action = union(Key) {
287292
reload_config,
288293
config_change,
289294
close_window,
295+
open_url,
290296
};
291297

292298
/// Sync with: ghostty_action_u
@@ -571,3 +577,37 @@ pub const ConfigChange = struct {
571577
};
572578
}
573579
};
580+
581+
/// The type of the data at the URL to open. This is used as a hint to
582+
/// potentially open the URL in a different way.
583+
/// Sync with: ghostty_action_open_url_kind_s
584+
pub const OpenUrlKind = enum(c_int) {
585+
text,
586+
unknown,
587+
};
588+
589+
/// Open a URL
590+
pub const OpenUrl = struct {
591+
/// The type of data that the URL refers to.
592+
kind: OpenUrlKind,
593+
/// The URL.
594+
url: []const u8,
595+
596+
// Sync with: ghostty_action_open_url_s
597+
pub const C = extern struct {
598+
/// The type of data that the URL refers to.
599+
kind: OpenUrlKind,
600+
/// The URL (not zero terminated).
601+
url: [*]const u8,
602+
/// The number of bytes in the URL.
603+
len: usize,
604+
};
605+
606+
pub fn cval(self: OpenUrl) C {
607+
return .{
608+
.kind = self.kind,
609+
.url = self.url.ptr,
610+
.len = self.url.len,
611+
};
612+
}
613+
};

Diff for: src/apprt/glfw.zig

+13
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ pub const App = struct {
215215

216216
.reload_config => try self.reloadConfig(target, value),
217217

218+
.open_url => self.openUrl(value),
219+
218220
// Unimplemented
219221
.new_split,
220222
.goto_split,
@@ -439,6 +441,17 @@ pub const App = struct {
439441
return .unknown;
440442
}
441443

444+
/// Open a URL. On Linux, use the new `openUrlLinux` otherwise fall back
445+
/// to `open`.
446+
fn openUrl(self: *App, value: apprt.action.OpenUrl) void {
447+
switch (builtin.os.tag) {
448+
.linux => internal_os.openUrlLinux(self.app.alloc, value.url),
449+
else => internal_os.open(self.app.alloc, value.kind, value.url) catch |err| {
450+
log.warn("unable to open url: {}", .{err});
451+
},
452+
}
453+
}
454+
442455
/// Mac-specific settings. This is only enabled when the target is
443456
/// Mac and the artifact is a standalone exe. We don't target libs because
444457
/// the embedded API doesn't do windowing.

Diff for: src/apprt/gtk/App.zig

+8
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ pub fn performAction(
497497
.prompt_title => try self.promptTitle(target),
498498
.toggle_quick_terminal => return try self.toggleQuickTerminal(),
499499
.secure_input => self.setSecureInput(target, value),
500+
.open_url => self.openUrl(value),
500501

501502
// Unimplemented
502503
.close_all_windows,
@@ -1675,3 +1676,10 @@ test "isValidAppId" {
16751676
try testing.expect(!isValidAppId(""));
16761677
try testing.expect(!isValidAppId("foo" ** 86));
16771678
}
1679+
1680+
pub fn openUrl(
1681+
app: *App,
1682+
value: apprt.action.OpenUrl,
1683+
) void {
1684+
internal_os.openUrlLinux(app.core_app.alloc, value.url);
1685+
}

Diff for: src/os/main.zig

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub const expandHome = homedir.expandHome;
4848
pub const ensureLocale = locale.ensureLocale;
4949
pub const clickInterval = mouse.clickInterval;
5050
pub const open = openpkg.open;
51+
pub const openUrlLinux = openpkg.openUrlLinux;
5152
pub const OpenType = openpkg.Type;
5253
pub const pipe = pipepkg.pipe;
5354
pub const resourcesDir = resourcesdir.resourcesDir;

Diff for: src/os/open.zig

+128-8
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@ const std = @import("std");
22
const builtin = @import("builtin");
33
const Allocator = std.mem.Allocator;
44

5-
/// The type of the data at the URL to open. This is used as a hint
6-
/// to potentially open the URL in a different way.
7-
pub const Type = enum {
8-
text,
9-
unknown,
10-
};
5+
const apprt = @import("../apprt.zig");
6+
const CircBuf = @import("../datastruct/circ_buf.zig").CircBuf;
7+
8+
const log = std.log.scoped(.os);
119

1210
/// Open a URL in the default handling application.
1311
///
1412
/// Any output on stderr is logged as a warning in the application logs.
1513
/// Output on stdout is ignored.
1614
pub fn open(
1715
alloc: Allocator,
18-
typ: Type,
16+
kind: apprt.action.OpenUrlKind,
1917
url: []const u8,
2018
) !void {
2119
const cmd: OpenCommand = switch (builtin.os.tag) {
@@ -31,7 +29,7 @@ pub fn open(
3129

3230
.macos => .{
3331
.child = std.process.Child.init(
34-
switch (typ) {
32+
switch (kind) {
3533
.text => &.{ "open", "-t", url },
3634
.unknown => &.{ "open", url },
3735
},
@@ -77,3 +75,125 @@ const OpenCommand = struct {
7775
child: std.process.Child,
7876
wait: bool = false,
7977
};
78+
79+
/// Use `xdg-open` to open a URL using the default application.
80+
///
81+
/// Any output on stderr is logged as a warning in the application logs. Output
82+
/// on stdout is ignored.
83+
pub fn openUrlLinux(
84+
alloc: Allocator,
85+
url: []const u8,
86+
) void {
87+
openUrlLinuxError(alloc, url) catch |err| {
88+
log.warn("unable to open url: {}", .{err});
89+
};
90+
}
91+
92+
fn openUrlLinuxError(
93+
alloc: Allocator,
94+
url: []const u8,
95+
) !void {
96+
// Make a copy of the URL so that we can use it in the thread without
97+
// worrying about it getting freed by other threads.
98+
const copy = try alloc.dupe(u8, url);
99+
errdefer alloc.free(copy);
100+
101+
// Run `xdg-open` in a thread so that it never blocks the main thread, no
102+
// matter how long it takes to execute.
103+
const thread = try std.Thread.spawn(.{}, _openUrlLinux, .{ alloc, copy });
104+
105+
// Don't worry about the thread any more.
106+
thread.detach();
107+
}
108+
109+
fn _openUrlLinux(alloc: Allocator, url: []const u8) void {
110+
_openUrlLinuxError(alloc, url) catch |err| {
111+
log.warn("error while opening url: {}", .{err});
112+
};
113+
}
114+
115+
fn _openUrlLinuxError(alloc: Allocator, url: []const u8) !void {
116+
defer alloc.free(url);
117+
118+
var exe = std.process.Child.init(
119+
&.{ "xdg-open", url },
120+
alloc,
121+
);
122+
123+
// We're only interested in stderr
124+
exe.stdin_behavior = .Ignore;
125+
exe.stdout_behavior = .Ignore;
126+
exe.stderr_behavior = .Pipe;
127+
128+
exe.spawn() catch |err| {
129+
switch (err) {
130+
error.FileNotFound => {
131+
log.err("Unable to find xdg-open. Please install xdg-open and ensure that it is available on the PATH.", .{});
132+
},
133+
else => |e| return e,
134+
}
135+
return;
136+
};
137+
138+
const stderr = exe.stderr orelse {
139+
log.warn("Unable to access the stderr of the spawned program!", .{});
140+
return;
141+
};
142+
143+
var cb = try CircBuf(u8, 0).init(alloc, 50 * 1024);
144+
defer cb.deinit(alloc);
145+
146+
// Read any error output and store it in a circular buffer so that we
147+
// get that _last_ 50K of output.
148+
while (true) {
149+
var buf: [1024]u8 = undefined;
150+
const len = try stderr.read(&buf);
151+
if (len == 0) break;
152+
try cb.appendSlice(buf[0..len]);
153+
}
154+
155+
// If we have any stderr output we log it. This makes it easier for users to
156+
// debug why some open commands may not work as expected.
157+
if (cb.len() > 0) log: {
158+
{
159+
var it = cb.iterator(.forward);
160+
while (it.next()) |char| {
161+
if (std.mem.indexOfScalar(u8, &std.ascii.whitespace, char.*)) |_| continue;
162+
break;
163+
}
164+
// it's all whitespace, don't log
165+
break :log;
166+
}
167+
var buf = std.ArrayList(u8).init(alloc);
168+
defer buf.deinit();
169+
var it = cb.iterator(.forward);
170+
while (it.next()) |char| {
171+
if (char.* == '\n') {
172+
log.err("xdg-open stderr: {s}", .{buf.items});
173+
buf.clearRetainingCapacity();
174+
}
175+
try buf.append(char.*);
176+
}
177+
if (buf.items.len > 0)
178+
log.err("xdg-open stderr: {s}", .{buf.items});
179+
}
180+
181+
const rc = try exe.wait();
182+
183+
switch (rc) {
184+
.Exited => |code| {
185+
if (code != 0) {
186+
log.warn("xdg-open exited with error code {d}", .{code});
187+
}
188+
},
189+
.Signal => |signal| {
190+
log.warn("xdg-open was terminaled with signal {}", .{signal});
191+
},
192+
.Stopped => |signal| {
193+
log.warn("xdg-open was stopped with signal {}", .{signal});
194+
},
195+
.Unknown => |code| {
196+
log.warn("xdg-open had an unknown error {}", .{code});
197+
},
198+
}
199+
}

0 commit comments

Comments
 (0)