diff --git a/src/apprt/gtk-ng/App.zig b/src/apprt/gtk-ng/App.zig index 428ecc457..0bd2c9603 100644 --- a/src/apprt/gtk-ng/App.zig +++ b/src/apprt/gtk-ng/App.zig @@ -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); } diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index a2fd05a67..df0b3c439 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -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]); + } +}