diff --git a/include/ghostty.h b/include/ghostty.h index 73c708c6b..2a4a7fb6e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -662,6 +662,19 @@ typedef struct { bool soft; } ghostty_action_reload_config_s; +// apprt.action.OpenUrlKind +typedef enum { + GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, + GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, +} 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 typedef enum { GHOSTTY_ACTION_QUIT, @@ -711,7 +724,8 @@ typedef enum { GHOSTTY_ACTION_RING_BELL, GHOSTTY_ACTION_UNDO, GHOSTTY_ACTION_REDO, - GHOSTTY_ACTION_CHECK_FOR_UPDATES + GHOSTTY_ACTION_CHECK_FOR_UPDATES, + GHOSTTY_ACTION_OPEN_URL, } ghostty_action_tag_e; typedef union { @@ -739,6 +753,7 @@ typedef union { ghostty_action_color_change_s color_change; ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; + ghostty_action_open_url_s open_url; } ghostty_action_u; typedef struct { diff --git a/src/Surface.zig b/src/Surface.zig index 33cf581af..a4a8d46df 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3724,7 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { .trim = false, }); defer self.alloc.free(str); - try internal_os.open(self.alloc, .unknown, str); + try self.openUrl(.{ .kind = .unknown, .url = str }); }, ._open_osc8 => { @@ -3732,13 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool { log.warn("failed to get URI for OSC8 hyperlink", .{}); return false; }; - try internal_os.open(self.alloc, .unknown, uri); + try self.openUrl(.{ .kind = .unknown, .url = uri }); }, } return true; } +fn openUrl( + self: *Surface, + action: apprt.action.OpenUrl, +) !void { + // If the apprt handles it then we're done. + if (try self.rt_app.performAction( + .{ .surface = self }, + .open_url, + action, + )) return; + + // apprt didn't handle it, fallback to our simple cross-platform + // URL opener. We log a warning because we want well-behaved + // apprts to handle this themselves. + log.warn("apprt did not handle open URL action, falling back to default opener", .{}); + try internal_os.open( + self.alloc, + action.kind, + action.url, + ); +} + /// Return the URI for an OSC8 hyperlink at the given position or null /// if there is no hyperlink. fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 { @@ -4957,7 +4979,7 @@ fn writeScreenFile( defer self.alloc.free(pathZ); try self.rt_surface.setClipboardString(pathZ, .standard, false); }, - .open => try internal_os.open(self.alloc, .text, path), + .open => try self.openUrl(.{ .kind = .text, .url = path }), .paste => self.io.queueMessage(try termio.Message.writeReq( self.alloc, path, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b4c5164c2..1c3c7c72c 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -267,6 +267,11 @@ pub const Action = union(Key) { check_for_updates, + /// 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 pub const Key = enum(c_int) { quit, @@ -317,6 +322,7 @@ pub const Action = union(Key) { undo, redo, check_for_updates, + open_url, }; /// Sync with: ghostty_action_u @@ -357,7 +363,11 @@ pub const Action = union(Key) { // For ABI compatibility, we expect that this is our union size. // At the time of writing, we don't promise ABI compatibility // so we can change this but I want to be aware of it. - assert(@sizeOf(CValue) == 16); + assert(@sizeOf(CValue) == switch (@sizeOf(usize)) { + 4 => 16, + 8 => 24, + else => unreachable, + }); } /// Returns the value type for the given key. @@ -614,3 +624,44 @@ pub const ConfigChange = struct { }; } }; + +/// Open a URL +pub const OpenUrl = struct { + /// The type of data that the URL refers to. + kind: Kind, + + /// The URL. + url: []const u8, + + /// 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_e + pub const Kind = enum(c_int) { + /// The type is unknown. This is the default and apprts should + /// open the URL in the most generic way possible. For example, + /// on macOS this would be the equivalent of `open` or on Linux + /// this would be `xdg-open`. + unknown, + + /// The URL is known to be a text file. In this case, the apprt + /// should try to open the URL in a text editor or viewer or + /// some equivalent, if possible. + text, + }; + + // Sync with: ghostty_action_open_url_s + pub const C = extern struct { + kind: Kind, + url: [*]const u8, + len: usize, + }; + + pub fn cval(self: OpenUrl) C { + return .{ + .kind = self.kind, + .url = self.url.ptr, + .len = self.url.len, + }; + } +}; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index c61254fbd..369090ee2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -519,6 +519,7 @@ pub fn performAction( .secure_input => self.setSecureInput(target, value), .ring_bell => try self.ringBell(target), .toggle_command_palette => try self.toggleCommandPalette(target), + .open_url => self.openUrl(value), // Unimplemented .close_all_windows, @@ -1757,3 +1758,19 @@ fn initActions(self: *App) void { action_map.addAction(action.as(gio.Action)); } } + +pub fn openUrl( + app: *App, + value: apprt.action.OpenUrl, +) void { + // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html + + // Fallback to the minimal cross-platform way of opening a URL. + // This is always a safe fallback and enables for example Windows + // to open URLs (GTK on Windows via WSL is a thing). + internal_os.open( + app.core_app.alloc, + value.kind, + value.url, + ) catch |err| log.warn("unable to open url: {}", .{err}); +} diff --git a/src/os/open.zig b/src/os/open.zig index ce62a7e0b..9b069c80f 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -1,24 +1,23 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; +const apprt = @import("../apprt.zig"); const log = std.log.scoped(.@"os-open"); -/// 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. -pub const Type = enum { - text, - unknown, -}; - /// 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. The allocator is used to buffer the /// log output and may allocate from another thread. +/// +/// 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( alloc: Allocator, - typ: Type, + kind: apprt.action.OpenUrl.Kind, url: []const u8, ) !void { var exe: std.process.Child = switch (builtin.os.tag) { @@ -33,7 +32,7 @@ pub fn open( ), .macos => .init( - switch (typ) { + switch (kind) { .text => &.{ "open", "-t", url }, .unknown => &.{ "open", url }, },