gtk: convert App to zig-gobject (#6787)

pull/6796/head
Leah Amelia Chen 2025-03-18 09:14:19 +01:00 committed by GitHub
commit d75c5ec038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 305 additions and 283 deletions

View File

@ -55,8 +55,8 @@
.gobject = .{ .gobject = .{
// https://github.com/jcollie/ghostty-gobject based on zig_gobject // https://github.com/jcollie/ghostty-gobject based on zig_gobject
// Temporary until we generate them at build time automatically. // Temporary until we generate them at build time automatically.
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-11-16-1/ghostty-gobject-0.14.0-2025-03-11-16-1.tar.gz", .url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst",
.hash = "gobject-0.2.0-Skun7H6DlQDWCiNQtdE5TXYcCvx7MyjW01OQe5M_n_jV", .hash = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR",
.lazy = true, .lazy = true,
}, },

6
build.zig.zon.json generated
View File

@ -29,10 +29,10 @@
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz", "url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=" "hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
}, },
"gobject-0.2.0-Skun7H6DlQDWCiNQtdE5TXYcCvx7MyjW01OQe5M_n_jV": { "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR": {
"name": "gobject", "name": "gobject",
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-11-16-1/ghostty-gobject-0.14.0-2025-03-11-16-1.tar.gz", "url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst",
"hash": "sha256-eMmS9oysZheHwSCCvmOUSDJmP9zN7cAr6qqDIbz6EmY=" "hash": "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="
}, },
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
"name": "gtk4_layer_shell", "name": "gtk4_layer_shell",

6
build.zig.zon.nix generated
View File

