Skip to content

Commit 20f4b88

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 0b7df75 commit 20f4b88

File tree

7 files changed

+219
-3
lines changed

7 files changed

+219
-3
lines changed

Diff for: include/ghostty.h

+14
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,18 @@ 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+
} ghostty_action_open_url_s;
572+
561573
// apprt.Action.Key
562574
typedef enum {
563575
GHOSTTY_ACTION_QUIT,
@@ -599,6 +611,7 @@ typedef enum {
599611
GHOSTTY_ACTION_COLOR_CHANGE,
600612
GHOSTTY_ACTION_RELOAD_CONFIG,
601613
GHOSTTY_ACTION_CONFIG_CHANGE,
614+
GHOSTTY_ACTION_OPEN_URL,
602615
} ghostty_action_tag_e;
603616

604617
typedef union {
@@ -625,6 +638,7 @@ typedef union {
625638
ghostty_action_color_change_s color_change;
626639
ghostty_action_reload_config_s reload_config;
627640
ghostty_action_config_change_s config_change;
641+
ghostty_action_open_url_s open_url;
628642
} ghostty_action_u;
629643

630644
typedef struct {

Diff for: src/Surface.zig

+21-3
Original file line numberDiff line numberDiff line change
@@ -3275,15 +3275,25 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
32753275
.trim = false,
32763276
});
32773277
defer self.alloc.free(str);
3278-
try internal_os.open(self.alloc, .unknown, str);
3278+
_ = try self.rt_app.performAction(
3279+
.{ .surface = self },
3280+
.open_url,
3281+
.{ .kind = .unknown, .url = str },
3282+
);
32793283
},
32803284

32813285
._open_osc8 => {
32823286
const uri = self.osc8URI(sel.start()) orelse {
32833287
log.warn("failed to get URI for OSC8 hyperlink", .{});
32843288
return false;
32853289
};
3286-
try internal_os.open(self.alloc, .unknown, uri);
3290+
const str = try self.alloc.dupeZ(u8, uri);
3291+
defer self.alloc.free(str);
3292+
_ = try self.rt_app.performAction(
3293+
.{ .surface = self },
3294+
.open_url,
3295+
.{ .kind = .unknown, .url = str },
3296+
);
32873297
},
32883298
}
32893299

