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.
This commit is contained in:
Jeffrey C. Ollie
2026-03-04 00:00:03 -06:00
parent f2ce7c348e
commit 002a6cc765
17 changed files with 300 additions and 432 deletions

View File

@@ -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,

View File

@@ -572,6 +572,7 @@ pub const Surface = struct {
app.core_app,
app,
self,
.none,
);
errdefer self.core_surface.deinit();

View File

@@ -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();
}

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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,

View File

@@ -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"

View File

@@ -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

View File

@@ -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 <string>;
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

View File

@@ -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=<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=<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 => {

View File

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

View File

@@ -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" {

View File

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

View File

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

View File

@@ -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),

View File

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