mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
core/gtk: open urls using an apprt action instead of doing it directly (#5988)
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 apprt. 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.
This commit is contained in:
@ -662,6 +662,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_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
|
// apprt.Action.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_QUIT,
|
GHOSTTY_ACTION_QUIT,
|
||||||
@ -711,7 +724,8 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_RING_BELL,
|
GHOSTTY_ACTION_RING_BELL,
|
||||||
GHOSTTY_ACTION_UNDO,
|
GHOSTTY_ACTION_UNDO,
|
||||||
GHOSTTY_ACTION_REDO,
|
GHOSTTY_ACTION_REDO,
|
||||||
GHOSTTY_ACTION_CHECK_FOR_UPDATES
|
GHOSTTY_ACTION_CHECK_FOR_UPDATES,
|
||||||
|
GHOSTTY_ACTION_OPEN_URL,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@ -739,6 +753,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 {
|
||||||
|
@ -3724,7 +3724,7 @@ 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.openUrl(.{ .kind = .unknown, .url = str });
|
||||||
},
|
},
|
||||||
|
|
||||||
._open_osc8 => {
|
._open_osc8 => {
|
||||||
@ -3732,13 +3732,35 @@ 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.openUrl(.{ .kind = .unknown, .url = uri });
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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
|
/// Return the URI for an OSC8 hyperlink at the given position or null
|
||||||
/// if there is no hyperlink.
|
/// if there is no hyperlink.
|
||||||
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
|
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
|
||||||
@ -4957,7 +4979,7 @@ fn writeScreenFile(
|
|||||||
defer self.alloc.free(pathZ);
|
defer self.alloc.free(pathZ);
|
||||||
try self.rt_surface.setClipboardString(pathZ, .standard, false);
|
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(
|
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
path,
|
path,
|
||||||
|
@ -267,6 +267,11 @@ pub const Action = union(Key) {
|
|||||||
|
|
||||||
check_for_updates,
|
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
|
/// Sync with: ghostty_action_tag_e
|
||||||
pub const Key = enum(c_int) {
|
pub const Key = enum(c_int) {
|
||||||
quit,
|
quit,
|
||||||
@ -317,6 +322,7 @@ pub const Action = union(Key) {
|
|||||||
undo,
|
undo,
|
||||||
redo,
|
redo,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
|
open_url,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sync with: ghostty_action_u
|
/// 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.
|
// 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) == switch (@sizeOf(usize)) {
|
||||||
|
4 => 16,
|
||||||
|
8 => 24,
|
||||||
|
else => unreachable,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the value type for the given key.
|
/// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -519,6 +519,7 @@ pub fn performAction(
|
|||||||
.secure_input => self.setSecureInput(target, value),
|
.secure_input => self.setSecureInput(target, value),
|
||||||
.ring_bell => try self.ringBell(target),
|
.ring_bell => try self.ringBell(target),
|
||||||
.toggle_command_palette => try self.toggleCommandPalette(target),
|
.toggle_command_palette => try self.toggleCommandPalette(target),
|
||||||
|
.open_url => self.openUrl(value),
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
.close_all_windows,
|
.close_all_windows,
|
||||||
@ -1757,3 +1758,19 @@ fn initActions(self: *App) void {
|
|||||||
action_map.addAction(action.as(gio.Action));
|
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});
|
||||||
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const apprt = @import("../apprt.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.@"os-open");
|
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.
|
/// Open a URL in the default handling application.
|
||||||
///
|
///
|
||||||
/// Any output on stderr is logged as a warning in the application logs.
|
/// 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
|
/// Output on stdout is ignored. The allocator is used to buffer the
|
||||||
/// log output and may allocate from another thread.
|
/// 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(
|
pub fn open(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
typ: Type,
|
kind: apprt.action.OpenUrl.Kind,
|
||||||
url: []const u8,
|
url: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var exe: std.process.Child = switch (builtin.os.tag) {
|
var exe: std.process.Child = switch (builtin.os.tag) {
|
||||||
@ -33,7 +32,7 @@ pub fn open(
|
|||||||
),
|
),
|
||||||
|
|
||||||
.macos => .init(
|
.macos => .init(
|
||||||
switch (typ) {
|
switch (kind) {
|
||||||
.text => &.{ "open", "-t", url },
|
.text => &.{ "open", "-t", url },
|
||||||
.unknown => &.{ "open", url },
|
.unknown => &.{ "open", url },
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user