const std = @import("std"); const Allocator = std.mem.Allocator; const gobject = @import("gobject"); const gtk = @import("gtk"); const configpkg = @import("../../../config.zig"); const CoreConfig = configpkg.Config; const Common = @import("../class.zig").Common; 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, .{ .accessor = gobject.ext.typedAccessor( Self, ?*gtk.TextBuffer, .{ .getter = Self.diagnosticsBuffer, .getter_transfer = .full, }, ), }, ); pub const @"has-diagnostics" = gobject.ext.defineProperty( "has-diagnostics", Self, bool, .{ .default = false, .accessor = gobject.ext.typedAccessor( Self, bool, .{ .getter = Self.hasDiagnostics, }, ), }, ); }; const Private = struct { config: CoreConfig, pub 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 writer: std.Io.Writer = .fixed(&buf); for (config._diagnostics.items()) |diag| { writer.end = 0; diag.format(&writer) catch |err| { log.warn( "error writing diagnostic to buffer err={}", .{err}, ); continue; }; text_buf.insertAtCursor(&buf, @intCast(writer.end)); text_buf.insertAtCursor("\n", 1); } 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), ); } const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; pub const unref = C.unref; const private = C.private; pub const Class = extern struct { parent_class: Parent.Class, var parent: *Parent.Class = undefined; pub const Instance = Self; fn init(class: *Class) callconv(.c) void { gobject.Object.virtual_methods.finalize.implement(class, &finalize); 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(); }