From 6961c2265e3b760dda9146aa285f11eee1e16abe Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 17 Feb 2026 20:19:33 -0600 Subject: [PATCH 1/6] gtk: `+new-window` now respects `--working-directory` and `-e` Fixes: #8862 Fixes: #10716 This adds the machinery to pass configuration settings received over DBus down to the GObject Surface so that that configuration information can be used to override some settings from the current "live" config when creating a new window. Currently it's only possible to override `--working-directory` and `--command`. `-e` on the `ghostty +new-window` CLI works as well. Adding more overridable settings is possible, but being able to fully override any possible setting would better be served with a major revamp of how Ghostty handles configs, which I is way out of scope at the moment. --- src/Surface.zig | 35 +++++++-- src/apprt/gtk/Surface.zig | 5 ++ src/apprt/gtk/class/application.zig | 85 +++++++++++++++++---- src/apprt/gtk/class/config_overrides.zig | 94 +++++++++++++++++++++++ src/apprt/gtk/class/split_tree.zig | 5 +- src/apprt/gtk/class/surface.zig | 35 ++++++++- src/apprt/gtk/class/tab.zig | 24 ++++-- src/apprt/gtk/class/window.zig | 18 ++--- src/apprt/gtk/ipc/new_window.zig | 10 +-- src/cli/new_window.zig | 71 ++++++++++++++---- src/config.zig | 1 + src/config/Config.zig | 38 +++++++--- src/config/ConfigOverrides.zig | 95 ++++++++++++++++++++++++ src/config/c_get.zig | 20 +---- src/config/key.zig | 6 +- src/lib/main.zig | 1 + src/lib/string.zig | 15 ++++ 17 files changed, 471 insertions(+), 87 deletions(-) create mode 100644 src/apprt/gtk/class/config_overrides.zig create mode 100644 src/config/ConfigOverrides.zig create mode 100644 src/lib/string.zig diff --git a/src/Surface.zig b/src/Surface.zig index e71af3939..9f32b087f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -30,6 +30,7 @@ const font = @import("font/main.zig"); const Command = @import("Command.zig"); const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); +const ConfigOverrides = configpkg.ConfigOverrides; const Duration = configpkg.Config.Duration; const input = @import("input.zig"); const App = @import("App.zig"); @@ -607,10 +608,27 @@ 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 (self.getConfigOverrides()) |config_overrides| { + if (config_overrides.isSet(.command)) + break :command config_overrides.get(.command); + } + if (app.first) { + if (config.@"initial-command") |command| { + break :command command; + } + } + break :command config.command; + }; + + // The working directory to execute the command in. + const working_directory: ?[]const u8 = wd: { + if (self.getConfigOverrides()) |config_overrides| { + if (config_overrides.isSet(.@"working-directory")) + break :wd config_overrides.get(.@"working-directory"); + } + break :wd config.@"working-directory"; + }; // Start our IO implementation // This separate block ({}) is important because our errdefers must @@ -635,7 +653,7 @@ pub fn init( .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .cursor_blink = config.@"cursor-style-blink", - .working_directory = config.@"working-directory", + .working_directory = working_directory, .resources_dir = global_state.resources_dir.host(), .term = config.term, .rt_pre_exec_info = .init(config), @@ -1789,6 +1807,13 @@ pub fn updateConfig( ); } +fn getConfigOverrides(self: *Surface) ?*const ConfigOverrides { + if (@hasDecl(apprt.runtime.Surface, "getConfigOverrides")) { + return self.rt_surface.getConfigOverrides(); + } + return null; +} + const InitialSizeError = error{ ContentScaleUnavailable, AppActionFailed, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 918e77146..f7a563d14 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; @@ -101,3 +102,7 @@ pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap { pub fn redrawInspector(self: *Self) void { self.surface.redrawInspector(); } + +pub fn getConfigOverrides(self: *Self) ?*const configpkg.ConfigOverrides { + return self.gobj().getConfigOverrides(); +} diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 00560fd13..8b5fa7094 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -32,6 +32,7 @@ const ApprtApp = @import("../App.zig"); const Common = @import("../class.zig").Common; const WeakRef = @import("../weak_ref.zig").WeakRef; const Config = @import("config.zig").Config; +const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Surface = @import("surface.zig").Surface; const SplitTree = @import("split_tree.zig").SplitTree; const Window = @import("window.zig").Window; @@ -709,6 +710,7 @@ pub const Application = extern struct { .app => null, .surface => |v| v, }, + null, ), .open_config => return Action.openConfig(self), @@ -1669,17 +1671,29 @@ pub const Application = extern struct { ) callconv(.c) void { log.debug("received new window action", .{}); - parameter: { + const config_overrides: ?*ConfigOverrides = config_overrides: { // were we given a parameter? - const parameter = parameter_ orelse break :parameter; + const parameter = parameter_ orelse break :config_overrides null; + + const alloc = Application.default().allocator(); + const config_overrides = ConfigOverrides.new(alloc) catch |err| { + log.warn("unable to create new config overrides: {t}", .{err}); + break :config_overrides null; + }; + errdefer config_overrides.unref(); + + const co = config_overrides.get(); 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 :config_overrides null; } const s_variant_type = glib.VariantType.new("s"); @@ -1688,7 +1702,16 @@ pub const Application = extern struct { var it: glib.VariantIter = undefined; _ = it.init(parameter); - while (it.nextValue()) |value| { + var args: std.ArrayList([:0]const u8) = .empty; + defer { + for (args.items) |arg| alloc.free(arg); + args.deinit(alloc); + } + + var e_seen: bool = false; + var i: usize = 0; + + while (it.nextValue()) |value| : (i += 1) { defer value.unref(); // just to be sure @@ -1698,13 +1721,45 @@ pub const Application = extern struct { const buf = value.getString(&len); const str = buf[0..len]; - log.debug("new-window command argument: {s}", .{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 :config_overrides null; + }; + errdefer alloc.free(cpy); + args.append(alloc, cpy) catch |err| { + log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err }); + break :config_overrides null; + }; + continue; + } - _ = self.core().mailbox.push(.{ - .new_window = .{}, - }, .{ .forever = {} }); + if (std.mem.eql(u8, str, "-e")) { + e_seen = true; + continue; + } + + co.parseCLI(str) catch |err| { + log.warn("unable to parse argument {d} {s}: {t}", .{ i, str, err }); + continue; + }; + + log.debug("new-window argument: {d} {s}", .{ i, str }); + } + + if (args.items.len > 0) { + co.set(.command, .{ .direct = args.items }) catch |err| { + log.warn("unable to set command on config overrides: {t}", .{err}); + break :config_overrides null; + }; + } + + break :config_overrides config_overrides; + }; + + defer if (config_overrides) |v| v.unref(); + + Action.newWindow(self, null, config_overrides) catch {}; } pub fn actionOpenConfig( @@ -2151,6 +2206,7 @@ const Action = struct { pub fn newWindow( self: *Application, parent: ?*CoreSurface, + config_overrides: ?*ConfigOverrides, ) !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 @@ -2160,13 +2216,14 @@ const Action = struct { self.private().requested_window = true; const win = Window.new(self); - initAndShowWindow(self, win, parent); + initAndShowWindow(self, win, parent, config_overrides); } fn initAndShowWindow( self: *Application, win: *Window, parent: ?*CoreSurface, + config_overrides: ?*ConfigOverrides, ) 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 +2237,7 @@ const Action = struct { ); // Create a new tab with window context (first tab in new window) - win.newTabForWindow(parent); + win.newTabForWindow(parent, config_overrides); // Estimate the initial window size before presenting so the window // manager can position it correctly. @@ -2506,7 +2563,7 @@ const Action = struct { .@"quick-terminal" = true, }); assert(win.isQuickTerminal()); - initAndShowWindow(self, win, null); + initAndShowWindow(self, win, null, null); return true; } diff --git a/src/apprt/gtk/class/config_overrides.zig b/src/apprt/gtk/class/config_overrides.zig new file mode 100644 index 000000000..6ca6ea77e --- /dev/null +++ b/src/apprt/gtk/class/config_overrides.zig @@ -0,0 +1,94 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gobject = @import("gobject"); + +const configpkg = @import("../../../config.zig"); +const Config = configpkg.Config; + +const Common = @import("../class.zig").Common; + +const log = std.log.scoped(.gtk_ghostty_config_overrides); + +/// Wrapper for a ConfigOverrides object that keeps track of which settings have +/// been changed. +pub const ConfigOverrides = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = gobject.Object; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyConfigOverrides", + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + pub const properties = struct {}; + + const Private = struct { + config_overrides: configpkg.ConfigOverrides, + + pub var offset: c_int = 0; + }; + + pub fn new(alloc: Allocator) Allocator.Error!*ConfigOverrides { + const self = gobject.ext.newInstance(Self, .{}); + errdefer self.unref(); + + const priv: *Private = self.private(); + try priv.config_overrides.init(alloc); + + return self; + } + + pub fn get(self: *ConfigOverrides) *configpkg.ConfigOverrides { + const priv: *Private = self.private(); + return &priv.config_overrides; + } + + fn finalize(self: *ConfigOverrides) callconv(.c) void { + const priv: *Private = self.private(); + priv.config_overrides.deinit(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + const C = Common(Self, Private); + pub const as = C.as; + pub const ref = C.ref; + pub const unref = C.unref; + const private = C.private; + + pub const Class = extern struct { + parent_class: Parent.Class, + var parent: *Parent.Class = undefined; + pub const Instance = Self; + + fn init(class: *Class) callconv(.c) void { + gobject.Object.virtual_methods.finalize.implement(class, &finalize); + } + }; +}; + +test "GhosttyConfigOverrides" { + const testing = std.testing; + const alloc = testing.allocator; + + const config_overrides: *ConfigOverrides = try .new(alloc); + defer config_overrides.unref(); + + const co = config_overrides.get(); + + try testing.expect(co.isSet(.@"font-size") == false); + try co.set(.@"font-size", 24.0); + try testing.expect(co.isSet(.@"font-size") == true); + try testing.expectApproxEqAbs(24.0, co.get(.@"font-size"), 0.01); + + try testing.expect(co.isSet(.@"working-directory") == false); + try co.parseCLI("--working-directory=/home/ghostty"); + try testing.expect(co.isSet(.@"working-directory") == true); + try testing.expectEqualStrings("/home/ghostty", co.get(.@"working-directory").?); +} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 0ff7e6044..0d47db958 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -16,6 +16,7 @@ const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const Surface = @import("surface.zig").Surface; const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow; +const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const log = std.log.scoped(.gtk_ghostty_split_tree); @@ -208,11 +209,12 @@ pub const SplitTree = extern struct { self: *Self, direction: Surface.Tree.Split.Direction, parent_: ?*Surface, + config_overrides: ?*ConfigOverrides, ) Allocator.Error!void { const alloc = Application.default().allocator(); // Create our new surface. - const surface: *Surface = .new(); + const surface: *Surface = .new(config_overrides); defer surface.unref(); _ = surface.refSink(); @@ -638,6 +640,7 @@ pub const SplitTree = extern struct { self.newSplit( direction, self.getActiveSurface(), + null, ) 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..423b5b7e8 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"); @@ -25,6 +26,7 @@ const ApprtSurface = @import("../Surface.zig"); const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; +const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const SearchOverlay = @import("search_overlay.zig").SearchOverlay; const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay; @@ -89,6 +91,18 @@ pub const Surface = extern struct { ); }; + pub const @"config-overrides" = struct { + pub const name = "config-overrides"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ConfigOverrides, + .{ + .accessor = C.privateObjFieldAccessor("config_overrides"), + }, + ); + }; + pub const @"child-exited" = struct { pub const name = "child-exited"; const impl = gobject.ext.defineProperty( @@ -551,6 +565,9 @@ pub const Surface = extern struct { /// The configuration that this surface is using. config: ?*Config = null, + /// Any configuration overrides that might apply to this surface. + config_overrides: ?*ConfigOverrides = null, + /// The default size for a window that embeds this surface. default_size: ?*Size = null, @@ -707,8 +724,10 @@ pub const Surface = extern struct { pub var offset: c_int = 0; }; - pub fn new() *Self { - return gobject.ext.newInstance(Self, .{}); + pub fn new(config_overrides: ?*ConfigOverrides) *Self { + return gobject.ext.newInstance(Self, .{ + .@"config-overrides" = config_overrides, + }); } pub fn core(self: *Self) ?*CoreSurface { @@ -1798,6 +1817,11 @@ pub const Surface = extern struct { priv.config = null; } + if (priv.config_overrides) |v| { + v.unref(); + priv.config_overrides = null; + } + if (priv.vadj_signal_group) |group| { group.setTarget(null); group.as(gobject.Object).unref(); @@ -2176,6 +2200,12 @@ pub const Surface = extern struct { self.private().search_overlay.setSearchSelected(selected); } + pub fn getConfigOverrides(self: *Self) ?*const configpkg.ConfigOverrides { + const priv: *Private = self.private(); + const config_overrides = priv.config_overrides orelse return null; + return config_overrides.get(); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -3578,6 +3608,7 @@ pub const Surface = extern struct { gobject.ext.registerProperties(class, &.{ properties.@"bell-ringing".impl, properties.config.impl, + properties.@"config-overrides".impl, properties.@"child-exited".impl, properties.@"default-size".impl, properties.@"error".impl, diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index 24caa4990..d45513e08 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -11,6 +11,7 @@ const ext = @import("../ext.zig"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; +const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Application = @import("application.zig").Application; const SplitTree = @import("split_tree.zig").SplitTree; const Surface = @import("surface.zig").Surface; @@ -186,22 +187,24 @@ 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, config_overrides: ?*ConfigOverrides) *Self { + const tab = gobject.ext.newInstance(Tab, .{}); - // Init our actions - self.initActionMap(); + 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, config_overrides) 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 +212,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..a71fea111 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -21,6 +21,7 @@ const gresource = @import("../build/gresource.zig"); const winprotopkg = @import("../winproto.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; +const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const SplitTree = @import("split_tree.zig").SplitTree; @@ -368,21 +369,20 @@ 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, null); } - pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface) void { - _ = self.newTabPage(parent_, .window); + pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface, config_overrides: ?*ConfigOverrides) void { + _ = self.newTabPage(parent_, .window, config_overrides); } - 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, config_overrides: ?*ConfigOverrides) *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, config_overrides); + if (parent_) |p| { // For a new window's first tab, inherit the parent's initial size hints. if (context == .window) { @@ -1253,7 +1253,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, null); } 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..98a93beb2 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,28 +15,38 @@ 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. + /// All of the arguments after `+new-window`. They will be sent to Ghosttty + /// for processing. _arguments: ?[][:0]const u8 = null, /// Enable arg parsing diagnostics so that we don't get an error if /// there is a "normal" config setting on the cli. _diagnostics: diagnostics.DiagnosticList = .{}, - /// Manual parse hook, used to deal with `-e` - pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) Allocator.Error!bool { - // If it's not `-e` continue with the standard argument parsning. - if (!std.mem.eql(u8, arg, "-e")) return true; - + /// 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 arguments: std.ArrayList([: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. + var e_seen: bool = std.mem.eql(u8, arg, "-e"); + // Include the argument that triggered the manual parse hook. + if (try self.checkArg(alloc, arg)) |a| try arguments.append(alloc, a); + + // 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 arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (std.mem.eql(u8, param, "-e")) { + e_seen = true; + try arguments.append(alloc, try alloc.dupeZ(u8, param)); + continue; + } + if (try self.checkArg(alloc, param)) |a| try arguments.append(alloc, a); } self._arguments = try arguments.toOwnedSlice(alloc); @@ -42,6 +54,27 @@ pub const Options = struct { 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 error.InvalidValue; + if (std.mem.eql(u8, stripped, "inherit")) return error.InvalidValue; + 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); + 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 +96,21 @@ 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` and `--command` 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. The special values `home` and `inherit` that are +/// available as "normal" CLI flags or configuration entries do not work when +/// used from the `+new-window` CLI action. /// /// GTK uses an application ID to identify instances of applications. If Ghostty /// is compiled with release optimizations, the default application ID will be diff --git a/src/config.zig b/src/config.zig index 0bf61a47f..d559ab171 100644 --- a/src/config.zig +++ b/src/config.zig @@ -3,6 +3,7 @@ const builtin = @import("builtin"); const file_load = @import("config/file_load.zig"); const formatter = @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); +pub const ConfigOverrides = @import("config/ConfigOverrides.zig"); pub const conditional = @import("config/conditional.zig"); pub const io = @import("config/io.zig"); pub const string = @import("config/string.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index 7020a2b57..ce891561c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -29,7 +29,8 @@ 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; +pub const Type = @import("key.zig").Type; const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); pub const Command = @import("command.zig").Command; @@ -95,6 +96,23 @@ pub const compatibility = std.StaticStringMap( .{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior }, }); +pub fn get(self: *const Config, comptime key: Key) Type(key) { + return @field(self, @tagName(key)); +} + +pub fn set(self: *Config, comptime key: Key, value: Type(key)) Allocator.Error!void { + const alloc = self.arenaAlloc(); + @field(self.*, @tagName(key)) = try cloneValue(alloc, Type(key), value); +} + +test "set/get" { + var config: Config = try .default(std.testing.allocator); + defer config.deinit(); + try std.testing.expect(config.get(.language) == null); + try config.set(.language, "en_US.UTF-8"); + try std.testing.expectEqualStrings("en_US.UTF-8", config.get(.language).?); +} + /// Set Ghostty's graphical user interface language to a language other than the /// system default language. For example: /// @@ -4757,8 +4775,8 @@ fn compatCursorInvertFgBg( // Realistically, these fields were mutually exclusive so anyone // relying on that behavior should just upgrade to the new // cursor-color/cursor-text fields. - 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.@"cursor-color" = .@"cell-foreground"; self.@"cursor-text" = .@"cell-background"; } @@ -4775,8 +4793,8 @@ fn compatSelectionInvertFgBg( _ = alloc; assert(std.mem.eql(u8, key, "selection-invert-fg-bg")); - 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.@"selection-foreground" = .@"cell-background"; self.@"selection-background" = .@"cell-foreground"; } @@ -4793,8 +4811,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; } @@ -7261,9 +7279,9 @@ pub const Keybinds = struct { defer arena.deinit(); const alloc = arena.allocator(); - var set: Keybinds = .{}; - try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); - try set.parseCLI(alloc, "shift+a=csi:hello"); + var keyset: Keybinds = .{}; + try keyset.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try keyset.parseCLI(alloc, "shift+a=csi:hello"); } test "formatConfig single" { diff --git a/src/config/ConfigOverrides.zig b/src/config/ConfigOverrides.zig new file mode 100644 index 000000000..325359aef --- /dev/null +++ b/src/config/ConfigOverrides.zig @@ -0,0 +1,95 @@ +//! Wrapper for a Config object that keeps track of which settings have been +//! changed. Settings will be marked as set even if they are set to whatever the +//! default value is for that setting. This allows overrides of a setting from +//! a non-default value to the default value. To remove an override it must be +//! explicitly removed from the set that keeps track of what config entries have +//! been changed. + +const ConfigOverrides = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const configpkg = @import("../config.zig"); +const args = @import("../cli/args.zig"); +const Config = configpkg.Config; +const Key = Config.Key; +const Type = Config.Type; + +const log = std.log.scoped(.config_overrides); + +/// Used to keep track of which settings have been overridden. +isset: std.EnumSet(configpkg.Config.Key), + +/// Storage for the overriding settings. +config: configpkg.Config, + +/// Create a new object that has no config settings overridden. +pub fn init(self: *ConfigOverrides, alloc: Allocator) Allocator.Error!void { + self.* = .{ + .isset = .initEmpty(), + .config = try .default(alloc), + }; +} + +/// Has a config setting been overridden? +pub fn isSet(self: *const ConfigOverrides, comptime key: Key) bool { + return self.isset.contains(key); +} + +/// Set a configuration entry and mark it as having been overridden. +pub fn set(self: *ConfigOverrides, comptime key: Key, value: Type(key)) Allocator.Error!void { + try self.config.set(key, value); + self.isset.insert(key); +} + +/// Mark a configuration entry as having not been overridden. +pub fn unset(self: *ConfigOverrides, comptime key: Key) void { + self.isset.remove(key); +} + +/// Get the value of a configuration entry. +pub fn get(self: *const ConfigOverrides, comptime key: Key) Type(key) { + return self.config.get(key); +} + +/// Parse a string that contains a CLI flag. +pub fn parseCLI(self: *ConfigOverrides, str: []const u8) !void { + const k: []const u8, const v: ?[]const u8 = kv: { + if (!std.mem.startsWith(u8, str, "--")) return; + if (std.mem.indexOfScalarPos(u8, str, 2, '=')) |pos| { + break :kv .{ + std.mem.trim(u8, str[2..pos], &std.ascii.whitespace), + std.mem.trim(u8, str[pos + 1 ..], &std.ascii.whitespace), + }; + } + break :kv .{ std.mem.trim(u8, str[2..], &std.ascii.whitespace), null }; + }; + + const key = std.meta.stringToEnum(Key, k) orelse return; + try args.parseIntoField(Config, self.config.arenaAlloc(), &self.config, k, v); + self.isset.insert(key); +} + +pub fn deinit(self: *ConfigOverrides) callconv(.c) void { + self.config.deinit(); +} + +test "ConfigOverrides" { + const testing = std.testing; + const alloc = testing.allocator; + + var config_overrides: ConfigOverrides = undefined; + try config_overrides.init(alloc); + defer config_overrides.deinit(); + + try testing.expect(config_overrides.isSet(.@"font-size") == false); + try config_overrides.set(.@"font-size", 24.0); + try testing.expect(config_overrides.isSet(.@"font-size") == true); + try testing.expectApproxEqAbs(24.0, config_overrides.get(.@"font-size"), 0.01); + + try testing.expect(config_overrides.isSet(.@"working-directory") == false); + try config_overrides.parseCLI("--working-directory=/home/ghostty"); + try testing.expect(config_overrides.isSet(.@"working-directory") == true); + try testing.expectEqualStrings("/home/ghostty", config_overrides.get(.@"working-directory").?); +} diff --git a/src/config/c_get.zig b/src/config/c_get.zig index dcfdc6716..a3a45d24c 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -4,7 +4,7 @@ const key = @import("key.zig"); const Config = @import("Config.zig"); const Color = Config.Color; const Key = key.Key; -const Value = key.Value; +const Type = key.Type; /// Get a value from the config by key into the given pointer. This is /// specifically for C-compatible APIs. If you're using Zig, just access @@ -17,7 +17,7 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { @setEvalBranchQuota(10_000); switch (k) { inline else => |tag| { - const value = fieldByKey(config, tag); + const value = config.get(tag); return getValue(ptr_raw, value); }, } @@ -102,22 +102,6 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool { return true; } -/// Get a value from the config by key. -fn fieldByKey(self: *const Config, comptime k: Key) Value(k) { - const field = comptime field: { - const fields = std.meta.fields(Config); - for (fields) |field| { - if (@field(Key, field.name) == k) { - break :field field; - } - } - - unreachable; - }; - - return @field(self, field.name); -} - test "c_get: u8" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/config/key.zig b/src/config/key.zig index 5709e2074..6c083673a 100644 --- a/src/config/key.zig +++ b/src/config/key.zig @@ -32,7 +32,7 @@ pub const Key = key: { }; /// Returns the value type for a key -pub fn Value(comptime key: Key) type { +pub fn Type(comptime key: Key) type { const field = comptime field: { @setEvalBranchQuota(100_000); @@ -52,6 +52,6 @@ pub fn Value(comptime key: Key) type { test "Value" { const testing = std.testing; - try testing.expectEqual(Config.RepeatableString, Value(.@"font-family")); - try testing.expectEqual(?bool, Value(.@"cursor-style-blink")); + try testing.expectEqual(Config.RepeatableString, Type(.@"font-family")); + try testing.expectEqual(?bool, Type(.@"cursor-style-blink")); } 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=")); +} From ec0f9ef4163ee8262a31c779a9062c21b7486d5c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Feb 2026 15:50:42 -0600 Subject: [PATCH 2/6] gtk: `+new-window` now respects `--title` --- src/apprt/gtk/class/application.zig | 2 +- src/apprt/gtk/class/window.zig | 115 ++++++++++++++++++++++++---- src/apprt/gtk/ui/1.5/window.blp | 2 +- 3 files changed, 102 insertions(+), 17 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 8b5fa7094..35c572338 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -2215,7 +2215,7 @@ 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); + const win = Window.new(self, config_overrides); initAndShowWindow(self, win, parent, config_overrides); } diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index a71fea111..d56857f28 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -80,6 +80,18 @@ pub const Window = extern struct { ); }; + pub const @"config-overrides" = struct { + pub const name = "config-overrides"; + const impl = gobject.ext.defineProperty( + name, + Self, + ?*ConfigOverrides, + .{ + .accessor = C.privateObjFieldAccessor("config_overrides"), + }, + ); + }; + pub const debug = struct { pub const name = "debug"; const impl = gobject.ext.defineProperty( @@ -232,6 +244,9 @@ pub const Window = extern struct { /// The configuration that this surface is using. config: ?*Config = null, + /// Configuration overrides. + config_overrides: ?*ConfigOverrides = null, + /// State and logic for windowing protocol for a window. winproto: winprotopkg.Window, @@ -267,10 +282,30 @@ 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, config_overrides_: ?*ConfigOverrides) *Self { + const win = gobject.ext.newInstance(Self, .{ .application = app, }); + + const priv: *Private = win.private(); + + if (config_overrides_) |v| { + priv.config_overrides = v.ref(); + const config_overrides = v.get(); + // If the config 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. + if (config_overrides.isSet(.title)) { + const title_ = config_overrides.get(.title); + if (title_) |title| { + win.as(gtk.Window).setTitle(title); + } + } + win.as(gobject.Object).notifyByPspec(properties.@"config-overrides".impl.param_spec); + } + + return win; } fn init(self: *Self, _: *Class) callconv(.c) void { @@ -279,10 +314,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. @@ -306,17 +345,25 @@ 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. + const title_ = title: { + if (priv.config_overrides) |co| { + const config_overrides = co.get(); + if (config_overrides.isSet(.title)) { + break :title config_overrides.get(.title); + } + } + break :title config.title; + }; + if (title_) |title| { + self.as(gtk.Window).setTitle(title); } // We always sync our appearance at the end because loading our @@ -1151,6 +1198,37 @@ pub const Window = extern struct { }); } + fn closureTitle( + _: *Self, + config_: ?*Config, + config_overrides_: ?*ConfigOverrides, + title_: ?[*:0]const u8, + ) callconv(.c) ?[*:0]const u8 { + config: { + if (config_overrides_) |v| { + const config_overrides = v.get(); + if (config_overrides.isSet(.title)) { + if (config_overrides.get(.title)) |title| { + return glib.ext.dupeZ(u8, title); + } + // The `title` has explicitly been set to `null`, skip + // checking the normal config for it's title setting. + break :config; + } + } + if (config_) |v| { + const config = v.get(); + if (config.title) |title| { + return glib.ext.dupeZ(u8, title); + } + } + } + if (title_) |title| { + return glib.ext.dupeZ(u8, std.mem.span(title)); + } + return null; + } + fn closureSubtitle( _: *Self, config_: ?*Config, @@ -1179,6 +1257,11 @@ pub const Window = extern struct { priv.config = null; } + if (priv.config_overrides) |v| { + v.unref(); + priv.config_overrides = null; + } + priv.tab_bindings.setSource(null); gtk.Widget.disposeTemplate( @@ -2019,6 +2102,7 @@ pub const Window = extern struct { gobject.ext.registerProperties(class, &.{ properties.@"active-surface".impl, properties.config.impl, + properties.@"config-overrides".impl, properties.debug.impl, properties.@"headerbar-visible".impl, properties.@"quick-terminal".impl, @@ -2057,6 +2141,7 @@ pub const Window = extern struct { class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal); class.bindTemplateCallback("notify_scale_factor", &propScaleFactor); class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab); + class.bindTemplateCallback("computed_title", &closureTitle); class.bindTemplateCallback("computed_subtitle", &closureSubtitle); // Virtual methods diff --git a/src/apprt/gtk/ui/1.5/window.blp b/src/apprt/gtk/ui/1.5/window.blp index b66a93093..514826b23 100644 --- a/src/apprt/gtk/ui/1.5/window.blp +++ b/src/apprt/gtk/ui/1.5/window.blp @@ -40,7 +40,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { visible: bind template.headerbar-visible; title-widget: Adw.WindowTitle { - title: bind template.title; + title: bind $computed_title(template.config, template.config-overrides, template.title) as ; // Blueprint auto-formatter won't let me split this into multiple // lines. Let me explain myself. All parameters to a closure are used // as notifications to recompute the value of the closure. All From f2ce7c348edbd635dd74cae9b3b330825768ba76 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 19 Feb 2026 15:56:02 -0600 Subject: [PATCH 3/6] gtk: `+new-window` document `--title` --- src/cli/new_window.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 98a93beb2..845a509a2 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -100,10 +100,10 @@ pub const Options = struct { /// `--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` and `--command` 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. +/// `--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 From 002a6cc76526240b19cee9792a79de05077bb09a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 4 Mar 2026 00:00:03 -0600 Subject: [PATCH 4/6] gtk: use simpler method for passing overrides around As discussed in Discord, this commit drops the `ConfigOverride` object in favor of a simpler method of passing the overrides around. Completely avoiding changes to the core wasn't possible but it's very minimal now. --- src/Surface.zig | 24 ++-- src/apprt/embedded.zig | 1 + src/apprt/gtk/Surface.zig | 4 - src/apprt/gtk/class/application.zig | 144 ++++++++++++++++------- src/apprt/gtk/class/config_overrides.zig | 94 --------------- src/apprt/gtk/class/split_tree.zig | 18 ++- src/apprt/gtk/class/surface.zig | 69 +++++------ src/apprt/gtk/class/tab.zig | 16 ++- src/apprt/gtk/class/window.zig | 138 +++++++++------------- src/apprt/gtk/ui/1.5/window.blp | 2 +- src/cli/new_window.zig | 58 +++++---- src/config.zig | 1 - src/config/Config.zig | 32 ++--- src/config/ConfigOverrides.zig | 95 --------------- src/config/c_get.zig | 20 +++- src/config/command.zig | 10 ++ src/config/key.zig | 6 +- 17 files changed, 300 insertions(+), 432 deletions(-) delete mode 100644 src/apprt/gtk/class/config_overrides.zig delete mode 100644 src/config/ConfigOverrides.zig diff --git a/src/Surface.zig b/src/Surface.zig index 9f32b087f..c13a29c4e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -30,7 +30,6 @@ const font = @import("font/main.zig"); const Command = @import("Command.zig"); const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); -const ConfigOverrides = configpkg.ConfigOverrides; const Duration = configpkg.Config.Duration; const input = @import("input.zig"); const App = @import("App.zig"); @@ -464,6 +463,12 @@ pub fn init( app: *App, rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, + overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }, ) !void { // Apply our conditional state. If we fail to apply the conditional state // then we log and attempt to move forward with the old config. @@ -609,9 +614,8 @@ pub fn init( // The command we're going to execute const command: ?configpkg.Command = command: { - if (self.getConfigOverrides()) |config_overrides| { - if (config_overrides.isSet(.command)) - break :command config_overrides.get(.command); + if (overrides.command) |command| { + break :command command; } if (app.first) { if (config.@"initial-command") |command| { @@ -623,9 +627,8 @@ pub fn init( // The working directory to execute the command in. const working_directory: ?[]const u8 = wd: { - if (self.getConfigOverrides()) |config_overrides| { - if (config_overrides.isSet(.@"working-directory")) - break :wd config_overrides.get(.@"working-directory"); + if (overrides.working_directory) |working_directory| { + break :wd working_directory; } break :wd config.@"working-directory"; }; @@ -1807,13 +1810,6 @@ pub fn updateConfig( ); } -fn getConfigOverrides(self: *Surface) ?*const ConfigOverrides { - if (@hasDecl(apprt.runtime.Surface, "getConfigOverrides")) { - return self.rt_surface.getConfigOverrides(); - } - return null; -} - const InitialSizeError = error{ ContentScaleUnavailable, AppActionFailed, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 54d5472c6..810334aff 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -572,6 +572,7 @@ pub const Surface = struct { app.core_app, app, self, + .none, ); errdefer self.core_surface.deinit(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index f7a563d14..715973671 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -102,7 +102,3 @@ pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap { pub fn redrawInspector(self: *Self) void { self.surface.redrawInspector(); } - -pub fn getConfigOverrides(self: *Self) ?*const configpkg.ConfigOverrides { - return self.gobj().getConfigOverrides(); -} diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 35c572338..55b392f76 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"); @@ -32,7 +33,6 @@ const ApprtApp = @import("../App.zig"); const Common = @import("../class.zig").Common; const WeakRef = @import("../weak_ref.zig").WeakRef; const Config = @import("config.zig").Config; -const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Surface = @import("surface.zig").Surface; const SplitTree = @import("split_tree.zig").SplitTree; const Window = @import("window.zig").Window; @@ -710,7 +710,7 @@ pub const Application = extern struct { .app => null, .surface => |v| v, }, - null, + .none, ), .open_config => return Action.openConfig(self), @@ -1671,18 +1671,26 @@ pub const Application = extern struct { ) callconv(.c) void { log.debug("received new window action", .{}); - const config_overrides: ?*ConfigOverrides = config_overrides: { + const alloc = Application.default().allocator(); + + var working_directory: ?[:0]const u8 = null; + defer if (working_directory) |wd| alloc.free(wd); + + var title: ?[:0]const u8 = null; + defer if (title) |t| alloc.free(t); + + var command: ?configpkg.Command = null; + defer if (command) |c| c.deinit(alloc); + + var args: std.ArrayList([:0]const u8) = .empty; + defer { + for (args.items) |arg| alloc.free(arg); + args.deinit(alloc); + } + + overrides: { // were we given a parameter? - const parameter = parameter_ orelse break :config_overrides null; - - const alloc = Application.default().allocator(); - const config_overrides = ConfigOverrides.new(alloc) catch |err| { - log.warn("unable to create new config overrides: {t}", .{err}); - break :config_overrides null; - }; - errdefer config_overrides.unref(); - - const co = config_overrides.get(); + const parameter = parameter_ orelse break :overrides; const as_variant_type = glib.VariantType.new("as"); defer as_variant_type.free(); @@ -1693,7 +1701,7 @@ pub const Application = extern struct { parameter.getTypeString(), as_variant_type.peekString()[0..as_variant_type.getStringLength()], }); - break :config_overrides null; + break :overrides; } const s_variant_type = glib.VariantType.new("s"); @@ -1702,12 +1710,6 @@ pub const Application = extern struct { var it: glib.VariantIter = undefined; _ = it.init(parameter); - var args: std.ArrayList([:0]const u8) = .empty; - defer { - for (args.items) |arg| alloc.free(arg); - args.deinit(alloc); - } - var e_seen: bool = false; var i: usize = 0; @@ -1721,15 +1723,17 @@ pub const Application = extern struct { const buf = value.getString(&len); const str = buf[0..len]; + 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 :config_overrides null; + break :overrides; }; errdefer alloc.free(cpy); args.append(alloc, cpy) catch |err| { log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err }); - break :config_overrides null; + break :overrides; }; continue; } @@ -1739,27 +1743,52 @@ pub const Application = extern struct { continue; } - co.parseCLI(str) catch |err| { - log.warn("unable to parse argument {d} {s}: {t}", .{ i, str, err }); + if (lib.cutPrefix(u8, str, "--command=")) |v| { + if (command) |c| c.deinit(alloc); + var cmd: configpkg.Command = undefined; + cmd.parseCLI(alloc, v) catch |err| { + log.warn("unable to parse command: {t}", .{err}); + continue; + }; + command = cmd; continue; - }; - - log.debug("new-window argument: {d} {s}", .{ i, str }); + } + if (lib.cutPrefix(u8, str, "--working-directory=")) |v| { + if (working_directory) |wd| alloc.free(wd); + 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| { + if (title) |t| alloc.free(t); + 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; + } } + } - if (args.items.len > 0) { - co.set(.command, .{ .direct = args.items }) catch |err| { - log.warn("unable to set command on config overrides: {t}", .{err}); - break :config_overrides null; - }; - } + if (args.items.len > 0) direct: { + if (command) |c| c.deinit(alloc); + command = .{ + .direct = args.toOwnedSlice(alloc) catch |err| { + log.warn("unable to convert list of arguments to owned slice: {t}", .{err}); + break :direct; + }, + }; + } - break :config_overrides config_overrides; + Action.newWindow(self, null, .{ + .command = command, + .working_directory = working_directory, + .title = title, + }) catch |err| { + log.warn("unable to create new window: {t}", .{err}); }; - - defer if (config_overrides) |v| v.unref(); - - Action.newWindow(self, null, config_overrides) catch {}; } pub fn actionOpenConfig( @@ -2206,7 +2235,13 @@ const Action = struct { pub fn newWindow( self: *Application, parent: ?*CoreSurface, - config_overrides: ?*ConfigOverrides, + 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 @@ -2215,15 +2250,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, config_overrides); - initAndShowWindow(self, win, parent, config_overrides); + 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, - config_overrides: ?*ConfigOverrides, + 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 @@ -2237,7 +2289,11 @@ const Action = struct { ); // Create a new tab with window context (first tab in new window) - win.newTabForWindow(parent, config_overrides); + 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. @@ -2563,7 +2619,7 @@ const Action = struct { .@"quick-terminal" = true, }); assert(win.isQuickTerminal()); - initAndShowWindow(self, win, null, null); + initAndShowWindow(self, win, null, .none); return true; } diff --git a/src/apprt/gtk/class/config_overrides.zig b/src/apprt/gtk/class/config_overrides.zig deleted file mode 100644 index 6ca6ea77e..000000000 --- a/src/apprt/gtk/class/config_overrides.zig +++ /dev/null @@ -1,94 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const gobject = @import("gobject"); - -const configpkg = @import("../../../config.zig"); -const Config = configpkg.Config; - -const Common = @import("../class.zig").Common; - -const log = std.log.scoped(.gtk_ghostty_config_overrides); - -/// Wrapper for a ConfigOverrides object that keeps track of which settings have -/// been changed. -pub const ConfigOverrides = extern struct { - const Self = @This(); - parent_instance: Parent, - pub const Parent = gobject.Object; - pub const getGObjectType = gobject.ext.defineClass(Self, .{ - .name = "GhosttyConfigOverrides", - .classInit = &Class.init, - .parent_class = &Class.parent, - .private = .{ .Type = Private, .offset = &Private.offset }, - }); - - pub const properties = struct {}; - - const Private = struct { - config_overrides: configpkg.ConfigOverrides, - - pub var offset: c_int = 0; - }; - - pub fn new(alloc: Allocator) Allocator.Error!*ConfigOverrides { - const self = gobject.ext.newInstance(Self, .{}); - errdefer self.unref(); - - const priv: *Private = self.private(); - try priv.config_overrides.init(alloc); - - return self; - } - - pub fn get(self: *ConfigOverrides) *configpkg.ConfigOverrides { - const priv: *Private = self.private(); - return &priv.config_overrides; - } - - fn finalize(self: *ConfigOverrides) callconv(.c) void { - const priv: *Private = self.private(); - priv.config_overrides.deinit(); - - gobject.Object.virtual_methods.finalize.call( - Class.parent, - self.as(Parent), - ); - } - - const C = Common(Self, Private); - pub const as = C.as; - pub const ref = C.ref; - pub const unref = C.unref; - const private = C.private; - - pub const Class = extern struct { - parent_class: Parent.Class, - var parent: *Parent.Class = undefined; - pub const Instance = Self; - - fn init(class: *Class) callconv(.c) void { - gobject.Object.virtual_methods.finalize.implement(class, &finalize); - } - }; -}; - -test "GhosttyConfigOverrides" { - const testing = std.testing; - const alloc = testing.allocator; - - const config_overrides: *ConfigOverrides = try .new(alloc); - defer config_overrides.unref(); - - const co = config_overrides.get(); - - try testing.expect(co.isSet(.@"font-size") == false); - try co.set(.@"font-size", 24.0); - try testing.expect(co.isSet(.@"font-size") == true); - try testing.expectApproxEqAbs(24.0, co.get(.@"font-size"), 0.01); - - try testing.expect(co.isSet(.@"working-directory") == false); - try co.parseCLI("--working-directory=/home/ghostty"); - try testing.expect(co.isSet(.@"working-directory") == true); - try testing.expectEqualStrings("/home/ghostty", co.get(.@"working-directory").?); -} diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 0d47db958..067546c44 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"); @@ -16,7 +17,6 @@ const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const Surface = @import("surface.zig").Surface; const SurfaceScrolledWindow = @import("surface_scrolled_window.zig").SurfaceScrolledWindow; -const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const log = std.log.scoped(.gtk_ghostty_split_tree); @@ -209,12 +209,22 @@ pub const SplitTree = extern struct { self: *Self, direction: Surface.Tree.Split.Direction, parent_: ?*Surface, - config_overrides: ?*ConfigOverrides, + 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(config_overrides); + const surface: *Surface = .new(.{ + .command = overrides.command, + .working_directory = overrides.working_directory, + .title = overrides.title, + }); defer surface.unref(); _ = surface.refSink(); @@ -640,7 +650,7 @@ pub const SplitTree = extern struct { self.newSplit( direction, self.getActiveSurface(), - null, + .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 423b5b7e8..64fdbe842 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -26,7 +26,6 @@ const ApprtSurface = @import("../Surface.zig"); const Common = @import("../class.zig").Common; const Application = @import("application.zig").Application; const Config = @import("config.zig").Config; -const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay; const SearchOverlay = @import("search_overlay.zig").SearchOverlay; const KeyStateOverlay = @import("key_state_overlay.zig").KeyStateOverlay; @@ -91,18 +90,6 @@ pub const Surface = extern struct { ); }; - pub const @"config-overrides" = struct { - pub const name = "config-overrides"; - const impl = gobject.ext.defineProperty( - name, - Self, - ?*ConfigOverrides, - .{ - .accessor = C.privateObjFieldAccessor("config_overrides"), - }, - ); - }; - pub const @"child-exited" = struct { pub const name = "child-exited"; const impl = gobject.ext.defineProperty( @@ -565,9 +552,6 @@ pub const Surface = extern struct { /// The configuration that this surface is using. config: ?*Config = null, - /// Any configuration overrides that might apply to this surface. - config_overrides: ?*ConfigOverrides = null, - /// The default size for a window that embeds this surface. default_size: ?*Size = null, @@ -721,13 +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(config_overrides: ?*ConfigOverrides) *Self { - return gobject.ext.newInstance(Self, .{ - .@"config-overrides" = config_overrides, + 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 { @@ -1817,11 +1821,6 @@ pub const Surface = extern struct { priv.config = null; } - if (priv.config_overrides) |v| { - v.unref(); - priv.config_overrides = null; - } - if (priv.vadj_signal_group) |group| { group.setTarget(null); group.as(gobject.Object).unref(); @@ -1877,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. @@ -1890,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; @@ -1923,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); @@ -2200,12 +2206,6 @@ pub const Surface = extern struct { self.private().search_overlay.setSearchSelected(selected); } - pub fn getConfigOverrides(self: *Self) ?*const configpkg.ConfigOverrides { - const priv: *Private = self.private(); - const config_overrides = priv.config_overrides orelse return null; - return config_overrides.get(); - } - fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -3387,6 +3387,10 @@ pub const Surface = extern struct { app.core(), app.rt(), &priv.rt_surface, + .{ + .command = priv.overrides.command, + .working_directory = priv.overrides.working_directory, + }, ) catch |err| { log.warn("failed to initialize surface err={}", .{err}); return error.SurfaceError; @@ -3608,7 +3612,6 @@ pub const Surface = extern struct { gobject.ext.registerProperties(class, &.{ properties.@"bell-ringing".impl, properties.config.impl, - properties.@"config-overrides".impl, properties.@"child-exited".impl, properties.@"default-size".impl, properties.@"error".impl, diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index d45513e08..0c60c8ccc 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -5,13 +5,13 @@ 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"); const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; -const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Application = @import("application.zig").Application; const SplitTree = @import("split_tree.zig").SplitTree; const Surface = @import("surface.zig").Surface; @@ -187,7 +187,13 @@ pub const Tab = extern struct { } } - pub fn new(config: ?*Config, config_overrides: ?*ConfigOverrides) *Self { + pub fn new(config: ?*Config, overrides: struct { + command: ?configpkg.Command = null, + working_directory: ?[:0]const u8 = null, + title: ?[:0]const u8 = null, + + pub const none: @This() = .{}; + }) *Self { const tab = gobject.ext.newInstance(Tab, .{}); const priv: *Private = tab.private(); @@ -204,7 +210,11 @@ pub const Tab = extern struct { tab.as(gobject.Object).notifyByPspec(properties.config.impl.param_spec); // Create our initial surface in the split tree. - priv.split_tree.newSplit(.right, null, config_overrides) 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" diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index d56857f28..c01cad618 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -21,7 +21,6 @@ const gresource = @import("../build/gresource.zig"); const winprotopkg = @import("../winproto.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; -const ConfigOverrides = @import("config_overrides.zig").ConfigOverrides; const Application = @import("application.zig").Application; const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const SplitTree = @import("split_tree.zig").SplitTree; @@ -80,18 +79,6 @@ pub const Window = extern struct { ); }; - pub const @"config-overrides" = struct { - pub const name = "config-overrides"; - const impl = gobject.ext.defineProperty( - name, - Self, - ?*ConfigOverrides, - .{ - .accessor = C.privateObjFieldAccessor("config_overrides"), - }, - ); - }; - pub const debug = struct { pub const name = "debug"; const impl = gobject.ext.defineProperty( @@ -244,9 +231,6 @@ pub const Window = extern struct { /// The configuration that this surface is using. config: ?*Config = null, - /// Configuration overrides. - config_overrides: ?*ConfigOverrides = null, - /// State and logic for windowing protocol for a window. winproto: winprotopkg.Window, @@ -282,27 +266,24 @@ pub const Window = extern struct { pub var offset: c_int = 0; }; - pub fn new(app: *Application, config_overrides_: ?*ConfigOverrides) *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, }); - const priv: *Private = win.private(); - - if (config_overrides_) |v| { - priv.config_overrides = v.ref(); - const config_overrides = v.get(); - // If the config overrides have a title set, we set that immediately + 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. - if (config_overrides.isSet(.title)) { - const title_ = config_overrides.get(.title); - if (title_) |title| { - win.as(gtk.Window).setTitle(title); - } - } - win.as(gobject.Object).notifyByPspec(properties.@"config-overrides".impl.param_spec); + win.as(gtk.Window).setTitle(title); } return win; @@ -353,16 +334,7 @@ pub const Window = extern struct { // an immediate title set when the window appears, rather than // waiting possibly a few event loop ticks for it to sync from // the surface. - const title_ = title: { - if (priv.config_overrides) |co| { - const config_overrides = co.get(); - if (config_overrides.isSet(.title)) { - break :title config_overrides.get(.title); - } - } - break :title config.title; - }; - if (title_) |title| { + if (config.title) |title| { self.as(gtk.Window).setTitle(title); } @@ -416,19 +388,55 @@ 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, null); + _ = self.newTabPage(parent_, .tab, .none); } - pub fn newTabForWindow(self: *Self, parent_: ?*CoreSurface, config_overrides: ?*ConfigOverrides) void { - _ = self.newTabPage(parent_, .window, config_overrides); + 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, config_overrides: ?*ConfigOverrides) *adw.TabPage { + 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 = Tab.new(priv.config, config_overrides); + 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. @@ -1198,37 +1206,6 @@ pub const Window = extern struct { }); } - fn closureTitle( - _: *Self, - config_: ?*Config, - config_overrides_: ?*ConfigOverrides, - title_: ?[*:0]const u8, - ) callconv(.c) ?[*:0]const u8 { - config: { - if (config_overrides_) |v| { - const config_overrides = v.get(); - if (config_overrides.isSet(.title)) { - if (config_overrides.get(.title)) |title| { - return glib.ext.dupeZ(u8, title); - } - // The `title` has explicitly been set to `null`, skip - // checking the normal config for it's title setting. - break :config; - } - } - if (config_) |v| { - const config = v.get(); - if (config.title) |title| { - return glib.ext.dupeZ(u8, title); - } - } - } - if (title_) |title| { - return glib.ext.dupeZ(u8, std.mem.span(title)); - } - return null; - } - fn closureSubtitle( _: *Self, config_: ?*Config, @@ -1257,11 +1234,6 @@ pub const Window = extern struct { priv.config = null; } - if (priv.config_overrides) |v| { - v.unref(); - priv.config_overrides = null; - } - priv.tab_bindings.setSource(null); gtk.Widget.disposeTemplate( @@ -1336,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, null); + return self.newTabPage(if (self.getActiveSurface()) |v| v.core() else null, .tab, .none); } fn tabOverviewOpen( @@ -2102,7 +2074,6 @@ pub const Window = extern struct { gobject.ext.registerProperties(class, &.{ properties.@"active-surface".impl, properties.config.impl, - properties.@"config-overrides".impl, properties.debug.impl, properties.@"headerbar-visible".impl, properties.@"quick-terminal".impl, @@ -2141,7 +2112,6 @@ pub const Window = extern struct { class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal); class.bindTemplateCallback("notify_scale_factor", &propScaleFactor); class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab); - class.bindTemplateCallback("computed_title", &closureTitle); class.bindTemplateCallback("computed_subtitle", &closureSubtitle); // Virtual methods diff --git a/src/apprt/gtk/ui/1.5/window.blp b/src/apprt/gtk/ui/1.5/window.blp index 514826b23..b66a93093 100644 --- a/src/apprt/gtk/ui/1.5/window.blp +++ b/src/apprt/gtk/ui/1.5/window.blp @@ -40,7 +40,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { visible: bind template.headerbar-visible; title-widget: Adw.WindowTitle { - title: bind $computed_title(template.config, template.config-overrides, template.title) as ; + title: bind template.title; // Blueprint auto-formatter won't let me split this into multiple // lines. Let me explain myself. All parameters to a closure are used // as notifications to recompute the value of the closure. All diff --git a/src/cli/new_window.zig b/src/cli/new_window.zig index 845a509a2..12acafadf 100644 --- a/src/cli/new_window.zig +++ b/src/cli/new_window.zig @@ -15,9 +15,12 @@ pub const Options = struct { /// If set, open up a new window in a custom instance of Ghostty. class: ?[: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: ?[][:0]const u8 = null, + _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. @@ -25,32 +28,25 @@ pub const Options = struct { /// 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 arguments: std.ArrayList([:0]const u8) = .empty; - errdefer { - for (arguments.items) |argument| alloc.free(argument); - arguments.deinit(alloc); - } - var e_seen: bool = std.mem.eql(u8, arg, "-e"); + // Include the argument that triggered the manual parse hook. - if (try self.checkArg(alloc, arg)) |a| try arguments.append(alloc, a); + if (try self.checkArg(alloc, arg)) |a| try self._arguments.append(alloc, a); // Gather up the rest of the arguments to use as the command. while (iter.next()) |param| { if (e_seen) { - try arguments.append(alloc, try alloc.dupeZ(u8, param)); + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); continue; } if (std.mem.eql(u8, param, "-e")) { e_seen = true; - try arguments.append(alloc, try alloc.dupeZ(u8, param)); + try self._arguments.append(alloc, try alloc.dupeZ(u8, param)); continue; } - if (try self.checkArg(alloc, param)) |a| try arguments.append(alloc, a); + if (try self.checkArg(alloc, param)) |a| try self._arguments.append(alloc, a); } - self._arguments = try arguments.toOwnedSlice(alloc); - return false; } @@ -62,13 +58,14 @@ pub const Options = struct { 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 error.InvalidValue; - if (std.mem.eql(u8, stripped, "inherit")) return error.InvalidValue; + 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); } @@ -108,9 +105,11 @@ pub const Options = struct { /// 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. The special values `home` and `inherit` that are -/// available as "normal" CLI flags or configuration entries do not work when -/// used from the `+new-window` CLI action. +/// 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 @@ -135,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 { @@ -186,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); @@ -202,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.zig b/src/config.zig index d559ab171..0bf61a47f 100644 --- a/src/config.zig +++ b/src/config.zig @@ -3,7 +3,6 @@ const builtin = @import("builtin"); const file_load = @import("config/file_load.zig"); const formatter = @import("config/formatter.zig"); pub const Config = @import("config/Config.zig"); -pub const ConfigOverrides = @import("config/ConfigOverrides.zig"); pub const conditional = @import("config/conditional.zig"); pub const io = @import("config/io.zig"); pub const string = @import("config/string.zig"); diff --git a/src/config/Config.zig b/src/config/Config.zig index ce891561c..ca93c85d6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -30,7 +30,6 @@ const formatterpkg = @import("formatter.zig"); const themepkg = @import("theme.zig"); const url = @import("url.zig"); pub const Key = @import("key.zig").Key; -pub const Type = @import("key.zig").Type; const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); pub const Command = @import("command.zig").Command; @@ -96,23 +95,6 @@ pub const compatibility = std.StaticStringMap( .{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior }, }); -pub fn get(self: *const Config, comptime key: Key) Type(key) { - return @field(self, @tagName(key)); -} - -pub fn set(self: *Config, comptime key: Key, value: Type(key)) Allocator.Error!void { - const alloc = self.arenaAlloc(); - @field(self.*, @tagName(key)) = try cloneValue(alloc, Type(key), value); -} - -test "set/get" { - var config: Config = try .default(std.testing.allocator); - defer config.deinit(); - try std.testing.expect(config.get(.language) == null); - try config.set(.language, "en_US.UTF-8"); - try std.testing.expectEqualStrings("en_US.UTF-8", config.get(.language).?); -} - /// Set Ghostty's graphical user interface language to a language other than the /// system default language. For example: /// @@ -4775,8 +4757,8 @@ fn compatCursorInvertFgBg( // Realistically, these fields were mutually exclusive so anyone // relying on that behavior should just upgrade to the new // cursor-color/cursor-text fields. - const isset = cli.args.parseBool(value_ orelse "t") catch return false; - if (isset) { + const set = cli.args.parseBool(value_ orelse "t") catch return false; + if (set) { self.@"cursor-color" = .@"cell-foreground"; self.@"cursor-text" = .@"cell-background"; } @@ -4793,8 +4775,8 @@ fn compatSelectionInvertFgBg( _ = alloc; assert(std.mem.eql(u8, key, "selection-invert-fg-bg")); - const isset = cli.args.parseBool(value_ orelse "t") catch return false; - if (isset) { + const set = cli.args.parseBool(value_ orelse "t") catch return false; + if (set) { self.@"selection-foreground" = .@"cell-background"; self.@"selection-background" = .@"cell-foreground"; } @@ -7279,9 +7261,9 @@ pub const Keybinds = struct { defer arena.deinit(); const alloc = arena.allocator(); - var keyset: Keybinds = .{}; - try keyset.parseCLI(alloc, "shift+a=copy_to_clipboard"); - try keyset.parseCLI(alloc, "shift+a=csi:hello"); + var set: Keybinds = .{}; + try set.parseCLI(alloc, "shift+a=copy_to_clipboard"); + try set.parseCLI(alloc, "shift+a=csi:hello"); } test "formatConfig single" { diff --git a/src/config/ConfigOverrides.zig b/src/config/ConfigOverrides.zig deleted file mode 100644 index 325359aef..000000000 --- a/src/config/ConfigOverrides.zig +++ /dev/null @@ -1,95 +0,0 @@ -//! Wrapper for a Config object that keeps track of which settings have been -//! changed. Settings will be marked as set even if they are set to whatever the -//! default value is for that setting. This allows overrides of a setting from -//! a non-default value to the default value. To remove an override it must be -//! explicitly removed from the set that keeps track of what config entries have -//! been changed. - -const ConfigOverrides = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const configpkg = @import("../config.zig"); -const args = @import("../cli/args.zig"); -const Config = configpkg.Config; -const Key = Config.Key; -const Type = Config.Type; - -const log = std.log.scoped(.config_overrides); - -/// Used to keep track of which settings have been overridden. -isset: std.EnumSet(configpkg.Config.Key), - -/// Storage for the overriding settings. -config: configpkg.Config, - -/// Create a new object that has no config settings overridden. -pub fn init(self: *ConfigOverrides, alloc: Allocator) Allocator.Error!void { - self.* = .{ - .isset = .initEmpty(), - .config = try .default(alloc), - }; -} - -/// Has a config setting been overridden? -pub fn isSet(self: *const ConfigOverrides, comptime key: Key) bool { - return self.isset.contains(key); -} - -/// Set a configuration entry and mark it as having been overridden. -pub fn set(self: *ConfigOverrides, comptime key: Key, value: Type(key)) Allocator.Error!void { - try self.config.set(key, value); - self.isset.insert(key); -} - -/// Mark a configuration entry as having not been overridden. -pub fn unset(self: *ConfigOverrides, comptime key: Key) void { - self.isset.remove(key); -} - -/// Get the value of a configuration entry. -pub fn get(self: *const ConfigOverrides, comptime key: Key) Type(key) { - return self.config.get(key); -} - -/// Parse a string that contains a CLI flag. -pub fn parseCLI(self: *ConfigOverrides, str: []const u8) !void { - const k: []const u8, const v: ?[]const u8 = kv: { - if (!std.mem.startsWith(u8, str, "--")) return; - if (std.mem.indexOfScalarPos(u8, str, 2, '=')) |pos| { - break :kv .{ - std.mem.trim(u8, str[2..pos], &std.ascii.whitespace), - std.mem.trim(u8, str[pos + 1 ..], &std.ascii.whitespace), - }; - } - break :kv .{ std.mem.trim(u8, str[2..], &std.ascii.whitespace), null }; - }; - - const key = std.meta.stringToEnum(Key, k) orelse return; - try args.parseIntoField(Config, self.config.arenaAlloc(), &self.config, k, v); - self.isset.insert(key); -} - -pub fn deinit(self: *ConfigOverrides) callconv(.c) void { - self.config.deinit(); -} - -test "ConfigOverrides" { - const testing = std.testing; - const alloc = testing.allocator; - - var config_overrides: ConfigOverrides = undefined; - try config_overrides.init(alloc); - defer config_overrides.deinit(); - - try testing.expect(config_overrides.isSet(.@"font-size") == false); - try config_overrides.set(.@"font-size", 24.0); - try testing.expect(config_overrides.isSet(.@"font-size") == true); - try testing.expectApproxEqAbs(24.0, config_overrides.get(.@"font-size"), 0.01); - - try testing.expect(config_overrides.isSet(.@"working-directory") == false); - try config_overrides.parseCLI("--working-directory=/home/ghostty"); - try testing.expect(config_overrides.isSet(.@"working-directory") == true); - try testing.expectEqualStrings("/home/ghostty", config_overrides.get(.@"working-directory").?); -} diff --git a/src/config/c_get.zig b/src/config/c_get.zig index a3a45d24c..dcfdc6716 100644 --- a/src/config/c_get.zig +++ b/src/config/c_get.zig @@ -4,7 +4,7 @@ const key = @import("key.zig"); const Config = @import("Config.zig"); const Color = Config.Color; const Key = key.Key; -const Type = key.Type; +const Value = key.Value; /// Get a value from the config by key into the given pointer. This is /// specifically for C-compatible APIs. If you're using Zig, just access @@ -17,7 +17,7 @@ pub fn get(config: *const Config, k: Key, ptr_raw: *anyopaque) bool { @setEvalBranchQuota(10_000); switch (k) { inline else => |tag| { - const value = config.get(tag); + const value = fieldByKey(config, tag); return getValue(ptr_raw, value); }, } @@ -102,6 +102,22 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool { return true; } +/// Get a value from the config by key. +fn fieldByKey(self: *const Config, comptime k: Key) Value(k) { + const field = comptime field: { + const fields = std.meta.fields(Config); + for (fields) |field| { + if (@field(Key, field.name) == k) { + break :field field; + } + } + + unreachable; + }; + + return @field(self, field.name); +} + test "c_get: u8" { const testing = std.testing; const alloc = testing.allocator; 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/config/key.zig b/src/config/key.zig index 6c083673a..5709e2074 100644 --- a/src/config/key.zig +++ b/src/config/key.zig @@ -32,7 +32,7 @@ pub const Key = key: { }; /// Returns the value type for a key -pub fn Type(comptime key: Key) type { +pub fn Value(comptime key: Key) type { const field = comptime field: { @setEvalBranchQuota(100_000); @@ -52,6 +52,6 @@ pub fn Type(comptime key: Key) type { test "Value" { const testing = std.testing; - try testing.expectEqual(Config.RepeatableString, Type(.@"font-family")); - try testing.expectEqual(?bool, Type(.@"cursor-style-blink")); + try testing.expectEqual(Config.RepeatableString, Value(.@"font-family")); + try testing.expectEqual(?bool, Value(.@"cursor-style-blink")); } From e27956fdde1b3964d689f8f0c038b29f6e7d5157 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 4 Mar 2026 14:03:38 -0600 Subject: [PATCH 5/6] gtk: remove modifications to the core for overrides --- src/Surface.zig | 19 +------------------ src/apprt/embedded.zig | 1 - src/apprt/gtk/class/surface.zig | 13 ++++++++----- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index c13a29c4e..a3691b53e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -463,12 +463,6 @@ pub fn init( app: *App, rt_app: *apprt.runtime.App, rt_surface: *apprt.runtime.Surface, - overrides: struct { - command: ?configpkg.Command = null, - working_directory: ?[:0]const u8 = null, - - pub const none: @This() = .{}; - }, ) !void { // Apply our conditional state. If we fail to apply the conditional state // then we log and attempt to move forward with the old config. @@ -614,9 +608,6 @@ pub fn init( // The command we're going to execute const command: ?configpkg.Command = command: { - if (overrides.command) |command| { - break :command command; - } if (app.first) { if (config.@"initial-command") |command| { break :command command; @@ -625,14 +616,6 @@ pub fn init( break :command config.command; }; - // The working directory to execute the command in. - const working_directory: ?[]const u8 = wd: { - if (overrides.working_directory) |working_directory| { - break :wd working_directory; - } - break :wd config.@"working-directory"; - }; - // Start our IO implementation // This separate block ({}) is important because our errdefers must // be scoped here to be valid. @@ -656,7 +639,7 @@ pub fn init( .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", .cursor_blink = config.@"cursor-style-blink", - .working_directory = working_directory, + .working_directory = config.@"working-directory", .resources_dir = global_state.resources_dir.host(), .term = config.term, .rt_pre_exec_info = .init(config), diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 810334aff..54d5472c6 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -572,7 +572,6 @@ pub const Surface = struct { app.core_app, app, self, - .none, ); errdefer self.core_surface.deinit(); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 64fdbe842..8d9e1bcf0 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -3343,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; @@ -3376,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; @@ -3387,10 +3394,6 @@ pub const Surface = extern struct { app.core(), app.rt(), &priv.rt_surface, - .{ - .command = priv.overrides.command, - .working_directory = priv.overrides.working_directory, - }, ) catch |err| { log.warn("failed to initialize surface err={}", .{err}); return error.SurfaceError; From 5bc5820f3255cc8dfbf6c30e3f7edb4a947add3d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 4 Mar 2026 16:01:12 -0600 Subject: [PATCH 6/6] gtk: simplify new-window action memory management with an arena --- src/apprt/gtk/class/application.zig | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 55b392f76..c3ff51e0f 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -1671,22 +1671,15 @@ pub const Application = extern struct { ) callconv(.c) void { log.debug("received new window action", .{}); - const alloc = Application.default().allocator(); + var arena: std.heap.ArenaAllocator = .init(Application.default().allocator()); + defer arena.deinit(); + + const alloc = arena.allocator(); var working_directory: ?[:0]const u8 = null; - defer if (working_directory) |wd| alloc.free(wd); - var title: ?[:0]const u8 = null; - defer if (title) |t| alloc.free(t); - var command: ?configpkg.Command = null; - defer if (command) |c| c.deinit(alloc); - var args: std.ArrayList([:0]const u8) = .empty; - defer { - for (args.items) |arg| alloc.free(arg); - args.deinit(alloc); - } overrides: { // were we given a parameter? @@ -1730,7 +1723,6 @@ pub const Application = extern struct { log.warn("unable to duplicate argument {d} {s}: {t}", .{ i, str, err }); break :overrides; }; - errdefer alloc.free(cpy); args.append(alloc, cpy) catch |err| { log.warn("unable to append argument {d} {s}: {t}", .{ i, str, err }); break :overrides; @@ -1744,7 +1736,6 @@ pub const Application = extern struct { } if (lib.cutPrefix(u8, str, "--command=")) |v| { - if (command) |c| c.deinit(alloc); var cmd: configpkg.Command = undefined; cmd.parseCLI(alloc, v) catch |err| { log.warn("unable to parse command: {t}", .{err}); @@ -1754,7 +1745,6 @@ pub const Application = extern struct { continue; } if (lib.cutPrefix(u8, str, "--working-directory=")) |v| { - if (working_directory) |wd| alloc.free(wd); 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; @@ -1762,7 +1752,6 @@ pub const Application = extern struct { continue; } if (lib.cutPrefix(u8, str, "--title=")) |v| { - if (title) |t| alloc.free(t); 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; @@ -1772,13 +1761,9 @@ pub const Application = extern struct { } } - if (args.items.len > 0) direct: { - if (command) |c| c.deinit(alloc); + if (args.items.len > 0) { command = .{ - .direct = args.toOwnedSlice(alloc) catch |err| { - log.warn("unable to convert list of arguments to owned slice: {t}", .{err}); - break :direct; - }, + .direct = args.items, }; }