mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-04-23 01:48:37 +03:00
Merge aa8e7f24fde542e42b231b68abaa18bf973e370b into 66636195f18d21bd65f8e7ced461f6b6770be189
This commit is contained in:
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
136
src/os/open.zig
136
src/os/open.zig
@ -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});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user