mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
cli/gtk: replace @hasDecl for performAction-style API
Instead of using @hasDecl, use a performAction-stype API. The C interface for interfacing with macOS (or any other apprt where Ghostty is embedded) is unfinished.
This commit is contained in:
@ -803,6 +803,21 @@ typedef struct {
|
||||
ghostty_runtime_close_surface_cb close_surface_cb;
|
||||
} ghostty_runtime_config_s;
|
||||
|
||||
// apprt.ipc.Action.NewWindow
|
||||
typedef struct {
|
||||
// This should be a null terminated list of strings.
|
||||
const char **arguments;
|
||||
} ghostty_ipc_action_new_window_s;
|
||||
|
||||
typedef union {
|
||||
ghostty_ipc_action_new_window_s new_window;
|
||||
} ghostty_ipc_action_u;
|
||||
|
||||
// apprt.ipc.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_IPC_ACTION_NEW_WINDOW,
|
||||
} ghostty_ipc_action_tag_e;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Published API
|
||||
|
||||
|
@ -15,6 +15,7 @@ const build_config = @import("build_config.zig");
|
||||
const structs = @import("apprt/structs.zig");
|
||||
|
||||
pub const action = @import("apprt/action.zig");
|
||||
pub const ipc = @import("apprt/ipc.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
pub const browser = @import("apprt/browser.zig");
|
||||
|
@ -1154,7 +1154,20 @@ pub const Inspector = struct {
|
||||
};
|
||||
|
||||
/// Functions for inter-process communication.
|
||||
pub const IPC = struct {};
|
||||
pub const IPC = struct {
|
||||
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
|
||||
/// able to be performed, `false` otherwise.
|
||||
pub fn sendIPC(
|
||||
_: Allocator,
|
||||
_: apprt.ipc.Target,
|
||||
comptime action: apprt.ipc.Action.Key,
|
||||
_: apprt.ipc.Action.Value(action),
|
||||
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
switch (action) {
|
||||
.new_window => return false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// C API
|
||||
pub const CAPI = struct {
|
||||
|
@ -1,8 +1,26 @@
|
||||
//! Functions for inter-process communication.
|
||||
const IPC = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
|
||||
pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow;
|
||||
|
||||
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
|
||||
/// able to be performed, `false` otherwise.
|
||||
pub fn sendIPC(
|
||||
alloc: Allocator,
|
||||
target: apprt.ipc.Target,
|
||||
comptime action: apprt.ipc.Action.Key,
|
||||
value: apprt.ipc.Action.Value(action),
|
||||
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
switch (action) {
|
||||
.new_window => return try openNewWindow(alloc, target, value),
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = openNewWindow;
|
||||
}
|
||||
|
@ -19,11 +19,18 @@ const apprt = @import("../../../apprt.zig");
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
|
||||
// ```
|
||||
pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.OpenNewWindowIPCOptions) (Allocator.Error || std.posix.WriteError)!u8 {
|
||||
pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
if (value.arguments.len > 256) {
|
||||
try stderr.print("The new window IPC supports at most 256 arguments.\n", .{});
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
// Get the appropriate bus name and object path for contacting the
|
||||
// Ghostty instance we're interested in.
|
||||
const bus_name: [:0]const u8, const object_path: [:0]const u8 = result: {
|
||||
switch (opts.instance) {
|
||||
switch (target) {
|
||||
.class => |class| {
|
||||
// Force the usage of the class specified on the CLI to determine the
|
||||
// bus name and object path.
|
||||
@ -61,12 +68,12 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
|
||||
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
|
||||
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
|
||||
return 1;
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
|
||||
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
|
||||
return 1;
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
const dbus = dbus: {
|
||||
@ -79,12 +86,12 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
"Unable to establish connection to D-Bus session bus: {s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return 1;
|
||||
return error.IPCFailed;
|
||||
}
|
||||
|
||||
break :dbus dbus_ orelse {
|
||||
try stderr.print("gio.busGetSync returned null\n", .{});
|
||||
return 1;
|
||||
return error.IPCFailed;
|
||||
};
|
||||
};
|
||||
defer dbus.unref();
|
||||
@ -100,7 +107,7 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
errdefer builder.unref();
|
||||
|
||||
// action
|
||||
if (opts.arguments.len == 0) {
|
||||
if (value.arguments.len == 0) {
|
||||
builder.add("s", "new-window");
|
||||
} else {
|
||||
builder.add("s", "new-window-command");
|
||||
@ -114,7 +121,7 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
var parameters: glib.VariantBuilder = undefined;
|
||||
parameters.init(av);
|
||||
|
||||
if (opts.arguments.len > 0) {
|
||||
if (value.arguments.len > 0) {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
@ -126,7 +133,7 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
var command: glib.VariantBuilder = undefined;
|
||||
command.init(as);
|
||||
|
||||
for (opts.arguments) |argument| {
|
||||
for (value.arguments) |argument| {
|
||||
command.add("s", argument.ptr);
|
||||
}
|
||||
|
||||
@ -173,9 +180,9 @@ pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.O
|
||||
"D-Bus method call returned an error err={s}\n",
|
||||
.{err.f_message orelse "(unknown)"},
|
||||
);
|
||||
return 1;
|
||||
return error.IPCFailed;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return true;
|
||||
}
|
||||
|
182
src/apprt/ipc.zig
Normal file
182
src/apprt/ipc.zig
Normal file
@ -0,0 +1,182 @@
|
||||
//! Inter-process Communication to a running Ghostty instance from a separate
|
||||
//! process.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const Errors = error{
|
||||
IPCFailed,
|
||||
};
|
||||
|
||||
pub const Target = union(Key) {
|
||||
/// Open up a new window in a release instance of Ghostty.
|
||||
release,
|
||||
|
||||
/// Open up a new window in a debug instance of Ghostty.
|
||||
debug,
|
||||
|
||||
/// Open up a new window in a custom instance of Ghostty.
|
||||
class: [:0]const u8,
|
||||
|
||||
/// Detect which instance to open a new window in.
|
||||
detect,
|
||||
|
||||
// Sync with: ghostty_ipc_target_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
release,
|
||||
debug,
|
||||
class,
|
||||
detect,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_ipc_target_u
|
||||
pub const CValue = extern union {
|
||||
release: void,
|
||||
debug: void,
|
||||
class: [*:0]const u8,
|
||||
detect: void,
|
||||
};
|
||||
|
||||
// Sync with: ghostty_ipc_target_s
|
||||
pub const C = extern struct {
|
||||
key: Key,
|
||||
value: CValue,
|
||||
};
|
||||
|
||||
/// Convert to ghostty_ipc_target_s.
|
||||
pub fn cval(self: Target) C {
|
||||
return .{
|
||||
.key = @as(Key, self),
|
||||
.value = switch (self) {
|
||||
.release => .{ .release = {} },
|
||||
.debug => .{ .debug = {} },
|
||||
.class => |class| .{ .class = class.ptr },
|
||||
.detect => .{ .detect = {} },
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Action = union(enum) {
|
||||
// A GUIDE TO ADDING NEW ACTIONS:
|
||||
//
|
||||
// 1. Add the action to the `Key` enum. The order of the enum matters
|
||||
// because it maps directly to the libghostty C enum. For ABI
|
||||
// compatibility, new actions should be added to the end of the enum.
|
||||
//
|
||||
// 2. Add the action and optional value to the Action union.
|
||||
//
|
||||
// 3. If the value type is not void, ensure the value is C ABI
|
||||
// compatible (extern). If it is not, add a `C` decl to the value
|
||||
// and a `cval` function to convert to the C ABI compatible value.
|
||||
//
|
||||
// 4. Update `include/ghostty.h`: add the new key, value, and union
|
||||
// entry. If the value type is void then only the key needs to be
|
||||
// added. Ensure the order matches exactly with the Zig code.
|
||||
|
||||
/// The arguments to pass to Ghostty as the command.
|
||||
new_window: NewWindow,
|
||||
|
||||
pub const NewWindow = struct {
|
||||
arguments: [][:0]const u8,
|
||||
|
||||
pub const C = extern struct {
|
||||
/// null terminated list of arguments
|
||||
arguments: [*]?[*:0]const u8,
|
||||
|
||||
pub fn deinit(self: *NewWindow.C, alloc: Allocator) void {
|
||||
alloc.free(self.arguments);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn cval(self: *NewWindow, alloc: Allocator) Allocator.Error!NewWindow.C {
|
||||
var result: NewWindow.C = undefined;
|
||||
|
||||
result.arguments = try alloc.alloc([*:0]const u8, self.arguments.len + 1);
|
||||
|
||||
for (self.arguments, 0..) |argument, i|
|
||||
result.arguments[i] = argument.ptr;
|
||||
|
||||
// add null terminator
|
||||
result.arguments[self.arguments.len] = null;
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_ipc_action_tag_e
|
||||
pub const Key = enum(c_uint) {
|
||||
new_window,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_ipc_action_u
|
||||
pub const CValue = cvalue: {
|
||||
const key_fields = @typeInfo(Key).@"enum".fields;
|
||||
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
|
||||
for (key_fields, 0..) |field, i| {
|
||||
const action = @unionInit(Action, field.name, undefined);
|
||||
const Type = t: {
|
||||
const Type = @TypeOf(@field(action, field.name));
|
||||
// Types can provide custom types for their CValue.
|
||||
if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
|
||||
break :t Type;
|
||||
};
|
||||
|
||||
union_fields[i] = .{
|
||||
.name = field.name,
|
||||
.type = Type,
|
||||
.alignment = @alignOf(Type),
|
||||
};
|
||||
}
|
||||
|
||||
break :cvalue @Type(.{ .@"union" = .{
|
||||
.layout = .@"extern",
|
||||
.tag_type = null,
|
||||
.fields = &union_fields,
|
||||
.decls = &.{},
|
||||
} });
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_ipc_action_s
|
||||
pub const C = extern struct {
|
||||
key: Key,
|
||||
value: CValue,
|
||||
};
|
||||
|
||||
comptime {
|
||||
// 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) == switch (@sizeOf(usize)) {
|
||||
4 => 4,
|
||||
8 => 8,
|
||||
else => unreachable,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the value type for the given key.
|
||||
pub fn Value(comptime key: Key) type {
|
||||
inline for (@typeInfo(Action).@"union".fields) |field| {
|
||||
const field_key = @field(Key, field.name);
|
||||
if (field_key == key) return field.type;
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Convert to ghostty_ipc_action_s.
|
||||
pub fn cval(self: Action, alloc: Allocator) C {
|
||||
const value: CValue = switch (self) {
|
||||
inline else => |v, tag| @unionInit(
|
||||
CValue,
|
||||
@tagName(tag),
|
||||
if (@TypeOf(v) != void and @hasDecl(@TypeOf(v), "cval")) v.cval(alloc) else v,
|
||||
),
|
||||
};
|
||||
|
||||
return .{
|
||||
.key = @as(Key, self),
|
||||
.value = value,
|
||||
};
|
||||
}
|
||||
};
|
@ -1,6 +1,21 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const apprt = @import("../apprt.zig");
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
|
||||
pub const App = struct {};
|
||||
pub const Surface = struct {};
|
||||
/// Functions for inter-process communication.
|
||||
pub const IPC = struct {};
|
||||
pub const IPC = struct {
|
||||
/// Always return false as there is no apprt to communicate with.
|
||||
pub fn sendIPC(
|
||||
_: Allocator,
|
||||
_: apprt.ipc.Target,
|
||||
comptime action: apprt.ipc.Action.Key,
|
||||
_: apprt.ipc.Action.Value(action),
|
||||
) !bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -72,23 +72,3 @@ pub const Selection = struct {
|
||||
offset_start: u32,
|
||||
offset_len: u32,
|
||||
};
|
||||
|
||||
pub const OpenNewWindowIPCOptions = struct {
|
||||
instance: union(enum) {
|
||||
/// Open up a new window in a release instance of Ghostty.
|
||||
release,
|
||||
|
||||
/// Open up a new window in a debug instance of Ghostty.
|
||||
debug,
|
||||
|
||||
/// Open up a new window in a custom instance of Ghostty.
|
||||
class: [:0]const u8,
|
||||
|
||||
/// Detect which instance to open a new window in.
|
||||
detect,
|
||||
},
|
||||
|
||||
/// If `-e` is found in the arguments, this will contain all of the
|
||||
/// arguments to pass to Ghostty as the command.
|
||||
arguments: [][:0]const u8,
|
||||
};
|
||||
|
@ -149,6 +149,10 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
||||
try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{});
|
||||
return 1;
|
||||
}
|
||||
if (opts._command and opts._arguments.items.len > 256) {
|
||||
try stderr.print("The -e flag supports at most 256 arguments.\n", .{});
|
||||
return 1;
|
||||
}
|
||||
|
||||
var count: usize = 0;
|
||||
if (opts.release) count += 1;
|
||||
@ -164,21 +168,29 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
if (@hasDecl(apprt.IPC, "openNewWindow")) {
|
||||
return try apprt.IPC.openNewWindow(
|
||||
alloc,
|
||||
stderr,
|
||||
.{
|
||||
.instance = instance: {
|
||||
if (opts.class) |class| break :instance .{ .class = class };
|
||||
if (opts.release) break :instance .release;
|
||||
if (opts.debug) break :instance .debug;
|
||||
break :instance .detect;
|
||||
},
|
||||
.arguments = opts._arguments.items,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (apprt.IPC.sendIPC(
|
||||
alloc,
|
||||
target: {
|
||||
if (opts.class) |class| break :target .{ .class = class };
|
||||
if (opts.release) break :target .release;
|
||||
if (opts.debug) break :target .debug;
|
||||
break :target .detect;
|
||||
},
|
||||
.new_window,
|
||||
.{
|
||||
.arguments = opts._arguments.items,
|
||||
},
|
||||
) catch |err| switch (err) {
|
||||
error.IPCFailed => {
|
||||
// The apprt should have printed a more specific error message
|
||||
// already.
|
||||
return 1;
|
||||
},
|
||||
else => {
|
||||
try stderr.print("Sending the IPC failed: {}", .{err});
|
||||
return 1;
|
||||
},
|
||||
}) return 0;
|
||||
|
||||
// If we get here, the platform is not supported.
|
||||
try stderr.print("+new-window is not supported on this platform.\n", .{});
|
||||
|
Reference in New Issue
Block a user