mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-23 20:26:09 +03:00
714 lines
26 KiB
Zig
714 lines
26 KiB
Zig
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 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;
|
|
|
|
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 Application = 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, .{
|
|
.name = "GhosttyApplication",
|
|
.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,
|
|
|
|
/// If non-null, we're currently showing a config errors dialog.
|
|
config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{},
|
|
|
|
var offset: c_int = 0;
|
|
};
|
|
|
|
/// Creates a new Application 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.
|
|
var config = CoreConfig.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: CoreConfig = try .default(alloc);
|
|
errdefer default.deinit();
|
|
try default.addDiagnosticFmt(
|
|
"error loading user configuration: {}",
|
|
.{err},
|
|
);
|
|
|
|
break :err default;
|
|
};
|
|
defer 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,
|
|
});
|
|
|
|
// Wrap our configuration in a GObject.
|
|
const config_obj: *Config = try .new(alloc, &config);
|
|
errdefer config_obj.unref();
|
|
|
|
// 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,
|
|
.config = config_obj,
|
|
};
|
|
|
|
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.unref();
|
|
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.get();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn startup(self: *Self) callconv(.C) void {
|
|
log.debug("startup", .{});
|
|
|
|
gio.Application.virtual_methods.startup.call(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
|
|
// Setup our event loop
|
|
self.startupXev();
|
|
|
|
// Setup our style manager (light/dark mode)
|
|
self.startupStyleManager();
|
|
|
|
// Setup our cgroup for the application.
|
|
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.
|
|
self.showConfigErrorsDialog();
|
|
}
|
|
|
|
/// Configure libxev to use a specific backend.
|
|
///
|
|
/// This must be called before any other xev APIs are used.
|
|
fn startupXev(self: *Self) void {
|
|
const priv = self.private();
|
|
const config = priv.config.get();
|
|
|
|
// 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: *Self) void {
|
|
const priv = self.private();
|
|
const config = priv.config.get();
|
|
|
|
// 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,
|
|
*Self,
|
|
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: *Self) CgroupError!void {
|
|
const priv = self.private();
|
|
const config = priv.config.get();
|
|
|
|
// 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: *Self) callconv(.C) void {
|
|
log.debug("activate", .{});
|
|
|
|
// Call the parent activate method.
|
|
gio.Application.virtual_methods.activate.call(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
|
|
// const win = Window.new(self);
|
|
// 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(
|
|
Class.parent,
|
|
self.as(Parent),
|
|
);
|
|
}
|
|
|
|
fn handleStyleManagerDark(
|
|
style: *adw.StyleManager,
|
|
_: *gobject.ParamSpec,
|
|
self: *Self,
|
|
) callconv(.c) void {
|
|
_ = self;
|
|
|
|
const color_scheme: apprt.ColorScheme = if (style.getDark() == 0)
|
|
.light
|
|
else
|
|
.dark;
|
|
|
|
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;
|
|
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.dispose.implement(class, &dispose);
|
|
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]);
|
|
}
|
|
}
|