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`