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