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:
Jeffrey C. Ollie
2025-07-13 21:59:13 -05:00
parent 72e47cf8bc
commit 81358c8dca
9 changed files with 291 additions and 48 deletions

View File

@ -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

View File

@ -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");

View File

@ -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 {

View File

@ -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;
}

View File

@ -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
View 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,
};
}
};

View File

@ -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;
}
};

View File

@ -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,
};

View File

@ -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", .{});