diff --git a/include/ghostty.h b/include/ghostty.h index 0c9b840e7..fcad7af30 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -803,6 +803,36 @@ typedef struct { ghostty_runtime_close_surface_cb close_surface_cb; } ghostty_runtime_config_s; +// apprt.ipc.Target.Key +typedef enum { + GHOSTTY_IPC_TARGET_CLASS, + GHOSTTY_IPC_TARGET_DETECT, +} ghostty_ipc_target_tag_e; + +typedef union { + char *klass; +} ghostty_ipc_target_u; + +typedef struct { + ghostty_ipc_target_tag_e tag; + ghostty_ipc_target_u target; +} chostty_ipc_target_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 cb542875e..c422aaeb4 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"); @@ -32,6 +33,7 @@ pub const ColorScheme = structs.ColorScheme; pub const CursorPos = structs.CursorPos; pub const IMEPos = structs.IMEPos; pub const Selection = structs.Selection; +pub const OpenNewWindowIPCOptions = structs.OpenNewWindowIPCOptions; pub const SurfaceSize = structs.SurfaceSize; /// The implementation to use for the app runtime. This is comptime chosen diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 30a2d9ff6..0b6512599 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -319,6 +319,23 @@ pub const App = struct { else => {}, } } + + /// Send the given IPC to a running Ghostty. Returns `true` if the action was + /// able to be performed, `false` otherwise. + /// + /// Note that this is a static function. Since this is called from a CLI app (or + /// some other process that is not Ghostty) there is no full-featured apprt App + /// to use. + pub fn performIpc( + _: 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, + } + } }; /// Platform-specific configuration for libghostty. diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index d6a50f0f6..314998285 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -34,6 +34,7 @@ const terminal = @import("../../terminal/main.zig"); const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); const CoreSurface = @import("../../Surface.zig"); +const ipc = @import("ipc.zig"); const cgroup = @import("cgroup.zig"); const Surface = @import("Surface.zig"); @@ -547,6 +548,23 @@ pub fn performAction( return true; } +/// Send the given IPC to a running Ghostty. Returns `true` if the action was +/// able to be performed, `false` otherwise. +/// +/// Note that this is a static function. Since this is called from a CLI app (or +/// some other process that is not Ghostty) there is no full-featured apprt App +/// to use. +pub fn performIpc( + 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 ipc.openNewWindow(alloc, target, value), + } +} + fn newTab(_: *App, target: apprt.Target) !void { switch (target) { .app => {}, @@ -1731,10 +1749,44 @@ fn gtkActionShowGTKInspector( fn gtkActionNewWindow( _: *gio.SimpleAction, - _: ?*glib.Variant, + parameter_: ?*glib.Variant, self: *App, ) callconv(.c) void { - log.info("received new window action", .{}); + log.debug("received new window action", .{}); + + parameter: { + // were we given a parameter? + const parameter = parameter_ orelse break :parameter; + + const as = glib.VariantType.new("as"); + defer as.free(); + + // ensure that the supplied parameter is an array of strings + if (glib.Variant.isOfType(parameter, as) == 0) { + log.warn("parameter is of type {s}", .{parameter.getTypeString()}); + break :parameter; + } + + const s = glib.VariantType.new("s"); + defer s.free(); + + var it: glib.VariantIter = undefined; + _ = it.init(parameter); + + while (it.nextValue()) |value| { + defer value.unref(); + + // just to be sure + if (value.isOfType(s) == 0) continue; + + var len: usize = undefined; + const buf = value.getString(&len); + const str = buf[0..len]; + + log.debug("new-window command argument: {s}", .{str}); + } + } + _ = self.core_app.mailbox.push(.{ .new_window = .{}, }, .{ .forever = {} }); @@ -1751,7 +1803,10 @@ fn initActions(self: *App) void { // For action names: // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html const t = glib.ext.VariantType.newFor(u64); - defer glib.VariantType.free(t); + defer t.free(); + + const as = glib.VariantType.new("as"); + defer as.free(); const actions = .{ .{ "quit", gtkActionQuit, null }, @@ -1760,6 +1815,7 @@ fn initActions(self: *App) void { .{ "present-surface", gtkActionPresentSurface, t }, .{ "show-gtk-inspector", gtkActionShowGTKInspector, null }, .{ "new-window", gtkActionNewWindow, null }, + .{ "new-window-command", gtkActionNewWindow, as }, }; inline for (actions) |entry| { diff --git a/src/apprt/gtk/ipc.zig b/src/apprt/gtk/ipc.zig new file mode 100644 index 000000000..7c2dc3887 --- /dev/null +++ b/src/apprt/gtk/ipc.zig @@ -0,0 +1 @@ +pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow; diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig new file mode 100644 index 000000000..1c29ebd3f --- /dev/null +++ b/src/apprt/gtk/ipc/new_window.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + +const gio = @import("gio"); +const glib = @import("glib"); +const apprt = @import("../../../apprt.zig"); + +// Use a D-Bus method call to open a new window on GTK. +// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI +// +// `ghostty +new-window` is equivalent to the following command (on a release build): +// +// ``` +// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] [] +// ``` +// +// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build): +// +// ``` +// 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, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool { + const stderr = std.io.getStdErr().writer(); + + // 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 = switch (target) { + .class => |class| result: { + // Force the usage of the class specified on the CLI to determine the + // bus name and object path. + const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); + + std.mem.replaceScalar(u8, object_path, '.', '/'); + std.mem.replaceScalar(u8, object_path, '-', '_'); + + break :result .{ class, object_path }; + }, + .detect => switch (builtin.mode) { + .Debug, .ReleaseSafe => .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }, + .ReleaseFast, .ReleaseSmall => .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }, + }, + }; + defer { + switch (target) { + .class => alloc.free(object_path), + .detect => {}, + } + } + + if (gio.Application.idIsValid(bus_name.ptr) == 0) { + try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); + 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 error.IPCFailed; + } + + const dbus = dbus: { + var err_: ?*glib.Error = null; + defer if (err_) |err| err.free(); + + const dbus_ = gio.busGetSync(.session, null, &err_); + if (err_) |err| { + try stderr.print( + "Unable to establish connection to D-Bus session bus: {s}\n", + .{err.f_message orelse "(unknown)"}, + ); + return error.IPCFailed; + } + + break :dbus dbus_ orelse { + try stderr.print("gio.busGetSync returned null\n", .{}); + return error.IPCFailed; + }; + }; + defer dbus.unref(); + + // use a builder to create the D-Bus method call payload + const payload = payload: { + const builder_type = glib.VariantType.new("(sava{sv})"); + defer glib.free(builder_type); + + // Initialize our builder to build up our parameters + var builder: glib.VariantBuilder = undefined; + builder.init(builder_type); + errdefer builder.clear(); + + // action + if (value.arguments == null) { + builder.add("s", "new-window"); + } else { + builder.add("s", "new-window-command"); + } + + // parameters + { + const av = glib.VariantType.new("av"); + defer av.free(); + + var parameters: glib.VariantBuilder = undefined; + parameters.init(av); + errdefer parameters.clear(); + + if (value.arguments) |arguments| { + // 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 + // to run. + { + const as = glib.VariantType.new("as"); + defer as.free(); + + var command: glib.VariantBuilder = undefined; + command.init(as); + errdefer command.clear(); + + for (arguments) |argument| { + command.add("s", argument.ptr); + } + + parameters.add("v", command.end()); + } + } + + builder.addValue(parameters.end()); + } + + { + const platform_data = glib.VariantType.new("a{sv}"); + defer platform_data.free(); + + builder.open(platform_data); + defer builder.close(); + + // we have no platform data + } + + break :payload builder.end(); + }; + + { + var err_: ?*glib.Error = null; + defer if (err_) |err| err.free(); + + const result_ = dbus.callSync( + bus_name, + object_path, + "org.gtk.Actions", + "Activate", + payload, + null, // We don't care about the return type, we don't do anything with it. + .{}, // no flags + -1, // default timeout + null, // not cancellable + &err_, + ); + defer if (result_) |result| result.unref(); + + if (err_) |err| { + try stderr.print( + "D-Bus method call returned an error err={s}\n", + .{err.f_message orelse "(unknown)"}, + ); + return error.IPCFailed; + } + } + + return true; +} diff --git a/src/apprt/ipc.zig b/src/apprt/ipc.zig new file mode 100644 index 000000000..6be8bdf07 --- /dev/null +++ b/src/apprt/ipc.zig @@ -0,0 +1,183 @@ +//! 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{ + /// The IPC failed. If a function returns this error, it's expected that + /// an a more specific error message will have been written to stderr (or + /// otherwise shown to the user in an appropriate way). + IPCFailed, +}; + +pub const Target = union(Key) { + /// 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) { + class, + detect, + }; + + // Sync with: ghostty_ipc_target_u + pub const CValue = extern union { + 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) { + .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 { + /// A list of command arguments to launch in the new window. If this is + /// `null` the command configured in the config or the user's default + /// shell should be launched. + /// + /// It is an error for this to be non-`null`, but zero length. + arguments: ?[][:0]const u8, + + pub const C = extern struct { + /// null terminated list of arguments + /// it will be null itself if there are no arguments + arguments: ?[*]?[*:0]const u8, + + pub fn deinit(self: *NewWindow.C, alloc: Allocator) void { + if (self.arguments) |arguments| alloc.free(arguments); + } + }; + + pub fn cval(self: *NewWindow, alloc: Allocator) Allocator.Error!NewWindow.C { + var result: NewWindow.C = undefined; + + if (self.arguments) |arguments| { + result.arguments = try alloc.alloc([*:0]const u8, arguments.len + 1); + + for (arguments, 0..) |argument, i| + result.arguments[i] = argument.ptr; + + // add null terminator + result.arguments[arguments.len] = null; + } else { + result.arguments = 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 76faa88af..0483d824c 100644 --- a/src/apprt/none.zig +++ b/src/apprt/none.zig @@ -1,4 +1,19 @@ +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 App = struct { + /// Always return false as there is no apprt to communicate with. + pub fn performIpc( + _: Allocator, + _: apprt.ipc.Target, + comptime action: apprt.ipc.Action.Key, + _: apprt.ipc.Action.Value(action), + ) !bool { + return false; + } +}; pub const Surface = struct {}; diff --git a/src/cli/args.zig b/src/cli/args.zig index 1af74df69..0ff3dc047 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -153,7 +153,7 @@ pub fn parse( // The error set is dependent on comptime T, so we always add // an extra error so we can have the "else" below. - const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory }; + const ErrSet = @TypeOf(err) || error{ Unknown, OutOfMemory } || Error; const message: [:0]const u8 = switch (@as(ErrSet, @errorCast(err))) { // OOM is not recoverable since we need to allocate to // track more error messages. diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig index c1b661f70..adb715d68 100644 --- a/src/cli/ghostty.zig +++ b/src/cli/ghostty.zig @@ -18,6 +18,7 @@ const validate_config = @import("validate_config.zig"); const crash_report = @import("crash_report.zig"); const show_face = @import("show_face.zig"); const boo = @import("boo.zig"); +const new_window = @import("new_window.zig"); /// Special commands that can be invoked via CLI flags. These are all /// invoked by using `+` as a CLI flag. The only exception is @@ -65,6 +66,9 @@ pub const Action = enum { // Boo! boo, + // Use IPC to tell the running Ghostty to open a new window. + @"new-window", + pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) { // If we see a "-e" and we haven't seen a command yet, then // we are done looking for commands. This special case enables @@ -136,6 +140,7 @@ pub const Action = enum { .@"crash-report" => try crash_report.run(alloc), .@"show-face" => try show_face.run(alloc), .boo => try boo.run(alloc), + .@"new-window" => try new_window.run(alloc), }; } @@ -174,6 +179,7 @@ pub const Action = enum { .@"crash-report" => crash_report.Options, .@"show-face" => show_face.Options, .boo => boo.Options, + .@"new-window" => new_window.Options, }; } } diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig new file mode 100644 index 000000000..343175b4e --- /dev/null +++ b/src/cli/new_window.zig @@ -0,0 +1,172 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Action = @import("../cli.zig").ghostty.Action; +const apprt = @import("../apprt.zig"); +const args = @import("args.zig"); +const diagnostics = @import("diagnostics.zig"); + +pub const Options = struct { + /// This is set by the CLI parser for deinit. + _arena: ?ArenaAllocator = null, + + /// If set, open up a new window in a custom instance of Ghostty. + class: ?[:0]const u8 = null, + + /// 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 = null, + + /// Enable arg parsing diagnostics so that we don't get an error if + /// there is a "normal" config setting on the cli. + _diagnostics: diagnostics.DiagnosticList = .{}, + + /// Manual parse hook, used to deal with `-e` + pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool { + // If it's not `-e` continue with the standard argument parsning. + if (!std.mem.eql(u8, arg, "-e")) return true; + + var arguments: std.ArrayListUnmanaged([:0]const u8) = .empty; + errdefer { + for (arguments.items) |argument| alloc.free(argument); + arguments.deinit(alloc); + } + + // Otherwise gather up the rest of the arguments to use as the command. + while (iter.next()) |param| { + try arguments.append(alloc, try alloc.dupeZ(u8, param)); + } + + self._arguments = try arguments.toOwnedSlice(alloc); + + return false; + } + + pub fn deinit(self: *Options) void { + if (self._arena) |arena| arena.deinit(); + self.* = undefined; + } + + /// Enables "-h" and "--help" to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// The `new-window` will use native platform IPC to open up a new window in a +/// running instance of Ghostty. +/// +/// If the `--class` flag is not set, the `new-window` command will try and +/// connect to a running instance of Ghostty based on what optimizations the +/// Ghostty CLI was compiled with. Otherwise the `new-window` command will try +/// and contact a running Ghostty instance that was configured with the same +/// `class` as was given on the command line. +/// +/// If the `-e` flag is included on the command line, any arguments that follow +/// will be sent to the running Ghostty instance and used as the command to run +/// in the new window rather than the default. If `-e` is not specified, Ghostty +/// will use the default command (either specified with `command` in your config +/// or your default shell as configured on your system). +/// +/// GTK uses an application ID to identify instances of applications. If Ghostty +/// is compiled with release optimizations, the default application ID will be +/// `com.mitchellh.ghostty`. If Ghostty is compiled with debug optimizations, +/// the default application ID will be `com.mitchellh.ghostty-debug`. The +/// `class` configuration entry can be used to set up a custom application +/// ID. The class name must follow the requirements defined [in the GTK +/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html) +/// or it will be ignored and Ghostty will use the default as defined above. +/// +/// On GTK, D-Bus activation must be properly configured. Ghostty does not need +/// to be running for this to open a new window, making it suitable for binding +/// to keys in your window manager (if other methods for configuring global +/// shortcuts are unavailable). D-Bus will handle launching a new instance +/// of Ghostty if it is not already running. See the Ghostty website for +/// information on properly configuring D-Bus activation. +/// +/// Only supported on GTK. +/// +/// Flags: +/// +/// * `--class=`: If set, open up a new window in a custom instance of +/// Ghostty. The class must be a valid GTK application ID. +/// +/// * `-e`: Any arguments after this will be interpreted as a command to +/// execute inside the new window instead of the default command. +/// +/// Available since: 1.2.0 +pub fn run(alloc: Allocator) !u8 { + var iter = try args.argsIterator(alloc); + defer iter.deinit(); + return try runArgs(alloc, &iter); +} + +fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { + const stderr = std.io.getStdErr().writer(); + + var opts: Options = .{}; + defer opts.deinit(); + + args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) { + error.ActionHelpRequested => return err, + else => { + try stderr.print("Error parsing args: {}\n", .{err}); + return 1; + }, + }; + + // Print out any diagnostics, unless it's likely that the diagnostic was + // generated trying to parse a "normal" configuration setting. Exit with an + // error code if any diagnostics were printed. + if (!opts._diagnostics.empty()) { + var exit: bool = false; + outer: for (opts._diagnostics.items()) |diagnostic| { + if (diagnostic.location != .cli) continue :outer; + inner: inline for (@typeInfo(Options).@"struct".fields) |field| { + if (field.name[0] == '_') continue :inner; + if (std.mem.eql(u8, field.name, diagnostic.key)) { + try stderr.writeAll("config error: "); + try diagnostic.write(stderr); + try stderr.writeAll("\n"); + exit = true; + } + } + } + if (exit) return 1; + } + + if (opts._arguments) |arguments| { + if (arguments.len == 0) { + try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{}); + return 1; + } + } + + var arena = ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + if (apprt.App.performIpc( + alloc, + if (opts.class) |class| .{ .class = class } else .detect, + .new_window, + .{ + .arguments = opts._arguments, + }, + ) 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", .{}); + return 1; +}