@ -130,11 +130,11 @@ in
}; };
} }
{ {
name = "gobject-0.2.0-Skun7H6DlQDWCiNQtdE5TXYcCvx7MyjW01OQe5M_n_jV"; name = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "gobject"; name = "gobject";
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-11-16-1/ghostty-gobject-0.14.0-2025-03-11-16-1.tar.gz"; url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst";
hash = "sha256-eMmS9oysZheHwSCCvmOUSDJmP9zN7cAr6qqDIbz6EmY="; hash = "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI=";
}; };
} }
{ {

2
build.zig.zon.txt generated
View File

@ -26,7 +26,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/73948e6c0f15b1053cf74b7c4e6b04fd36e97e29.zip https://github.com/glfw/glfw/archive/73948e6c0f15b1053cf74b7c4e6b04fd36e97e29.zip
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-11-16-1/ghostty-gobject-0.14.0-2025-03-11-16-1.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e348884a00ef6c98dc837a873c4a867c9164d8a0.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e348884a00ef6c98dc837a873c4a867c9164d8a0.tar.gz
https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz

View File

@ -40,7 +40,6 @@ const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const CloseDialog = @import("CloseDialog.zig"); const CloseDialog = @import("CloseDialog.zig");
const Split = @import("Split.zig"); const Split = @import("Split.zig");
const c = @import("c.zig").c;
const version = @import("version.zig"); const version = @import("version.zig");
const inspector = @import("inspector.zig"); const inspector = @import("inspector.zig");
const key = @import("key.zig"); const key = @import("key.zig");
@ -48,6 +47,11 @@ const winprotopkg = @import("winproto.zig");
const testing = std.testing; const testing = std.testing;
const adwaita = @import("adwaita.zig"); const adwaita = @import("adwaita.zig");
pub const c = @cImport({
// generated header files
@cInclude("ghostty_resources.h");
});
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
pub const Options = struct {}; pub const Options = struct {};
@ -55,8 +59,8 @@ pub const Options = struct {};
core_app: *CoreApp, core_app: *CoreApp,
config: Config, config: Config,
app: *c.GtkApplication, app: *adw.Application,
ctx: *c.GMainContext, ctx: *glib.MainContext,
/// State and logic for the underlying windowing protocol. /// State and logic for the underlying windowing protocol.
winproto: winprotopkg.App, winproto: winprotopkg.App,
@ -86,15 +90,15 @@ running: bool = true,
transient_cgroup_base: ?[]const u8 = null, transient_cgroup_base: ?[]const u8 = null,
/// CSS Provider for any styles based on ghostty configuration values /// CSS Provider for any styles based on ghostty configuration values
css_provider: *c.GtkCssProvider, css_provider: *gtk.CssProvider,
/// Providers for loading custom stylesheets defined by user /// Providers for loading custom stylesheets defined by user
custom_css_providers: std.ArrayListUnmanaged(*c.GtkCssProvider) = .{}, custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .{},
/// The timer used to quit the application after the last window is closed. /// The timer used to quit the application after the last window is closed.
quit_timer: union(enum) { quit_timer: union(enum) {
off: void, off: void,
active: c.guint, active: c_uint,
expired: void, expired: void,
} = .{ .off = {} }, } = .{ .off = {} },
@ -102,22 +106,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
_ = opts; _ = opts;
// Log our GTK version // Log our GTK version
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{ version.logVersion();
c.GTK_MAJOR_VERSION,
c.GTK_MINOR_VERSION,
c.GTK_MICRO_VERSION,
c.gtk_get_major_version(),
c.gtk_get_minor_version(),
c.gtk_get_micro_version(),
});
// log the adwaita version // log the adwaita version
log.info("libadwaita version build={s} runtime={}.{}.{}", .{ adwaita.logVersion();
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
// Set gettext global domain to be our app so that our unqualified // Set gettext global domain to be our app so that our unqualified
// translations map to our translations. // translations map to our translations.
@ -271,9 +263,9 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
} }
} }
c.adw_init(); adw.init();
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse { const display: *gdk.Display = gdk.Display.getDefault() orelse {
// I'm unsure of any scenario where this happens. Because we don't // I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here. // want to litter null checks everywhere, we just exit here.
log.warn("gdk display is null, exiting", .{}); log.warn("gdk display is null, exiting", .{});
@ -291,9 +283,9 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}; };
// Setup the flags for our application. // Setup the flags for our application.
const app_flags: c.GApplicationFlags = app_flags: { const app_flags: gio.ApplicationFlags = app_flags: {
var flags: c.GApplicationFlags = c.G_APPLICATION_DEFAULT_FLAGS; var flags: gio.ApplicationFlags = .flags_default_flags;
if (!single_instance) flags |= c.G_APPLICATION_NON_UNIQUE; if (!single_instance) flags.non_unique = true;
break :app_flags flags; break :app_flags flags;
}; };
@ -321,91 +313,87 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// Using an AdwApplication lets us use Adwaita widgets and access things // Using an AdwApplication lets us use Adwaita widgets and access things
// such as the color scheme. // such as the color scheme.
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new( const adw_app = adw.Application.new(
app_id.ptr, app_id.ptr,
app_flags, app_flags,
))) orelse return error.GtkInitFailed; );
errdefer c.g_object_unref(adw_app); errdefer adw_app.unref();
const style_manager = c.adw_application_get_style_manager(adw_app); const style_manager = adw_app.getStyleManager();
c.adw_style_manager_set_color_scheme( style_manager.setColorScheme(
style_manager,
switch (config.@"window-theme") { switch (config.@"window-theme") {
.auto, .ghostty => auto: { .auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance(); const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5) break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT .prefer_light
else else
c.ADW_COLOR_SCHEME_PREFER_DARK; .prefer_dark;
}, },
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT, .system => .prefer_light,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK, .dark => .prefer_dark,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT, .light => .force_dark,
}, },
); );
const app: *c.GtkApplication = @ptrCast(adw_app); const gio_app = adw_app.as(gio.Application);
const gapp: *c.GApplication = @ptrCast(app);
// force the resource path to a known value so that it doesn't depend on // force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources // the app id and load in compiled resources
c.g_application_set_resource_base_path(gapp, "/com/mitchellh/ghostty"); gio_app.setResourceBasePath("/com/mitchellh/ghostty");
c.g_resources_register(c.ghostty_get_resource()); gio.resourcesRegister(@ptrCast(@alignCast(c.ghostty_get_resource() orelse {
log.err("unable to load resources", .{});
return error.GtkNoResources;
})));
// The `activate` signal is used when Ghostty is first launched and when a // The `activate` signal is used when Ghostty is first launched and when a
// secondary Ghostty is launched and requests a new window. // secondary Ghostty is launched and requests a new window.
_ = c.g_signal_connect_data( _ = gio.Application.signals.activate.connect(
app, adw_app,
"activate", *CoreApp,
c.G_CALLBACK(&gtkActivate), gtkActivate,
core_app, core_app,
null, .{},
c.G_CONNECT_DEFAULT,
); );
// Other signals // Other signals
_ = c.g_signal_connect_data( _ = gtk.Application.signals.window_added.connect(
app, adw_app,
"window-added", *CoreApp,
c.G_CALLBACK(&gtkWindowAdded), gtkWindowAdded,
core_app, core_app,
null, .{},
c.G_CONNECT_DEFAULT,
); );
_ = c.g_signal_connect_data( _ = gtk.Application.signals.window_removed.connect(
app, adw_app,
"window-removed", *CoreApp,
c.G_CALLBACK(&gtkWindowRemoved), gtkWindowRemoved,
core_app, core_app,
null, .{},
c.G_CONNECT_DEFAULT,
); );
// We don't use g_application_run, we want to manually control the // We don't use g_application_run, we want to manually control the
// loop so we have to do the same things the run function does: // loop so we have to do the same things the run function does:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
const ctx = c.g_main_context_default() orelse return error.GtkContextFailed; const ctx = glib.MainContext.default();
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed; if (glib.MainContext.acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer c.g_main_context_release(ctx); errdefer glib.MainContext.release(ctx);
var err_: ?*c.GError = null; var err_: ?*glib.Error = null;
if (c.g_application_register( if (gio_app.register(
gapp,
null, null,
@ptrCast(&err_), &err_,
) == 0) { ) == 0) {
if (err_) |err| { if (err_) |err| {
log.warn("error registering application: {s}", .{err.message}); log.warn("error registering application: {s}", .{err.f_message orelse "(unknown)"});
c.g_error_free(err); err.free();
} }
return error.GtkApplicationRegisterFailed; return error.GtkApplicationRegisterFailed;
} }
// FIXME: when App.zig is converted to zig-gobject
// Setup our windowing protocol logic // Setup our windowing protocol logic
var winproto_app = try winprotopkg.App.init( var winproto_app = try winprotopkg.App.init(
core_app.alloc, core_app.alloc,
@ptrCast(@alignCast(display)), display,
app_id, app_id,
&config, &config,
); );
@ -419,20 +407,20 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// //
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
if (config.@"initial-window") if (config.@"initial-window")
c.g_application_activate(gapp); gio_app.activate();
// Internally, GTK ensures that only one instance of this provider exists in the provider list // Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display. // for the display.
const css_provider = c.gtk_css_provider_new(); const css_provider = gtk.CssProvider.new();
c.gtk_style_context_add_provider_for_display( gtk.StyleContext.addProviderForDisplay(
display, display,
@ptrCast(css_provider), css_provider.as(gtk.StyleProvider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 3, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
); );
return .{ return .{
.core_app = core_app, .core_app = core_app,
.app = app, .app = adw_app,
.config = config, .config = config,
.ctx = ctx, .ctx = ctx,
.cursor_none = cursor_none, .cursor_none = cursor_none,
@ -441,7 +429,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// If we are NOT the primary instance, then we never want to run. // 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 // This means that another instance of the GTK app is running and
// our "activate" call above will open a window. // our "activate" call above will open a window.
.running = c.g_application_get_is_remote(gapp) == 0, .running = gio_app.getIsRemote() == 0,
.css_provider = css_provider, .css_provider = css_provider,
}; };
} }
@ -449,16 +437,16 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// Terminate the application. The application will not be restarted after // Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up. // this so all global state can be cleaned up.
pub fn terminate(self: *App) void { pub fn terminate(self: *App) void {
c.g_settings_sync(); gio.Settings.sync();
while (c.g_main_context_iteration(self.ctx, 0) != 0) {} while (glib.MainContext.iteration(self.ctx, 0) != 0) {}
c.g_main_context_release(self.ctx); glib.MainContext.release(self.ctx);
c.g_object_unref(self.app); self.app.unref();
if (self.cursor_none) |cursor| c.g_object_unref(cursor); if (self.cursor_none) |cursor| cursor.unref();
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path); if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
for (self.custom_css_providers.items) |provider| { for (self.custom_css_providers.items) |provider| {
c.g_object_unref(provider); provider.unref();
} }
self.custom_css_providers.deinit(self.core_app.alloc); self.custom_css_providers.deinit(self.core_app.alloc);
@ -922,29 +910,25 @@ fn showDesktopNotification(
else => n.title, else => n.title,
}; };
const notification = c.g_notification_new(t.ptr); const notification = gio.Notification.new(t);
defer c.g_object_unref(notification); defer notification.unref();
c.g_notification_set_body(notification, n.body.ptr); notification.setBody(n.body);
const icon = c.g_themed_icon_new("com.mitchellh.ghostty"); const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
defer c.g_object_unref(icon); defer icon.unref();
c.g_notification_set_icon(notification, icon); notification.setIcon(icon.as(gio.Icon));
const pointer = c.g_variant_new_uint64(switch (target) { const pointer = glib.Variant.newUint64(switch (target) {
.app => 0, .app => 0,
.surface => |v| @intFromPtr(v), .surface => |v| @intFromPtr(v),
}); });
c.g_notification_set_default_action_and_target_value( notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
notification,
"app.present-surface",
pointer,
);
const g_app: *c.GApplication = @ptrCast(self.app); const gio_app = self.app.as(gio.Application);
// We set the notification ID to the body content. If the content is the // We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification // same, this notification may replace a previous notification
c.g_application_send_notification(g_app, n.body.ptr, notification); gio_app.sendNotification(n.body, notification);
} }
fn configChange( fn configChange(
@ -1076,29 +1060,29 @@ fn syncActionAccelerator(
gtk_action: [:0]const u8, gtk_action: [:0]const u8,
action: input.Binding.Action, action: input.Binding.Action,
) !void { ) !void {
const gtk_app = self.app.as(gtk.Application);
// Reset it initially // Reset it initially
const zero = [_]?[*:0]const u8{null}; const zero = [_:null]?[*:0]const u8{};
c.gtk_application_set_accels_for_action(@ptrCast(self.app), gtk_action.ptr, &zero); gtk_app.setAccelsForAction(gtk_action, &zero);
const trigger = self.config.keybind.set.getTrigger(action) orelse return; const trigger = self.config.keybind.set.getTrigger(action) orelse return;
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
const accel = try key.accelFromTrigger(&buf, trigger) orelse return; const accel = try key.accelFromTrigger(&buf, trigger) orelse return;
const accels = [_]?[*:0]const u8{ accel, null }; const accels = [_:null]?[*:0]const u8{accel};
c.gtk_application_set_accels_for_action( gtk_app.setAccelsForAction(gtk_action, &accels);
@ptrCast(self.app),
gtk_action.ptr,
&accels,
);
} }
fn loadRuntimeCss( fn loadRuntimeCss(
self: *const App, self: *const App,
) Allocator.Error!void { ) Allocator.Error!void {
var stack_alloc = std.heap.stackFallback(4096, self.core_app.alloc); const alloc = self.core_app.alloc;
var buf = std.ArrayList(u8).init(stack_alloc.get());
defer buf.deinit(); var buf: std.ArrayListUnmanaged(u8) = .empty;
const writer = buf.writer(); defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config: *const Config = &self.config; const config: *const Config = &self.config;
const window_theme = config.@"window-theme"; const window_theme = config.@"window-theme";
@ -1190,20 +1174,28 @@ fn loadRuntimeCss(
}); });
} }
const data = try alloc.dupeZ(u8, buf.items);
defer alloc.free(data);
// Clears any previously loaded CSS from this provider // Clears any previously loaded CSS from this provider
loadCssProviderFromData(self.css_provider, buf.items); loadCssProviderFromData(self.css_provider, data);
} }
fn loadCustomCss(self: *App) !void { fn loadCustomCss(self: *App) !void {
const display = c.gdk_display_get_default(); const alloc = self.core_app.alloc;
const display = gdk.Display.getDefault() orelse {
log.warn("unable to get display", .{});
return;
};
// unload the previously loaded style providers // unload the previously loaded style providers
for (self.custom_css_providers.items) |provider| { for (self.custom_css_providers.items) |provider| {
c.gtk_style_context_remove_provider_for_display( gtk.StyleContext.removeProviderForDisplay(
display, display,
@ptrCast(provider), provider.as(gtk.StyleProvider),
); );
c.g_object_unref(provider); provider.unref();
} }
self.custom_css_providers.clearRetainingCapacity(); self.custom_css_providers.clearRetainingCapacity();
@ -1214,49 +1206,51 @@ fn loadCustomCss(self: *App) !void {
}; };
const file = std.fs.openFileAbsolute(path, .{}) catch |err| { const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
if (err != error.FileNotFound or !optional) { if (err != error.FileNotFound or !optional) {
log.err("error opening gtk-custom-css file {s}: {}", .{ path, err }); log.err(
"error opening gtk-custom-css file {s}: {}",
.{ path, err },
);
} }
continue; continue;
}; };
defer file.close(); defer file.close();
log.info("loading gtk-custom-css path={s}", .{path}); log.info("loading gtk-custom-css path={s}", .{path});
const contents = try file.reader().readAllAlloc(self.core_app.alloc, 5 * 1024 * 1024 // 5MB const contents = try file.reader().readAllAlloc(
self.core_app.alloc,
5 * 1024 * 1024, // 5MB,
); );
defer self.core_app.alloc.free(contents); defer alloc.free(contents);
const provider = c.gtk_css_provider_new(); const data = try alloc.dupeZ(u8, contents);
c.gtk_style_context_add_provider_for_display( defer alloc.free(data);
const provider = gtk.CssProvider.new();
loadCssProviderFromData(provider, data);
gtk.StyleContext.addProviderForDisplay(
display, display,
@ptrCast(provider), provider.as(gtk.StyleProvider),
c.GTK_STYLE_PROVIDER_PRIORITY_USER, gtk.STYLE_PROVIDER_PRIORITY_USER,
); );
loadCssProviderFromData(provider, contents);
try self.custom_css_providers.append(self.core_app.alloc, provider); try self.custom_css_providers.append(self.core_app.alloc, provider);
} }
} }
fn loadCssProviderFromData(provider: *c.GtkCssProvider, data: []const u8) void { fn loadCssProviderFromData(provider: *gtk.CssProvider, data: [:0]const u8) void {
if (version.atLeast(4, 12, 0)) { if (version.atLeast(4, 12, 0)) {
const g_bytes = c.g_bytes_new(data.ptr, data.len); const g_bytes = glib.Bytes.new(data.ptr, data.len);
defer c.g_bytes_unref(g_bytes); defer g_bytes.unref();
c.gtk_css_provider_load_from_bytes(provider, g_bytes); provider.loadFromBytes(g_bytes);
} else { } else {
c.gtk_css_provider_load_from_data( provider.loadFromData(data, @intCast(data.len));
provider,
data.ptr,
@intCast(data.len),
);
} }
} }
/// Called by CoreApp to wake up the event loop. /// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: App) void { pub fn wakeup(_: App) void {
_ = self; glib.MainContext.wakeup(null);
c.g_main_context_wakeup(null);
} }
/// Run the event loop. This doesn't return until the app exits. /// Run the event loop. This doesn't return until the app exits.
@ -1297,8 +1291,7 @@ pub fn run(self: *App) !void {
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"}); } else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
// Setup color scheme notifications // Setup color scheme notifications
const adw_app: *adw.Application = @ptrCast(@alignCast(self.app)); const style_manager: *adw.StyleManager = self.app.getStyleManager();
const style_manager: *adw.StyleManager = adw_app.getStyleManager();
_ = gobject.Object.signals.notify.connect( _ = gobject.Object.signals.notify.connect(
style_manager, style_manager,
*App, *App,
@ -1324,7 +1317,7 @@ pub fn run(self: *App) !void {
}; };
while (self.running) { while (self.running) {
_ = c.g_main_context_iteration(self.ctx, 1); _ = glib.MainContext.iteration(self.ctx, 1);
// Tick the terminal app and see if we should quit. // Tick the terminal app and see if we should quit.
try self.core_app.tick(self); try self.core_app.tick(self);
@ -1363,7 +1356,13 @@ fn startQuitTimer(self: *App) void {
if (self.config.@"quit-after-last-window-closed-delay") |v| { if (self.config.@"quit-after-last-window-closed-delay") |v| {
// If a delay is configured, set a timeout function to quit after the delay. // If a delay is configured, set a timeout function to quit after the delay.
self.quit_timer = .{ .active = c.g_timeout_add(v.asMilliseconds(), gtkQuitTimerExpired, self) }; self.quit_timer = .{
.active = glib.timeoutAdd(
v.asMilliseconds(),
gtkQuitTimerExpired,
self,
),
};
} else { } else {
// If no delay is configured, treat it as expired. // If no delay is configured, treat it as expired.
self.quit_timer = .{ .expired = {} }; self.quit_timer = .{ .expired = {} };
@ -1453,14 +1452,13 @@ fn quit(self: *App) void {
/// This immediately destroys all windows, forcing the application to quit. /// This immediately destroys all windows, forcing the application to quit.
pub fn quitNow(self: *App) void { pub fn quitNow(self: *App) void {
const list = c.gtk_window_list_toplevels(); const list = gtk.Window.listToplevels();
defer c.g_list_free(list); defer list.free();
c.g_list_foreach(list, struct { list.foreach(struct {
fn callback(data: c.gpointer, _: c.gpointer) callconv(.C) void { fn callback(data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
const ptr = data orelse return; const ptr = data orelse return;
const widget: *c.GtkWidget = @ptrCast(@alignCast(ptr)); const window: *gtk.Window = @ptrCast(@alignCast(ptr));
const window: *c.GtkWindow = @ptrCast(widget); window.destroy();
c.gtk_window_destroy(window);
} }
}.callback, null); }.callback, null);
@ -1469,11 +1467,7 @@ pub fn quitNow(self: *App) void {
/// This is called by the `activate` signal. This is sent on program startup and /// This is called by the `activate` signal. This is sent on program startup and
/// also when a secondary instance launches and requests a new window. /// also when a secondary instance launches and requests a new window.
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {
_ = app;
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
// Queue a new window // Queue a new window
_ = core_app.mailbox.push(.{ _ = core_app.mailbox.push(.{
.new_window = .{}, .new_window = .{},
@ -1481,46 +1475,41 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
} }
fn gtkWindowAdded( fn gtkWindowAdded(
_: *c.GtkApplication, _: *adw.Application,
window: *c.GtkWindow, window: *gtk.Window,
ud: ?*anyopaque, core_app: *CoreApp,
) callconv(.C) void { ) callconv(.c) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
// Request the is-active property change so we can detect // Request the is-active property change so we can detect
// when our app loses focus. // when our app loses focus.
_ = c.g_signal_connect_data( _ = gobject.Object.signals.notify.connect(
window, window,
"notify::is-active", *CoreApp,
c.G_CALLBACK(&gtkWindowIsActive), gtkWindowIsActive,
core_app, core_app,
null, .{
c.G_CONNECT_DEFAULT, .detail = "is-active",
},
); );
} }
fn gtkWindowRemoved( fn gtkWindowRemoved(
_: *c.GtkApplication, _: *adw.Application,
_: *c.GtkWindow, _: *gtk.Window,
ud: ?*anyopaque, core_app: *CoreApp,
) callconv(.C) void { ) callconv(.c) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
// Recheck if we are focused // Recheck if we are focused
gtkWindowIsActive(null, undefined, core_app); gtkWindowIsActive(null, undefined, core_app);
} }
fn gtkWindowIsActive( fn gtkWindowIsActive(
window: ?*c.GtkWindow, window: ?*gtk.Window,
_: *c.GParamSpec, _: *gobject.ParamSpec,
ud: ?*anyopaque, core_app: *CoreApp,
) callconv(.C) void { ) callconv(.c) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
// If our window is active, then we can tell the app // If our window is active, then we can tell the app
// that we are focused. // that we are focused.
if (window) |w| { if (window) |w| {
if (c.gtk_window_is_active(w) == 1) { if (w.isActive() != 0) {
core_app.focusEvent(true); core_app.focusEvent(true);
return; return;
} }
@ -1529,16 +1518,17 @@ fn gtkWindowIsActive(
// If the window becomes inactive, we need to check if any // If the window becomes inactive, we need to check if any
// other windows are active. If not, then we are no longer // other windows are active. If not, then we are no longer
// focused. // focused.
if (c.gtk_window_list_toplevels()) |list| { {
defer c.g_list_free(list); const list = gtk.Window.listToplevels();
var current: ?*c.GList = list; defer list.free();
while (current) |elem| : (current = elem.next) { var current: ?*glib.List = list;
while (current) |elem| : (current = elem.f_next) {
// If the window is active then we are still focused. // If the window is active then we are still focused.
// This is another window since we did our check above. // This is another window since we did our check above.
// That window should trigger its own is-active // That window should trigger its own is-active
// callback so we don't need to call it here. // callback so we don't need to call it here.
const w: *c.GtkWindow = @alignCast(@ptrCast(elem.data)); const w: *gtk.Window = @alignCast(@ptrCast(elem.f_data));
if (c.gtk_window_is_active(w) == 1) return; if (w.isActive() == 1) return;
} }
} }
@ -1575,33 +1565,30 @@ fn colorSchemeEvent(
} }
fn gtkActionOpenConfig( fn gtkActionOpenConfig(
_: *c.GSimpleAction, _: *gio.SimpleAction,
_: *c.GVariant, _: ?*glib.Variant,
ud: ?*anyopaque, self: *App,
) callconv(.C) void { ) callconv(.c) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_ = self.core_app.mailbox.push(.{ _ = self.core_app.mailbox.push(.{
.open_config = {}, .open_config = {},
}, .{ .forever = {} }); }, .{ .forever = {} });
} }
fn gtkActionReloadConfig( fn gtkActionReloadConfig(
_: *c.GSimpleAction, _: *gio.SimpleAction,
_: *c.GVariant, _: ?*glib.Variant,
ud: ?*anyopaque, self: *App,
) callconv(.C) void { ) callconv(.c) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
self.reloadConfig(.app, .{}) catch |err| { self.reloadConfig(.app, .{}) catch |err| {
log.err("error reloading configuration: {}", .{err}); log.err("error reloading configuration: {}", .{err});
}; };
} }
fn gtkActionQuit( fn gtkActionQuit(
_: *c.GSimpleAction, _: *gio.SimpleAction,
_: *c.GVariant, _: ?*glib.Variant,
ud: ?*anyopaque, self: *App,
) callconv(.C) void { ) callconv(.c) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
self.core_app.performAction(self, .quit) catch |err| { self.core_app.performAction(self, .quit) catch |err| {
log.err("error quitting err={}", .{err}); log.err("error quitting err={}", .{err});
}; };
@ -1610,21 +1597,24 @@ fn gtkActionQuit(
/// Action sent by the window manager asking us to present a specific surface to /// Action sent by the window manager asking us to present a specific surface to
/// the user. Usually because the user clicked on a desktop notification. /// the user. Usually because the user clicked on a desktop notification.
fn gtkActionPresentSurface( fn gtkActionPresentSurface(
_: *c.GSimpleAction, _: *gio.SimpleAction,
parameter: *c.GVariant, parameter_: ?*glib.Variant,
ud: ?*anyopaque, self: *App,
) callconv(.C) void { ) callconv(.c) void {
const self: *App = @ptrCast(@alignCast(ud orelse return)); const parameter = parameter_ orelse return;
const t = glib.ext.VariantType.newFor(u64);
defer glib.VariantType.free(t);
// Make sure that we've receiived a u64 from the system. // Make sure that we've receiived a u64 from the system.
if (c.g_variant_is_of_type(parameter, c.G_VARIANT_TYPE("t")) == 0) { if (glib.Variant.isOfType(parameter, t) == 0) {
return; return;
} }
// Convert that u64 to pointer to a core surface. A value of zero // Convert that u64 to pointer to a core surface. A value of zero
// means that there was no target surface for the notification so // means that there was no target surface for the notification so
// we don't focus any surface. // we don't focus any surface.
const ptr_int: u64 = c.g_variant_get_uint64(parameter); const ptr_int = parameter.getUint64();
if (ptr_int == 0) return; if (ptr_int == 0) return;
const surface: *CoreSurface = @ptrFromInt(ptr_int); const surface: *CoreSurface = @ptrFromInt(ptr_int);
@ -1654,25 +1644,27 @@ fn initActions(self: *App) void {
// //
// For action names: // For action names:
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html // https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
const actions = .{ const t = glib.ext.VariantType.newFor(u64);
.{ "quit", &gtkActionQuit, null }, defer glib.VariantType.free(t);
.{ "open-config", &gtkActionOpenConfig, null },
.{ "reload-config", &gtkActionReloadConfig, null },
.{ "present-surface", &gtkActionPresentSurface, c.G_VARIANT_TYPE("t") },
};
const actions = .{
.{ "quit", gtkActionQuit, null },
.{ "open-config", gtkActionOpenConfig, null },
.{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t },
};
inline for (actions) |entry| { inline for (actions) |entry| {
const action = c.g_simple_action_new(entry[0], entry[2]); const action = gio.SimpleAction.new(entry[0], entry[2]);
defer c.g_object_unref(action); defer action.unref();
_ = c.g_signal_connect_data( _ = gio.SimpleAction.signals.activate.connect(
action, action,
"activate", *App,
c.G_CALLBACK(entry[1]), entry[1],
self, self,
null, .{},
c.G_CONNECT_DEFAULT,
); );
c.g_action_map_add_action(@ptrCast(self.app), @ptrCast(action)); const action_map = self.app.as(gio.ActionMap);
action_map.addAction(action.as(gio.Action));
} }
} }

View File

@ -1341,8 +1341,7 @@ pub fn showDesktopNotification(
const pointer = glib.Variant.newUint64(@intFromPtr(&self.core_surface)); const pointer = glib.Variant.newUint64(@intFromPtr(&self.core_surface));
notification.setDefaultActionAndTargetValue("app.present-surface", pointer); notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
// FIXME: when App.zig gets converted to zig-gobject const app = self.app.app.as(gio.Application);
const app: gio.Application = @ptrCast(@alignCast(self.app.app));
// We set the notification ID to the body content. If the content is the // We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification // same, this notification may replace a previous notification

View File

@ -141,9 +141,8 @@ pub fn init(self: *Window, app: *App) !void {
.winproto = .none, .winproto = .none,
}; };
// FIXME: when App.zig is converted to zig-gobject
// Create the window // Create the window
self.window = adw.ApplicationWindow.new(@ptrCast(@alignCast(app.app))); self.window = adw.ApplicationWindow.new(app.app.as(gtk.Application));
const gtk_window = self.window.as(gtk.Window); const gtk_window = self.window.as(gtk.Window);
const gtk_widget = self.window.as(gtk.Widget); const gtk_widget = self.window.as(gtk.Widget);
errdefer gtk_window.destroy(); errdefer gtk_window.destroy();

View File

@ -1,27 +1,45 @@
const std = @import("std"); const std = @import("std");
const c = @import("c.zig").c;
// 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 fn logVersion() void {
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
adw.getMajorVersion(),
adw.getMinorVersion(),
adw.getMicroVersion(),
});
}
/// Verifies that the running libadwaita version is at least the given /// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to /// version. This will return false if Ghostty is configured to not build with
/// not build with libadwaita. /// libadwaita.
/// ///
/// This can be run in both a comptime and runtime context. If it /// This can be run in both a comptime and runtime context. If it is run in a
/// is run in a comptime context, it will only check the version /// comptime context, it will only check the version in the headers. If it is
/// in the headers. If it is run in a runtime context, it will /// run in a runtime context, it will check the actual version of the library we
/// check the actual version of the library we are linked against. /// are linked against. So generally you probably want to do both checks!
/// So generally you probably want to do both checks!
/// ///
/// This is inlined so that the comptime checks will disable the /// This is inlined so that the comptime checks will disable the runtime checks
/// runtime checks if the comptime checks fail. /// if the comptime checks fail.
pub inline fn versionAtLeast( pub inline fn versionAtLeast(
comptime major: u16, comptime major: u16,
comptime minor: u16, comptime minor: u16,
comptime micro: u16, comptime micro: u16,
) bool { ) bool {
// If our header has lower versions than the given version, // If our header has lower versions than the given version, we can return
// we can return false immediately. This prevents us from // false immediately. This prevents us from compiling against unknown
// compiling against unknown symbols and makes runtime checks // symbols and makes runtime checks very slightly faster.
// very slightly faster.
if (comptime c.ADW_MAJOR_VERSION < major or if (comptime c.ADW_MAJOR_VERSION < major or
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro)) (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro))
@ -30,14 +48,13 @@ pub inline fn versionAtLeast(
// If we're in comptime then we can't check the runtime version. // If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true; if (@inComptime()) return true;
// We use the functions instead of the constants such as // We use the functions instead of the constants such as c.ADW_MINOR_VERSION
// c.ADW_MINOR_VERSION because the function gets the actual // because the function gets the actual runtime version.
// runtime version. if (adw.getMajorVersion() >= major) {
if (c.adw_get_major_version() >= major) { if (adw.getMajorVersion() > major) return true;
if (c.adw_get_major_version() > major) return true; if (adw.getMinorVersion() >= minor) {
if (c.adw_get_minor_version() >= minor) { if (adw.getMinorVersion() > minor) return true;
if (c.adw_get_minor_version() > minor) return true; return adw.getMicroVersion() >= micro;
return c.adw_get_micro_version() >= micro;
} }
} }

View File

@ -3,9 +3,6 @@ pub const c = @cImport({
@cInclude("gtk/gtk.h"); @cInclude("gtk/gtk.h");
@cInclude("adwaita.h"); @cInclude("adwaita.h");
// generated header files
@cInclude("ghostty_resources.h");
// compatibility // compatibility
@cInclude("ghostty_gtk_compat.h"); @cInclude("ghostty_gtk_compat.h");
}); });

View File

@ -113,9 +113,8 @@ fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
/// On success this will return the name of the transient scope /// On success this will return the name of the transient scope
/// cgroup prefix, allocated with the given allocator. /// cgroup prefix, allocated with the given allocator.
fn createScope(app: *App, pid_: std.os.linux.pid_t) !void { fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
// FIXME: when app.app gets converted to gobject const gio_app = app.app.as(gio.Application);
const g_app: *gio.Application = @ptrCast(@alignCast(app.app)); const connection = gio_app.getDbusConnection() orelse
const connection = g_app.getDbusConnection() orelse
return error.DbusConnectionRequired; return error.DbusConnectionRequired;
const pid: u32 = @intCast(pid_); const pid: u32 = @intCast(pid_);

View File

@ -136,7 +136,7 @@ const Window = struct {
}; };
// Create the window // Create the window
const window = c.gtk_application_window_new(inspector.surface.app.app); const window = c.gtk_application_window_new(@ptrCast(@alignCast(inspector.surface.app.app)));
const gtk_window: *c.GtkWindow = @ptrCast(window); const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window); errdefer c.gtk_window_destroy(gtk_window);
self.window = gtk_window; self.window = gtk_window;

View File

@ -1,19 +1,41 @@
const c = @import("c.zig").c; 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 fn logVersion() void {
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{
c.GTK_MAJOR_VERSION,
c.GTK_MINOR_VERSION,
c.GTK_MICRO_VERSION,
gtk.getMajorVersion(),
gtk.getMinorVersion(),
gtk.getMicroVersion(),
});
}
/// Verifies that the GTK version is at least the given version. /// Verifies that the GTK version is at least the given version.
/// ///
/// This can be run in both a comptime and runtime context. If it /// This can be run in both a comptime and runtime context. If it is run in a
/// is run in a comptime context, it will only check the version /// comptime context, it will only check the version in the headers. If it is
/// in the headers. If it is run in a runtime context, it will /// run in a runtime context, it will check the actual version of the library we
/// check the actual version of the library we are linked against. /// are linked against.
/// ///
/// This function should be used in cases where the version check /// This function should be used in cases where the version check would affect
/// would affect code generation, such as using symbols that are /// code generation, such as using symbols that are only available beyond a
/// only available beyond a certain version. For checks which only /// certain version. For checks which only depend on GTK's runtime behavior,
/// depend on GTK's runtime behavior, use `runtimeAtLeast`. /// use `runtimeAtLeast`.
/// ///
/// This is inlined so that the comptime checks will disable the /// This is inlined so that the comptime checks will disable the runtime checks
/// runtime checks if the comptime checks fail. /// if the comptime checks fail.
pub inline fn atLeast( pub inline fn atLeast(
comptime major: u16, comptime major: u16,
comptime minor: u16, comptime minor: u16,
@ -34,25 +56,23 @@ pub inline fn atLeast(
return runtimeAtLeast(major, minor, micro); return runtimeAtLeast(major, minor, micro);
} }
/// Verifies that the GTK version at runtime is at least the given /// Verifies that the GTK version at runtime is at least the given version.
/// version.
/// ///
/// This function should be used in cases where the only the runtime /// This function should be used in cases where the only the runtime behavior
/// behavior is affected by the version check. For checks which would /// is affected by the version check. For checks which would affect code
/// affect code generation, use `atLeast`. /// generation, use `atLeast`.
pub inline fn runtimeAtLeast( pub inline fn runtimeAtLeast(
comptime major: u16, comptime major: u16,
comptime minor: u16, comptime minor: u16,
comptime micro: u16, comptime micro: u16,
) bool { ) bool {
// We use the functions instead of the constants such as // We use the functions instead of the constants such as c.GTK_MINOR_VERSION
// c.GTK_MINOR_VERSION because the function gets the actual // because the function gets the actual runtime version.
// runtime version. if (gtk.getMajorVersion() >= major) {
if (c.gtk_get_major_version() >= major) { if (gtk.getMajorVersion() > major) return true;
if (c.gtk_get_major_version() > major) return true; if (gtk.getMinorVersion() >= minor) {
if (c.gtk_get_minor_version() >= minor) { if (gtk.getMinorVersion() > minor) return true;
if (c.gtk_get_minor_version() > minor) return true; return gtk.getMicroVersion() >= micro;
return c.gtk_get_micro_version() >= micro;
} }
} }
@ -60,7 +80,6 @@ pub inline fn runtimeAtLeast(
} }
test "atLeast" { test "atLeast" {
const std = @import("std");
const testing = std.testing; const testing = std.testing;
const funs = &.{ atLeast, runtimeAtLeast }; const funs = &.{ atLeast, runtimeAtLeast };