2025-07-17 09:55:58 -07:00

185 lines
5.8 KiB
Zig

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 CoreConfig = 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 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 = "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: 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);
}
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();
}