Merge aa8e7f24fde542e42b231b68abaa18bf973e370b into 66636195f18d21bd65f8e7ced461f6b6770be189

This commit is contained in:
Jeffrey C. Ollie
2025-04-12 00:22:22 +01:00
committed by GitHub
7 changed files with 223 additions and 12 deletions

View File

@ -558,6 +558,19 @@ typedef struct {
bool soft; bool soft;
} ghostty_action_reload_config_s; } ghostty_action_reload_config_s;
// apprt.action.OpenUrlKind
typedef enum {
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
} ghostty_action_open_url_kind_e;
// apprt.action.OpenUrl.C
typedef struct {
ghostty_action_open_url_kind_e kind;
const char* url;
uintptr_t len;
} ghostty_action_open_url_s;
// apprt.Action.Key // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_QUIT, GHOSTTY_ACTION_QUIT,
@ -601,6 +614,7 @@ typedef enum {
GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE, GHOSTTY_ACTION_CONFIG_CHANGE,
GHOSTTY_ACTION_CLOSE_WINDOW, GHOSTTY_ACTION_CLOSE_WINDOW,
GHOSTTY_ACTION_OPEN_URL,
} ghostty_action_tag_e; } ghostty_action_tag_e;
typedef union { typedef union {
@ -627,6 +641,7 @@ typedef union {
ghostty_action_color_change_s color_change; ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config; ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change; ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {

View File

@ -3288,7 +3288,11 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false, .trim = false,
}); });
defer self.alloc.free(str); defer self.alloc.free(str);
try internal_os.open(self.alloc, .unknown, str); _ = try self.rt_app.performAction(
.{ .surface = self },
.open_url,
.{ .kind = .unknown, .url = str },
);
}, },
._open_osc8 => { ._open_osc8 => {
@ -3296,7 +3300,11 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{}); log.warn("failed to get URI for OSC8 hyperlink", .{});
return false; return false;
}; };
try internal_os.open(self.alloc, .unknown, uri); _ = try self.rt_app.performAction(
.{ .surface = self },
.open_url,
.{ .kind = .unknown, .url = uri },
);
}, },
} }
@ -4452,7 +4460,13 @@ fn writeScreenFile(
const path = try tmp_dir.dir.realpath(filename, &path_buf); const path = try tmp_dir.dir.realpath(filename, &path_buf);
switch (write_action) { switch (write_action) {
.open => try internal_os.open(self.alloc, .text, path), .open => {
_ = try self.rt_app.performAction(
.{ .surface = self },
.open_url,
.{ .kind = .text, .url = path },
);
},
.paste => self.io.queueMessage(try termio.Message.writeReq( .paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc, self.alloc,
path, path,

View File

@ -244,6 +244,11 @@ pub const Action = union(Key) {
/// Closes the currently focused window. /// Closes the currently focused window.
close_window, close_window,
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
/// apprt.
open_url: OpenUrl,
/// Sync with: ghostty_action_tag_e /// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) { pub const Key = enum(c_int) {
quit, quit,
@ -287,6 +292,7 @@ pub const Action = union(Key) {
reload_config, reload_config,
config_change, config_change,
close_window, close_window,
open_url,
}; };
/// Sync with: ghostty_action_u /// Sync with: ghostty_action_u
@ -327,7 +333,7 @@ pub const Action = union(Key) {
// For ABI compatibility, we expect that this is our union size. // For ABI compatibility, we expect that this is our union size.
// At the time of writing, we don't promise ABI compatibility // At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it. // so we can change this but I want to be aware of it.
assert(@sizeOf(CValue) == 16); assert(@sizeOf(CValue) == 24);
} }
/// Returns the value type for the given key. /// Returns the value type for the given key.
@ -578,3 +584,37 @@ pub const ConfigChange = struct {
}; };
} }
}; };
/// The type of the data at the URL to open. This is used as a hint to
/// potentially open the URL in a different way.
/// Sync with: ghostty_action_open_url_kind_s
pub const OpenUrlKind = enum(c_int) {
text,
unknown,
};
/// Open a URL
pub const OpenUrl = struct {
/// The type of data that the URL refers to.
kind: OpenUrlKind,
/// The URL.
url: []const u8,
// Sync with: ghostty_action_open_url_s
pub const C = extern struct {
/// The type of data that the URL refers to.
kind: OpenUrlKind,
/// The URL (not zero terminated).
url: [*]const u8,
/// The number of bytes in the URL.
len: usize,
};
pub fn cval(self: OpenUrl) C {
return .{
.kind = self.kind,
.url = self.url.ptr,
.len = self.url.len,
};
}
};

