apprt/gtk-ng: move our app initialization all into the App class

This commit is contained in:
Mitchell Hashimoto
2025-07-16 12:20:09 -07:00
parent faa0c36def
commit 426fa8d8f9
2 changed files with 202 additions and 180 deletions

View File

@ -9,7 +9,6 @@ const gio = @import("gio");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const internal_os = @import("../../os/main.zig");
const xev = @import("../../global.zig").xev;
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
@ -31,185 +30,13 @@ pub fn init(
opts: struct {},
) !void {
_ = opts;
const alloc = core_app.alloc;
// Log our GTK versions
gtk_version.logVersion();
adw_version.logVersion();
// Set gettext global domain to be our app so that our unqualified
// translations map to our translations.
try internal_os.i18n.initGlobalDomain();
// Load our configuration.
const config: *Config = try alloc.create(Config);
errdefer alloc.destroy(config);
config.* = try Config.load(core_app.alloc);
errdefer config.deinit();
// If we had configuration errors, then log them.
if (!config._diagnostics.empty()) {
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
for (config._diagnostics.items()) |diag| {
try diag.write(buf.writer());
log.warn("configuration error: {s}", .{buf.items});
buf.clearRetainingCapacity();
}
// If we have any CLI errors, exit.
if (config._diagnostics.containsLocation(.cli)) {
log.warn("CLI errors detected, exiting", .{});
std.posix.exit(1);
}
}
// Setup our event loop backend
if (config.@"async-backend" != .auto) {
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
.epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
.io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
};
if (result) {
log.info(
"libxev manual backend={s}",
.{@tagName(xev.backend)},
);
} else {
log.warn(
"libxev manual backend failed, using default={s}",
.{@tagName(xev.backend)},
);
}
}
// Setup GTK
setGtkEnv(config) catch |err| switch (err) {
error.NoSpaceLeft => {
// If we fail to set GTK environment variables then we still
// try to start the application...
log.warn(
"error setting GTK environment variables err={}",
.{err},
);
},
};
adw.init();
// Initialize our application class
const app: *GhosttyApplication = .new(core_app, config);
const app: *GhosttyApplication = try .new(core_app);
errdefer app.unref();
self.* = .{
.app = app,
};
self.* = .{ .app = app };
return;
}
/// This sets various GTK-related environment variables as necessary
/// given the runtime environment or configuration.
fn setGtkEnv(config: *const Config) error{NoSpaceLeft}!void {
var gdk_debug: struct {
/// output OpenGL debug information
opengl: bool = false,
/// disable GLES, Ghostty can't use GLES
@"gl-disable-gles": bool = false,
// GTK's new renderer can cause blurry font when using fractional scaling.
@"gl-no-fractional": bool = false,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
@"vulkan-disable": bool = false,
} = .{
.opengl = config.@"gtk-opengl-debug",
};
var gdk_disable: struct {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
/// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
@"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
} = .{};
environment: {
if (gtk_version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
if (gtk_version.runtimeAtLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
gdk_disable.@"gles-api" = true;
gdk_disable.vulkan = true;
break :environment;
}
if (gtk_version.runtimeAtLeast(4, 14, 0)) {
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
// Older versions of GTK do not support these values so it is safe
// to always set this. Forwards versions are uncertain so we'll have
// to reassess...
//
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
gdk_debug.@"gl-disable-gles" = true;
gdk_debug.@"vulkan-disable" = true;
if (gtk_version.runtimeUntil(4, 17, 5)) {
// Removed at GTK v4.17.5
gdk_debug.@"gl-no-fractional" = true;
}
break :environment;
}
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
// is an environment that isn't tested well and we don't have a
// good understanding of what we may need to do.
gdk_debug.@"vulkan-disable" = true;
}
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
if (@field(gdk_debug, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
}
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
if (@field(gdk_disable, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
}
}
pub fn run(self: *App) !void {
try self.app.run(self);
}

View File

@ -1,5 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const adw = @import("adw");
const gio = @import("gio");
@ -12,8 +13,12 @@ const apprt = @import("../../../apprt.zig");
const cgroup = @import("../cgroup.zig");
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 adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig");
const GhosttyWindow = @import("window.zig").GhosttyWindow;
const log = std.log.scoped(.gtk_ghostty_application);
@ -69,8 +74,64 @@ pub const GhosttyApplication = extern struct {
/// Creates a new GhosttyApplication instance.
///
/// Takes ownership of the `config` argument.
pub fn new(core_app: *CoreApp, config: *Config) *Self {
/// This does a lot more work than a typical class instantiation,
/// because we expect that this is the main program entrypoint.
///
/// 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 {
const alloc = core_app.alloc;
// Log our GTK versions
gtk_version.logVersion();
adw_version.logVersion();
// Set gettext global domain to be our app so that our unqualified
// translations map to our translations.
internal_os.i18n.initGlobalDomain() catch |err| {
// Failures shuldn't stop application startup. Our app may
// not translate correctly but it should still work. In the
// future we may want to add this to the GUI to show.
log.warn("i18n initialization failed error={}", .{err});
};
// Load our configuration.
const config: *Config = try alloc.create(Config);
errdefer alloc.destroy(config);
config.* = Config.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);
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},
),
});
break :err default;
};
errdefer config.deinit();
// Setup our GTK init env vars
setGtkEnv(config) catch |err| switch (err) {
error.NoSpaceLeft => {
// If we fail to set GTK environment variables then we still
// try to start the application...
log.warn(
"error setting GTK environment variables err={}",
.{err},
);
},
};
adw.init();
const single_instance = switch (config.@"gtk-single-instance") {
.true => true,
.false => false,
@ -256,9 +317,9 @@ pub const GhosttyApplication = extern struct {
fn startup(self: *GhosttyApplication) callconv(.C) void {
log.debug("startup", .{});
const priv = self.private();
const config = priv.config;
_ = config;
// Setup our event loop
self.startupXev();
// Setup our style manager (light/dark mode)
self.startupStyleManager();
@ -274,6 +335,36 @@ pub const GhosttyApplication = extern struct {
);
}
/// Configure libxev to use a specific backend.
///
/// This must be called before any other xev APIs are used.
fn startupXev(self: *GhosttyApplication) void {
const priv = self.private();
const config = priv.config;
// If our backend is auto then we have no setup to do.
if (config.@"async-backend" == .auto) return;
// Setup our event loop backend to the preferred method
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
.epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
.io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
};
if (result) {
log.info(
"libxev manual backend={s}",
.{@tagName(xev.backend)},
);
} else {
log.warn(
"libxev manual backend failed, using default={s}",
.{@tagName(xev.backend)},
);
}
}
/// 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.
@ -444,3 +535,107 @@ pub const GhosttyApplication = extern struct {
}
};
};
/// 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 {
var gdk_debug: struct {
/// output OpenGL debug information
opengl: bool = false,
/// disable GLES, Ghostty can't use GLES
@"gl-disable-gles": bool = false,
// GTK's new renderer can cause blurry font when using fractional scaling.
@"gl-no-fractional": bool = false,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
@"vulkan-disable": bool = false,
} = .{
.opengl = config.@"gtk-opengl-debug",
};
var gdk_disable: struct {
@"gles-api": bool = false,
/// current gtk implementation for color management is not good enough.
/// see: https://bugs.kde.org/show_bug.cgi?id=495647
/// gtk issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6864
@"color-mgmt": bool = true,
/// Disabling Vulkan can improve startup times by hundreds of
/// milliseconds on some systems. We don't use Vulkan so we can just
/// disable it.
vulkan: bool = false,
} = .{};
environment: {
if (gtk_version.runtimeAtLeast(4, 18, 0)) {
gdk_disable.@"color-mgmt" = false;
}
if (gtk_version.runtimeAtLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
gdk_disable.@"gles-api" = true;
gdk_disable.vulkan = true;
break :environment;
}
if (gtk_version.runtimeAtLeast(4, 14, 0)) {
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
// Older versions of GTK do not support these values so it is safe
// to always set this. Forwards versions are uncertain so we'll have
// to reassess...
//
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
gdk_debug.@"gl-disable-gles" = true;
gdk_debug.@"vulkan-disable" = true;
if (gtk_version.runtimeUntil(4, 17, 5)) {
// Removed at GTK v4.17.5
gdk_debug.@"gl-no-fractional" = true;
}
break :environment;
}
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
// is an environment that isn't tested well and we don't have a
// good understanding of what we may need to do.
gdk_debug.@"vulkan-disable" = true;
}
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
if (@field(gdk_debug, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
}
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
if (@field(gdk_disable, field.name)) {
if (!first) try writer.writeAll(",");
try writer.writeAll(field.name);
first = false;
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
}
}