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"); const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const build_config = @import("../../../build_config.zig"); 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); /// The primary entrypoint for the Ghostty GTK application. /// /// This requires a `ghostty.App` and `ghostty.Config` and takes /// care of the rest. Call `run` to run the application to completion. pub const GhosttyApplication = extern struct { /// 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 /// how this all works and where you can find resources for it, but /// this applies to any other GObject class within this apprt. /// /// The various fields (parent_instance) and constants (Parent, /// getGObjectType, etc.) are mandatory "interfaces" for zig-gobject /// to create a GObject class. /// /// I found these to be the best resources: /// /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/extensions/gobject2.zig /// * https://github.com/ianprime0509/zig-gobject/blob/d7f1edaf50193d49b56c60568dfaa9f23195565b/example/src/custom_class.zig /// const Self = @This(); parent_instance: Parent, pub const Parent = adw.Application; pub const getGObjectType = gobject.ext.defineClass(Self, .{ .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, }); const Private = struct { /// The libghostty App instance. core_app: *CoreApp, /// The configuration for the application. config: *Config, /// The base path of the transient cgroup used to put all surfaces /// into their own cgroup. This is only set if cgroups are enabled /// and initialization was successful. transient_cgroup_base: ?[]const u8 = null, /// This is set to false internally when the event loop /// should exit and the application should quit. This must /// only be set by the main loop thread. running: bool = false, var offset: c_int = 0; }; /// Creates a new GhosttyApplication instance. /// /// 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, .desktop => switch (config.@"launched-from".?) { .desktop, .systemd, .dbus => true, .cli => false, }, }; // Setup the flags for our application. const app_flags: gio.ApplicationFlags = app_flags: { var flags: gio.ApplicationFlags = .flags_default_flags; if (!single_instance) flags.non_unique = true; break :app_flags flags; }; // Our app ID determines uniqueness and maps to our desktop file. // We append "-debug" to the ID if we're in debug mode so that we // can develop Ghostty in Ghostty. const app_id: [:0]const u8 = app_id: { if (config.class) |class| { if (gio.Application.idIsValid(class) != 0) { break :app_id class; } else { log.warn("invalid 'class' in config, ignoring", .{}); } } const default_id = comptime build_config.bundle_id; break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id; }; // Create our GTK Application which encapsulates our process. log.debug("creating GTK application id={s} single-instance={}", .{ app_id, single_instance, }); // Initialize the app. const self = gobject.ext.newInstance(Self, .{ .application_id = app_id.ptr, .flags = app_flags, // Force the resource path to a known value so it doesn't depend // on the app id (which changes between debug/release and can be // user-configured) and force it to load in compiled resources. .resource_base_path = "/com/mitchellh/ghostty", }); // Setup our private state. More setup is done in the init // 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; return self; } /// Force deinitialize the application. /// /// Normally in a GObject lifecycle, this would be called by the /// finalizer. But applications are never fully unreferenced so this /// ensures that our memory is cleaned up properly. pub fn deinit(self: *Self) void { const alloc = self.allocator(); const priv = self.private(); priv.config.deinit(); alloc.destroy(priv.config); if (priv.transient_cgroup_base) |base| alloc.free(base); } /// Run the application. This is a replacement for `gio.Application.run` /// because we want more tight control over our event loop so we can /// integrate it with libghostty. pub fn run(self: *Self, rt_app: *apprt.gtk_ng.App) !void { // Based on the actual `gio.Application.run` implementation: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 // Acquire the default context for the application const ctx = glib.MainContext.default(); if (glib.MainContext.acquire(ctx) == 0) return error.ContextAcquireFailed; // The final cleanup that is always required at the end of running. defer { // Sync any remaining settings gio.Settings.sync(); // Clear out the event loop, don't block. while (glib.MainContext.iteration(ctx, 0) != 0) {} // Release the context so something else can use it. defer glib.MainContext.release(ctx); } // Register the application var err_: ?*glib.Error = null; if (self.as(gio.Application).register( null, &err_, ) == 0) { if (err_) |err| { defer err.free(); log.warn( "error registering application: {s}", .{err.f_message orelse "(unknown)"}, ); } return error.ApplicationRegisterFailed; } assert(err_ == null); // This just calls the `activate` signal but its part of the normal startup // routine so we just call it, but only if the config allows it (this allows // for launching Ghostty in the "background" without immediately opening // a window). An initial window will not be immediately created if we were // launched by D-Bus activation or systemd. D-Bus activation will send it's // own `activate` or `new-window` signal later. // // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 const priv = self.private(); const config = priv.config; if (config.@"initial-window") switch (config.@"launched-from".?) { .desktop, .cli => self.as(gio.Application).activate(), .dbus, .systemd => {}, }; // If we are NOT the primary instance, then we never want to run. // This means that another instance of the GTK app is running and // our "activate" call above will open a window. if (self.as(gio.Application).getIsRemote() != 0) { log.debug( "application is remote, exiting run loop after activation", .{}, ); return; } log.debug("entering runloop", .{}); defer log.debug("exiting runloop", .{}); priv.running = true; while (priv.running) { _ = glib.MainContext.iteration(ctx, 1); // Tick the core Ghostty terminal app try priv.core_app.tick(rt_app); // Check if we must quit based on the current state. const must_quit = q: { // If we are configured to always stay running, don't quit. if (!config.@"quit-after-last-window-closed") break :q false; // If the quit timer has expired, quit. // if (self.quit_timer == .expired) break :q true; // There's no quit timer running, or it hasn't expired, don't quit. break :q false; }; if (must_quit) { //self.quit(); priv.running = false; } } } 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: *GhosttyApplication) *Private { return gobject.ext.impl_helpers.getPrivate( self, Private, Private.offset, ); } fn startup(self: *GhosttyApplication) callconv(.C) void { log.debug("startup", .{}); // Setup our event loop self.startupXev(); // Setup our style manager (light/dark mode) self.startupStyleManager(); // Setup our cgroup for the application. self.startupCgroup() catch { log.warn("TODO", .{}); }; gio.Application.virtual_methods.startup.call( Class.parent, self.as(Parent), ); } /// 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. fn startupStyleManager(self: *GhosttyApplication) void { const priv = self.private(); const config = priv.config; // Setup our initial light/dark const style = self.as(adw.Application).getStyleManager(); style.setColorScheme(switch (config.@"window-theme") { .auto, .ghostty => auto: { const lum = config.background.toTerminalRGB().perceivedLuminance(); break :auto if (lum > 0.5) .prefer_light else .prefer_dark; }, .system => .prefer_light, .dark => .force_dark, .light => .force_light, }); // Setup color change notifications _ = gobject.Object.signals.notify.connect( style, *GhosttyApplication, handleStyleManagerDark, self, .{ .detail = "dark" }, ); } const CgroupError = error{ DbusConnectionFailed, CgroupInitFailed, }; /// Setup our cgroup for the application, if enabled. /// /// The setup for cgroups involves creating the cgroup for our /// application, moving ourselves into it, and storing the base path /// so that created surfaces can also have their own cgroups. fn startupCgroup(self: *GhosttyApplication) CgroupError!void { const priv = self.private(); const config = priv.config; // If cgroup isolation isn't enabled then we don't do this. if (!switch (config.@"linux-cgroup") { .never => false, .always => true, .@"single-instance" => single: { const flags = self.as(gio.Application).getFlags(); break :single !flags.non_unique; }, }) { log.info( "cgroup isolation disabled via config={}", .{config.@"linux-cgroup"}, ); return; } // We need a dbus connection to do anything else const dbus = self.as(gio.Application).getDbusConnection() orelse { if (config.@"linux-cgroup-hard-fail") { log.err("dbus connection required for cgroup isolation, exiting", .{}); return error.DbusConnectionFailed; } return; }; const alloc = priv.core_app.alloc; const path = cgroup.init(alloc, dbus, .{ .memory_high = config.@"linux-cgroup-memory-limit", .pids_max = config.@"linux-cgroup-processes-limit", }) catch |err| { // If we can't initialize cgroups then that's okay. We // want to continue to run so we just won't isolate surfaces. // NOTE(mitchellh): do we want a config to force it? log.warn( "failed to initialize cgroups, terminals will not be isolated err={}", .{err}, ); // If we have hard fail enabled then we exit now. if (config.@"linux-cgroup-hard-fail") { log.err("linux-cgroup-hard-fail enabled, exiting", .{}); return error.CgroupInitFailed; } return; }; log.info("cgroup isolation enabled base={s}", .{path}); priv.transient_cgroup_base = path; } fn activate(self: *GhosttyApplication) callconv(.C) void { log.debug("activate", .{}); // Call the parent activate method. gio.Application.virtual_methods.activate.call( Class.parent, self.as(Parent), ); const win = GhosttyWindow.new(self); gtk.Window.present(win.as(gtk.Window)); } fn finalize(self: *GhosttyApplication) callconv(.C) void { self.deinit(); gobject.Object.virtual_methods.finalize.call( Class.parent, self.as(Parent), ); } fn handleStyleManagerDark( style: *adw.StyleManager, _: *gobject.ParamSpec, self: *GhosttyApplication, ) callconv(.c) void { _ = self; const color_scheme: apprt.ColorScheme = if (style.getDark() == 0) .light else .dark; log.debug("style manager changed scheme={}", .{color_scheme}); } fn allocator(self: *GhosttyApplication) std.mem.Allocator { return self.private().core_app.alloc; } 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 { // Register our compiled resources exactly once. { const c = @cImport({ // generated header files @cInclude("ghostty_resources.h"); }); if (c.ghostty_get_resource()) |ptr| { gio.resourcesRegister(@ptrCast(@alignCast(ptr))); } else { // If we fail to load resources then things will // probably look really bad but it shouldn't stop our // app from loading. log.warn("unable to load resources", .{}); } } // Virtual methods gio.Application.virtual_methods.activate.implement(class, &activate); gio.Application.virtual_methods.startup.implement(class, &startup); gobject.Object.virtual_methods.finalize.implement(class, &finalize); } }; }; /// 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 { assert(gtk.isInitialized() == 0); 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]); } }