gtk-ng: port ConfigErrorsDialog (#7968)

This ports the config errors dialog from `apprt/gtk` to `gtk-ng`. 

The major change here is that we now use proper template bindings for
the content. To do this, a `ghostty.Config` is now wrapped in a GObject
`GhosttyConfig` to make it safe to pass around (ref count) and to
provide helpful properties like the diagnostics buffer we bind to.

As a minor change, I stripped the `Ghostty` prefix from our GObject
classes in Zig code. For templates its all still there as is the norm.

This retains the exact same version requirements and layout as the
existing one.
This commit is contained in:
Mitchell Hashimoto
2025-07-17 20:26:43 -07:00
committed by GitHub
14 changed files with 2802 additions and 79 deletions

View File

@ -6,4 +6,9 @@ pub const Surface = @import("gtk-ng/Surface.zig");
pub const resourcesDir = internal_os.resourcesDir; pub const resourcesDir = internal_os.resourcesDir;
// The exported API, custom for the apprt. // 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());
}

View File

@ -12,15 +12,15 @@ const internal_os = @import("../../os/main.zig");
const Config = configpkg.Config; const Config = configpkg.Config;
const CoreApp = @import("../../App.zig"); 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 Surface = @import("Surface.zig");
const gtk_version = @import("gtk_version.zig"); const gtk_version = @import("gtk_version.zig");
const adw_version = @import("adw_version.zig"); const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
/// The GObject GhosttyApplication instance /// The GObject Application instance
app: *GhosttyApplication, app: *Application,
pub fn init( pub fn init(
self: *App, self: *App,
@ -31,14 +31,14 @@ pub fn init(
) !void { ) !void {
_ = opts; _ = opts;
const app: *GhosttyApplication = try .new(core_app); const app: *Application = try .new(self, core_app);
errdefer app.unref(); errdefer app.unref();
self.* = .{ .app = app }; self.* = .{ .app = app };
return; return;
} }
pub fn run(self: *App) !void { pub fn run(self: *App) !void {
try self.app.run(self); try self.app.run();
} }
pub fn terminate(self: *App) void { pub fn terminate(self: *App) void {
@ -54,10 +54,7 @@ pub fn performAction(
comptime action: apprt.Action.Key, comptime action: apprt.Action.Key,
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !bool { ) !bool {
_ = self; return try self.app.performAction(target, action, value);
_ = target;
_ = value;
return false;
} }
pub fn performIpc( pub fn performIpc(

View File

@ -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. /// These will be asserted to exist at runtime.
pub const blueprints: []const Blueprint = &.{ 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" }, .{ .major = 1, .minor = 5, .name = "window" },
}; };

View File

@ -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));
}

View File

@ -15,11 +15,15 @@ const CoreApp = @import("../../../App.zig");
const configpkg = @import("../../../config.zig"); const configpkg = @import("../../../config.zig");
const internal_os = @import("../../../os/main.zig"); const internal_os = @import("../../../os/main.zig");
const xev = @import("../../../global.zig").xev; const xev = @import("../../../global.zig").xev;
const Config = configpkg.Config; const CoreConfig = configpkg.Config;
const adw_version = @import("../adw_version.zig"); const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_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); 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 /// This requires a `ghostty.App` and `ghostty.Config` and takes
/// care of the rest. Call `run` to run the application to completion. /// 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 /// 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 /// 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 /// 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, parent_instance: Parent,
pub const Parent = adw.Application; pub const Parent = adw.Application;
pub const getGObjectType = gobject.ext.defineClass(Self, .{ pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyApplication",
.classInit = &Class.init, .classInit = &Class.init,
.parent_class = &Class.parent, .parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset }, .private = .{ .Type = Private, .offset = &Private.offset },
}); });
const Private = 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. /// The libghostty App instance.
core_app: *CoreApp, core_app: *CoreApp,
@ -69,10 +79,15 @@ pub const GhosttyApplication = extern struct {
/// only be set by the main loop thread. /// only be set by the main loop thread.
running: bool = false, 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; 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, /// This does a lot more work than a typical class instantiation,
/// because we expect that this is the main program entrypoint. /// 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. /// The only failure mode of initializing the application is early OOM.
/// Early OOM can't be recovered from. Every other error is mapped to /// 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. /// 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; const alloc = core_app.alloc;
// Log our GTK versions // Log our GTK versions
@ -97,30 +115,24 @@ pub const GhosttyApplication = extern struct {
}; };
// Load our configuration. // Load our configuration.
const config: *Config = try alloc.create(Config); var config = CoreConfig.load(alloc) catch |err| err: {
errdefer alloc.destroy(config);
config.* = Config.load(alloc) catch |err| err: {
// If we fail to load the configuration, then we should log // If we fail to load the configuration, then we should log
// the error in the diagnostics so it can be shown to the user. // 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 // We can still load a default which only fails for OOM, allowing
// us to startup. // us to startup.
var default = try Config.default(alloc); var default: CoreConfig = try .default(alloc);
errdefer default.deinit(); errdefer default.deinit();
const config_arena = default._arena.?.allocator(); try default.addDiagnosticFmt(
try default._diagnostics.append(config_arena, .{ "error loading user configuration: {}",
.message = try std.fmt.allocPrintZ( .{err},
config_arena, );
"error loading user configuration: {}",
.{err},
),
});
break :err default; break :err default;
}; };
errdefer config.deinit(); defer config.deinit();
// Setup our GTK init env vars // Setup our GTK init env vars
setGtkEnv(config) catch |err| switch (err) { setGtkEnv(&config) catch |err| switch (err) {
error.NoSpaceLeft => { error.NoSpaceLeft => {
// If we fail to set GTK environment variables then we still // If we fail to set GTK environment variables then we still
// try to start the application... // try to start the application...
@ -170,6 +182,10 @@ pub const GhosttyApplication = extern struct {
single_instance, single_instance,
}); });
// Wrap our configuration in a GObject.
const config_obj: *Config = try .new(alloc, &config);
errdefer config_obj.unref();
// Initialize the app. // Initialize the app.
const self = gobject.ext.newInstance(Self, .{ const self = gobject.ext.newInstance(Self, .{
.application_id = app_id.ptr, .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 // 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. // to there (and we don't need it there directly) so this is here.
const priv = self.private(); const priv = self.private();
priv.core_app = core_app; priv.* = .{
priv.config = config; .rt_app = rt_app,
.core_app = core_app,
.config = config_obj,
};
return self; return self;
} }
@ -199,15 +218,14 @@ pub const GhosttyApplication = extern struct {
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
const alloc = self.allocator(); const alloc = self.allocator();
const priv = self.private(); const priv = self.private();
priv.config.deinit(); priv.config.unref();
alloc.destroy(priv.config);
if (priv.transient_cgroup_base) |base| alloc.free(base); if (priv.transient_cgroup_base) |base| alloc.free(base);
} }
/// Run the application. This is a replacement for `gio.Application.run` /// Run the application. This is a replacement for `gio.Application.run`
/// because we want more tight control over our event loop so we can /// because we want more tight control over our event loop so we can
/// integrate it with libghostty. /// 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: // Based on the actual `gio.Application.run` implementation:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 // 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 // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
const priv = self.private(); const priv = self.private();
const config = priv.config; const config = priv.config.get();
if (config.@"initial-window") switch (config.@"launched-from".?) { if (config.@"initial-window") switch (config.@"launched-from".?) {
.desktop, .cli => self.as(gio.Application).activate(), .desktop, .cli => self.as(gio.Application).activate(),
.dbus, .systemd => {}, .dbus, .systemd => {},
@ -278,7 +296,7 @@ pub const GhosttyApplication = extern struct {
_ = glib.MainContext.iteration(ctx, 1); _ = glib.MainContext.iteration(ctx, 1);
// Tick the core Ghostty terminal app // 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. // Check if we must quit based on the current state.
const must_quit = q: { const must_quit = q: {
@ -299,25 +317,108 @@ pub const GhosttyApplication = extern struct {
} }
} }
pub fn as(app: *Self, comptime T: type) *T { /// apprt API to perform an action.
return gobject.ext.as(T, app); 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 { /// Reload the configuration for the application and propagate it
gobject.Object.unref(self.as(gobject.Object)); /// 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( // Virtual Methods
self,
Private,
Private.offset,
);
}
fn startup(self: *GhosttyApplication) callconv(.C) void { fn startup(self: *Self) callconv(.C) void {
log.debug("startup", .{}); log.debug("startup", .{});
gio.Application.virtual_methods.startup.call(
Class.parent,
self.as(Parent),
);
// Setup our event loop // Setup our event loop
self.startupXev(); self.startupXev();
@ -325,22 +426,34 @@ pub const GhosttyApplication = extern struct {
self.startupStyleManager(); self.startupStyleManager();
// Setup our cgroup for the application. // Setup our cgroup for the application.
self.startupCgroup() catch { self.startupCgroup() catch |err| {
log.warn("TODO", .{}); 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( // If we have any config diagnostics from loading, then we
Class.parent, // show the diagnostics dialog. We show this one as a general
self.as(Parent), // 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. /// Configure libxev to use a specific backend.
/// ///
/// This must be called before any other xev APIs are used. /// 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 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 our backend is auto then we have no setup to do.
if (config.@"async-backend" == .auto) return; 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 the style manager on startup. The primary task here is to
/// setup our initial light/dark mode based on the configuration and /// setup our initial light/dark mode based on the configuration and
/// setup listeners for changes to the style manager. /// setup listeners for changes to the style manager.
fn startupStyleManager(self: *GhosttyApplication) void { fn startupStyleManager(self: *Self) void {
const priv = self.private(); const priv = self.private();
const config = priv.config; const config = priv.config.get();
// Setup our initial light/dark // Setup our initial light/dark
const style = self.as(adw.Application).getStyleManager(); const style = self.as(adw.Application).getStyleManager();
@ -390,7 +503,7 @@ pub const GhosttyApplication = extern struct {
// Setup color change notifications // Setup color change notifications
_ = gobject.Object.signals.notify.connect( _ = gobject.Object.signals.notify.connect(
style, style,
*GhosttyApplication, *Self,
handleStyleManagerDark, handleStyleManagerDark,
self, self,
.{ .detail = "dark" }, .{ .detail = "dark" },
@ -407,9 +520,9 @@ pub const GhosttyApplication = extern struct {
/// The setup for cgroups involves creating the cgroup for our /// The setup for cgroups involves creating the cgroup for our
/// application, moving ourselves into it, and storing the base path /// application, moving ourselves into it, and storing the base path
/// so that created surfaces can also have their own cgroups. /// 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 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 cgroup isolation isn't enabled then we don't do this.
if (!switch (config.@"linux-cgroup") { if (!switch (config.@"linux-cgroup") {
@ -463,10 +576,7 @@ pub const GhosttyApplication = extern struct {
priv.transient_cgroup_base = path; priv.transient_cgroup_base = path;
} }
fn activate(self: *GhosttyApplication) callconv(.C) void { fn activate(self: *Self) 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", .{}); log.debug("activate", .{});
// Call the parent activate method. // Call the parent activate method.
@ -475,11 +585,24 @@ pub const GhosttyApplication = extern struct {
self.as(Parent), self.as(Parent),
); );
const win = GhosttyWindow.new(self); // const win = Window.new(self);
gtk.Window.present(win.as(gtk.Window)); // 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(); self.deinit();
gobject.Object.virtual_methods.finalize.call( gobject.Object.virtual_methods.finalize.call(
Class.parent, Class.parent,
@ -487,10 +610,13 @@ pub const GhosttyApplication = extern struct {
); );
} }
//---------------------------------------------------------------
// Signal Handlers
fn handleStyleManagerDark( fn handleStyleManagerDark(
style: *adw.StyleManager, style: *adw.StyleManager,
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
self: *GhosttyApplication, self: *Self,
) callconv(.c) void { ) callconv(.c) void {
_ = self; _ = self;
@ -502,10 +628,93 @@ pub const GhosttyApplication = extern struct {
log.debug("style manager changed scheme={}", .{color_scheme}); 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; 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 { pub const Class = extern struct {
parent_class: Parent.Class, parent_class: Parent.Class,
var parent: *Parent.Class = undefined; var parent: *Parent.Class = undefined;
@ -531,16 +740,46 @@ pub const GhosttyApplication = extern struct {
// Virtual methods // Virtual methods
gio.Application.virtual_methods.activate.implement(class, &activate); gio.Application.virtual_methods.activate.implement(class, &activate);
gio.Application.virtual_methods.startup.implement(class, &startup); gio.Application.virtual_methods.startup.implement(class, &startup);
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize); 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 /// This sets various GTK-related environment variables as necessary
/// given the runtime environment or configuration. /// given the runtime environment or configuration.
/// ///
/// This must be called BEFORE GTK initialization. /// 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); assert(gtk.isInitialized() == 0);
var gdk_debug: struct { var gdk_debug: struct {

View File

@ -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();
}

View File

@ -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);
}
};
};

View File

@ -4,15 +4,16 @@ const gobject = @import("gobject");
const gtk = @import("gtk"); const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig"); 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); const log = std.log.scoped(.gtk_ghostty_window);
pub const GhosttyWindow = extern struct { pub const Window = extern struct {
const Self = @This(); const Self = @This();
parent_instance: Parent, parent_instance: Parent,
pub const Parent = adw.ApplicationWindow; pub const Parent = adw.ApplicationWindow;
pub const getGObjectType = gobject.ext.defineClass(Self, .{ pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyWindow",
.instanceInit = &init, .instanceInit = &init,
.classInit = &Class.init, .classInit = &Class.init,
.parent_class = &Class.parent, .parent_class = &Class.parent,
@ -20,20 +21,40 @@ pub const GhosttyWindow = extern struct {
}); });
const Private = struct { const Private = struct {
_todo: u8 = 0, _todo: u8,
var offset: c_int = 0; var offset: c_int = 0;
}; };
pub fn new(app: *GhosttyApplication) *Self { pub fn new(app: *Application) *Self {
return gobject.ext.newInstance(Self, .{ .application = app }); return gobject.ext.newInstance(Self, .{ .application = app });
} }
fn init(win: *GhosttyWindow, _: *Class) callconv(.C) void { fn init(self: *Self, _: *Class) callconv(.C) void {
gtk.Widget.initTemplate(win.as(gtk.Widget)); gtk.Widget.initTemplate(self.as(gtk.Widget));
} }
pub fn as(win: *Self, comptime T: type) *T { fn dispose(self: *Self) callconv(.C) void {
return gobject.ext.as(T, win); 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 { pub const Class = extern struct {
@ -50,6 +71,8 @@ pub const GhosttyWindow = extern struct {
.name = "window", .name = "window",
}), }),
); );
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
} }
pub fn as(class: *Class, comptime T: type) *T { pub fn as(class: *Class, comptime T: type) *T {

View File

@ -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;
}
};
}

View File

@ -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;
}
};
}

View File

@ -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);
}

View File

@ -4068,6 +4068,23 @@ fn compatBoldIsBright(
return true; 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 /// 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 /// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit` /// any changes or new allocations. The config should have `deinit`

View File

@ -51,6 +51,8 @@ DECID = "DECID"
flate = "flate" flate = "flate"
typ = "typ" typ = "typ"
kend = "kend" kend = "kend"
# GTK
GIR = "GIR"
[type.po] [type.po]
extend-glob = ["*.po"] extend-glob = ["*.po"]

1929
valgrind.supp Normal file

File diff suppressed because it is too large Load Diff