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