View File

@ -215,6 +215,8 @@ pub const App = struct {
.reload_config => try self.reloadConfig(target, value), .reload_config => try self.reloadConfig(target, value),
.open_url => self.openUrl(value),
// Unimplemented // Unimplemented
.new_split, .new_split,
.goto_split, .goto_split,
@ -439,6 +441,17 @@ pub const App = struct {
return .unknown; return .unknown;
} }
/// Open a URL. On Linux, use the new `openUrlLinux` otherwise fall back
/// to `open`.
fn openUrl(self: *App, value: apprt.action.OpenUrl) void {
switch (builtin.os.tag) {
.linux => internal_os.openUrlLinux(self.app.alloc, value.url),
else => internal_os.open(self.app.alloc, value.kind, value.url) catch |err| {
log.warn("unable to open url: {}", .{err});
},
}
}
/// Mac-specific settings. This is only enabled when the target is /// Mac-specific settings. This is only enabled when the target is
/// Mac and the artifact is a standalone exe. We don't target libs because /// Mac and the artifact is a standalone exe. We don't target libs because
/// the embedded API doesn't do windowing. /// the embedded API doesn't do windowing.

View File

@ -484,6 +484,7 @@ pub fn performAction(
.prompt_title => try self.promptTitle(target), .prompt_title => try self.promptTitle(target),
.toggle_quick_terminal => return try self.toggleQuickTerminal(), .toggle_quick_terminal => return try self.toggleQuickTerminal(),
.secure_input => self.setSecureInput(target, value), .secure_input => self.setSecureInput(target, value),
.open_url => self.openUrl(value),
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
@ -1662,3 +1663,10 @@ test "isValidAppId" {
try testing.expect(!isValidAppId("")); try testing.expect(!isValidAppId(""));
try testing.expect(!isValidAppId("foo" ** 86)); try testing.expect(!isValidAppId("foo" ** 86));
} }
pub fn openUrl(
app: *App,
value: apprt.action.OpenUrl,
) void {
internal_os.openUrlLinux(app.core_app.alloc, value.url);
}

View File

@ -48,6 +48,7 @@ pub const expandHome = homedir.expandHome;
pub const ensureLocale = locale.ensureLocale; pub const ensureLocale = locale.ensureLocale;
pub const clickInterval = mouse.clickInterval; pub const clickInterval = mouse.clickInterval;
pub const open = openpkg.open; pub const open = openpkg.open;
pub const openUrlLinux = openpkg.openUrlLinux;
pub const OpenType = openpkg.Type; pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe; pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir; pub const resourcesDir = resourcesdir.resourcesDir;

View File

