From 7845399c00ff52635ce1d90dcf6da4a3d7309ced Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 9 Jul 2025 23:05:15 -0500 Subject: [PATCH 01/11] cli/gtk: add +new-window action This will (on GTK) use a D-Bus method call to tell a running Ghostty instance to open a new window. If D-Bus activation is configured properly, Ghostty does not need to be running first. This could be extended to other platforms, e.g. AppleScript on macOS eventually. When Ghostty develops a native API, that could be used instead to create a new window. --- src/cli/ghostty.zig | 6 ++ src/cli/new_window.zig | 231 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/cli/new_window.zig 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..eff187dcf --- /dev/null +++ b/src/cli/new_window.zig @@ -0,0 +1,231 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const build_config = @import("../build_config.zig"); +const Action = @import("../cli.zig").ghostty.Action; +const args = @import("args.zig"); +const diagnostics = @import("diagnostics.zig"); +const font = @import("../font/main.zig"); +const configpkg = @import("../config.zig"); +const Config = configpkg.Config; + +pub const Options = struct { + /// This is set by the CLI parser for deinit. + _arena: ?ArenaAllocator = null, + + /// If `true`, open up a new window in a debug instance of Ghostty. + /// Otherwise open up a new window in a release instance of Ghostty. + debug: bool = false, + + /// If set, open up a new window in a custom instance of Ghostty. Takes + /// precedence over `--debug`. + class: ?[: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 = .{}, + + 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. +/// +/// On GTK, if D-Bus activation is 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 of configuring global +/// shortcuts are unavailable). See the Ghostty website for information on +/// properly configuring D-Bus activation. +/// +/// If Ghostty is already running, D-Bus activation is unnecessary and this +/// command should cause the running instance to open a new window. +/// +/// Only supported on GTK. +/// +/// Flags: +/// +/// * `--debug`: If `true`, open up a new window in a debug instance of +/// Ghostty. Otherwise open up a new window in a release instance of +/// Ghostty. +/// +/// * `--class`: If set, open up a new window in a custom instance of Ghostty. +/// This takes precedence over the `--debug` flag. +/// +/// 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; + } + + var arena = ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Use a D-Bus method call to open a new window on GTK. + // See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI + if (comptime build_config.app_runtime == .gtk) { + const gio = @import("gio"); + const glib = @import("glib"); + + // 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: { + if (opts.class) |class| { + 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 }; + } + if (opts.debug) { + break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; + } + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + }; + + if (gio.Application.idIsValid(bus_name.ptr) == 0) { + try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); + return 1; + } + + if (glib.Variant.isObjectPath(object_path.ptr) == 0) { + try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path}); + return 1; + } + + 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 1; + } + + break :dbus dbus_ orelse { + try stderr.print("gio.busGetSync returned null\n", .{}); + return 1; + }; + }; + 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.unref(); + + // action + builder.add("s", "new-window"); + + // parameters + { + const parameters = glib.VariantType.new("av"); + defer glib.free(parameters); + + builder.open(parameters); + defer builder.close(); + + // we have no parameters + } + { + const platform_data = glib.VariantType.new("a{sv}"); + defer glib.free(platform_data); + + 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 1; + } + } + + return 0; + } + + // If we get here, the platform is unsupported. + try stderr.print("+new-window is unsupported on this platform\n", .{}); + return 1; +} From 340d190bf0b6db89b04433d4334b280d214fd2de Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 10 Jul 2025 12:11:01 -0500 Subject: [PATCH 02/11] cli/gtk: clarify +new-window documentation and improve instance discovery - Add a `GHOSTTY_CLASS` environment variables to any command executed by Ghostty to make discovering the correct application ID easier. - Add a flag to force the relelase application ID. - Ensure that CLI flags to `+new-window` are mutually exclusive. - Fix documentation about D-Bus activation requirements. --- src/apprt/gtk/Surface.zig | 7 +++++ src/cli/new_window.zig | 61 ++++++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index a468bd48d..59b78c29a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2358,6 +2358,13 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { try window.winproto.addSubprocessEnv(&env); } + class: { + const gio_app = self.app.app.as(gio.Application); + const class = std.mem.span(gio_app.getApplicationId() orelse break :class); + + try env.put("GHOSTTY_CLASS", class); + } + return env; } diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index eff187dcf..e440360a5 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -14,8 +14,10 @@ pub const Options = struct { /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, + /// If `true`, open up a new window in a release instance of Ghostty. + release: bool = false, + /// If `true`, open up a new window in a debug instance of Ghostty. - /// Otherwise open up a new window in a release instance of Ghostty. debug: bool = false, /// If set, open up a new window in a custom instance of Ghostty. Takes @@ -41,25 +43,41 @@ pub const Options = struct { /// The `new-window` will use native platform IPC to open up a new window in a /// running instance of Ghostty. /// -/// On GTK, if D-Bus activation is properly configured, Ghostty does not need +/// 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 of configuring global -/// shortcuts are unavailable). See the Ghostty website for information on -/// properly configuring D-Bus activation. +/// 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. /// -/// If Ghostty is already running, D-Bus activation is unnecessary and this -/// command should cause the running instance to open a new window. +/// GTK uses an application ID to identify instances of applications. If +/// Ghostty is compiled with debug optimizations, the application ID will +/// be `com.mitchellh.ghostty-debug`. If Ghostty is compiled with release +/// optimizations, the application ID will be `com.mitchellh.ghostty`. +/// +/// 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 application ID as +/// defined above. +/// +/// The `new-window` command will try and find the application ID of the running +/// Ghostty instance in the `GHOSTTY_CLASS` environment variable. If this +/// environment variable is not set, and any of the command line flags defined +/// below are not set, a release instance of Ghostty will be opened. /// /// Only supported on GTK. /// /// Flags: /// -/// * `--debug`: If `true`, open up a new window in a debug instance of -/// Ghostty. Otherwise open up a new window in a release instance of +/// * `--release`: If `true`, force opening up a new window in a release instance of /// Ghostty. /// -/// * `--class`: If set, open up a new window in a custom instance of Ghostty. -/// This takes precedence over the `--debug` flag. +/// * `--debug`: If `true`, force opening up a new window in a debug instance of +/// Ghostty. +/// +/// * `--class=`: If set, open up a new window in a custom instance of Ghostty. The +/// class must be a valid GTK application ID. /// /// Available since: 1.2.0 pub fn run(alloc: Allocator) !u8 { @@ -102,6 +120,16 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { if (exit) return 1; } + var count: usize = 0; + if (opts.release) count += 1; + if (opts.debug) count += 1; + if (opts.class) |_| count += 1; + + if (count > 1) { + try stderr.print("The --release, --debug, and --class flags are mutually exclusive, only one may be specified at a time.\n", .{}); + return 1; + } + var arena = ArenaAllocator.init(alloc_gpa); defer arena.deinit(); const alloc = arena.allocator(); @@ -123,9 +151,20 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { break :result .{ class, object_path }; } + if (opts.release) { + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + } if (opts.debug) { break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; } + if (std.posix.getenv("GHOSTTY_CLASS")) |class| { + 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 }; + } break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; }; From 721702fce4104b55a40b20562380793cd4d44cb8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 11 Jul 2025 09:59:14 -0500 Subject: [PATCH 03/11] cli/gtk: move GTK-specific code to a new file in a subdirectory --- src/cli/new_window.zig | 129 +--------------------------------- src/cli/new_window/gtk.zig | 140 +++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 127 deletions(-) create mode 100644 src/cli/new_window/gtk.zig diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index e440360a5..e17305ed5 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -134,134 +134,9 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { defer arena.deinit(); const alloc = arena.allocator(); - // Use a D-Bus method call to open a new window on GTK. - // See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI if (comptime build_config.app_runtime == .gtk) { - const gio = @import("gio"); - const glib = @import("glib"); - - // 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: { - if (opts.class) |class| { - 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 }; - } - if (opts.release) { - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; - } - if (opts.debug) { - break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; - } - if (std.posix.getenv("GHOSTTY_CLASS")) |class| { - 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 }; - } - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; - }; - - if (gio.Application.idIsValid(bus_name.ptr) == 0) { - try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); - return 1; - } - - if (glib.Variant.isObjectPath(object_path.ptr) == 0) { - try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path}); - return 1; - } - - 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 1; - } - - break :dbus dbus_ orelse { - try stderr.print("gio.busGetSync returned null\n", .{}); - return 1; - }; - }; - 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.unref(); - - // action - builder.add("s", "new-window"); - - // parameters - { - const parameters = glib.VariantType.new("av"); - defer glib.free(parameters); - - builder.open(parameters); - defer builder.close(); - - // we have no parameters - } - { - const platform_data = glib.VariantType.new("a{sv}"); - defer glib.free(platform_data); - - 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 1; - } - } - - return 0; + const new_window = @import("new_window/gtk.zig").new_window; + return try new_window(alloc, stderr, opts); } // If we get here, the platform is unsupported. diff --git a/src/cli/new_window/gtk.zig b/src/cli/new_window/gtk.zig new file mode 100644 index 000000000..0da00826a --- /dev/null +++ b/src/cli/new_window/gtk.zig @@ -0,0 +1,140 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gio = @import("gio"); +const glib = @import("glib"); +const Options = @import("../new_window.zig").Options; + +// Use a D-Bus method call to open a new window on GTK. +// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI +pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) (Allocator.Error || std.posix.WriteError)!u8 { + // 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: { + // Force the usage of the class specified on the CLI to determine the + // bus name and object path. + if (opts.class) |class| { + 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 }; + } + // Force the usage of the release bus name and object path. + if (opts.release) { + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + } + // Force the usage of the debug bus name and object path. + if (opts.debug) { + break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; + } + // If there is a `GHOSTTY_CLASS` environment variable, use that as the basis + // for the bus name and object path. + if (std.posix.getenv("GHOSTTY_CLASS")) |class| { + 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 }; + } + // Otherwise fall back to the release bus name and object path. + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + }; + + if (gio.Application.idIsValid(bus_name.ptr) == 0) { + try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name}); + return 1; + } + + if (glib.Variant.isObjectPath(object_path.ptr) == 0) { + try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path}); + return 1; + } + + 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 1; + } + + break :dbus dbus_ orelse { + try stderr.print("gio.busGetSync returned null\n", .{}); + return 1; + }; + }; + 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.unref(); + + // action + builder.add("s", "new-window"); + + // parameters + { + const parameters = glib.VariantType.new("av"); + defer glib.free(parameters); + + builder.open(parameters); + defer builder.close(); + + // we have no parameters + } + { + const platform_data = glib.VariantType.new("a{sv}"); + defer glib.free(platform_data); + + 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 1; + } + } + + return 0; +} From 824185f23ed6027901ff90c04f13237038e16862 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 12 Jul 2025 18:05:48 -0500 Subject: [PATCH 04/11] cli/gtk: add -e to +new-window This adds the `-e` flag to the `+new-window` CLI action. This allows a command to be passed from the CLI to the running instance of Ghostty. Nothing is done with that command besides logging its presence. --- src/apprt/gtk/App.zig | 44 +++++++++++++++++++++++++++++++--- src/cli/new_window.zig | 42 ++++++++++++++++++++++++++++++--- src/cli/new_window/gtk.zig | 48 +++++++++++++++++++++++++++++++++----- 3 files changed, 122 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index d6a50f0f6..ab12d1f33 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1731,10 +1731,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 +1785,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 +1797,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/cli/new_window.zig b/src/cli/new_window.zig index e17305ed5..e46923b21 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -24,10 +24,32 @@ pub const Options = struct { /// precedence over `--debug`. class: ?[: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. + /// Set to `true` if `-e` was found on the command line. + _command: bool = false, + + /// If `-e` is found in the arguments, this will contain all of the + /// arguments to pass to Ghostty as the command. + _arguments: std.ArrayListUnmanaged([:0]const u8) = .empty, + + /// 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; + + self._command = true; + + // Otherwise gather up the rest of the arguments to use as the command. + while (iter.next()) |param| { + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + } + + return false; + } + pub fn deinit(self: *Options) void { if (self._arena) |arena| arena.deinit(); self.* = undefined; @@ -43,6 +65,12 @@ pub const Options = struct { /// The `new-window` will use native platform IPC to open up a new window in a /// running instance of Ghostty. /// +/// 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). +/// /// 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 @@ -79,6 +107,9 @@ pub const Options = struct { /// * `--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); @@ -120,6 +151,11 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { if (exit) return 1; } + if (opts._command and opts._arguments.items.len == 0) { + try stderr.print("The -e flag was specified on the command line, but no other arguments were found.\n", .{}); + return 1; + } + var count: usize = 0; if (opts.release) count += 1; if (opts.debug) count += 1; @@ -140,6 +176,6 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { } // If we get here, the platform is unsupported. - try stderr.print("+new-window is unsupported on this platform\n", .{}); + try stderr.print("+new-window is unsupported on this platform.\n", .{}); return 1; } diff --git a/src/cli/new_window/gtk.zig b/src/cli/new_window/gtk.zig index 0da00826a..351ff1394 100644 --- a/src/cli/new_window/gtk.zig +++ b/src/cli/new_window/gtk.zig @@ -7,6 +7,18 @@ const Options = @import("../new_window.zig").Options; // 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 --release` is equivalent to the following command: +// +// ``` +// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] [] +// ``` +// +// `ghostty +new-window --release -e echo hello` would be equivalent to the following command: +// +// ``` +// gdbus call --session --dest con.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' [] +// ``` pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) (Allocator.Error || std.posix.WriteError)!u8 { // Get the appropriate bus name and object path for contacting the // Ghostty instance we're interested in. @@ -84,18 +96,42 @@ pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) ( errdefer builder.unref(); // action - builder.add("s", "new-window"); + if (opts._arguments.items.len == 0) { + builder.add("s", "new-window"); + } else { + builder.add("s", "new-window-command"); + } // parameters { - const parameters = glib.VariantType.new("av"); - defer glib.free(parameters); + const av = glib.VariantType.new("av"); + defer av.free(); - builder.open(parameters); - defer builder.close(); + var parameters: glib.VariantBuilder = undefined; + parameters.init(av); - // we have no parameters + if (opts._arguments.items.len > 0) { + // If `-e` was specified on the command line, he first parameter + // is an array of strings that contain the arguments that came + // afer `-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); + + for (opts._arguments.items) |argument| { + command.add("s", argument.ptr); + } + + parameters.add("v", command.end()); + } + } + + builder.addValue(parameters.end()); } + { const platform_data = glib.VariantType.new("a{sv}"); defer glib.free(platform_data); From 72e47cf8bc9d846e6be5b2dd2f6fef8f33e76fb9 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 12 Jul 2025 22:59:44 -0500 Subject: [PATCH 05/11] cli/gtk: move actual IPC code tp apprt --- src/apprt.zig | 2 + src/apprt/embedded.zig | 3 + src/apprt/gtk.zig | 1 + src/apprt/gtk/IPC.zig | 8 ++ .../gtk.zig => apprt/gtk/ipc/new_window.zig} | 75 ++++++++++--------- src/apprt/none.zig | 2 + src/apprt/structs.zig | 20 +++++ src/cli/new_window.zig | 57 +++++++------- 8 files changed, 107 insertions(+), 61 deletions(-) create mode 100644 src/apprt/gtk/IPC.zig rename src/{cli/new_window/gtk.zig => apprt/gtk/ipc/new_window.zig} (63%) diff --git a/src/apprt.zig b/src/apprt.zig index cb542875e..2cad25f2f 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -32,6 +32,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 @@ -49,6 +50,7 @@ pub const runtime = switch (build_config.artifact) { pub const App = runtime.App; pub const Surface = runtime.Surface; +pub const IPC = runtime.IPC; /// Runtime is the runtime to use for Ghostty. All runtimes do not provide /// equivalent feature sets. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 30a2d9ff6..9ffcf53e6 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1153,6 +1153,9 @@ pub const Inspector = struct { } }; +/// Functions for inter-process communication. +pub const IPC = struct {}; + // C API pub const CAPI = struct { const global = &@import("../global.zig").state; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 3193065c4..970923d37 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -3,6 +3,7 @@ pub const App = @import("gtk/App.zig"); pub const Surface = @import("gtk/Surface.zig"); pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir; +pub const IPC = @import("gtk/IPC.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk/IPC.zig b/src/apprt/gtk/IPC.zig new file mode 100644 index 000000000..83529aae3 --- /dev/null +++ b/src/apprt/gtk/IPC.zig @@ -0,0 +1,8 @@ +//! Functions for inter-process communication. +const IPC = @This(); + +pub const openNewWindow = @import("ipc/new_window.zig").openNewWindow; + +test { + _ = openNewWindow; +} diff --git a/src/cli/new_window/gtk.zig b/src/apprt/gtk/ipc/new_window.zig similarity index 63% rename from src/cli/new_window/gtk.zig rename to src/apprt/gtk/ipc/new_window.zig index 351ff1394..6b360c952 100644 --- a/src/cli/new_window/gtk.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -3,7 +3,7 @@ const Allocator = std.mem.Allocator; const gio = @import("gio"); const glib = @import("glib"); -const Options = @import("../new_window.zig").Options; +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 @@ -17,42 +17,46 @@ const Options = @import("../new_window.zig").Options; // `ghostty +new-window --release -e echo hello` would be equivalent to the following command: // // ``` -// gdbus call --session --dest con.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' [] +// 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 new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) (Allocator.Error || std.posix.WriteError)!u8 { +pub fn openNewWindow(alloc: Allocator, stderr: std.fs.File.Writer, opts: apprt.OpenNewWindowIPCOptions) (Allocator.Error || std.posix.WriteError)!u8 { // 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: { - // Force the usage of the class specified on the CLI to determine the - // bus name and object path. - if (opts.class) |class| { - const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); + switch (opts.instance) { + .class => |class| { + // 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, '-', '_'); + std.mem.replaceScalar(u8, object_path, '.', '/'); + std.mem.replaceScalar(u8, object_path, '-', '_'); - break :result .{ class, object_path }; - } - // Force the usage of the release bus name and object path. - if (opts.release) { - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; - } - // Force the usage of the debug bus name and object path. - if (opts.debug) { - break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; - } - // If there is a `GHOSTTY_CLASS` environment variable, use that as the basis - // for the bus name and object path. - if (std.posix.getenv("GHOSTTY_CLASS")) |class| { - const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); + break :result .{ class, object_path }; + }, + .release => { + // Force the usage of the release bus name and object path. + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + }, + .debug => { + // Force the usage of the debug bus name and object path. + break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; + }, + .detect => { + // If there is a `GHOSTTY_CLASS` environment variable, use that as the basis + // for the bus name and object path. + if (std.posix.getenv("GHOSTTY_CLASS")) |class| { + const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class}); - std.mem.replaceScalar(u8, object_path, '.', '/'); - std.mem.replaceScalar(u8, object_path, '-', '_'); + std.mem.replaceScalar(u8, object_path, '.', '/'); + std.mem.replaceScalar(u8, object_path, '-', '_'); - break :result .{ class, object_path }; + break :result .{ class, object_path }; + } + // Otherwise fall back to the release bus name and object path. + break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; + }, } - // Otherwise fall back to the release bus name and object path. - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; }; if (gio.Application.idIsValid(bus_name.ptr) == 0) { @@ -96,7 +100,7 @@ pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) ( errdefer builder.unref(); // action - if (opts._arguments.items.len == 0) { + if (opts.arguments.len == 0) { builder.add("s", "new-window"); } else { builder.add("s", "new-window-command"); @@ -110,10 +114,11 @@ pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) ( var parameters: glib.VariantBuilder = undefined; parameters.init(av); - if (opts._arguments.items.len > 0) { - // If `-e` was specified on the command line, he first parameter - // is an array of strings that contain the arguments that came - // afer `-e`, which will be interpreted as a command to run. + if (opts.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 + // to run. { const as = glib.VariantType.new("as"); defer as.free(); @@ -121,7 +126,7 @@ pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) ( var command: glib.VariantBuilder = undefined; command.init(as); - for (opts._arguments.items) |argument| { + for (opts.arguments) |argument| { command.add("s", argument.ptr); } @@ -134,7 +139,7 @@ pub fn new_window(alloc: Allocator, stderr: std.fs.File.Writer, opts: Options) ( { const platform_data = glib.VariantType.new("a{sv}"); - defer glib.free(platform_data); + defer platform_data.free(); builder.open(platform_data); defer builder.close(); diff --git a/src/apprt/none.zig b/src/apprt/none.zig index 76faa88af..695735c3b 100644 --- a/src/apprt/none.zig +++ b/src/apprt/none.zig @@ -2,3 +2,5 @@ const internal_os = @import("../os/main.zig"); pub const resourcesDir = internal_os.resourcesDir; pub const App = struct {}; pub const Surface = struct {}; +/// Functions for inter-process communication. +pub const IPC = struct {}; diff --git a/src/apprt/structs.zig b/src/apprt/structs.zig index e2e9b913d..a993d61d2 100644 --- a/src/apprt/structs.zig +++ b/src/apprt/structs.zig @@ -72,3 +72,23 @@ 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 e46923b21..5ca4be147 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -1,14 +1,10 @@ const std = @import("std"); -const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const build_config = @import("../build_config.zig"); const Action = @import("../cli.zig").ghostty.Action; +const apprt = @import("../apprt.zig"); const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); -const font = @import("../font/main.zig"); -const configpkg = @import("../config.zig"); -const Config = configpkg.Config; pub const Options = struct { /// This is set by the CLI parser for deinit. @@ -65,12 +61,26 @@ pub const Options = struct { /// The `new-window` will use native platform IPC to open up a new window in a /// running instance of Ghostty. /// +/// If none of `--release`, `--debug`, and `--class` flags are not set, the +/// `new-window` command will try and find the class of the running Ghostty +/// instance in the `GHOSTTY_CLASS` environment variable. If this environment +/// variable is not set, a release instance of Ghostty will be opened. +/// /// 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 @@ -78,22 +88,6 @@ pub const Options = struct { /// of Ghostty if it is not already running. See the Ghostty website for /// information on properly configuring D-Bus activation. /// -/// GTK uses an application ID to identify instances of applications. If -/// Ghostty is compiled with debug optimizations, the application ID will -/// be `com.mitchellh.ghostty-debug`. If Ghostty is compiled with release -/// optimizations, the application ID will be `com.mitchellh.ghostty`. -/// -/// 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 application ID as -/// defined above. -/// -/// The `new-window` command will try and find the application ID of the running -/// Ghostty instance in the `GHOSTTY_CLASS` environment variable. If this -/// environment variable is not set, and any of the command line flags defined -/// below are not set, a release instance of Ghostty will be opened. -/// /// Only supported on GTK. /// /// Flags: @@ -170,12 +164,23 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { defer arena.deinit(); const alloc = arena.allocator(); - if (comptime build_config.app_runtime == .gtk) { - const new_window = @import("new_window/gtk.zig").new_window; - return try new_window(alloc, stderr, opts); + 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 we get here, the platform is unsupported. - try stderr.print("+new-window is unsupported on this platform.\n", .{}); + // If we get here, the platform is not supported. + try stderr.print("+new-window is not supported on this platform.\n", .{}); return 1; } From 81358c8dcaba470ce630a0ebddeeb2e84b43772f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 13 Jul 2025 21:59:13 -0500 Subject: [PATCH 06/11] 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. --- include/ghostty.h | 15 +++ src/apprt.zig | 1 + src/apprt/embedded.zig | 15 ++- src/apprt/gtk/IPC.zig | 18 +++ src/apprt/gtk/ipc/new_window.zig | 29 +++-- src/apprt/ipc.zig | 182 +++++++++++++++++++++++++++++++ src/apprt/none.zig | 17 ++- src/apprt/structs.zig | 20 ---- src/cli/new_window.zig | 42 ++++--- 9 files changed, 291 insertions(+), 48 deletions(-) create mode 100644 src/apprt/ipc.zig 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", .{}); From 58867b0717ba74542c3cc6b3a0934c12bbce5193 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 13 Jul 2025 22:07:38 -0500 Subject: [PATCH 07/11] cli/gtk: add some more C bits --- include/ghostty.h | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index d57665a5a..fcad7af30 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.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. From 361d03b578a9e5f5e9fedb0f054f68c3825bddb2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 13 Jul 2025 22:58:54 -0500 Subject: [PATCH 08/11] cli/gtk: remove --release and --debug flags, use optional for arguments --- src/apprt/gtk/ipc/new_window.zig | 62 ++++++++------------------ src/apprt/ipc.zig | 31 ++++++------- src/cli/args.zig | 2 +- src/cli/new_window.zig | 74 +++++++++++--------------------- 4 files changed, 57 insertions(+), 112 deletions(-) diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index df657beea..d9bbad426 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const gio = @import("gio"); @@ -8,13 +9,13 @@ 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 --release` is equivalent to the following command: +// `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 --release -e echo hello` would be equivalent to the following command: +// `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"]>]' [] @@ -22,48 +23,23 @@ const apprt = @import("../../../apprt.zig"); 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 (target) { - .class => |class| { - // 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}); + 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, '-', '_'); + std.mem.replaceScalar(u8, object_path, '.', '/'); + std.mem.replaceScalar(u8, object_path, '-', '_'); - break :result .{ class, object_path }; - }, - .release => { - // Force the usage of the release bus name and object path. - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; - }, - .debug => { - // Force the usage of the debug bus name and object path. - break :result .{ "com.mitchellh.ghostty-debug", "/com/mitchellh/ghostty_debug" }; - }, - .detect => { - // If there is a `GHOSTTY_CLASS` environment variable, use that as the basis - // for the bus name and object path. - if (std.posix.getenv("GHOSTTY_CLASS")) |class| { - 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 }; - } - // Otherwise fall back to the release bus name and object path. - break :result .{ "com.mitchellh.ghostty", "/com/mitchellh/ghostty" }; - }, - } + 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" }, + }, }; if (gio.Application.idIsValid(bus_name.ptr) == 0) { @@ -107,7 +83,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip errdefer builder.unref(); // action - if (value.arguments.len == 0) { + if (value.arguments == null) { builder.add("s", "new-window"); } else { builder.add("s", "new-window-command"); @@ -121,7 +97,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip var parameters: glib.VariantBuilder = undefined; parameters.init(av); - if (value.arguments.len > 0) { + 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 @@ -133,7 +109,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip var command: glib.VariantBuilder = undefined; command.init(as); - for (value.arguments) |argument| { + for (arguments) |argument| { command.add("s", argument.ptr); } diff --git a/src/apprt/ipc.zig b/src/apprt/ipc.zig index 359381b08..76f6cb5df 100644 --- a/src/apprt/ipc.zig +++ b/src/apprt/ipc.zig @@ -9,12 +9,6 @@ pub const Errors = error{ }; 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, @@ -23,16 +17,12 @@ pub const Target = union(Key) { // 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, }; @@ -78,27 +68,32 @@ pub const Action = union(enum) { new_window: NewWindow, pub const NewWindow = struct { - arguments: [][:0]const u8, + arguments: ?[][:0]const u8, pub const C = extern struct { /// null terminated list of arguments - arguments: [*]?[*:0]const u8, + /// it will be null itself if there are no arguments + arguments: ?[*]?[*:0]const u8, pub fn deinit(self: *NewWindow.C, alloc: Allocator) void { - alloc.free(self.arguments); + if (self.arguments) |arguments| alloc.free(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); + if (self.arguments) |arguments| { + result.arguments = try alloc.alloc([*:0]const u8, arguments.len + 1); - for (self.arguments, 0..) |argument, i| - result.arguments[i] = argument.ptr; + for (arguments, 0..) |argument, i| + result.arguments[i] = argument.ptr; - // add null terminator - result.arguments[self.arguments.len] = null; + // add null terminator + result.arguments[arguments.len] = null; + } else { + result.arguments = null; + } return result; } 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/new_window.zig b/src/cli/new_window.zig index b81dc0880..995c8b2f0 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -10,22 +10,12 @@ pub const Options = struct { /// This is set by the CLI parser for deinit. _arena: ?ArenaAllocator = null, - /// If `true`, open up a new window in a release instance of Ghostty. - release: bool = false, - - /// If `true`, open up a new window in a debug instance of Ghostty. - debug: bool = false, - - /// If set, open up a new window in a custom instance of Ghostty. Takes - /// precedence over `--debug`. + /// If set, open up a new window in a custom instance of Ghostty. class: ?[:0]const u8 = null, - /// Set to `true` if `-e` was found on the command line. - _command: bool = false, - /// If `-e` is found in the arguments, this will contain all of the /// arguments to pass to Ghostty as the command. - _arguments: std.ArrayListUnmanaged([:0]const u8) = .empty, + _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. @@ -36,13 +26,19 @@ pub const Options = struct { // If it's not `-e` continue with the standard argument parsning. if (!std.mem.eql(u8, arg, "-e")) return true; - self._command = 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 self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + try arguments.append(alloc, try alloc.dupeZ(u8, param)); } + self._arguments = try arguments.toOwnedSlice(alloc); + return false; } @@ -61,10 +57,11 @@ pub const Options = struct { /// The `new-window` will use native platform IPC to open up a new window in a /// running instance of Ghostty. /// -/// If none of `--release`, `--debug`, and `--class` flags are not set, the -/// `new-window` command will try and find the class of the running Ghostty -/// instance in the `GHOSTTY_CLASS` environment variable. If this environment -/// variable is not set, a release instance of Ghostty will be opened. +/// 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 @@ -92,14 +89,8 @@ pub const Options = struct { /// /// Flags: /// -/// * `--release`: If `true`, force opening up a new window in a release instance of -/// Ghostty. -/// -/// * `--debug`: If `true`, force opening up a new window in a debug instance of -/// Ghostty. -/// -/// * `--class=`: If set, open up a new window in a custom instance of Ghostty. The -/// class must be a valid GTK application ID. +/// * `--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. @@ -145,23 +136,11 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { if (exit) return 1; } - if (opts._command and opts._arguments.items.len == 0) { - 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; - if (opts.debug) count += 1; - if (opts.class) |_| count += 1; - - if (count > 1) { - try stderr.print("The --release, --debug, and --class flags are mutually exclusive, only one may be specified at a time.\n", .{}); - 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); @@ -170,15 +149,10 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { 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; - }, + if (opts.class) |class| .{ .class = class } else .detect, .new_window, .{ - .arguments = opts._arguments.items, + .arguments = opts._arguments, }, ) catch |err| switch (err) { error.IPCFailed => { From 7d05f4c0c59af156f08b09ed25eac7a95a9d2b85 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 13 Jul 2025 23:13:00 -0500 Subject: [PATCH 09/11] cli/gtk: don't set GHOSTTY_CLASS --- src/apprt/gtk/Surface.zig | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 59b78c29a..a468bd48d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2358,13 +2358,6 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { try window.winproto.addSubprocessEnv(&env); } - class: { - const gio_app = self.app.app.as(gio.Application); - const class = std.mem.span(gio_app.getApplicationId() orelse break :class); - - try env.put("GHOSTTY_CLASS", class); - } - return env; } From f5eb413c31bbcd67408c337c71633ac4206ee431 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 14 Jul 2025 12:06:42 -0500 Subject: [PATCH 10/11] cli/gtk: clean ups and better error handling in GTK new-window IPC --- src/apprt/gtk/ipc/new_window.zig | 10 +++++++++- src/apprt/ipc.zig | 10 ++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index d9bbad426..1c29ebd3f 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -41,6 +41,12 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip .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}); @@ -80,7 +86,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip // Initialize our builder to build up our parameters var builder: glib.VariantBuilder = undefined; builder.init(builder_type); - errdefer builder.unref(); + errdefer builder.clear(); // action if (value.arguments == null) { @@ -96,6 +102,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip 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 @@ -108,6 +115,7 @@ pub fn openNewWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ip var command: glib.VariantBuilder = undefined; command.init(as); + errdefer command.clear(); for (arguments) |argument| { command.add("s", argument.ptr); diff --git a/src/apprt/ipc.zig b/src/apprt/ipc.zig index 76f6cb5df..6be8bdf07 100644 --- a/src/apprt/ipc.zig +++ b/src/apprt/ipc.zig @@ -5,6 +5,9 @@ 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, }; @@ -38,8 +41,6 @@ pub const Target = union(Key) { return .{ .key = @as(Key, self), .value = switch (self) { - .release => .{ .release = {} }, - .debug => .{ .debug = {} }, .class => |class| .{ .class = class.ptr }, .detect => .{ .detect = {} }, }, @@ -68,6 +69,11 @@ pub const Action = union(enum) { 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 { From dd3853abebb25e5895d55e83a67e83d7c1c63937 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 14 Jul 2025 14:45:46 -0500 Subject: [PATCH 11/11] cli/gtk: move IPC/sendIPC to App/performIpc --- src/apprt.zig | 1 - src/apprt/embedded.zig | 33 +++++++++++++++++---------------- src/apprt/gtk.zig | 1 - src/apprt/gtk/App.zig | 18 ++++++++++++++++++ src/apprt/gtk/IPC.zig | 26 -------------------------- src/apprt/gtk/ipc.zig | 1 + src/apprt/none.zig | 8 +++----- src/cli/new_window.zig | 2 +- 8 files changed, 40 insertions(+), 50 deletions(-) delete mode 100644 src/apprt/gtk/IPC.zig create mode 100644 src/apprt/gtk/ipc.zig diff --git a/src/apprt.zig b/src/apprt.zig index 147086f5f..c422aaeb4 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -51,7 +51,6 @@ pub const runtime = switch (build_config.artifact) { pub const App = runtime.App; pub const Surface = runtime.Surface; -pub const IPC = runtime.IPC; /// Runtime is the runtime to use for Ghostty. All runtimes do not provide /// equivalent feature sets. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index e8c9db8f4..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. @@ -1153,22 +1170,6 @@ pub const Inspector = struct { } }; -/// Functions for inter-process communication. -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 { const global = &@import("../global.zig").state; diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 970923d37..3193065c4 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -3,7 +3,6 @@ pub const App = @import("gtk/App.zig"); pub const Surface = @import("gtk/Surface.zig"); pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir; -pub const IPC = @import("gtk/IPC.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index ab12d1f33..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 => {}, diff --git a/src/apprt/gtk/IPC.zig b/src/apprt/gtk/IPC.zig deleted file mode 100644 index 780ddaf8d..000000000 --- a/src/apprt/gtk/IPC.zig +++ /dev/null @@ -1,26 +0,0 @@ -//! 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.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/none.zig b/src/apprt/none.zig index 2359197ad..0483d824c 100644 --- a/src/apprt/none.zig +++ b/src/apprt/none.zig @@ -5,12 +5,9 @@ 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 App = struct { /// Always return false as there is no apprt to communicate with. - pub fn sendIPC( + pub fn performIpc( _: Allocator, _: apprt.ipc.Target, comptime action: apprt.ipc.Action.Key, @@ -19,3 +16,4 @@ pub const IPC = struct { return false; } }; +pub const Surface = struct {}; diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 995c8b2f0..343175b4e 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -147,7 +147,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { defer arena.deinit(); const alloc = arena.allocator(); - if (apprt.IPC.sendIPC( + if (apprt.App.performIpc( alloc, if (opts.class) |class| .{ .class = class } else .detect, .new_window,