From e76a151b42f7f988e9a78c581db822d7b687e032 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Jul 2025 12:46:15 -0700 Subject: [PATCH 01/14] apprt/gtk-ng: GhosttyConfig --- src/apprt/gtk-ng.zig | 6 ++ src/apprt/gtk-ng/class/application.zig | 3 - src/apprt/gtk-ng/class/config.zig | 107 +++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/apprt/gtk-ng/class/config.zig diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index 19d450a54..ae57dd6fa 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -7,3 +7,9 @@ pub const resourcesDir = internal_os.resourcesDir; // The exported API, custom for the apprt. pub const GhosttyApplication = @import("gtk-ng/class/application.zig").GhosttyApplication; +pub const GhosttyWindow = @import("gtk-ng/class/window.zig").GhosttyWindow; +pub const GhosttyConfig = @import("gtk-ng/class/config.zig").GhosttyConfig; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index e713d1de2..07dc365a3 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -464,9 +464,6 @@ pub const GhosttyApplication = extern struct { } fn activate(self: *GhosttyApplication) callconv(.C) void { - // This is called when the application is activated, but we - // don't need to do anything here since we handle activation - // in the `run` method. log.debug("activate", .{}); // Call the parent activate method. diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig new file mode 100644 index 000000000..74ab14923 --- /dev/null +++ b/src/apprt/gtk-ng/class/config.zig @@ -0,0 +1,107 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const adw = @import("adw"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const configpkg = @import("../../../config.zig"); +const Config = configpkg.Config; + +const log = std.log.scoped(.gtk_ghostty_config); + +/// Wraps a `Ghostty.Config` object in a GObject so it can be reference +/// counted. When this object is freed, the underlying config is also freed. +/// +/// It is highly recommended to NOT take a reference to this object, +/// since configuration takes up a lot of memory (relatively). Instead, +/// receivers of this should usually create a `DerivedConfig` struct from +/// this, copy any memory they require, and own that structure instead. +/// +/// This can also expose helpers to access configuration in ways that +/// may be more egonomic to GTK primitives. +pub const GhosttyConfig = extern struct { + const Self = @This(); + parent_instance: Parent, + pub const Parent = gobject.Object; + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + const Private = struct { + config: Config, + + var offset: c_int = 0; + }; + + /// Create a new GhosttyConfig from a loaded configuration. + /// + /// This clones the given configuration, so it is safe for the + /// caller to free the original configuration after this call. + pub fn new(alloc: Allocator, config: *const Config) Allocator.Error!*Self { + const self = gobject.ext.newInstance(Self, .{}); + errdefer self.unref(); + + const priv = self.private(); + priv.config = try config.clone(alloc); + + return self; + } + + /// Get the wrapped configuration. It's unsafe to store this or access + /// it in any way that may live beyond the lifetime of this object. + pub fn getConfig(self: *Self) *const Config { + return self.private().config; + } + + fn finalize(self: *Self) callconv(.C) void { + self.private().config.deinit(); + + gobject.Object.virtual_methods.finalize.call( + Class.parent, + self.as(Parent), + ); + } + + pub fn as(self: *Self, comptime T: type) *T { + return gobject.ext.as(T, self); + } + + pub fn ref(self: *Self) *Self { + return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object)))); + } + + pub fn unref(self: *Self) void { + gobject.Object.unref(self.as(gobject.Object)); + } + + fn private(self: *Self) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); + } + + 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); + } + }; +}; + +// This test verifies our memory management works as expected. Since +// we use the testing allocator any leaks are detected. +test "GhosttyConfig" { + const testing = std.testing; + const alloc = testing.allocator; + var config: Config = try .default(alloc); + defer config.deinit(); + const obj: *GhosttyConfig = try .new(alloc, &config); + obj.unref(); +} From 897649a3afd192f8e7552e16b815d4c11602ba2c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Jul 2025 13:44:59 -0700 Subject: [PATCH 02/14] apprt/gtk-ng: GhosttyConfigErrors --- src/apprt/gtk-ng/build/gresource.zig | 1 + src/apprt/gtk-ng/class/config_errors.zig | 70 +++++++++++++++++++ .../gtk-ng/ui/1.5/config-errors-dialog.blp | 26 +++++++ 3 files changed, 97 insertions(+) create mode 100644 src/apprt/gtk-ng/class/config_errors.zig create mode 100644 src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index db5c2cf6e..433c504da 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -30,6 +30,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 }; /// /// These will be asserted to exist at runtime. pub const blueprints: []const Blueprint = &.{ + .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 5, .name = "window" }, }; diff --git a/src/apprt/gtk-ng/class/config_errors.zig b/src/apprt/gtk-ng/class/config_errors.zig new file mode 100644 index 000000000..fdf16602b --- /dev/null +++ b/src/apprt/gtk-ng/class/config_errors.zig @@ -0,0 +1,70 @@ +const std = @import("std"); +const adw = @import("adw"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const adw_version = @import("../adw_version.zig"); +const GhosttyConfig = @import("config.zig").GhosttyConfig; + +const log = std.log.scoped(.gtk_ghostty_window); + +pub const GhosttyConfigErrors = extern struct { + const Self = @This(); + parent_instance: Parent, + + pub const Parent = if (adw_version.supportsDialogs()) + adw.AlertDialog + else + adw.MessageDialog; + + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .instanceInit = &init, + .classInit = &Class.init, + .parent_class = &Class.parent, + .private = .{ .Type = Private, .offset = &Private.offset }, + }); + + const Private = struct { + _todo: u8 = 0, + var offset: c_int = 0; + }; + + pub fn new(config: *GhosttyConfig) *Self { + return gobject.ext.newInstance(Self, .{ + .config = config, + }); + } + + fn init(self: *Self, _: *Class) callconv(.C) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + + pub fn as(win: *Self, comptime T: type) *T { + return gobject.ext.as(T, win); + } + + 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 { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + switch (Parent) { + adw.AlertDialog => comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "config-errors-dialog", + }), + else => unreachable, + }, + ); + } + + pub fn as(class: *Class, comptime T: type) *T { + return gobject.ext.as(T, class); + } + }; +}; diff --git a/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp new file mode 100644 index 000000000..2669ef390 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp @@ -0,0 +1,26 @@ +using Gtk 4.0; +using Adw 1; + +Adw.AlertDialog config_errors_dialog { + heading: _("Configuration Errors"); + body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors."); + + responses [ + ignore: _("Ignore"), + reload: _("Reload Configuration") suggested, + ] + + extra-child: ScrolledWindow { + min-content-width: 500; + min-content-height: 100; + + TextView { + editable: false; + cursor-visible: false; + top-margin: 8; + bottom-margin: 8; + left-margin: 8; + right-margin: 8; + } + }; +} From c3ba6e252e5df9c484803153bb6a5aa101e35283 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Jul 2025 14:01:39 -0700 Subject: [PATCH 03/14] apprt/gtk-ng: use the GhosttyConfig class to wrap our config --- src/apprt/gtk-ng/class/application.zig | 30 ++++++++++++++------------ src/apprt/gtk-ng/class/config.zig | 4 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 07dc365a3..ef1c76366 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -19,6 +19,7 @@ const Config = configpkg.Config; const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); +const GhosttyConfig = @import("config.zig").GhosttyConfig; const GhosttyWindow = @import("window.zig").GhosttyWindow; const log = std.log.scoped(.gtk_ghostty_application); @@ -57,7 +58,7 @@ pub const GhosttyApplication = extern struct { core_app: *CoreApp, /// The configuration for the application. - config: *Config, + config: *GhosttyConfig, /// The base path of the transient cgroup used to put all surfaces /// into their own cgroup. This is only set if cgroups are enabled @@ -97,14 +98,12 @@ pub const GhosttyApplication = extern struct { }; // Load our configuration. - const config: *Config = try alloc.create(Config); - errdefer alloc.destroy(config); - config.* = Config.load(alloc) catch |err| err: { + var config = Config.load(alloc) catch |err| err: { // If we fail to load the configuration, then we should log // the error in the diagnostics so it can be shown to the user. // We can still load a default which only fails for OOM, allowing // us to startup. - var default = try Config.default(alloc); + var default: Config = try .default(alloc); errdefer default.deinit(); const config_arena = default._arena.?.allocator(); try default._diagnostics.append(config_arena, .{ @@ -117,10 +116,10 @@ pub const GhosttyApplication = extern struct { break :err default; }; - errdefer config.deinit(); + defer config.deinit(); // Setup our GTK init env vars - setGtkEnv(config) catch |err| switch (err) { + setGtkEnv(&config) catch |err| switch (err) { error.NoSpaceLeft => { // If we fail to set GTK environment variables then we still // try to start the application... @@ -170,6 +169,10 @@ pub const GhosttyApplication = extern struct { single_instance, }); + // Wrap our configuration in a GObject. + const config_obj: *GhosttyConfig = try .new(alloc, &config); + errdefer config_obj.unref(); + // Initialize the app. const self = gobject.ext.newInstance(Self, .{ .application_id = app_id.ptr, @@ -186,7 +189,7 @@ pub const GhosttyApplication = extern struct { // to there (and we don't need it there directly) so this is here. const priv = self.private(); priv.core_app = core_app; - priv.config = config; + priv.config = config_obj; return self; } @@ -199,8 +202,7 @@ pub const GhosttyApplication = extern struct { pub fn deinit(self: *Self) void { const alloc = self.allocator(); const priv = self.private(); - priv.config.deinit(); - alloc.destroy(priv.config); + priv.config.unref(); if (priv.transient_cgroup_base) |base| alloc.free(base); } @@ -254,7 +256,7 @@ pub const GhosttyApplication = extern struct { // // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 const priv = self.private(); - const config = priv.config; + const config = priv.config.get(); if (config.@"initial-window") switch (config.@"launched-from".?) { .desktop, .cli => self.as(gio.Application).activate(), .dbus, .systemd => {}, @@ -340,7 +342,7 @@ pub const GhosttyApplication = extern struct { /// This must be called before any other xev APIs are used. fn startupXev(self: *GhosttyApplication) void { const priv = self.private(); - const config = priv.config; + const config = priv.config.get(); // If our backend is auto then we have no setup to do. if (config.@"async-backend" == .auto) return; @@ -370,7 +372,7 @@ pub const GhosttyApplication = extern struct { /// setup listeners for changes to the style manager. fn startupStyleManager(self: *GhosttyApplication) void { const priv = self.private(); - const config = priv.config; + const config = priv.config.get(); // Setup our initial light/dark const style = self.as(adw.Application).getStyleManager(); @@ -409,7 +411,7 @@ pub const GhosttyApplication = extern struct { /// so that created surfaces can also have their own cgroups. fn startupCgroup(self: *GhosttyApplication) CgroupError!void { const priv = self.private(); - const config = priv.config; + const config = priv.config.get(); // If cgroup isolation isn't enabled then we don't do this. if (!switch (config.@"linux-cgroup") { diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index 74ab14923..b9c42edc2 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -51,8 +51,8 @@ pub const GhosttyConfig = extern struct { /// Get the wrapped configuration. It's unsafe to store this or access /// it in any way that may live beyond the lifetime of this object. - pub fn getConfig(self: *Self) *const Config { - return self.private().config; + pub fn get(self: *Self) *const Config { + return &self.private().config; } fn finalize(self: *Self) callconv(.C) void { From 531d4a480ed01ef549ec670db9c33e62c718fb48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Jul 2025 20:49:53 -0700 Subject: [PATCH 04/14] apprt/gtk-ng: hook up all the bindings for the config errors dialog --- src/apprt/gtk-ng/class/application.zig | 23 ++++-- src/apprt/gtk-ng/class/config.zig | 76 +++++++++++++++++++ ...ig_errors.zig => config_errors_dialog.zig} | 40 +++++++++- src/apprt/gtk-ng/class/window.zig | 18 +++-- .../gtk-ng/ui/1.5/config-errors-dialog.blp | 3 +- 5 files changed, 144 insertions(+), 16 deletions(-) rename src/apprt/gtk-ng/class/{config_errors.zig => config_errors_dialog.zig} (63%) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index ef1c76366..511c68eee 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -21,6 +21,7 @@ const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); const GhosttyConfig = @import("config.zig").GhosttyConfig; const GhosttyWindow = @import("window.zig").GhosttyWindow; +const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog; const log = std.log.scoped(.gtk_ghostty_application); @@ -320,6 +321,11 @@ pub const GhosttyApplication = extern struct { fn startup(self: *GhosttyApplication) callconv(.C) void { log.debug("startup", .{}); + gio.Application.virtual_methods.startup.call( + Class.parent, + self.as(Parent), + ); + // Setup our event loop self.startupXev(); @@ -331,10 +337,15 @@ pub const GhosttyApplication = extern struct { log.warn("TODO", .{}); }; - gio.Application.virtual_methods.startup.call( - Class.parent, - self.as(Parent), - ); + // If we have any config diagnostics from loading, then we + // show the diagnostics dialog. We show this one as a general + // modal (not to any specific window) because we don't even + // know if the window will load. + const priv = self.private(); + if (priv.config.hasDiagnostics()) { + const dialog: *ConfigErrorsDialog = .new(priv.config); + dialog.present(null); + } } /// Configure libxev to use a specific backend. @@ -474,8 +485,8 @@ pub const GhosttyApplication = extern struct { self.as(Parent), ); - const win = GhosttyWindow.new(self); - gtk.Window.present(win.as(gtk.Window)); + // const win = GhosttyWindow.new(self); + // gtk.Window.present(win.as(gtk.Window)); } fn finalize(self: *GhosttyApplication) callconv(.C) void { diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index b9c42edc2..1342dfb50 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -29,6 +29,36 @@ pub const GhosttyConfig = extern struct { .private = .{ .Type = Private, .offset = &Private.offset }, }); + pub const properties = struct { + pub const @"diagnostics-buffer" = gobject.ext.defineProperty( + "diagnostics-buffer", + Self, + ?*gtk.TextBuffer, + .{ + .nick = "Dignostics Buffer", + .blurb = "A TextBuffer that contains the diagnostics.", + .default = null, + .accessor = .{ + .getter = Self.diagnosticsBuffer, + }, + }, + ); + + pub const @"has-diagnostics" = gobject.ext.defineProperty( + "has-diagnostics", + Self, + bool, + .{ + .nick = "has-diagnostics", + .blurb = "Whether the configuration has diagnostics.", + .default = false, + .accessor = .{ + .getter = Self.hasDiagnostics, + }, + }, + ); + }; + const Private = struct { config: Config, @@ -55,6 +85,48 @@ pub const GhosttyConfig = extern struct { return &self.private().config; } + /// Get the mutable configuration. This is usually NOT recommended + /// because any changes to the config won't be propagated to anyone + /// with a reference to this object. If you know what you're doing, then + /// you can use this. + pub fn getMut(self: *Self) *Config { + return &self.private().config; + } + + /// Returns whether this configuration has any diagnostics. + pub fn hasDiagnostics(self: *Self) bool { + const config = self.get(); + return !config._diagnostics.empty(); + } + + /// Reads the diagnostics of this configuration as a TextBuffer, + /// or returns null if there are no diagnostics. + pub fn diagnosticsBuffer(self: *Self) ?*gtk.TextBuffer { + const config = self.get(); + if (config._diagnostics.empty()) return null; + + const text_buf: *gtk.TextBuffer = .new(null); + errdefer text_buf.unref(); + + var buf: [4095:0]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + for (config._diagnostics.items()) |diag| { + fbs.reset(); + diag.write(fbs.writer()) catch |err| { + log.warn( + "error writing diagnostic to buffer err={}", + .{err}, + ); + continue; + }; + + text_buf.insertAtCursor(&buf, @intCast(fbs.pos)); + text_buf.insertAtCursor("\n", 1); + } + + return text_buf; + } + fn finalize(self: *Self) callconv(.C) void { self.private().config.deinit(); @@ -91,6 +163,10 @@ pub const GhosttyConfig = extern struct { fn init(class: *Class) callconv(.C) void { gobject.Object.virtual_methods.finalize.implement(class, &finalize); + gobject.ext.registerProperties(class, &.{ + properties.@"diagnostics-buffer", + properties.@"has-diagnostics", + }); } }; }; diff --git a/src/apprt/gtk-ng/class/config_errors.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig similarity index 63% rename from src/apprt/gtk-ng/class/config_errors.zig rename to src/apprt/gtk-ng/class/config_errors_dialog.zig index fdf16602b..7824c437b 100644 --- a/src/apprt/gtk-ng/class/config_errors.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -5,11 +5,11 @@ const gtk = @import("gtk"); const gresource = @import("../build/gresource.zig"); const adw_version = @import("../adw_version.zig"); -const GhosttyConfig = @import("config.zig").GhosttyConfig; +const Config = @import("config.zig").GhosttyConfig; const log = std.log.scoped(.gtk_ghostty_window); -pub const GhosttyConfigErrors = extern struct { +pub const ConfigErrorsDialog = extern struct { const Self = @This(); parent_instance: Parent, @@ -19,18 +19,38 @@ pub const GhosttyConfigErrors = extern struct { adw.MessageDialog; pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyConfigErrorsDialog", .instanceInit = &init, .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, }); + pub const properties = struct { + pub const config = gobject.ext.defineProperty( + "config", + Self, + ?*Config, + .{ + .nick = "config", + .blurb = "The configuration that this dialog is showing errors for.", + .default = null, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "config", + ), + }, + ); + }; + const Private = struct { - _todo: u8 = 0, + config: ?*Config, var offset: c_int = 0; }; - pub fn new(config: *GhosttyConfig) *Self { + pub fn new(config: *Config) *Self { return gobject.ext.newInstance(Self, .{ .config = config, }); @@ -40,6 +60,13 @@ pub const GhosttyConfigErrors = extern struct { gtk.Widget.initTemplate(self.as(gtk.Widget)); } + pub fn present(self: *Self, parent: ?*gtk.Widget) void { + switch (Parent) { + adw.AlertDialog => self.as(adw.Dialog).present(parent), + else => unreachable, + } + } + pub fn as(win: *Self, comptime T: type) *T { return gobject.ext.as(T, win); } @@ -58,9 +85,14 @@ pub const GhosttyConfigErrors = extern struct { .minor = 5, .name = "config-errors-dialog", }), + else => unreachable, }, ); + + gobject.ext.registerProperties(class, &.{ + properties.config, + }); } pub fn as(class: *Class, comptime T: type) *T { diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index df8b99cee..b1bb27cba 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -20,7 +20,7 @@ pub const GhosttyWindow = extern struct { }); const Private = struct { - _todo: u8 = 0, + _todo: u8, var offset: c_int = 0; }; @@ -28,12 +28,20 @@ pub const GhosttyWindow = extern struct { return gobject.ext.newInstance(Self, .{ .application = app }); } - fn init(win: *GhosttyWindow, _: *Class) callconv(.C) void { - gtk.Widget.initTemplate(win.as(gtk.Widget)); + fn init(self: *GhosttyWindow, _: *Class) callconv(.C) void { + gtk.Widget.initTemplate(self.as(gtk.Widget)); } - pub fn as(win: *Self, comptime T: type) *T { - return gobject.ext.as(T, win); + pub fn as(self: *Self, comptime T: type) *T { + return gobject.ext.as(T, self); + } + + fn private(self: *Self) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); } pub const Class = extern struct { diff --git a/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp index 2669ef390..17ecac4e4 100644 --- a/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp @@ -1,7 +1,7 @@ using Gtk 4.0; using Adw 1; -Adw.AlertDialog config_errors_dialog { +template $GhosttyConfigErrorsDialog: Adw.AlertDialog { heading: _("Configuration Errors"); body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors."); @@ -21,6 +21,7 @@ Adw.AlertDialog config_errors_dialog { bottom-margin: 8; left-margin: 8; right-margin: 8; + buffer: bind (template.config as <$GhosttyConfig>).diagnostics-buffer; } }; } From b1aab1e7bf4fdc97e645cd0f5ecaa9e7868d574f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 06:10:41 -0700 Subject: [PATCH 05/14] apprt/gtk-ng: remove Ghostty-prefix from Zig-side classes --- src/apprt/gtk-ng.zig | 6 +-- src/apprt/gtk-ng/App.zig | 8 ++-- src/apprt/gtk-ng/class/application.zig | 41 ++++++++++--------- src/apprt/gtk-ng/class/config.zig | 17 ++++---- .../gtk-ng/class/config_errors_dialog.zig | 2 +- src/apprt/gtk-ng/class/window.zig | 9 ++-- 6 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index ae57dd6fa..5e3e1e752 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -6,9 +6,9 @@ pub const Surface = @import("gtk-ng/Surface.zig"); pub const resourcesDir = internal_os.resourcesDir; // The exported API, custom for the apprt. -pub const GhosttyApplication = @import("gtk-ng/class/application.zig").GhosttyApplication; -pub const GhosttyWindow = @import("gtk-ng/class/window.zig").GhosttyWindow; -pub const GhosttyConfig = @import("gtk-ng/class/config.zig").GhosttyConfig; +pub const Application = @import("gtk-ng/class/application.zig").Application; +pub const Window = @import("gtk-ng/class/window.zig").Window; +pub const Config = @import("gtk-ng/class/config.zig").Config; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk-ng/App.zig b/src/apprt/gtk-ng/App.zig index 0bd2c9603..035c4c16f 100644 --- a/src/apprt/gtk-ng/App.zig +++ b/src/apprt/gtk-ng/App.zig @@ -12,15 +12,15 @@ const internal_os = @import("../../os/main.zig"); const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); -const GhosttyApplication = @import("class/application.zig").GhosttyApplication; +const Application = @import("class/application.zig").Application; const Surface = @import("Surface.zig"); const gtk_version = @import("gtk_version.zig"); const adw_version = @import("adw_version.zig"); const log = std.log.scoped(.gtk); -/// The GObject GhosttyApplication instance -app: *GhosttyApplication, +/// The GObject Application instance +app: *Application, pub fn init( self: *App, @@ -31,7 +31,7 @@ pub fn init( ) !void { _ = opts; - const app: *GhosttyApplication = try .new(core_app); + const app: *Application = try .new(core_app); errdefer app.unref(); self.* = .{ .app = app }; return; diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 511c68eee..6abecea0d 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -15,12 +15,12 @@ const CoreApp = @import("../../../App.zig"); const configpkg = @import("../../../config.zig"); const internal_os = @import("../../../os/main.zig"); const xev = @import("../../../global.zig").xev; -const Config = configpkg.Config; +const CoreConfig = configpkg.Config; const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); -const GhosttyConfig = @import("config.zig").GhosttyConfig; -const GhosttyWindow = @import("window.zig").GhosttyWindow; +const Config = @import("config.zig").Config; +const Window = @import("window.zig").Window; const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog; const log = std.log.scoped(.gtk_ghostty_application); @@ -29,7 +29,7 @@ const log = std.log.scoped(.gtk_ghostty_application); /// /// This requires a `ghostty.App` and `ghostty.Config` and takes /// care of the rest. Call `run` to run the application to completion. -pub const GhosttyApplication = extern struct { +pub const Application = extern struct { /// This type creates a new GObject class. Since the Application is /// the primary entrypoint I'm going to use this as a place to document /// how this all works and where you can find resources for it, but @@ -49,6 +49,7 @@ pub const GhosttyApplication = extern struct { parent_instance: Parent, pub const Parent = adw.Application; pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyApplication", .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, @@ -59,7 +60,7 @@ pub const GhosttyApplication = extern struct { core_app: *CoreApp, /// The configuration for the application. - config: *GhosttyConfig, + config: *Config, /// The base path of the transient cgroup used to put all surfaces /// into their own cgroup. This is only set if cgroups are enabled @@ -74,7 +75,7 @@ pub const GhosttyApplication = extern struct { var offset: c_int = 0; }; - /// Creates a new GhosttyApplication instance. + /// Creates a new Application instance. /// /// This does a lot more work than a typical class instantiation, /// because we expect that this is the main program entrypoint. @@ -99,12 +100,12 @@ pub const GhosttyApplication = extern struct { }; // Load our configuration. - var config = Config.load(alloc) catch |err| err: { + var config = CoreConfig.load(alloc) catch |err| err: { // If we fail to load the configuration, then we should log // the error in the diagnostics so it can be shown to the user. // We can still load a default which only fails for OOM, allowing // us to startup. - var default: Config = try .default(alloc); + var default: CoreConfig = try .default(alloc); errdefer default.deinit(); const config_arena = default._arena.?.allocator(); try default._diagnostics.append(config_arena, .{ @@ -171,7 +172,7 @@ pub const GhosttyApplication = extern struct { }); // Wrap our configuration in a GObject. - const config_obj: *GhosttyConfig = try .new(alloc, &config); + const config_obj: *Config = try .new(alloc, &config); errdefer config_obj.unref(); // Initialize the app. @@ -310,7 +311,7 @@ pub const GhosttyApplication = extern struct { gobject.Object.unref(self.as(gobject.Object)); } - fn private(self: *GhosttyApplication) *Private { + fn private(self: *Self) *Private { return gobject.ext.impl_helpers.getPrivate( self, Private, @@ -318,7 +319,7 @@ pub const GhosttyApplication = extern struct { ); } - fn startup(self: *GhosttyApplication) callconv(.C) void { + fn startup(self: *Self) callconv(.C) void { log.debug("startup", .{}); gio.Application.virtual_methods.startup.call( @@ -351,7 +352,7 @@ pub const GhosttyApplication = extern struct { /// Configure libxev to use a specific backend. /// /// This must be called before any other xev APIs are used. - fn startupXev(self: *GhosttyApplication) void { + fn startupXev(self: *Self) void { const priv = self.private(); const config = priv.config.get(); @@ -381,7 +382,7 @@ pub const GhosttyApplication = extern struct { /// Setup the style manager on startup. The primary task here is to /// setup our initial light/dark mode based on the configuration and /// setup listeners for changes to the style manager. - fn startupStyleManager(self: *GhosttyApplication) void { + fn startupStyleManager(self: *Self) void { const priv = self.private(); const config = priv.config.get(); @@ -403,7 +404,7 @@ pub const GhosttyApplication = extern struct { // Setup color change notifications _ = gobject.Object.signals.notify.connect( style, - *GhosttyApplication, + *Self, handleStyleManagerDark, self, .{ .detail = "dark" }, @@ -420,7 +421,7 @@ pub const GhosttyApplication = extern struct { /// The setup for cgroups involves creating the cgroup for our /// application, moving ourselves into it, and storing the base path /// so that created surfaces can also have their own cgroups. - fn startupCgroup(self: *GhosttyApplication) CgroupError!void { + fn startupCgroup(self: *Self) CgroupError!void { const priv = self.private(); const config = priv.config.get(); @@ -476,7 +477,7 @@ pub const GhosttyApplication = extern struct { priv.transient_cgroup_base = path; } - fn activate(self: *GhosttyApplication) callconv(.C) void { + fn activate(self: *Self) callconv(.C) void { log.debug("activate", .{}); // Call the parent activate method. @@ -485,11 +486,11 @@ pub const GhosttyApplication = extern struct { self.as(Parent), ); - // const win = GhosttyWindow.new(self); + // const win = Window.new(self); // gtk.Window.present(win.as(gtk.Window)); } - fn finalize(self: *GhosttyApplication) callconv(.C) void { + fn finalize(self: *Self) callconv(.C) void { self.deinit(); gobject.Object.virtual_methods.finalize.call( Class.parent, @@ -500,7 +501,7 @@ pub const GhosttyApplication = extern struct { fn handleStyleManagerDark( style: *adw.StyleManager, _: *gobject.ParamSpec, - self: *GhosttyApplication, + self: *Self, ) callconv(.c) void { _ = self; @@ -512,7 +513,7 @@ pub const GhosttyApplication = extern struct { log.debug("style manager changed scheme={}", .{color_scheme}); } - fn allocator(self: *GhosttyApplication) std.mem.Allocator { + fn allocator(self: *Self) std.mem.Allocator { return self.private().core_app.alloc; } diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index 1342dfb50..e7022f5ec 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -5,7 +5,7 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); const configpkg = @import("../../../config.zig"); -const Config = configpkg.Config; +const CoreConfig = configpkg.Config; const log = std.log.scoped(.gtk_ghostty_config); @@ -19,11 +19,12 @@ const log = std.log.scoped(.gtk_ghostty_config); /// /// This can also expose helpers to access configuration in ways that /// may be more egonomic to GTK primitives. -pub const GhosttyConfig = extern struct { +pub const Config = extern struct { const Self = @This(); parent_instance: Parent, pub const Parent = gobject.Object; pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyConfig", .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, @@ -60,7 +61,7 @@ pub const GhosttyConfig = extern struct { }; const Private = struct { - config: Config, + config: CoreConfig, var offset: c_int = 0; }; @@ -69,7 +70,7 @@ pub const GhosttyConfig = extern struct { /// /// This clones the given configuration, so it is safe for the /// caller to free the original configuration after this call. - pub fn new(alloc: Allocator, config: *const Config) Allocator.Error!*Self { + pub fn new(alloc: Allocator, config: *const CoreConfig) Allocator.Error!*Self { const self = gobject.ext.newInstance(Self, .{}); errdefer self.unref(); @@ -81,7 +82,7 @@ pub const GhosttyConfig = extern struct { /// Get the wrapped configuration. It's unsafe to store this or access /// it in any way that may live beyond the lifetime of this object. - pub fn get(self: *Self) *const Config { + pub fn get(self: *Self) *const CoreConfig { return &self.private().config; } @@ -89,7 +90,7 @@ pub const GhosttyConfig = extern struct { /// because any changes to the config won't be propagated to anyone /// with a reference to this object. If you know what you're doing, then /// you can use this. - pub fn getMut(self: *Self) *Config { + pub fn getMut(self: *Self) *CoreConfig { return &self.private().config; } @@ -176,8 +177,8 @@ pub const GhosttyConfig = extern struct { test "GhosttyConfig" { const testing = std.testing; const alloc = testing.allocator; - var config: Config = try .default(alloc); + var config: CoreConfig = try .default(alloc); defer config.deinit(); - const obj: *GhosttyConfig = try .new(alloc, &config); + const obj: *Config = try .new(alloc, &config); obj.unref(); } diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index 7824c437b..e89056b3f 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -5,7 +5,7 @@ const gtk = @import("gtk"); const gresource = @import("../build/gresource.zig"); const adw_version = @import("../adw_version.zig"); -const Config = @import("config.zig").GhosttyConfig; +const Config = @import("config.zig").Config; const log = std.log.scoped(.gtk_ghostty_window); diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index b1bb27cba..1c6619264 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -4,15 +4,16 @@ const gobject = @import("gobject"); const gtk = @import("gtk"); const gresource = @import("../build/gresource.zig"); -const GhosttyApplication = @import("application.zig").GhosttyApplication; +const Application = @import("application.zig").Application; const log = std.log.scoped(.gtk_ghostty_window); -pub const GhosttyWindow = extern struct { +pub const Window = extern struct { const Self = @This(); parent_instance: Parent, pub const Parent = adw.ApplicationWindow; pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyWindow", .instanceInit = &init, .classInit = &Class.init, .parent_class = &Class.parent, @@ -24,11 +25,11 @@ pub const GhosttyWindow = extern struct { var offset: c_int = 0; }; - pub fn new(app: *GhosttyApplication) *Self { + pub fn new(app: *Application) *Self { return gobject.ext.newInstance(Self, .{ .application = app }); } - fn init(self: *GhosttyWindow, _: *Class) callconv(.C) void { + fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); } From b253e2efe271dc1ff97b53c7d95c391083a43aa7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 06:32:53 -0700 Subject: [PATCH 06/14] apprt/gtk-ng: proper memory management for config errors dialog --- .../gtk-ng/class/config_errors_dialog.zig | 78 +++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index e89056b3f..24dc7fbab 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -35,16 +35,27 @@ pub const ConfigErrorsDialog = extern struct { .nick = "config", .blurb = "The configuration that this dialog is showing errors for.", .default = null, - .accessor = gobject.ext.privateFieldAccessor( - Self, - Private, - &Private.offset, - "config", - ), + .accessor = .{ + .getter = Self.getConfig, + .setter = Self.setConfig, + }, }, ); }; + pub const signals = struct { + pub const @"reload-config" = struct { + pub const name = "reload-config"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; + }; + const Private = struct { config: ?*Config, var offset: c_int = 0; @@ -67,10 +78,57 @@ pub const ConfigErrorsDialog = extern struct { } } + fn response( + self: *Self, + response_id: [*:0]const u8, + ) callconv(.C) void { + if (std.mem.orderZ(u8, response_id, "reload") != .eq) return; + signals.@"reload-config".impl.emit( + self, + null, + .{}, + null, + ); + } + + fn dispose(self: *Self) callconv(.C) void { + gtk.Widget.disposeTemplate(self.as(gtk.Widget), getGObjectType()); + + const priv = self.private(); + if (priv.config) |v| { + v.unref(); + priv.config = null; + } + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + + fn getConfig(self: *Self) ?*Config { + return self.private().config; + } + + fn setConfig(self: *Self, config: ?*Config) void { + const priv = self.private(); + if (priv.config) |old| old.unref(); + if (config) |newv| _ = newv.ref(); + priv.config = config; + } + pub fn as(win: *Self, comptime T: type) *T { return gobject.ext.as(T, win); } + fn private(self: *Self) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); + } + pub const Class = extern struct { parent_class: Parent.Class, var parent: *Parent.Class = undefined; @@ -90,9 +148,17 @@ pub const ConfigErrorsDialog = extern struct { }, ); + // Properties gobject.ext.registerProperties(class, &.{ properties.config, }); + + // Signals + signals.@"reload-config".impl.register(.{}); + + // Virtual methods + gobject.Object.virtual_methods.dispose.implement(class, &dispose); + Parent.virtual_methods.response.implement(class, &response); } pub fn as(class: *Class, comptime T: type) *T { From 0da47903f4d986c2c83fcf425b6bed53bea54e65 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 08:28:58 -0700 Subject: [PATCH 07/14] apprt/gtk-ng: hook up all the refs to show the dialog --- src/apprt/gtk-ng.zig | 2 + src/apprt/gtk-ng/class/application.zig | 125 +++++++++++++----- .../gtk-ng/class/config_errors_dialog.zig | 18 ++- src/apprt/gtk-ng/weak_ref.zig | 43 ++++++ src/config/Config.zig | 17 +++ 5 files changed, 171 insertions(+), 34 deletions(-) create mode 100644 src/apprt/gtk-ng/weak_ref.zig diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index 5e3e1e752..2ca01c4ab 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -10,6 +10,8 @@ pub const Application = @import("gtk-ng/class/application.zig").Application; pub const Window = @import("gtk-ng/class/window.zig").Window; pub const Config = @import("gtk-ng/class/config.zig").Config; +pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef; + test { @import("std").testing.refAllDecls(@This()); } diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 6abecea0d..f91f53f54 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -19,6 +19,7 @@ const CoreConfig = configpkg.Config; const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); +const WeakRef = @import("../weak_ref.zig").WeakRef; const Config = @import("config.zig").Config; const Window = @import("window.zig").Window; const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog; @@ -72,6 +73,9 @@ pub const Application = extern struct { /// only be set by the main loop thread. running: bool = false, + /// If non-null, we're currently showing a config errors dialog. + config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{}, + var offset: c_int = 0; }; @@ -107,14 +111,10 @@ pub const Application = extern struct { // us to startup. var default: CoreConfig = try .default(alloc); errdefer default.deinit(); - const config_arena = default._arena.?.allocator(); - try default._diagnostics.append(config_arena, .{ - .message = try std.fmt.allocPrintZ( - config_arena, - "error loading user configuration: {}", - .{err}, - ), - }); + try default.addDiagnosticFmt( + "error loading user configuration: {}", + .{err}, + ); break :err default; }; @@ -190,8 +190,10 @@ pub const Application = extern struct { // callback that GObject calls, but we can't pass this data through // to there (and we don't need it there directly) so this is here. const priv = self.private(); - priv.core_app = core_app; - priv.config = config_obj; + priv.* = .{ + .core_app = core_app, + .config = config_obj, + }; return self; } @@ -303,22 +305,6 @@ pub const Application = extern struct { } } - pub fn as(app: *Self, comptime T: type) *T { - return gobject.ext.as(T, app); - } - - pub fn unref(self: *Self) void { - gobject.Object.unref(self.as(gobject.Object)); - } - - fn private(self: *Self) *Private { - return gobject.ext.impl_helpers.getPrivate( - self, - Private, - Private.offset, - ); - } - fn startup(self: *Self) callconv(.C) void { log.debug("startup", .{}); @@ -334,19 +320,26 @@ pub const Application = extern struct { self.startupStyleManager(); // Setup our cgroup for the application. - self.startupCgroup() catch { - log.warn("TODO", .{}); + self.startupCgroup() catch |err| { + log.warn("cgroup initialization failed err={}", .{err}); + + // Add it to our config diagnostics so it shows up in a GUI dialog. + // Admittedly this has two issues: (1) we shuldn't be using the + // config errors dialog for this long term and (2) using a mut + // ref to the config wouldn't propagate changes to UI properly, + // but we're in startup mode so its okay. + const config = self.private().config.getMut(); + config.addDiagnosticFmt( + "cgroup initialization failed: {}", + .{err}, + ) catch {}; }; // If we have any config diagnostics from loading, then we // show the diagnostics dialog. We show this one as a general // modal (not to any specific window) because we don't even // know if the window will load. - const priv = self.private(); - if (priv.config.hasDiagnostics()) { - const dialog: *ConfigErrorsDialog = .new(priv.config); - dialog.present(null); - } + self.showConfigErrorsDialog(); } /// Configure libxev to use a specific backend. @@ -490,6 +483,19 @@ pub const Application = extern struct { // gtk.Window.present(win.as(gtk.Window)); } + fn dispose(self: *Self) callconv(.C) void { + const priv = self.private(); + if (priv.config_errors_dialog.get()) |diag| { + diag.close(); + diag.unref(); // strong ref from get() + } + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + fn finalize(self: *Self) callconv(.C) void { self.deinit(); gobject.Object.virtual_methods.finalize.call( @@ -513,10 +519,62 @@ pub const Application = extern struct { log.debug("style manager changed scheme={}", .{color_scheme}); } + /// Show the config errors dialog if the config on our application + /// has diagnostics. + fn showConfigErrorsDialog(self: *Self) void { + const priv = self.private(); + + // If we already have a dialog, just update the config. + if (priv.config_errors_dialog.get()) |diag| { + defer diag.unref(); // get gets a strong ref + + var value = gobject.ext.Value.newFrom(priv.config); + defer value.unset(); + gobject.Object.setProperty( + diag.as(gobject.Object), + "config", + &value, + ); + + if (!priv.config.hasDiagnostics()) { + diag.close(); + } else { + diag.present(null); + } + + return; + } + + // No diagnostics, do nothing. + if (!priv.config.hasDiagnostics()) return; + + // No dialog yet, initialize a new one. There's no need to unref + // here because the widget that it becomes a part of takes ownership. + const dialog: *ConfigErrorsDialog = .new(priv.config); + dialog.present(null); + priv.config_errors_dialog.set(dialog); + } + fn allocator(self: *Self) std.mem.Allocator { return self.private().core_app.alloc; } + pub fn as(app: *Self, comptime T: type) *T { + return gobject.ext.as(T, app); + } + + pub fn unref(self: *Self) void { + gobject.Object.unref(self.as(gobject.Object)); + } + + fn private(self: *Self) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); + } + pub const Class = extern struct { parent_class: Parent.Class, var parent: *Parent.Class = undefined; @@ -542,6 +600,7 @@ pub const Application = extern struct { // Virtual methods gio.Application.virtual_methods.activate.implement(class, &activate); gio.Application.virtual_methods.startup.implement(class, &startup); + gobject.Object.virtual_methods.dispose.implement(class, &dispose); gobject.Object.virtual_methods.finalize.implement(class, &finalize); } }; diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index 24dc7fbab..46ec5671d 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -7,7 +7,7 @@ const gresource = @import("../build/gresource.zig"); const adw_version = @import("../adw_version.zig"); const Config = @import("config.zig").Config; -const log = std.log.scoped(.gtk_ghostty_window); +const log = std.log.scoped(.gtk_ghostty_config_errors_dialog); pub const ConfigErrorsDialog = extern struct { const Self = @This(); @@ -78,6 +78,13 @@ pub const ConfigErrorsDialog = extern struct { } } + pub fn close(self: *Self) void { + switch (Parent) { + adw.AlertDialog => self.as(adw.Dialog).forceClose(), + else => unreachable, + } + } + fn response( self: *Self, response_id: [*:0]const u8, @@ -92,6 +99,7 @@ pub const ConfigErrorsDialog = extern struct { } fn dispose(self: *Self) callconv(.C) void { + log.warn("DISPOSE", .{}); gtk.Widget.disposeTemplate(self.as(gtk.Widget), getGObjectType()); const priv = self.private(); @@ -121,6 +129,14 @@ pub const ConfigErrorsDialog = extern struct { return gobject.ext.as(T, win); } + pub fn ref(self: *Self) *Self { + return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object)))); + } + + pub fn unref(self: *Self) void { + gobject.Object.unref(self.as(gobject.Object)); + } + fn private(self: *Self) *Private { return gobject.ext.impl_helpers.getPrivate( self, diff --git a/src/apprt/gtk-ng/weak_ref.zig b/src/apprt/gtk-ng/weak_ref.zig new file mode 100644 index 000000000..a31b9e020 --- /dev/null +++ b/src/apprt/gtk-ng/weak_ref.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const gtk = @import("gtk"); +const gobject = @import("gobject"); + +/// A lightweight wrapper around gobject.WeakRef to make it type-safe +/// to hold a single type of value. +pub fn WeakRef(comptime T: type) type { + return struct { + const Self = @This(); + + ref: gobject.WeakRef = std.mem.zeroes(gobject.WeakRef), + + /// Set the weak reference to the given object. This will not + /// increase the reference count of the object. + pub fn set(self: *Self, v: *T) void { + self.ref.set(v.as(gobject.Object)); + } + + /// Get a strong reference to the object, or null if the object + /// has been finalized. This increases the reference count by one. + pub fn get(self: *Self) ?*T { + // The GIR of g_weak_ref_get has a bug where the optional + // is not encoded. Or, it may be a bug in zig-gobject. + const obj_: ?*gobject.Object = @ptrCast(self.ref.get()); + const obj = obj_ orelse return null; + + // We can't use `as` because `as` guarantees conversion and + // that can't be statically guaranteed. + return gobject.ext.cast(T, obj); + } + }; +} + +test WeakRef { + const testing = std.testing; + + var ref: WeakRef(gtk.TextBuffer) = .{}; + const obj: *gtk.TextBuffer = .new(null); + ref.set(obj); + ref.get().?.unref(); // The "?" asserts non-null + obj.unref(); + try testing.expect(ref.get() == null); +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 1e2086876..db6a368e3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4068,6 +4068,23 @@ fn compatBoldIsBright( return true; } +/// Add a diagnostic message to the config with the given string. +/// This is always added with a location of "none". +pub fn addDiagnosticFmt( + self: *Config, + comptime fmt: []const u8, + args: anytype, +) Allocator.Error!void { + const alloc = self._arena.?.allocator(); + try self._diagnostics.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + fmt, + args, + ), + }); +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` From 31c5af74c55889e3d76fd3a0aa48563981604c60 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 09:07:23 -0700 Subject: [PATCH 08/14] apprt/gtk-ng: handle config reloading --- src/apprt/gtk-ng/App.zig | 9 +- src/apprt/gtk-ng/class/application.zig | 175 ++++++++++++++++++++++++- src/apprt/gtk-ng/weak_ref.zig | 8 +- 3 files changed, 180 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk-ng/App.zig b/src/apprt/gtk-ng/App.zig index 035c4c16f..7ce233359 100644 --- a/src/apprt/gtk-ng/App.zig +++ b/src/apprt/gtk-ng/App.zig @@ -31,14 +31,14 @@ pub fn init( ) !void { _ = opts; - const app: *Application = try .new(core_app); + const app: *Application = try .new(self, core_app); errdefer app.unref(); self.* = .{ .app = app }; return; } pub fn run(self: *App) !void { - try self.app.run(self); + try self.app.run(); } pub fn terminate(self: *App) void { @@ -54,10 +54,7 @@ pub fn performAction( comptime action: apprt.Action.Key, value: apprt.Action.Value(action), ) !bool { - _ = self; - _ = target; - _ = value; - return false; + return try self.app.performAction(target, action, value); } pub fn performIpc( diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index f91f53f54..d6ebe7c40 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -19,6 +19,7 @@ const CoreConfig = configpkg.Config; const adw_version = @import("../adw_version.zig"); const gtk_version = @import("../gtk_version.zig"); +const ApprtApp = @import("../App.zig"); const WeakRef = @import("../weak_ref.zig").WeakRef; const Config = @import("config.zig").Config; const Window = @import("window.zig").Window; @@ -57,6 +58,11 @@ pub const Application = extern struct { }); const Private = struct { + /// The apprt App. This is annoying that we need this it'd be + /// nicer to just make THIS the apprt app but the current libghostty + /// API doesn't allow that. + rt_app: *ApprtApp, + /// The libghostty App instance. core_app: *CoreApp, @@ -87,7 +93,10 @@ pub const Application = extern struct { /// The only failure mode of initializing the application is early OOM. /// Early OOM can't be recovered from. Every other error is mapped to /// some degraded state where we can at least show a window with an error. - pub fn new(core_app: *CoreApp) Allocator.Error!*Self { + pub fn new( + rt_app: *ApprtApp, + core_app: *CoreApp, + ) Allocator.Error!*Self { const alloc = core_app.alloc; // Log our GTK versions @@ -191,6 +200,7 @@ pub const Application = extern struct { // to there (and we don't need it there directly) so this is here. const priv = self.private(); priv.* = .{ + .rt_app = rt_app, .core_app = core_app, .config = config_obj, }; @@ -213,7 +223,7 @@ pub const Application = extern struct { /// Run the application. This is a replacement for `gio.Application.run` /// because we want more tight control over our event loop so we can /// integrate it with libghostty. - pub fn run(self: *Self, rt_app: *apprt.gtk_ng.App) !void { + pub fn run(self: *Self) !void { // Based on the actual `gio.Application.run` implementation: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -284,7 +294,7 @@ pub const Application = extern struct { _ = glib.MainContext.iteration(ctx, 1); // Tick the core Ghostty terminal app - try priv.core_app.tick(rt_app); + try priv.core_app.tick(priv.rt_app); // Check if we must quit based on the current state. const must_quit = q: { @@ -305,6 +315,100 @@ pub const Application = extern struct { } } + /// apprt API to perform an action. + pub fn performAction( + self: *Self, + target: apprt.Target, + comptime action: apprt.Action.Key, + value: apprt.Action.Value(action), + ) !bool { + switch (action) { + .config_change => try Action.configChange( + self, + target, + value.config, + ), + + // Unimplemented + .quit, + .new_window, + .close_window, + .toggle_maximize, + .toggle_fullscreen, + .new_tab, + .close_tab, + .goto_tab, + .move_tab, + .new_split, + .resize_split, + .equalize_splits, + .goto_split, + .open_config, + .reload_config, + .inspector, + .show_gtk_inspector, + .desktop_notification, + .set_title, + .pwd, + .present_terminal, + .initial_size, + .size_limit, + .mouse_visibility, + .mouse_shape, + .mouse_over_link, + .toggle_tab_overview, + .toggle_split_zoom, + .toggle_window_decorations, + .quit_timer, + .prompt_title, + .toggle_quick_terminal, + .secure_input, + .ring_bell, + .toggle_command_palette, + .open_url, + .show_child_exited, + .close_all_windows, + .float_window, + .toggle_visibility, + .cell_size, + .key_sequence, + .render_inspector, + .renderer_health, + .color_change, + .reset_window_size, + .check_for_updates, + .undo, + .redo, + => { + log.warn("unimplemented action={}", .{action}); + return false; + }, + } + + // Assume it was handled. The unhandled case must be explicit + // in the switch above. + return true; + } + + /// Reload the configuration for the application and propagate it + /// across the entire application and all terminals. + pub fn reloadConfig(self: *Self) !void { + const alloc = self.allocator(); + + // Read our new config. We can always deinit this because + // we'll clone and store it if libghostty accepts it and + // emits a `config_change` action. + var config = try CoreConfig.load(alloc); + defer config.deinit(); + + // Notify the app that we've updated. + const priv = self.private(); + try priv.core_app.updateConfig(priv.rt_app, &config); + } + + //--------------------------------------------------------------- + // Virtual Methods + fn startup(self: *Self) callconv(.C) void { log.debug("startup", .{}); @@ -504,6 +608,9 @@ pub const Application = extern struct { ); } + //--------------------------------------------------------------- + // Signal Handlers + fn handleStyleManagerDark( style: *adw.StyleManager, _: *gobject.ParamSpec, @@ -519,6 +626,23 @@ pub const Application = extern struct { log.debug("style manager changed scheme={}", .{color_scheme}); } + fn handleReloadConfig( + _: *ConfigErrorsDialog, + self: *Self, + ) callconv(.c) void { + // We clear our dialog reference because its going to close + // after response handling and we don't want to reuse it. + const priv = self.private(); + priv.config_errors_dialog.set(null); + + self.reloadConfig() catch |err| { + // If we fail to reload the configuration, then we want the + // user to know it. For now we log but we should show another + // GUI. + log.warn("error reloading config: {}", .{err}); + }; + } + /// Show the config errors dialog if the config on our application /// has diagnostics. fn showConfigErrorsDialog(self: *Self) void { @@ -551,10 +675,24 @@ pub const Application = extern struct { // No dialog yet, initialize a new one. There's no need to unref // here because the widget that it becomes a part of takes ownership. const dialog: *ConfigErrorsDialog = .new(priv.config); - dialog.present(null); priv.config_errors_dialog.set(dialog); + + // Connect to the reload signal so we know to reload our config. + _ = ConfigErrorsDialog.signals.@"reload-config".connect( + dialog, + *Application, + handleReloadConfig, + self, + .{}, + ); + + // Show it + dialog.present(null); } + //---------------------------------------------------------------- + // Boilerplate/Noise + fn allocator(self: *Self) std.mem.Allocator { return self.private().core_app.alloc; } @@ -606,6 +744,35 @@ pub const Application = extern struct { }; }; +/// All apprt action handlers +const Action = struct { + pub fn configChange( + self: *Application, + target: apprt.Target, + new_config: *const CoreConfig, + ) !void { + // Wrap our config in a GObject. This will clone it. + const alloc = self.allocator(); + const config_obj: *Config = try .new(alloc, new_config); + errdefer config_obj.unref(); + + switch (target) { + // TODO: when we implement surfaces in gtk-ng + .surface => @panic("TODO"), + + .app => { + // Set it on our private + const priv = self.private(); + priv.config.unref(); + priv.config = config_obj; + + // Show our errors if we have any + self.showConfigErrorsDialog(); + }, + } + } +}; + /// This sets various GTK-related environment variables as necessary /// given the runtime environment or configuration. /// diff --git a/src/apprt/gtk-ng/weak_ref.zig b/src/apprt/gtk-ng/weak_ref.zig index a31b9e020..7ee5cf730 100644 --- a/src/apprt/gtk-ng/weak_ref.zig +++ b/src/apprt/gtk-ng/weak_ref.zig @@ -12,8 +12,12 @@ pub fn WeakRef(comptime T: type) type { /// Set the weak reference to the given object. This will not /// increase the reference count of the object. - pub fn set(self: *Self, v: *T) void { - self.ref.set(v.as(gobject.Object)); + pub fn set(self: *Self, v_: ?*T) void { + if (v_) |v| { + self.ref.set(v.as(gobject.Object)); + } else { + self.ref.set(null); + } } /// Get a strong reference to the object, or null if the object From 039e248ec9528e3fc3b3fbc2499f8e1fcaff443c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 09:44:25 -0700 Subject: [PATCH 09/14] typos --- src/apprt/gtk-ng/class/config.zig | 4 ++-- typos.toml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index e7022f5ec..bad4b2aff 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -18,7 +18,7 @@ const log = std.log.scoped(.gtk_ghostty_config); /// this, copy any memory they require, and own that structure instead. /// /// This can also expose helpers to access configuration in ways that -/// may be more egonomic to GTK primitives. +/// may be more ergonomic to GTK primitives. pub const Config = extern struct { const Self = @This(); parent_instance: Parent, @@ -36,7 +36,7 @@ pub const Config = extern struct { Self, ?*gtk.TextBuffer, .{ - .nick = "Dignostics Buffer", + .nick = "Diagnostics Buffer", .blurb = "A TextBuffer that contains the diagnostics.", .default = null, .accessor = .{ diff --git a/typos.toml b/typos.toml index 1fb54ecc6..0d58ea654 100644 --- a/typos.toml +++ b/typos.toml @@ -51,6 +51,8 @@ DECID = "DECID" flate = "flate" typ = "typ" kend = "kend" +# GTK +GIR = "GIR" [type.po] extend-glob = ["*.po"] From cac32fc60dcea3c83aaab6ea11768e8ab3603ca1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 09:49:17 -0700 Subject: [PATCH 10/14] apprt/gtk-ng: hook up adw < 1.5 support --- src/apprt/gtk-ng/build/gresource.zig | 1 + .../gtk-ng/class/config_errors_dialog.zig | 14 +++++++--- .../gtk-ng/ui/1.2/config-errors-dialog.blp | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index 433c504da..f38e73b01 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -30,6 +30,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 }; /// /// These will be asserted to exist at runtime. pub const blueprints: []const Blueprint = &.{ + .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 5, .name = "window" }, }; diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index 46ec5671d..ad04e182a 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -74,14 +74,16 @@ pub const ConfigErrorsDialog = extern struct { pub fn present(self: *Self, parent: ?*gtk.Widget) void { switch (Parent) { adw.AlertDialog => self.as(adw.Dialog).present(parent), - else => unreachable, + adw.MessageDialog => self.as(gtk.Window).present(), + else => comptime unreachable, } } pub fn close(self: *Self) void { switch (Parent) { adw.AlertDialog => self.as(adw.Dialog).forceClose(), - else => unreachable, + adw.MessageDialog => self.as(gtk.Window).close(), + else => comptime unreachable, } } @@ -160,7 +162,13 @@ pub const ConfigErrorsDialog = extern struct { .name = "config-errors-dialog", }), - else => unreachable, + adw.MessageDialog => comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "config-errors-dialog", + }), + + else => comptime unreachable, }, ); diff --git a/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp b/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp new file mode 100644 index 000000000..7c16c3363 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp @@ -0,0 +1,27 @@ +using Gtk 4.0; +using Adw 1; + +template $GhosttyConfigErrorsDialog: Adw.MessageDialog { + heading: _("Configuration Errors"); + body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors."); + + responses [ + ignore: _("Ignore"), + reload: _("Reload Configuration") suggested, + ] + + extra-child: ScrolledWindow { + min-content-width: 500; + min-content-height: 100; + + TextView { + editable: false; + cursor-visible: false; + top-margin: 8; + bottom-margin: 8; + left-margin: 8; + right-margin: 8; + buffer: bind (template.config as <$GhosttyConfig>).diagnostics-buffer; + } + }; +} From 562bd7e45866b528929cf3790c6a3da23d38c27d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 09:51:36 -0700 Subject: [PATCH 11/14] comments --- src/apprt/gtk-ng/class/application.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index d6ebe7c40..bbe3bfcb5 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -80,6 +80,8 @@ pub const Application = extern struct { running: bool = false, /// If non-null, we're currently showing a config errors dialog. + /// This is a WeakRef because the dialog can close on its own + /// outside of our own lifecycle and that's okay. config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{}, var offset: c_int = 0; From b2fe9fd7db976f9a1968d3c76a11d280e795147b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 10:00:05 -0700 Subject: [PATCH 12/14] apprt/gtk-ng: fix merge conflict --- src/apprt/gtk-ng/class/application.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index bbe3bfcb5..fc6f574d5 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -779,7 +779,7 @@ const Action = struct { /// given the runtime environment or configuration. /// /// This must be called BEFORE GTK initialization. -fn setGtkEnv(config: *const Config) error{NoSpaceLeft}!void { +fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void { assert(gtk.isInitialized() == 0); var gdk_debug: struct { From 8556877883fcdc6ed61444f9f1c81b15f143835c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 11:42:54 -0700 Subject: [PATCH 13/14] Add valgrind suppression file --- src/apprt/gtk-ng/class/application.zig | 4 +- .../gtk-ng/class/config_errors_dialog.zig | 8 +- src/apprt/gtk-ng/class/window.zig | 14 + valgrind.supp | 1915 +++++++++++++++++ 4 files changed, 1936 insertions(+), 5 deletions(-) create mode 100644 valgrind.supp diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index fc6f574d5..169dc1530 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -585,8 +585,8 @@ pub const Application = extern struct { self.as(Parent), ); - // const win = Window.new(self); - // gtk.Window.present(win.as(gtk.Window)); + const win = Window.new(self); + gtk.Window.present(win.as(gtk.Window)); } fn dispose(self: *Self) callconv(.C) void { diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index ad04e182a..13cc56026 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -101,15 +101,17 @@ pub const ConfigErrorsDialog = extern struct { } fn dispose(self: *Self) callconv(.C) void { - log.warn("DISPOSE", .{}); - gtk.Widget.disposeTemplate(self.as(gtk.Widget), getGObjectType()); - const priv = self.private(); if (priv.config) |v| { v.unref(); priv.config = null; } + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 1c6619264..f6f71abb7 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -33,6 +33,18 @@ pub const Window = extern struct { gtk.Widget.initTemplate(self.as(gtk.Widget)); } + fn dispose(self: *Self) callconv(.C) void { + gtk.Widget.disposeTemplate( + self.as(gtk.Widget), + getGObjectType(), + ); + + gobject.Object.virtual_methods.dispose.call( + Class.parent, + self.as(Parent), + ); + } + pub fn as(self: *Self, comptime T: type) *T { return gobject.ext.as(T, self); } @@ -59,6 +71,8 @@ pub const Window = extern struct { .name = "window", }), ); + + gobject.Object.virtual_methods.dispose.implement(class, &dispose); } pub fn as(class: *Class, comptime T: type) *T { diff --git a/valgrind.supp b/valgrind.supp new file mode 100644 index 000000000..bb319b350 --- /dev/null +++ b/valgrind.supp @@ -0,0 +1,1915 @@ +# This Valgrind suppression file contains the suppressions necessary +# to run Ghostty with GTK under Valgrind. There may be some false suppressions +# here so we should scrutinize this file periodically. +# +# To run Ghostty under Valgrind: +# +# valgrind \ +# --leak-check=full \ +# --num-callers=50 \ +# --suppressions=valgrind.supp \ +# ./zig-out/bin/ghostty +# +# You must gracefully exit Ghostty (do not SIGINT) by closing all windows +# and quitting. Otherwise, we leave a number of GTK resources around. + +# Weird gtk_tooltip_init leak I can't figure out +{ + Non-builder tooltip create + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gtk_at_context_init + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_valist + fun:g_object_new + fun:gtk_at_spi_create_context + fun:gtk_at_context_create + fun:gtk_widget_init + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_with_properties + fun:g_object_new + fun:gtk_tooltip_init + ... +} + +{ + Not sure about this one, I can't figure it out. + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gtk_accessible_attribute_set_new + fun:gtk_at_context_init + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_valist + fun:g_object_new + fun:gtk_at_spi_create_context + fun:gtk_at_context_create + fun:gtk_widget_init + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_with_properties + fun:g_object_new + fun:_gtk_builder_construct + fun:builder_construct + ... + fun:_gtk_buildable_parser_replay_precompiled + fun:_gtk_builder_parser_parse_buffer + fun:gtk_builder_extend_with_template + fun:gtk_widget_init_template + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_with_properties + fun:g_object_new + fun:gtk_tooltip_init + ... +} + +{ + GTK init + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gtk_init +} + +{ + GTK init + Memcheck:Leak + match-leak-kinds: possible + ... + fun:gtk_init +} + +{ + GTK FontConfig data is never freed + Memcheck:Leak + match-leak-kinds: possible + ... + fun:FcConfigInit + fun:fc_thread_func + fun:g_thread_proxy + fun:start_thread + fun:clone +} + +{ + GTK EGL resources never freed + Memcheck:Leak + match-leak-kinds: possible + ... + fun:__eglLoadVendors + fun:eglQueryString + fun:epoxy_has_egl_extension + fun:gdk_display_init_egl +} + +{ + GTK EGL dlopen never freed + Memcheck:Leak + match-leak-kinds: possible + ... + fun:dlopen@@GLIBC_* + fun:get_dlopen_handle.part.0 + fun:epoxy_has_egl + fun:gdk_display_init_egl +} + +{ + libgl init never frees + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:RegisterStubCallbacks + fun:__glDispatchRegisterStubCallbacks + fun:__libGLInit + ... +} + +{ + pango font map + Memcheck:Leak + match-leak-kinds: possible + fun:calloc + fun:g_malloc0 + fun:g_rc_box_alloc_full + fun:pango_fc_font_map_load_fontset + ... +} + +{ + Adwaita Stylesheet Load + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:g_malloc0 + fun:gtk_css_value_alloc + fun:_gtk_css_reference_value_new + fun:parse_ruleset + fun:gtk_css_provider_load_internal + fun:gtk_css_provider_load_from_file + fun:gtk_css_provider_load_from_resource + fun:update_stylesheet + fun:g_object_new_internal.part.0 + fun:g_object_new_valist + fun:g_object_new +} + +# Mesa leaks all sorts of stuff that we can't directly control, +# so we ignore all of that. + +{ + Mesa + Memcheck:Leak + match-leak-kinds: possible + ... + fun:_mesa_* +} + +{ + Mesa + Memcheck:Leak + match-leak-kinds: possible + ... + fun:mesa_* +} + +{ + Graphics Driver dri + Memcheck:Leak + match-leak-kinds: possible + fun:*alloc + ... + fun:dri* +} + +{ + Radeon + Memcheck:Leak + match-leak-kinds: possible + fun:*alloc + ... + fun:radeonsi_* +} + +{ + Mesa Shader + Memcheck:Leak + match-leak-kinds: possible + ... + fun:si_* +} + +#-------------------------------------------------------------------- +# GTK +#-------------------------------------------------------------------- + +# Actual GTK things +{ + GtkWidgetClass action GPtrArray + Memcheck:Leak + fun:malloc + fun:g_malloc + fun:g_slice_alloc + fun:g_ptr_array_sized_new + fun:g_ptr_array_new + fun:gtk_widget_class_add_action +} + +{ + GIO modules + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + ... + fun:_g_io_module_get_default +} + +{ + GTK media extension gio modules + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + ... + fun:g_io_module_new + ... + fun:gtk_media_file_extension_init +} + +# AMD driver +{ + radeonsi_dri general + Memcheck:Leak + fun:calloc + ... + obj:/usr/lib*/dri/radeonsi_dri.so +} +{ + radeonsi_dri general + Memcheck:Leak + fun:malloc + ... + obj:/usr/lib*/dri/radeonsi_dri.so +} + +# mesa driver stuff +{ + i965 addr4 + Memcheck:Addr4 + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + i965 addr8 + Memcheck:Addr8 + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + i965 memcpy + Memcheck:Addr8 + fun:memcpy* + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + i965 memcpy + Memcheck:Addr2 + fun:memcpy* + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + mesa memcmp 8 + Memcheck:Addr8 + fun:*memcmp* + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + mesa memcmp 1 + Memcheck:Addr1 + fun:*memcmp* + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + mesa memset 8 + Memcheck:Addr8 + fun:*memset* + obj:/usr/lib*/dri/i965_dri.so +} + +{ + mesa realpath + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:realpath@@GLIBC_2.3 + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_eglInitialize_global_rewrite_ptr +} + +{ + mesa calloc + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_eglInitialize_global_rewrite_ptr +} + +{ + epoxy strncmp + Memcheck:Addr8 + fun:strncmp + ... + fun:epoxy_eglInitialize_global_rewrite_ptr +} + +{ + mesa malloc + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + obj:/usr/lib*/dri/i965_dri.so* +} + +{ + mesa glReadPixels + Memcheck:Addr16 + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_glReadPixels_global_rewrite_ptr +} + +{ + epoxy glxQueryServerString 1 + Memcheck:Leak + fun:malloc + fun:XextAddDisplay + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_glXQueryServerString_global_rewrite_ptr + +} + +{ + epoxy glxQueryServerString 2 + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:realpath* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_glXQueryServerString_global_rewrite_ptr +} + +{ + epoxy glGetTexImage + Memcheck:Addr16 + obj:* + obj:* + obj:* + obj:* + obj:* + fun:epoxy_glGetTexImage_global_rewrite_ptr +} + + + + +# Fontconfig +{ + FcFontSetList + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcFontSetList +} + +{ + FcPatternObjectInsertElt + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:FcPatternObjectInsertElt +} + +{ + FcPatternObjectInsertElt2 + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + fun:FcPatternObjectInsertElt +} + +{ + FcFontRenderPrepare + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcFontRenderPrepare +} + +{ + FcDefaultSubstitute + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcDefaultSubstitute +} + +{ + FcDefaultSubstituteWithPat + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcDefaultSubstituteWithPat +} + +{ + FcConfigSubstituteWithPat + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:FcConfigSubstituteWithPat +} + +# Pixman +{ + pixman_image_composite32 + Memcheck:Cond + obj:/usr/lib*/libpixman-1.so* + obj:/usr/lib*/libpixman-1.so* + fun:pixman_image_composite32 +} + +# Pango +{ + pango 1 + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libcairo.so* + fun:pango_cairo_fc_font_map_fontset_key_substitute +} + +{ + pango 2 + Memcheck:Leak + fun:realloc + obj:/usr/lib*/libfontconfig.so* + obj:/usr/lib*/libfontconfig.so* + fun:_cairo_ft_font_options_substitute +} + +# GLib +{ + glib GQuark + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:g_quark_* +} +{ + glib GQuark + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:g_intern_static_string +} +{ + glib GQuark + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:g_intern_string +} +{ + xdg-mime init + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:xdg_mime_init* +} +{ + xdg-mime init + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + ... + fun:xdg_mime_init* +} +{ + glib init + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:glib_init_ctor +} + +# Threads +{ + pthread + Memcheck:Leak + fun:calloc + fun:_dl_allocate_tls +} + +#-------------------------------------------------------------------- +# GLib +#-------------------------------------------------------------------- + +# GLib Valgrind suppressions file +# +# This provides a list of suppressions for all of GLib (including GIO), for all +# Valgrind tools (memcheck, drd, helgrind, etc.) for the false positives and +# deliberate one-time leaks which GLib causes to be reported when running under +# Valgrind. +# +# When running an application which links to GLib under Valgrind, you can pass +# this suppression file to Valgrind using --suppressions=/path/to/glib-2.0.supp. +# +# http://valgrind.org/docs/manual/manual-core.html#manual-core.suppress +# +# Note that there is currently no way for Valgrind to load this automatically +# (https://bugs.kde.org/show_bug.cgi?id=160905), so the best GLib can currently +# do is to install this file as part of its development package. +# +# This file should be updated if GLib introduces a new deliberate one-time leak, +# or another false race positive in Valgrind: please file bugs at: +# +# https://gitlab.gnome.org/GNOME/glib/issues/new + +{ + gnutls-init-calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:gtls_gnutls_init +} + +{ + gnutls-init-realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:gtls_gnutls_init +} + +{ + g-tls-backend-gnutls-init + Memcheck:Leak + match-leak-kinds:reachable + fun:g_once_impl + fun:g_tls_backend_gnutls_init +} + +{ + p11-tokens-init + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:create_tokens_inlock + fun:initialize_module_inlock_reentrant +} + +# One-time allocation from libc for getpwnam() results +{ + g-local-vfs-getpwnam + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:getpwnam + fun:g_local_vfs_parse_name +} + +{ + glib-init-malloc + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_quark_init +} + +{ + glib-init-calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_quark_init +} + +{ + gobject-init-malloc + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:gobject_init* +} + +{ + gobject-init-realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:gobject_init* +} + +{ + gobject-init-calloc + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:calloc + ... + fun:gobject_init* +} + +{ + g-type-register-dynamic + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_type_register_dynamic +} + +{ + g-type-register-static + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:malloc + ... + fun:g_type_register_static +} + +{ + g-type-register-static-realloc + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:realloc + ... + fun:g_type_register_static +} + +{ + g-type-register-static-calloc + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:calloc + ... + fun:g_type_register_static +} + +{ + g-type-register-fundamental + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:malloc + ... + fun:g_type_register_fundamental +} + +{ + g-type-register-fundamental-calloc + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:calloc + ... + fun:g_type_register_fundamental +} + +{ + g-type-add-interface-dynamic + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_type_add_interface_dynamic +} + +{ + g-type-add-interface-static + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_type_add_interface_static +} + +{ + g-type-add-interface-static-realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:g_type_add_interface_static +} + +{ + g-type-add-interface-static-calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_type_add_interface_static +} + +{ + g-test-rand-init + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_rand_new_with_seed_array + fun:test_run_seed + ... + fun:g_test_run +} + +{ + g-rand-init2 + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_rand_new_with_seed_array + ... + fun:get_global_random +} + +{ + g-quark-table-new + Memcheck:Leak + match-leak-kinds:reachable + fun:g_hash_table_new + ... + fun:quark_new +} + +{ + g-quark-table-resize + Memcheck:Leak + match-leak-kinds:reachable + ... + fun:g_hash_table_resize + ... + fun:quark_new +} + +{ + g-type-interface-init + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:type_iface_vtable_base_init_Wm +} + +{ + g-type-class-init-calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:type_class_init_Wm +} + +{ + g-type-class-init + Memcheck:Leak + match-leak-kinds:reachable + fun:g_type_create_instance + ... + fun:type_class_init_Wm +} + +{ + g-object-do-class-init-signals + Memcheck:Leak + match-leak-kinds:reachable + ... + fun:g_signal_new + ... + fun:type_class_init_Wm +} + +{ + g-type-prerequisites + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:type_iface_add_prerequisite_W +} + +{ + g-type-add-interface-check + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_type_add_interface_check + ... + fun:type_class_init_Wm +} + +{ + g-type-add-interface-check-realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:g_type_add_interface_check + ... + fun:type_class_init_Wm +} + +{ + g-object-class-install-property + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:validate_and_install_class_property + ... + fun:type_class_init_Wm +} + +{ + g-param-spec-pool-new + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_param_spec_pool_new + ... + fun:type_class_init_Wm +} + +# weak_locations_lock in gobject.c +{ + g-weak-ref-lock + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_rw_lock_get_impl + ... + fun:g_weak_ref_set +} + +{ + g-object-base-class-init-construct-pproperties + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_slist_copy + fun:g_object_base_class_init + fun:type_class_init_Wm +} + +{ + g-type-class-ref + Memcheck:Leak + fun:calloc + ... + fun:type_class_init_Wm + ... + fun:g_type_class_ref +} + +{ + g-type-class-ref-inlined + Memcheck:Leak + fun:calloc + ... + fun:UnknownInlinedFun + ... + fun:g_type_class_ref +} + +{ + g-io-module-default-singleton-malloc + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_type_create_instance + ... + fun:_g_io_module_get_default +} + +{ + g-io-module-default-singleton-calloc + Memcheck:Leak + match-leak-kinds:reachable,definite + fun:calloc + ... + fun:g_type_create_instance + ... + fun:_g_io_module_get_default* +} + +# This one seems to show up sometimes with g_type_create_instance() at the top +# of the stack, as well. +{ + g-io-module-default-singleton + Memcheck:Leak + match-leak-kinds:reachable + fun:g_type_create_instance + ... + fun:_g_io_module_get_default +} + +{ + g-io-module-default-singleton-module + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_module_open + ... + fun:_g_io_module_get_default +} + +{ + g-io-module-default-singleton-name + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_strdup + ... + fun:_g_io_module_get_default* +} + +{ + g-io-module-default-singleton-weak-ref + Memcheck:Leak + fun:calloc + ... + fun:_g_io_module_get_default +} + +{ + g-get-language-names-malloc + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_get_language_names +} + +{ + g-get-language-names-calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_get_language_names +} + +{ + g-get-language_names-with-category-malloc + Memcheck:Leak + match-leak-kinds:possible,reachable,definite + fun:malloc + ... + fun:g_get_language_names_with_category +} + +{ + g-get-language_names-with-category-calloc + Memcheck:Leak + match-leak-kinds:possible,reachable,definite + fun:calloc + ... + fun:g_get_language_names_with_category +} + +{ + g-get-language_names-with-category-realloc + Memcheck:Leak + match-leak-kinds:possible,reachable,definite + fun:realloc + ... + fun:g_get_language_names_with_category +} + +{ + g-static-mutex + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_static_mutex_get_mutex_impl +} + +{ + g-system-thread-init + Memcheck:Leak + match-leak-kinds:possible,reachable + fun:calloc + ... + fun:g_system_thread_new +} + +{ + g-system-thread-init-malloc + Memcheck:Leak + match-leak-kinds:possible,reachable,definite + fun:malloc + ... + fun:g_system_thread_new +} + +{ + g-task-thread-pool-init + Memcheck:Leak + match-leak-kinds:possible,reachable,definite + fun:malloc + ... + fun:g_thread_new + ... + fun:g_task_thread_pool_init +} + +{ + g-io-module-default-proxy-resolver-gnome + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_proxy_resolver_gnome_init + ... + fun:_g_io_module_get_default +} + +# One-time getaddrinfo() configuration loading +{ + g-threaded-resolver-getaddrinfo-config + Memcheck:Leak + match-leak-kinds:reachable,definite + fun:malloc + ... + fun:__resolv_conf_allocate + ... + fun:getaddrinfo + fun:do_lookup_by_name +} + +# memcheck checks that the third argument to ioctl() is a valid pointer, but +# some ioctls use that argument as an integer +{ + ioctl-with-non-pointer-param + Memcheck:Param + ioctl(generic) + fun:ioctl + fun:btrfs_reflink_with_progress +} + +{ + g-private-get + drd:ConflictingAccess + fun:g_private_get +} +{ + g-private-get-helgrind + Helgrind:Race + fun:g_private_get +} + + +{ + g-private-set + drd:ConflictingAccess + fun:g_private_set +} +{ + g-private-set-helgrind + Helgrind:Race + fun:g_private_set +} + +{ + g-type-construct-free + drd:ConflictingAccess + fun:g_type_free_instance +} +{ + g-type-construct-free-helgrind + Helgrind:Race + fun:g_type_free_instance +} + +{ + g-variant-unref + drd:ConflictingAccess + fun:g_variant_unref +} +{ + g-variant-unref-helgrind + Helgrind:Race + fun:g_variant_unref +} + +{ + g-unix-signals-main + drd:ConflictingAccess + fun:_g_main_create_unix_signal_watch +} +{ + g-unix-signals-dispatch + drd:ConflictingAccess + ... + fun:dispatch_unix_signals* +} +{ + g-unix-signals-dispatch-helgrind + Helgrind:Race + ... + fun:dispatch_unix_signals* +} +{ + g-unix-signals-other + drd:ConflictingAccess + fun:g_unix_signal_watch* +} +{ + g-unix-signals-other-helgrind + Helgrind:Race + fun:g_unix_signal_watch* +} +{ + g-unix-signals-handler + drd:ConflictingAccess + fun:g_unix_signal_handler* +} +{ + g-unix-signals-handler-helgrind + Helgrind:Race + fun:g_unix_signal_handler* +} +{ + g-unix-signals-worker + drd:ConflictingAccess + fun:glib_worker_main +} +{ + g-unix-signals-worker-helgrind + Helgrind:Race + fun:glib_worker_main +} + +{ + g-wakeup-acknowledge + drd:ConflictingAccess + fun:read + fun:g_wakeup_acknowledge +} + +{ + g-type-fundamental + drd:ConflictingAccess + fun:g_type_fundamental +} +{ + g-type-fundamental-helgrind + Helgrind:Race + fun:g_type_fundamental +} +{ + g-type-class-peek-static + drd:ConflictingAccess + fun:g_type_class_peek_static +} +{ + g-type-class-peek-static-helgrind + Helgrind:Race + fun:g_type_class_peek_static +} +{ + g-type-is-a + drd:ConflictingAccess + ... + fun:g_type_is_a +} +{ + g-type-is-a-helgrind + Helgrind:Race + ... + fun:g_type_is_a +} + +{ + g-inet-address-get-type + drd:ConflictingAccess + fun:g_inet_address_get_type +} +{ + g-inet-address-get-type-helgrind + Helgrind:Race + fun:g_inet_address_get_type +} + +# From: https://github.com/fredericgermain/valgrind/blob/HEAD/glibc-2.X-drd.supp +{ + drd-libc-stdio + drd:ConflictingAccess + obj:*/lib*/libc-* +} +{ + drd-libc-recv + drd:ConflictingAccess + fun:recv +} +{ + drd-libc-send + drd:ConflictingAccess + fun:send +} + +# GSources do an opportunistic ref count check +{ + g-source-set-ready-time + drd:ConflictingAccess + fun:g_source_set_ready_time +} +{ + g-source-set-ready-time-helgrind + Helgrind:Race + fun:g_source_set_ready_time +} + +{ + g-source-iter-next + Helgrind:Race + fun:g_source_iter_next + fun:g_main_context_* + fun:g_main_context_iterate +} + +{ + g-object-instance-private + drd:ConflictingAccess + fun:*_get_instance_private +} +{ + g-object-instance-private-helgrind + Helgrind:Race + fun:*_get_instance_private +} + +# GLib legitimately calls pthread_cond_signal without a mutex held +{ + g-task-thread-complete + drd:CondErr + ... + fun:g_cond_signal + fun:g_task_thread_complete +} +{ + g-task-thread-complete + Helgrind:Misc + ... + fun:g_cond_signal + fun:g_task_thread_complete +} + +# False positive, but I can't explain how (FIXME) +{ + g-task-cond + Helgrind:Misc + ... + fun:g_cond_clear + fun:g_task_finalize +} + +# Real race, but is_cancelled() is an opportunistic function anyway +{ + g-cancellable-is-cancelled + Helgrind:Race + fun:g_cancellable_is_cancelled +} + +# False positive +{ + g-main-context-cond + Helgrind:Misc + ... + fun:g_cond_clear + fun:g_main_context_unref +} + +# False positives +{ + g-source-unlocked + Helgrind:Race + fun:g_source_*_unlocked +} +{ + g-source-internal + Helgrind:Race + fun:g_source_*_internal +} + +# False positive +{ + g_object_real_dispose + Helgrind:Race + fun:g_object_real_dispose +} + +# False positive +{ + g_object_new_valist + Helgrind:Race + ... + fun:g_object_new_valist +} + +# g_set_user_dirs() deliberately leaks the previous cached g_get_user_*() values. +# These will not all be reachable on exit. +{ + g_set_user_dirs_str + Memcheck:Leak + match-leak-kinds:definite,reachable,possible + fun:malloc + ... + fun:set_str_if_different + fun:g_set_user_dirs +} + +# g_set_user_dirs() deliberately leaks the previous cached g_get_user_*() values. +# These will not all be reachable on exit. +{ + g_set_user_dirs_strv + Memcheck:Leak + match-leak-kinds:definite,reachable,possible + fun:malloc + ... + fun:set_strv_if_different + fun:g_set_user_dirs +} + +# _g_unset_cached_tmp_dir() deliberately leaks the previous cached g_get_tmp_dir() values. +# These will not all be reachable on exit. +{ + g_get_tmp_dir_test_init + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_get_tmp_dir + ... + fun:g_test_init +} + +# g_get_tmp_dir() caches a one-time allocation +{ + g_get_tmp_dir + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_get_tmp_dir +} + +# g_get_system_data_dirs() caches a one-time allocation +{ + g_get_system_data_dirs + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_build_system_data_dirs + fun:g_get_system_data_dirs +} + +# g_get_user_data_dir() caches a one-time allocation +{ + g_get_user_data_dir + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:realloc + ... + fun:g_build_user_data_dir + fun:g_get_user_data_dir +} + +# g_get_home_dir() caches a one-time allocation +{ + g_get_home_dir + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_build_home_dir + fun:g_get_home_dir +} + +# gcontenttype-fdo.c caches a one-time allocation global array of @global_mime_dirs. +{ + content_type_mime_dirs_realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:_g_content_type_set_mime_dirs_locked +} + +# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs. +{ + desktop_file_dirs_malloc + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:desktop_file_dirs_lock +} + +# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs. +{ + desktop_file_dirs_realloc + Memcheck:Leak + match-leak-kinds:reachable + fun:realloc + ... + fun:desktop_file_dirs_lock +} + +# gdesktopappinfo.c caches a one-time allocation global table of @desktop_file_dirs. +{ + desktop_file_dir_unindexed_setup_search + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:desktop_file_dir_unindexed_setup_search + fun:desktop_file_dir_unindexed_setup_search +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_data_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_data_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_filename + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_filename +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_home_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_home_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_path + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_path +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_system_config_dirs + Memcheck:Leak + match-leak-kinds:definite + fun:realloc + ... + fun:g_build_system_config_dirs +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_system_data_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_system_data_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_system_data_dirs + Memcheck:Leak + match-leak-kinds:definite + fun:realloc + ... + fun:g_build_system_data_dirs +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_cache_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_cache_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_config_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_config_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_data_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_data_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_runtime_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_runtime_dir +} + +#gutils.c caches system and user dirs and may need to replace them during tests. +{ + g_build_user_state_dir + Memcheck:Leak + match-leak-kinds:definite + fun:malloc + ... + fun:g_build_user_state_dir +} + +# g_io_extension_point_register() caches a one-time allocation global table of @extension_points. +{ + g_io_extension_point_register + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_io_extension_point_register +} + +# g_strerror() caches a one-time allocation global table of @errors. +{ + g_strerror + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_locale_to_utf8 + fun:g_strerror +} + +# g_socket_connection_factory_register_type() caches a one-time allocation global table of @connection_types. +{ + g_socket_connection_factory_register_type + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_socket_connection_factory_register_type +} + +# g_dbus_error_quark() never unregisters itself as a GDBusError domain, as it’s always available +{ + g_dbus_error_quark + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_dbus_error_register_error_domain + fun:g_dbus_error_quark +} + +# g_win32_registry_get_os_dirs_w*() caches an array of strings that is allocated only once. +{ + g_win32_registry_get_os_dirs + Memcheck:Leak + match-leak-kinds:reachable,definite + fun:malloc + ... + fun:g_win32_registry_get_os_dirs* +} + +# Thread-private data allocated once per thread +{ + g_private_set_alloc0 + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_private_set_alloc0 +} +{ + g_private_set_alloc0-calloc + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:calloc + ... + fun:g_private_set_alloc0 +} + +# Keys for thread-private data +{ + g_private_key + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + fun:g_private_impl_new +} + +# Thread-private GMainContext stack +{ + g_main_context_push_thread_default + Memcheck:Leak + match-leak-kinds:definite,reachable + fun:malloc + ... + fun:g_queue_new + fun:g_main_context_push_thread_default +} + +# One-time allocations for #GFileInfo attribute cache +{ + g_file_info_attribute_cache + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:ensure_attribute_hash + ... + fun:g_file_* +} +{ + g_file_info_attribute_cache2 + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:ensure_attribute_hash + ... + fun:g_file_* +} +{ + g_file_info_attribute_cache3 + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:lookup_namespace + ... + fun:g_file_* +} +{ + g_file_info_attribute_cache4 + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:lookup_namespace + ... + fun:g_file_* +} + +# Cached charset +{ + g_get_charset + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_get_charset +} + +{ + g_get_charset_calloc + Memcheck:Leak + match-leak-kinds:reachable + fun:calloc + ... + fun:g_get_charset +} + +# Global unused thread queue +{ + g_thread_pool_unused_thread_queue + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_async_queue_new_full + ... + fun:g_thread_pool_new +} + +# One-time program name storage +{ + g_set_prgname + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_set_prgname +} + +# Error domains hash +{ + g_error_init + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + ... + fun:g_hash_table_new_full + fun:g_error_init +} + +# Error domain static registration +{ + g_error_domain_register_static + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + ... + fun:g_hash_table_insert + fun:error_domain_register + fun:g_error_domain_register_static +} + +{ + new_quark + Memcheck:Leak + match-leak-kinds:reachable + fun:malloc + ... + fun:g_hash_table_insert + fun:quark_new +} + +{ + xdg_mime_init_malloc + Memcheck:Leak + fun:malloc + ... + fun:xdg_mime_init +} + +{ + xdg_mime_init_calloc + Memcheck:Leak + fun:calloc + ... + fun:xdg_mime_init +} + +# One-time allocations for default log writer lock and domains +{ + should_drop_message_rw_lock + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:g_rw_lock_impl_new + fun:g_rw_lock_get_impl + fun:g_rw_lock_reader_lock + fun:should_drop_message +} + +{ + should_drop_message_strdup + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:g_malloc + fun:g_strdup + fun:g_strdup_inline + fun:should_drop_message +} + +{ + g_log_writer_default_set_debug_strdup + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:g_malloc + fun:g_strdup_inline + fun:g_log_writer_default_set_debug_domains +} + +{ + g_log_writer_default_set_debug_rw_lock + Memcheck:Leak + match-leak-kinds: reachable + fun:malloc + fun:g_rw_lock_impl_new + fun:g_rw_lock_get_impl + fun:g_rw_lock_writer_lock + fun:g_log_writer_default_set_debug_domains +} + +# This can be removed when versions of valgrind including the fix are widely used. +# See https://gitlab.gnome.org/GNOME/glib/-/issues/3292 +{ + g_utf8_collate_key wcsxfrm false-positive + Memcheck:Addr32 + ... + fun:wcsxfrm* + fun:g_utf8_collate_key +} + +# sysprof deliberately leaks one SysprofCollector per thread +{ + glib-trace-collector + Memcheck:Leak + match-leak-kinds:definite + fun:calloc + ... + fun:sysprof_collector_get + ... + fun:g_trace_mark +} From 9b99e41cb2b65837478526f113b07dacd36d3b68 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Jul 2025 13:17:51 -0700 Subject: [PATCH 14/14] apprt/gtk-ng: fix config textbuffer memory leak --- src/apprt/gtk-ng.zig | 5 +---- src/apprt/gtk-ng/class.zig | 26 ++++++++++++++++++++++++++ src/apprt/gtk-ng/class/application.zig | 4 ++-- src/apprt/gtk-ng/class/config.zig | 4 ++++ valgrind.supp | 14 ++++++++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/apprt/gtk-ng/class.zig diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index 2ca01c4ab..de9255fe9 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -6,10 +6,7 @@ pub const Surface = @import("gtk-ng/Surface.zig"); pub const resourcesDir = internal_os.resourcesDir; // The exported API, custom for the apprt. -pub const Application = @import("gtk-ng/class/application.zig").Application; -pub const Window = @import("gtk-ng/class/window.zig").Window; -pub const Config = @import("gtk-ng/class/config.zig").Config; - +pub const class = @import("gtk-ng/class.zig"); pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef; test { diff --git a/src/apprt/gtk-ng/class.zig b/src/apprt/gtk-ng/class.zig new file mode 100644 index 000000000..c0d3d7c7c --- /dev/null +++ b/src/apprt/gtk-ng/class.zig @@ -0,0 +1,26 @@ +//! This files contains all the GObject classes for the GTK apprt +//! along with helpers to work with them. + +const glib = @import("glib"); +const gobject = @import("gobject"); + +pub const Application = @import("class/application.zig").Application; +pub const Window = @import("class/window.zig").Window; +pub const Config = @import("class/config.zig").Config; + +/// Unrefs the given GObject on the next event loop tick. +/// +/// This works around an issue with zig-object where dynamically +/// generated gobjects in property getters can't unref themselves +/// normally: https://github.com/ianprime0509/zig-gobject/issues/108 +pub fn unrefLater(obj: anytype) void { + _ = glib.idleAdd((struct { + fn callback(data_: ?*anyopaque) callconv(.c) c_int { + const remove = @intFromBool(glib.SOURCE_REMOVE); + const data = data_ orelse return remove; + const object: *gobject.Object = @ptrCast(@alignCast(data)); + object.unref(); + return remove; + } + }).callback, obj.as(gobject.Object)); +} diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 169dc1530..fc6f574d5 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -585,8 +585,8 @@ pub const Application = extern struct { self.as(Parent), ); - const win = Window.new(self); - gtk.Window.present(win.as(gtk.Window)); + // const win = Window.new(self); + // gtk.Window.present(win.as(gtk.Window)); } fn dispose(self: *Self) callconv(.C) void { diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig index bad4b2aff..68d3d9242 100644 --- a/src/apprt/gtk-ng/class/config.zig +++ b/src/apprt/gtk-ng/class/config.zig @@ -1,12 +1,15 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const adw = @import("adw"); +const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const configpkg = @import("../../../config.zig"); const CoreConfig = configpkg.Config; +const unrefLater = @import("../class.zig").unrefLater; + const log = std.log.scoped(.gtk_ghostty_config); /// Wraps a `Ghostty.Config` object in a GObject so it can be reference @@ -125,6 +128,7 @@ pub const Config = extern struct { text_buf.insertAtCursor("\n", 1); } + unrefLater(text_buf); // See unrefLater docs for why this is needed return text_buf; } diff --git a/valgrind.supp b/valgrind.supp index bb319b350..0e1a90f6a 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -140,6 +140,20 @@ ... } +{ + pango font map to cairgo + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + ... + fun:FcConfigValues + fun:FcConfigSubstituteWithPat + fun:FcConfigSubstitute + fun:pango_cairo_fc_font_map_fontset_key_substitute + fun:pango_fc_font_map_load_fontset + ... +} + { Adwaita Stylesheet Load Memcheck:Leak