@ -2,12 +2,10 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
/// The type of the data at the URL to open. This is used as a hint const apprt = @import("../apprt.zig");
/// to potentially open the URL in a different way. const CircBuf = @import("../datastruct/circ_buf.zig").CircBuf;
pub const Type = enum {
text, const log = std.log.scoped(.os);
unknown,
};
/// Open a URL in the default handling application. /// Open a URL in the default handling application.
/// ///
@ -15,7 +13,7 @@ pub const Type = enum {
/// Output on stdout is ignored. /// Output on stdout is ignored.
pub fn open( pub fn open(
alloc: Allocator, alloc: Allocator,
typ: Type, kind: apprt.action.OpenUrlKind,
url: []const u8, url: []const u8,
) !void { ) !void {
const cmd: OpenCommand = switch (builtin.os.tag) { const cmd: OpenCommand = switch (builtin.os.tag) {
@ -31,7 +29,7 @@ pub fn open(
.macos => .{ .macos => .{
.child = std.process.Child.init( .child = std.process.Child.init(
switch (typ) { switch (kind) {
.text => &.{ "open", "-t", url }, .text => &.{ "open", "-t", url },
.unknown => &.{ "open", url }, .unknown => &.{ "open", url },
}, },
@ -77,3 +75,125 @@ const OpenCommand = struct {
child: std.process.Child, child: std.process.Child,
wait: bool = false, wait: bool = false,
}; };
/// Use `xdg-open` to open a URL using the default application.
///
/// Any output on stderr is logged as a warning in the application logs. Output
/// on stdout is ignored.
pub fn openUrlLinux(
alloc: Allocator,
url: []const u8,
) void {
openUrlLinuxError(alloc, url) catch |err| {
log.warn("unable to open url: {}", .{err});
};
}
fn openUrlLinuxError(
alloc: Allocator,
url: []const u8,
) !void {
// Make a copy of the URL so that we can use it in the thread without
// worrying about it getting freed by other threads.
const copy = try alloc.dupe(u8, url);
errdefer alloc.free(copy);
// Run `xdg-open` in a thread so that it never blocks the main thread, no
// matter how long it takes to execute.
const thread = try std.Thread.spawn(.{}, _openUrlLinux, .{ alloc, copy });
// Don't worry about the thread any more.
thread.detach();
}
fn _openUrlLinux(alloc: Allocator, url: []const u8) void {
_openUrlLinuxError(alloc, url) catch |err| {
log.warn("error while opening url: {}", .{err});
};
}
fn _openUrlLinuxError(alloc: Allocator, url: []const u8) !void {
defer alloc.free(url);
var exe = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
);
// We're only interested in stderr
exe.stdin_behavior = .Ignore;
exe.stdout_behavior = .Ignore;
exe.stderr_behavior = .Pipe;
exe.spawn() catch |err| {
switch (err) {
error.FileNotFound => {
log.err("Unable to find xdg-open. Please install xdg-open and ensure that it is available on the PATH.", .{});
},
else => |e| return e,
}
return;
};
const stderr = exe.stderr orelse {
log.warn("Unable to access the stderr of the spawned program!", .{});
return;
};
var cb = try CircBuf(u8, 0).init(alloc, 50 * 1024);
defer cb.deinit(alloc);
// Read any error output and store it in a circular buffer so that we
// get that _last_ 50K of output.
while (true) {
var buf: [1024]u8 = undefined;
const len = try stderr.read(&buf);
if (len == 0) break;
try cb.appendSlice(buf[0..len]);
}
// If we have any stderr output we log it. This makes it easier for users to
// debug why some open commands may not work as expected.
if (cb.len() > 0) log: {
{
var it = cb.iterator(.forward);
while (it.next()) |char| {
if (std.mem.indexOfScalar(u8, &std.ascii.whitespace, char.*)) |_| continue;
break;
}
// it's all whitespace, don't log
break :log;
}
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
var it = cb.iterator(.forward);
while (it.next()) |char| {
if (char.* == '\n') {
log.err("xdg-open stderr: {s}", .{buf.items});
buf.clearRetainingCapacity();
}
try buf.append(char.*);
}
if (buf.items.len > 0)
log.err("xdg-open stderr: {s}", .{buf.items});
}
const rc = try exe.wait();
switch (rc) {
.Exited => |code| {
if (code != 0) {
log.warn("xdg-open exited with error code {d}", .{code});
}
},
.Signal => |signal| {
log.warn("xdg-open was terminaled with signal {}", .{signal});
},
.Stopped => |signal| {
log.warn("xdg-open was stopped with signal {}", .{signal});
},
.Unknown => |code| {
log.warn("xdg-open had an unknown error {}", .{code});
},
}
}