mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-20 18:56:08 +03:00
apprt/gtk-ng: GhosttyApplication and boilerplate to run
This commit is contained in:
@ -1,5 +1,9 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
// The required comptime API for any apprt.
|
||||
pub const App = @import("gtk-ng/App.zig");
|
||||
pub const Surface = @import("gtk-ng/Surface.zig");
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
|
||||
// The exported API, custom for the apprt.
|
||||
pub const GhosttyApplication = @import("gtk-ng/class/application.zig").GhosttyApplication;
|
||||
|
@ -1,28 +1,236 @@
|
||||
/// This is the main entrypoint to the apprt for Ghostty. Ghostty will
|
||||
/// initialize this in main to start the application..
|
||||
const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
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");
|
||||
|
||||
const GhosttyApplication = @import("class/application.zig").GhosttyApplication;
|
||||
const Surface = @import("Surface.zig");
|
||||
const gtk_version = @import("gtk_version.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// The GObject GhosttyApplication instance
|
||||
app: *GhosttyApplication,
|
||||
|
||||
pub fn init(
|
||||
self: *App,
|
||||
core_app: *CoreApp,
|
||||
|
||||
// Required by the apprt interface but we don't use it.
|
||||
opts: struct {},
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = core_app;
|
||||
_ = 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);
|
||||
errdefer app.unref();
|
||||
|
||||
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 {
|
||||
_ = self;
|
||||
try self.app.run(self);
|
||||
}
|
||||
|
||||
pub fn terminate(self: *App) void {
|
||||
// We force deinitialize the app. We don't unref because other things
|
||||
// tend to have a reference at this point, so this just forces the
|
||||
// disposal now.
|
||||
self.app.deinit();
|
||||
}
|
||||
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !bool {
|
||||
_ = self;
|
||||
_ = target;
|
||||
_ = value;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn performIpc(
|
||||
@ -36,3 +244,15 @@ pub fn performIpc(
|
||||
_ = value;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Close the given surface.
|
||||
pub fn redrawSurface(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
}
|
||||
|
||||
/// Redraw the inspector for the given surface.
|
||||
pub fn redrawInspector(self: *App, surface: *Surface) void {
|
||||
_ = self;
|
||||
_ = surface;
|
||||
}
|
||||
|
@ -1,5 +1,57 @@
|
||||
const Surface = @This();
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
core_surface: CoreSurface,
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn close(self: *Surface, process_active: bool) void {
|
||||
_ = self;
|
||||
_ = process_active;
|
||||
}
|
||||
|
||||
pub fn shouldClose(self: *Surface) bool {
|
||||
_ = self;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
_ = self;
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getContentScale(self: *const Surface) !apprt.ContentScale {
|
||||
_ = self;
|
||||
return .{ .x = 1, .y = 1 };
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Surface) !apprt.CursorPos {
|
||||
_ = self;
|
||||
return .{ .x = 0, .y = 0 };
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
self: *Surface,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = clipboard_type;
|
||||
_ = state;
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
self: *Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
_ = self;
|
||||
_ = val;
|
||||
_ = clipboard_type;
|
||||
_ = confirm;
|
||||
}
|
||||
|
122
src/apprt/gtk-ng/adw_version.zig
Normal file
122
src/apprt/gtk-ng/adw_version.zig
Normal file
@ -0,0 +1,122 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
|
||||
// macros match the version of `libadwaita` that we are building/linking
|
||||
// against.
|
||||
const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adw = @import("adw");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = adw.getMajorVersion(),
|
||||
.minor = adw.getMinorVersion(),
|
||||
.patch = adw.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("libadwaita version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the running libadwaita version is at least the given
|
||||
/// version. This will return false if Ghostty is configured to not build with
|
||||
/// libadwaita.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against. So generally you probably want to do both checks!
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version, we can return
|
||||
// false immediately. This prevents us from compiling against unknown
|
||||
// symbols and makes runtime checks very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the libadwaita version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
test "versionAtLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
|
||||
pub inline fn supportsDialogs() bool {
|
||||
return atLeast(1, 5, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsTabOverview() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsSwitchRow() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsToolbarView() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsBanner() bool {
|
||||
return atLeast(1, 3, 0);
|
||||
}
|
283
src/apprt/gtk-ng/class/application.zig
Normal file
283
src/apprt/gtk-ng/class/application.zig
Normal file
@ -0,0 +1,283 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const CoreApp = @import("../../../App.zig");
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// 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,
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Takes ownership of the `config` argument.
|
||||
pub fn new(core_app: *CoreApp, config: *Config) *Self {
|
||||
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,
|
||||
});
|
||||
|
||||
const self = gobject.ext.newInstance(Self, .{
|
||||
.application_id = app_id.ptr,
|
||||
.flags = app_flags,
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
// This is where we would initialize the application, but we
|
||||
// do that in the `run` method instead.
|
||||
log.debug("GhosttyApplication started", .{});
|
||||
|
||||
gio.Application.virtual_methods.startup.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn activate(self: *GhosttyApplication) 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("GhosttyApplication activated", .{});
|
||||
|
||||
// Call the parent activate method.
|
||||
gio.Application.virtual_methods.activate.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize(self: *GhosttyApplication) callconv(.C) void {
|
||||
self.deinit();
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
gio.Application.virtual_methods.activate.implement(class, &activate);
|
||||
gio.Application.virtual_methods.startup.implement(class, &startup);
|
||||
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
|
||||
}
|
||||
};
|
||||
};
|
140
src/apprt/gtk-ng/gtk_version.zig
Normal file
140
src/apprt/gtk-ng/gtk_version.zig
Normal file
@ -0,0 +1,140 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
|
||||
// macros match the version of `gtk4` that we are building/linking against.
|
||||
const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
});
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.GTK_MAJOR_VERSION,
|
||||
.minor = c.GTK_MINOR_VERSION,
|
||||
.patch = c.GTK_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = gtk.getMajorVersion(),
|
||||
.minor = gtk.getMinorVersion(),
|
||||
.patch = gtk.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("GTK version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version is at least the given version.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against.
|
||||
///
|
||||
/// This function should be used in cases where the version check would affect
|
||||
/// code generation, such as using symbols that are only available beyond a
|
||||
/// certain version. For checks which only depend on GTK's runtime behavior,
|
||||
/// use `runtimeAtLeast`.
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version,
|
||||
// we can return false immediately. This prevents us from
|
||||
// compiling against unknown symbols and makes runtime checks
|
||||
// very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
pub inline fn runtimeUntil(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt;
|
||||
}
|
||||
|
||||
test "atLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
test "runtimeUntil" {
|
||||
const testing = std.testing;
|
||||
|
||||
// This is an array in case we add a comptime variant.
|
||||
const funs = &.{runtimeUntil};
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user