@@ -4429,7 +4439,15 @@ fn writeScreenFile(
44294439
const path = try tmp_dir.dir.realpath(filename, &path_buf);
44304440

44314441
switch (write_action) {
4432-
.open => try internal_os.open(self.alloc, .text, path),
4442+
.open => {
4443+
const str = try self.alloc.dupeZ(u8, path);
4444+
defer self.alloc.free(str);
4445+
_ = try self.rt_app.performAction(
4446+
.{ .surface = self },
4447+
.open_url,
4448+
.{ .kind = .text, .url = str },
4449+
);
4450+
},
44334451
.paste => self.io.queueMessage(try termio.Message.writeReq(
44344452
self.alloc,
44354453
path,

Diff for: src/apprt/action.zig

+30
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ pub const Action = union(Key) {
230230
/// for changes.
231231
config_change: ConfigChange,
232232

233+
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
234+
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
235+
/// apprt.
236+
open_url: OpenUrl,
237+
233238
/// Sync with: ghostty_action_tag_e
234239
pub const Key = enum(c_int) {
235240
quit,
@@ -271,6 +276,7 @@ pub const Action = union(Key) {
271276
color_change,
272277
reload_config,
273278
config_change,
279+
open_url,
274280
};
275281

276282
/// Sync with: ghostty_action_u
@@ -555,3 +561,27 @@ pub const ConfigChange = struct {
555561
};
556562
}
557563
};
564+
565+
// Sync with: ghostty_action_open_url_kind_s
566+
pub const OpenUrlKind = enum(c_int) {
567+
text,
568+
unknown,
569+
};
570+
571+
pub const OpenUrl = struct {
572+
kind: OpenUrlKind,
573+
url: [:0]const u8,
574+
575+
// Sync with: ghostty_action_open_url_s
576+
pub const C = extern struct {
577+
kind: OpenUrlKind,
578+
url: [*:0]const u8,
579+
};
580+
581+
pub fn cval(self: OpenUrl) C {
582+
return .{
583+
.kind = self.kind,
584+
.url = self.url.ptr,
585+
};
586+
}
587+
};

Diff for: src/apprt/glfw.zig

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

212212
.reload_config => try self.reloadConfig(target, value),
213213

214+
.open_url => self.openUrl(value),
215+
214216
// Unimplemented
215217
.new_split,
216218
.goto_split,
@@ -433,6 +435,17 @@ pub const App = struct {
433435
return .unknown;
434436
}
435437

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

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

+8
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ pub fn performAction(
496496
.toggle_window_decorations => self.toggleWindowDecorations(target),
497497
.quit_timer => self.quitTimer(value),
498498
.prompt_title => try self.promptTitle(target),
499+
.open_url => self.openUrl(value),
499500

500501
// Unimplemented
501502
.close_all_windows,
@@ -1807,3 +1808,10 @@ test "isValidAppId" {
18071808
try testing.expect(!isValidAppId(""));
18081809
try testing.expect(!isValidAppId("foo" ** 86));
18091810
}
1811+
1812+
pub fn openUrl(
1813+
app: *App,
1814+
value: apprt.action.OpenUrl,
1815+
) void {
1816+
internal_os.openUrlLinux(app.core_app.alloc, value.url);
1817+
}

Diff for: src/os/main.zig

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

Diff for: src/os/open.zig

+132
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ const std = @import("std");
22
const builtin = @import("builtin");
33
const Allocator = std.mem.Allocator;
44

5+
const CircBuf = @import("../datastruct/circ_buf.zig").CircBuf;
6+
7+
const log = std.log.scoped(.os);
8+
59
/// The type of the data at the URL to open. This is used as a hint
610
/// to potentially open the URL in a different way.
711
pub const Type = enum {
@@ -77,3 +81,131 @@ const OpenCommand = struct {
7781
child: std.process.Child,
7882
wait: bool = false,
7983
};
84+
85+
/// Use `xdg-open` to open a URL using the default application.
86+
///
87+
/// Any output on stderr is logged as a warning in the application logs. Output
88+
/// on stdout is ignored.
89+
pub fn openUrlLinux(
90+
alloc: Allocator,
91+
url: [:0]const u8,
92+
) void {
93+
// Make a copy of the URL so that we can use it in the thread without
94+
// worrying about it getting freed by other threads.
95+
const copy = alloc.dupe(u8, url) catch |err| {
96+
log.warn("unable to copy URL before opening: {}", .{err});
97+
return;
98+
};
99+
// Run `xdg-open` in a thread so that it never blocks the main thread, no
100+
// matter how long it takes to execute.
101+
const thread = std.Thread.spawn(.{}, _openUrlLinux, .{ alloc, copy }) catch |err| {
102+
alloc.free(url);
103+
log.warn("unable to start thread to launch url: {}", .{err});
104+
return;
105+
};
106+
// Don't worry about the thread any more.
107+
thread.detach();
108+
}
109+
110+
fn _openUrlLinux(alloc: Allocator, url: []const u8) void {
111+
defer alloc.free(url);
112+
113+
var exe = std.process.Child.init(
114+
&.{ "xdg-open", url },
115+
alloc,
116+
);
117+
118+
// We're only interested in stderr
119+
exe.stdin_behavior = .Ignore;
120+
exe.stdout_behavior = .Ignore;
121+
exe.stderr_behavior = .Pipe;
122+
123+
exe.spawn() catch |err| {
124+
switch (err) {
125+
error.FileNotFound => {
126+
log.err("Unable to find xdg-open. Please install xdg-open and ensure that it is available on the PATH.", .{});
127+
},
128+
else => {
129+
log.warn("Unable to spawn xdg-open: {}", .{err});
130+
},
131+
}
132+
return;
133+
};
134+
135+
const stderr = exe.stderr orelse {
136+
log.warn("Unable to access the stderr of the spawned program!", .{});
137+
return;
138+
};
139+
140+
var cb = CircBuf(u8, 0).init(alloc, 50 * 1024) catch |err| {
141+
log.warn("Unable to create circular buffer: {}", .{err});
142+
return;
143+
};
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 = stderr.read(&buf) catch |err| {
151+
log.warn("Unable to read data from xdg-open: {}", .{err});
152+
return;
153+
};
154+
if (len == 0) break;
155+
cb.appendSlice(buf[0..len]) catch |err| {
156+
log.warn("Unable to append data to circular buffer: {}", .{err});
157+
return;
158+
};
159+
}
160+
161+
// If we have any stderr output we log it. This makes it easier for users to
162+
// debug why some open commands may not work as expected.
163+
if (cb.len() > 0) log: {
164+
{
165+
var it = cb.iterator(.forward);
166+
while (it.next()) |char| {
167+
if (std.mem.indexOfScalar(u8, &std.ascii.whitespace, char.*)) |_| continue;
168+
break;
169+
}
170+
// it's all whitespace, don't log
171+
break :log;
172+
}
173+
var buf = std.ArrayList(u8).init(alloc);
174+
defer buf.deinit();
175+
var it = cb.iterator(.forward);
176+
while (it.next()) |char| {
177+
if (char.* == '\n') {
178+
log.err("xdg-open stderr: {s}", .{buf.items});
179+
buf.clearRetainingCapacity();
180+
}
181+
buf.append(char.*) catch |err| {
182+
log.warn("Unable to append data to buffer: {}", .{err});
183+
return;
184+
};
185+
}
186+
if (buf.items.len > 0)
187+
log.err("xdg-open stderr: {s}", .{buf.items});
188+
}
189+
190+
const rc = exe.wait() catch |err| {
191+
log.warn("Unable to wait for xdg-open: {}", .{err});
192+
return;
193+
};
194+
195+
switch (rc) {
196+
.Exited => |code| {
197+
if (code != 0) {
198+
log.warn("xdg-open exited with error code {d}", .{code});
199+
}
200+
},
201+
.Signal => |signal| {
202+
log.warn("xdg-open was terminaled with signal {}", .{signal});
203+
},
204+
.Stopped => |signal| {
205+
log.warn("xdg-open was stopped with signal {}", .{signal});
206+
},
207+
.Unknown => |code| {
208+
log.warn("xdg-open had an unknown error {}", .{code});
209+
},
210+
}
211+
}

0 commit comments

Comments
 (0)