diff --git a/include/ghostty.h b/include/ghostty.h index 0c9b840e7..d57665a5a 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -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 diff --git a/src/apprt.zig b/src/apprt.zig index 2cad25f2f..147086f5f 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -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"); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 9ffcf53e6..e8c9db8f4 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.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 { diff --git a/src/apprt/gtk/IPC.zig b/src/apprt/gtk/IPC.zig index 83529aae3..780ddaf8d 100644 --- a/src/apprt/gtk/IPC.zig +++ b/src/apprt/gtk/IPC.zig @@ -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; } diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index 6b360c952..df657beea 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -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; } diff --git a/src/apprt/ipc.zig b/src/apprt/ipc.zig new file mode 100644 index 000000000..359381b08 --- /dev/null +++ b/src/apprt/ipc.zig @@ -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, + }; + } +}; diff --git a/src/apprt/none.zig b/src/apprt/none.zig index 695735c3b..2359197ad 100644 --- a/src/apprt/none.zig +++ b/src/apprt/none.zig @@ -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; + } +}; diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index a993d61d2..e2e9b913d 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -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, -}; diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 5ca4be147..b81dc0880 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -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", .{});