diff --git a/src/apprt/gtk-ng.zig b/src/apprt/gtk-ng.zig index 19d450a54..de9255fe9 100644 --- a/src/apprt/gtk-ng.zig +++ b/src/apprt/gtk-ng.zig @@ -6,4 +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 class = @import("gtk-ng/class.zig"); +pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/apprt/gtk-ng/App.zig b/src/apprt/gtk-ng/App.zig index 0bd2c9603..7ce233359 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,14 +31,14 @@ pub fn init( ) !void { _ = opts; - const app: *GhosttyApplication = 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/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index db5c2cf6e..f38e73b01 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -30,6 +30,8 @@ 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.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 e713d1de2..fc6f574d5 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -15,11 +15,15 @@ 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 GhosttyWindow = @import("window.zig").GhosttyWindow; +const ApprtApp = @import("../App.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; const log = std.log.scoped(.gtk_ghostty_application); @@ -27,7 +31,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 @@ -47,12 +51,18 @@ 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 }, }); 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, @@ -69,10 +79,15 @@ pub const GhosttyApplication = extern struct { /// only be set by the main loop thread. 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; }; - /// 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. @@ -80,7 +95,10 @@ pub const GhosttyApplication = 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 @@ -97,30 +115,24 @@ 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 = 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 = try Config.default(alloc); + 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; }; - 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 +182,10 @@ pub const GhosttyApplication = extern struct { single_instance, }); + // Wrap our configuration in a GObject. + const config_obj: *Config = try .new(alloc, &config); + errdefer config_obj.unref(); + // Initialize the app. const self = gobject.ext.newInstance(Self, .{ .application_id = app_id.ptr, @@ -185,8 +201,11 @@ pub const GhosttyApplication = 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; + priv.* = .{ + .rt_app = rt_app, + .core_app = core_app, + .config = config_obj, + }; return self; } @@ -199,15 +218,14 @@ 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); } /// 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 @@ -254,7 +272,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 => {}, @@ -278,7 +296,7 @@ pub const GhosttyApplication = 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: { @@ -299,25 +317,108 @@ pub const GhosttyApplication = extern struct { } } - pub fn as(app: *Self, comptime T: type) *T { - return gobject.ext.as(T, app); + /// 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; } - pub fn unref(self: *Self) void { - gobject.Object.unref(self.as(gobject.Object)); + /// 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); } - fn private(self: *GhosttyApplication) *Private { - return gobject.ext.impl_helpers.getPrivate( - self, - Private, - Private.offset, - ); - } + //--------------------------------------------------------------- + // Virtual Methods - fn startup(self: *GhosttyApplication) callconv(.C) void { + fn startup(self: *Self) callconv(.C) void { log.debug("startup", .{}); + gio.Application.virtual_methods.startup.call( + Class.parent, + self.as(Parent), + ); + // Setup our event loop self.startupXev(); @@ -325,22 +426,34 @@ pub const GhosttyApplication = 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 {}; }; - 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. + self.showConfigErrorsDialog(); } /// 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; + const config = priv.config.get(); // If our backend is auto then we have no setup to do. if (config.@"async-backend" == .auto) return; @@ -368,9 +481,9 @@ 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; + const config = priv.config.get(); // Setup our initial light/dark const style = self.as(adw.Application).getStyleManager(); @@ -390,7 +503,7 @@ pub const GhosttyApplication = extern struct { // Setup color change notifications _ = gobject.Object.signals.notify.connect( style, - *GhosttyApplication, + *Self, handleStyleManagerDark, self, .{ .detail = "dark" }, @@ -407,9 +520,9 @@ 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; + const config = priv.config.get(); // If cgroup isolation isn't enabled then we don't do this. if (!switch (config.@"linux-cgroup") { @@ -463,10 +576,7 @@ pub const GhosttyApplication = extern struct { priv.transient_cgroup_base = path; } - 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. + fn activate(self: *Self) callconv(.C) void { log.debug("activate", .{}); // Call the parent activate method. @@ -475,11 +585,24 @@ pub const GhosttyApplication = extern struct { self.as(Parent), ); - const win = GhosttyWindow.new(self); - gtk.Window.present(win.as(gtk.Window)); + // const win = Window.new(self); + // gtk.Window.present(win.as(gtk.Window)); } - fn finalize(self: *GhosttyApplication) callconv(.C) void { + 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( Class.parent, @@ -487,10 +610,13 @@ pub const GhosttyApplication = extern struct { ); } + //--------------------------------------------------------------- + // Signal Handlers + fn handleStyleManagerDark( style: *adw.StyleManager, _: *gobject.ParamSpec, - self: *GhosttyApplication, + self: *Self, ) callconv(.c) void { _ = self; @@ -502,10 +628,93 @@ pub const GhosttyApplication = extern struct { log.debug("style manager changed scheme={}", .{color_scheme}); } - fn allocator(self: *GhosttyApplication) std.mem.Allocator { + 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 { + 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); + 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; } + 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; @@ -531,16 +740,46 @@ pub const GhosttyApplication = 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); } }; }; +/// 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. /// /// 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 { diff --git a/src/apprt/gtk-ng/class/config.zig b/src/apprt/gtk-ng/class/config.zig new file mode 100644 index 000000000..68d3d9242 --- /dev/null +++ b/src/apprt/gtk-ng/class/config.zig @@ -0,0 +1,188 @@ +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 +/// 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 ergonomic to GTK primitives. +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 }, + }); + + pub const properties = struct { + pub const @"diagnostics-buffer" = gobject.ext.defineProperty( + "diagnostics-buffer", + Self, + ?*gtk.TextBuffer, + .{ + .nick = "Diagnostics 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: CoreConfig, + + 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 CoreConfig) 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 get(self: *Self) *const CoreConfig { + 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) *CoreConfig { + 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); + } + + unrefLater(text_buf); // See unrefLater docs for why this is needed + return text_buf; + } + + 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); + gobject.ext.registerProperties(class, &.{ + properties.@"diagnostics-buffer", + properties.@"has-diagnostics", + }); + } + }; +}; + +// 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: CoreConfig = try .default(alloc); + defer config.deinit(); + 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 new file mode 100644 index 000000000..13cc56026 --- /dev/null +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -0,0 +1,194 @@ +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 Config = @import("config.zig").Config; + +const log = std.log.scoped(.gtk_ghostty_config_errors_dialog); + +pub const ConfigErrorsDialog = 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, .{ + .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 = .{ + .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; + }; + + pub fn new(config: *Config) *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 present(self: *Self, parent: ?*gtk.Widget) void { + switch (Parent) { + adw.AlertDialog => self.as(adw.Dialog).present(parent), + 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(), + adw.MessageDialog => self.as(gtk.Window).close(), + else => comptime unreachable, + } + } + + 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 { + 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), + ); + } + + 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); + } + + 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 { + gtk.Widget.Class.setTemplateFromResource( + class.as(gtk.Widget.Class), + switch (Parent) { + adw.AlertDialog => comptime gresource.blueprint(.{ + .major = 1, + .minor = 5, + .name = "config-errors-dialog", + }), + + adw.MessageDialog => comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "config-errors-dialog", + }), + + else => comptime unreachable, + }, + ); + + // 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 { + return gobject.ext.as(T, class); + } + }; +}; diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index df8b99cee..f6f71abb7 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, @@ -20,20 +21,40 @@ pub const GhosttyWindow = extern struct { }); const Private = struct { - _todo: u8 = 0, + _todo: u8, 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(win: *GhosttyWindow, _: *Class) callconv(.C) void { - gtk.Widget.initTemplate(win.as(gtk.Widget)); + 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); + 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); + } + + fn private(self: *Self) *Private { + return gobject.ext.impl_helpers.getPrivate( + self, + Private, + Private.offset, + ); } pub const Class = extern struct { @@ -50,6 +71,8 @@ pub const GhosttyWindow = extern struct { .name = "window", }), ); + + gobject.Object.virtual_methods.dispose.implement(class, &dispose); } pub fn as(class: *Class, comptime T: type) *T { 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; + } + }; +} 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..17ecac4e4 --- /dev/null +++ b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp @@ -0,0 +1,27 @@ +using Gtk 4.0; +using Adw 1; + +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."); + + 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; + } + }; +} diff --git a/src/apprt/gtk-ng/weak_ref.zig b/src/apprt/gtk-ng/weak_ref.zig new file mode 100644 index 000000000..7ee5cf730 --- /dev/null +++ b/src/apprt/gtk-ng/weak_ref.zig @@ -0,0 +1,47 @@ +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 { + 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 + /// 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` 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"] diff --git a/valgrind.supp b/valgrind.supp new file mode 100644 index 000000000..0e1a90f6a --- /dev/null +++ b/valgrind.supp @@ -0,0 +1,1929 @@ +# 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 + ... +} + +{ + 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 + 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 +}