diff --git a/src/Surface.zig b/src/Surface.zig index e71af3939..a3691b53e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -607,10 +607,14 @@ pub fn init( }; // The command we're going to execute - const command: ?configpkg.Command = if (app.first) - config.@"initial-command" orelse config.command - else - config.command; + const command: ?configpkg.Command = command: { + if (app.first) { + if (config.@"initial-command") |command| { + break :command command; + } + } + break :command config.command; + }; // Start our IO implementation // This separate block ({}) is important because our errdefers must diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 918e77146..715973671 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2,6 +2,7 @@ const Self = @This(); const std = @import("std"); const apprt = @import("../../apprt.zig"); +const configpkg = @import("../../config.zig"); const CoreSurface = @import("../../Surface.zig"); const ApprtApp = @import("App.zig"); const Application = @import("class/application.zig").Application; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 00560fd13..c3ff51e0f 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -22,6 +22,7 @@ const xev = @import("../../../global.zig").xev; const Binding = @import("../../../input.zig").Binding; const CoreConfig = configpkg.Config; const CoreSurface = @import("../../../Surface.zig"); +const lib = @import("../../../lib/main.zig"); const ext = @import("../ext.zig"); const key = @import("../key.zig"); @@ -709,6 +710,7 @@ pub const Application = extern struct { .app => null, .surface => |v| v, }, + .none, ), .open_config => return Action.openConfig(self), @@ -1669,17 +1671,30 @@ pub const Application = extern struct { ) callconv(.c) void { log.debug("received new window action", .{}); - parameter: { + var arena: std.heap.ArenaAllocator = .init(Application.default().allocator()); + defer arena.deinit(); + + const alloc = arena.allocator(); + + var working_directory: ?[:0]const u8 = null; + var title: ?[:0]const u8 = null; + var command: ?configpkg.Command = null; + var args: std.ArrayList([:0]const u8) = .empty; + + overrides: { // were we given a parameter? - const parameter = parameter_ orelse break :parameter; + const parameter = parameter_ orelse break :overrides; const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); // ensure that the supplied parameter is an array of strings if (glib.Variant.isOfType(parameter, as_variant_type) == 0) { - log.warn("parameter is of type {s}", .{parameter.getTypeString()}); - break :parameter; + log.warn("parameter is of type '{s}', not '{s}'", .{ + parameter.getTypeString(), + as_variant_type.peekString()[0..as_variant_type.getStringLength()], + }); + break :overrides; } const s_variant_type = glib.VariantType.new("s"); @@ -1688,7 +1703,10 @@ pub const Application = extern struct { var it: glib.VariantIter = undefined; _ = it.init(parameter); - while (it.nextValue()) |value| { + var e_seen: bool = false; + var i: usize = 0; + + while (it.nextValue()) |value| : (i += 1) { defer value.unref(); // just to be sure @@ -1698,13 +1716,64 @@ pub const Application = extern struct { const buf = value.getString(&len); const str = buf[0..len]; - log.debug("new-window command argument: {s}", .{str}); + log.debug("new-window argument: {d} {s}", .{ i, str }); + + if (e_seen) { + const cpy = alloc.dupeZ(u8, str) catch |err| { + log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err }); + break :overrides; + }; + args.append(alloc, cpy) catch |err| { + log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err }); + break :overrides; + }; + continue; + } + + if (std.mem.eql(u8, str, "-e")) { + e_seen = true; + continue; + } + + if (lib.cutPrefix(u8, str, "--command=")) |v| { + var cmd: configpkg.Command = undefined; + cmd.parseCLI(alloc, v) catch |err| { + log.warn("unable to parse command: {t}", .{err}); + continue; + }; + command = cmd; + continue; + } + if (lib.cutPrefix(u8, str, "--working-directory=")) |v| { + working_directory = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| wd: { + log.warn("unable to duplicate working directory: {t}", .{err}); + break :wd null; + }; + continue; + } + if (lib.cutPrefix(u8, str, "--title=")) |v| { + title = alloc.dupeZ(u8, std.mem.trim(u8, v, &std.ascii.whitespace)) catch |err| t: { + log.warn("unable to duplicate title: {t}", .{err}); + break :t null; + }; + continue; + } } } - _ = self.core().mailbox.push(.{ - .new_window = .{}, - }, .{ .forever = {} }); + if (args.items.len > 0) { + command = .{ + .direct = args.items, + }; + } + + Action.newWindow(self, null, .{ + .command = command, + .working_directory = working_directory, + .title = title, + }) catch |err| { + log.warn("unable to create new window: {t}", .{err}); + }; } pub fn actionOpenConfig( @@ -2151,6 +2220,13 @@ const Action = struct { pub fn newWindow( self: *Application, parent: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) !void { // Note that we've requested a window at least once. This is used // to trigger quit on no windows. Note I'm not sure if this is REALLY @@ -2159,14 +2235,32 @@ const Action = struct { // was a delay in the event loop before we created a Window. self.private().requested_window = true; - const win = Window.new(self); - initAndShowWindow(self, win, parent); + const win = Window.new(self, .{ + .title = overrides.title, + }); + initAndShowWindow( + self, + win, + parent, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); } fn initAndShowWindow( self: *Application, win: *Window, parent: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) void { // Setup a binding so that whenever our config changes so does the // window. There's never a time when the window config should be out @@ -2180,7 +2274,11 @@ const Action = struct { ); // Create a new tab with window context (first tab in new window) - win.newTabForWindow(parent); + win.newTabForWindow(parent, .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }); // Estimate the initial window size before presenting so the window // manager can position it correctly. @@ -2506,7 +2604,7 @@ const Action = struct { .@"quick-terminal" = true, }); assert(win.isQuickTerminal()); - initAndShowWindow(self, win, null); + initAndShowWindow(self, win, null, .none); return true; } diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 497188b1d..311fbd8a6 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -7,6 +7,7 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const configpkg = @import("../../../config.zig"); const apprt = @import("../../../apprt.zig"); const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); @@ -203,11 +204,22 @@ pub const SplitTree = extern struct { self: *Self, direction: Surface.Tree.Split.Direction, parent_: ?*Surface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) Allocator.Error!void { const alloc = Application.default().allocator(); // Create our new surface. - const surface: *Surface = .new(); + const surface: *Surface = .new(.{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }); defer surface.unref(); _ = surface.refSink(); @@ -612,6 +624,7 @@ pub const SplitTree = extern struct { self.newSplit( direction, self.getActiveSurface(), + .none, ) catch |err| { log.warn("new split failed error={}", .{err}); }; diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index b76ddba7e..8d9e1bcf0 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -10,6 +10,7 @@ const gtk = @import("gtk"); const apprt = @import("../../../apprt.zig"); const build_config = @import("../../../build_config.zig"); +const configpkg = @import("../../../config.zig"); const datastruct = @import("../../../datastruct/main.zig"); const font = @import("../../../font/main.zig"); const input = @import("../../../input.zig"); @@ -704,11 +705,33 @@ pub const Surface = extern struct { /// stops scrolling. pending_horizontal_scroll_reset: ?c_uint = null, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + } = .none, + pub var offset: c_int = 0; }; - pub fn new() *Self { - return gobject.ext.newInstance(Self, .{}); + pub fn new(overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }) *Self { + const self = gobject.ext.newInstance(Self, .{ + .@"title-override" = overrides.title, + }); + const alloc = Application.default().allocator(); + const priv: *Private = self.private(); + priv.overrides = .{ + .command = if (overrides.command) |c| c.clone(alloc) catch null else null, + .working_directory = if (overrides.working_directory) |wd| alloc.dupeZ(u8, wd) catch null else null, + }; + return self; } pub fn core(self: *Self) ?*CoreSurface { @@ -1853,6 +1876,7 @@ pub const Surface = extern struct { } fn finalize(self: *Self) callconv(.c) void { + const alloc = Application.default().allocator(); const priv = self.private(); if (priv.core_surface) |v| { // Remove ourselves from the list of known surfaces in the app. @@ -1866,7 +1890,6 @@ pub const Surface = extern struct { // Deinit the surface v.deinit(); - const alloc = Application.default().allocator(); alloc.destroy(v); priv.core_surface = null; @@ -1899,9 +1922,16 @@ pub const Surface = extern struct { glib.free(@ptrCast(@constCast(v))); priv.title_override = null; } + if (priv.overrides.command) |c| { + c.deinit(alloc); + priv.overrides.command = null; + } + if (priv.overrides.working_directory) |wd| { + alloc.free(wd); + priv.overrides.working_directory = null; + } // Clean up key sequence and key table state - const alloc = Application.default().allocator(); for (priv.key_sequence.items) |s| alloc.free(s); priv.key_sequence.deinit(alloc); for (priv.key_tables.items) |s| alloc.free(s); @@ -3313,7 +3343,7 @@ pub const Surface = extern struct { }; fn initSurface(self: *Self) InitError!void { - const priv = self.private(); + const priv: *Private = self.private(); assert(priv.core_surface == null); const gl_area = priv.gl_area; @@ -3346,6 +3376,13 @@ pub const Surface = extern struct { ); defer config.deinit(); + if (priv.overrides.command) |c| { + config.command = try c.clone(config._arena.?.allocator()); + } + if (priv.overrides.working_directory) |wd| { + config.@"working-directory" = try config._arena.?.allocator().dupeZ(u8, wd); + } + // Properties that can impact surface init if (priv.font_size_request) |size| config.@"font-size" = size.points; if (priv.pwd) |pwd| config.@"working-directory" = pwd; diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 24caa4990..0c60c8ccc 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -5,6 +5,7 @@ const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); +const configpkg = @import("../../../config.zig"); const apprt = @import("../../../apprt.zig"); const CoreSurface = @import("../../../Surface.zig"); const ext = @import("../ext.zig"); @@ -186,22 +187,34 @@ pub const Tab = extern struct { } } - fn init(self: *Self, _: *Class) callconv(.c) void { - gtk.Widget.initTemplate(self.as(gtk.Widget)); + pub fn new(config: ?*Config, overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, - // Init our actions - self.initActionMap(); + pub const none: @This() = .{}; + }) *Self { + const tab = gobject.ext.newInstance(Tab, .{}); + + const priv: *Private = tab.private(); + + if (config) |c| priv.config = c.ref(); // If our configuration is null then we get the configuration // from the application. - const priv = self.private(); if (priv.config == null) { const app = Application.default(); priv.config = app.getConfig(); } + tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); + // Create our initial surface in the split tree. - priv.split_tree.newSplit(.right, null) catch |err| switch (err) { + priv.split_tree.newSplit(.right, null, .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }) catch |err| switch (err) { error.OutOfMemory => { // TODO: We should make our "no surfaces" state more aesthetically // pleasing and show something like an "Oops, something went wrong" @@ -209,6 +222,15 @@ pub const Tab = extern struct { @panic("oom"); }, }; + + return tab; + } + + fn init(self: *Self, _: *Class) callconv(.c) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + + // Init our actions + self.initActionMap(); } fn initActionMap(self: *Self) void { diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index a79945991..c01cad618 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -266,10 +266,27 @@ pub const Window = extern struct { pub var offset: c_int = 0; }; - pub fn new(app: *Application) *Self { - return gobject.ext.newInstance(Self, .{ + pub fn new( + app: *Application, + overrides: struct { + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) *Self { + const win = gobject.ext.newInstance(Self, .{ .application = app, }); + + if (overrides.title) |title| { + // If the overrides have a title set, we set that immediately + // so that any applications inspecting the window states see an + // immediate title set when the window appears, rather than waiting + // possibly a few event loop ticks for it to sync from the surface. + win.as(gtk.Window).setTitle(title); + } + + return win; } fn init(self: *Self, _: *Class) callconv(.c) void { @@ -278,10 +295,14 @@ pub const Window = extern struct { // If our configuration is null then we get the configuration // from the application. const priv = self.private(); - if (priv.config == null) { + + const config = config: { + if (priv.config) |config| break :config config.get(); const app = Application.default(); - priv.config = app.getConfig(); - } + const config = app.getConfig(); + priv.config = config; + break :config config.get(); + }; // We initialize our windowing protocol to none because we can't // actually initialize this until we get realized. @@ -305,17 +326,16 @@ pub const Window = extern struct { self.initActionMap(); // Start states based on config. - if (priv.config) |config_obj| { - const config = config_obj.get(); - if (config.maximize) self.as(gtk.Window).maximize(); - if (config.fullscreen != .false) self.as(gtk.Window).fullscreen(); + if (config.maximize) self.as(gtk.Window).maximize(); + if (config.fullscreen != .false) self.as(gtk.Window).fullscreen(); - // If we have an explicit title set, we set that immediately - // so that any applications inspecting the window states see - // an immediate title set when the window appears, rather than - // waiting possibly a few event loop ticks for it to sync from - // the surface. - if (config.title) |v| self.as(gtk.Window).setTitle(v); + // If we have an explicit title set, we set that immediately + // so that any applications inspecting the window states see + // an immediate title set when the window appears, rather than + // waiting possibly a few event loop ticks for it to sync from + // the surface. + if (config.title) |title| { + self.as(gtk.Window).setTitle(title); } // We always sync our appearance at the end because loading our @@ -368,21 +388,56 @@ pub const Window = extern struct { /// at the position dictated by the `window-new-tab-position` config. /// The new tab will be selected. pub fn newTab(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_, .tab); + _ = self.newTabPage(parent_, .tab, .none); } - pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_, .window); + pub fn newTabForWindow( + self: *Self, + parent_: ?*CoreSurface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) void { + _ = self.newTabPage( + parent_, + .window, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); } - fn newTabPage(self: *Self, parent_: ?*CoreSurface, context: apprt.surface.NewSurfaceContext) *adw.TabPage { - const priv = self.private(); + fn newTabPage( + self: *Self, + parent_: ?*CoreSurface, + context: apprt.surface.NewSurfaceContext, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, + ) *adw.TabPage { + const priv: *Private = self.private(); const tab_view = priv.tab_view; // Create our new tab object - const tab = gobject.ext.newInstance(Tab, .{ - .config = priv.config, - }); + const tab = Tab.new( + priv.config, + .{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }, + ); + if (parent_) |p| { // For a new window's first tab, inherit the parent's initial size hints. if (context == .window) { @@ -1253,7 +1308,7 @@ pub const Window = extern struct { _: *adw.TabOverview, self: *Self, ) callconv(.c) *adw.TabPage { - return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab); + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none); } fn tabOverviewOpen( diff --git a/src/apprt/gtk/ipc/new_window.zig b/src/apprt/gtk/ipc/new_window.zig index 19c46e3aa..02fed3229 100644 --- a/src/apprt/gtk/ipc/new_window.zig +++ b/src/apprt/gtk/ipc/new_window.zig @@ -18,7 +18,7 @@ const DBus = @import("DBus.zig"); // `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"]>]' [] +// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["-e" "echo" "hello"]>]' [] // ``` pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!bool { var dbus = try DBus.init( @@ -32,10 +32,10 @@ pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Ac defer dbus.deinit(alloc); 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. + // If any arguments were specified on the command line, the first + // parameter is an array of strings that contain the arguments. They + // will be sent to the main Ghostty instance and interpreted as CLI + // arguments. const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index f3f4740d1..12acafadf 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -5,6 +5,8 @@ const Action = @import("../cli.zig").ghostty.Action; const apprt = @import("../apprt.zig"); const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); +const lib = @import("../lib/main.zig"); +const homedir = @import("../os/homedir.zig"); pub const Options = struct { /// This is set by the CLI parser for deinit. @@ -13,35 +15,63 @@ pub const Options = struct { /// 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, + /// Did the user specify a `--working-directory` argument on the command line? + _working_directory_seen: bool = false, + + /// All of the arguments after `+new-window`. They will be sent to Ghosttty + /// for processing. + _arguments: std.ArrayList([: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; + /// Manual parse hook, collect all of the arguments after `+new-window`. + pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool { + var e_seen: bool = std.mem.eql(u8, arg, "-e"); - var arguments: std.ArrayList([:0]const u8) = .empty; - errdefer { - for (arguments.items) |argument| alloc.free(argument); - arguments.deinit(alloc); - } + // Include the argument that triggered the manual parse hook. + if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a); - // Otherwise gather up the rest of the arguments to use as the command. + // 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)); + if (e_seen) { + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (std.mem.eql(u8, param, "-e")) { + e_seen = true; + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a); } - self._arguments = try arguments.toOwnedSlice(alloc); - return false; } + fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 { + if (lib.cutPrefix(u8, arg, "--class=")) |rest| { + self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace)); + return null; + } + + if (lib.cutPrefix(u8, arg, "--working-directory=")) |rest| { + const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace); + if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg); + if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg); + const cwd: std.fs.Dir = std.fs.cwd(); + var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined; + const expanded = try homedir.expandHome(stripped, &expandhome_buf); + var realpath_buf: [std.fs.max_path_bytes]u8 = undefined; + const realpath = try cwd.realpath(expanded, &realpath_buf); + self._working_directory_seen = true; + return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0); + } + + return try alloc.dupeZ(u8, arg); + } + pub fn deinit(self: *Options) void { if (self._arena) |arena| arena.deinit(); self.* = undefined; @@ -63,11 +93,23 @@ pub const Options = struct { /// 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). +/// All of the arguments after the `+new-window` argument (except for the +/// `--class` flag) will be sent to the remote Ghostty instance and will be +/// parsed as command line flags. These flags will override certain settings +/// when creating the first surface in the new window. Currently, only +/// `--working-directory`, `--command`, and `--title` are supported. `-e` will +/// also work as an alias for `--command`, except that if `-e` is found on the +/// command line all following arguments will become part of the command and no +/// more arguments will be parsed for configuration settings. +/// +/// If `--working-directory` is found on the command line and is a relative +/// path (i.e. doesn't start with `/`) it will be resolved to an absolute path +/// relative to the current working directory that the `ghostty +new-window` +/// command is run from. `~/` prefixes will also be expanded to the user's home +/// directory. +/// +/// If `--working-directory` is _not_ found on the command line, the working +/// directory that `ghostty +new-window` is run from will be passed to Ghostty. /// /// GTK uses an application ID to identify instances of applications. If Ghostty /// is compiled with release optimizations, the default application ID will be @@ -92,8 +134,16 @@ 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. /// +/// * `--command`: The command to be executed in the first surface of the new window. +/// +/// * `--working-directory=`: The working directory to pass to Ghostty. +/// +/// * `--title`: A title that will override the title of the first surface in +/// the new window. The title override may be edited or removed later. +/// /// * `-e`: Any arguments after this will be interpreted as a command to -/// execute inside the new window instead of the default command. +/// execute inside the first surface of the new window instead of the +/// default command. /// /// Available since: 1.2.0 pub fn run(alloc: Allocator) !u8 { @@ -143,11 +193,12 @@ fn runArgs( 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; - } + if (!opts._working_directory_seen) { + const alloc = opts._arena.?.allocator(); + const cwd: std.fs.Dir = std.fs.cwd(); + var buf: [std.fs.max_path_bytes]u8 = undefined; + const wd = try cwd.realpath(".", &buf); + try opts._arguments.append(alloc, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0)); } var arena = ArenaAllocator.init(alloc_gpa); @@ -159,7 +210,7 @@ fn runArgs( if (opts.class) |class| .{ .class = class } else .detect, .new_window, .{ - .arguments = opts._arguments, + .arguments = if (opts._arguments.items.len == 0) null else opts._arguments.items, }, ) catch |err| switch (err) { error.IPCFailed => { diff --git a/src/config/Config.zig b/src/config/Config.zig index 7020a2b57..ca93c85d6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -29,7 +29,7 @@ const file_load = @import("file_load.zig"); const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); -const Key = @import("key.zig").Key; +pub const Key = @import("key.zig").Key; const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); pub const Command = @import("command.zig").Command; @@ -4793,8 +4793,8 @@ fn compatBoldIsBright( _ = alloc; assert(std.mem.eql(u8, key, "bold-is-bright")); - const set = cli.args.parseBool(value_ orelse "t") catch return false; - if (set) { + const isset = cli.args.parseBool(value_ orelse "t") catch return false; + if (isset) { self.@"bold-color" = .bright; } diff --git a/src/config/command.zig b/src/config/command.zig index 7e16ad5c7..7cd70acb3 100644 --- a/src/config/command.zig +++ b/src/config/command.zig @@ -165,6 +165,16 @@ pub const Command = union(enum) { }; } + pub fn deinit(self: *const Self, alloc: Allocator) void { + switch (self.*) { + .shell => |v| alloc.free(v), + .direct => |l| { + for (l) |v| alloc.free(v); + alloc.free(l); + }, + } + } + pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void { switch (self) { .shell => |v| try formatter.formatEntry([]const u8, v), diff --git a/src/lib/main.zig b/src/lib/main.zig index e4a67454e..89c6f6c47 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -10,6 +10,7 @@ pub const String = types.String; pub const Struct = @import("struct.zig").Struct; pub const Target = @import("target.zig").Target; pub const TaggedUnion = unionpkg.TaggedUnion; +pub const cutPrefix = @import("string.zig").cutPrefix; test { std.testing.refAllDecls(@This()); diff --git a/src/lib/string.zig b/src/lib/string.zig new file mode 100644 index 000000000..795823c25 --- /dev/null +++ b/src/lib/string.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported +// to 0.16 this can be removed. + +/// If slice starts with prefix, returns the rest of slice starting at +/// prefix.len. +pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T { + return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null; +} + +test cutPrefix { + try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?); + try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example=")); +}