gtk: convert App to zig-gobject

This commit is contained in:
Jeffrey C. Ollie
2025-03-17 23:39:45 -05:00
parent 742bca713d
commit ee95a5f3e0
12 changed files with 305 additions and 283 deletions

View File

@ -55,8 +55,8 @@
.gobject = .{
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
// 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",
.hash = "gobject-0.2.0-Skun7H6DlQDWCiNQtdE5TXYcCvx7MyjW01OQe5M_n_jV",
.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-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR",
.lazy = true,
},

6
build.zig.zon.json generated
View File

@ -29,10 +29,10 @@
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
},
"gobject-0.2.0-Skun7H6DlQDWCiNQtdE5TXYcCvx7MyjW01OQe5M_n_jV": {
"gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR": {
"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",
"hash": "sha256-eMmS9oysZheHwSCCvmOUSDJmP9zN7cAr6qqDIbz6EmY="
"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-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="
},
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
"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 {
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";
hash = "sha256-eMmS9oysZheHwSCCvmOUSDJmP9zN7cAr6qqDIbz6EmY=";
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-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/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
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/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.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 CloseDialog = @import("CloseDialog.zig");
const Split = @import("Split.zig");
const c = @import("c.zig").c;
const version = @import("version.zig");
const inspector = @import("inspector.zig");
const key = @import("key.zig");
@ -48,6 +47,11 @@ const winprotopkg = @import("winproto.zig");
const testing = std.testing;
const adwaita = @import("adwaita.zig");
pub const c = @cImport({
// generated header files
@cInclude("ghostty_resources.h");
});
const log = std.log.scoped(.gtk);
pub const Options = struct {};
@ -55,8 +59,8 @@ pub const Options = struct {};
core_app: *CoreApp,
config: Config,
app: *c.GtkApplication,
ctx: *c.GMainContext,
app: *adw.Application,
ctx: *glib.MainContext,
/// State and logic for the underlying windowing protocol.
winproto: winprotopkg.App,
@ -86,15 +90,15 @@ running: bool = true,
transient_cgroup_base: ?[]const u8 = null,
/// 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
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.
quit_timer: union(enum) {
off: void,
active: c.guint,
active: c_uint,
expired: void,
} = .{ .off = {} },
@ -102,22 +106,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
_ = opts;
// Log our GTK version
log.info("GTK version build={d}.{d}.{d} runtime={d}.{d}.{d}", .{
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(),
});
version.logVersion();
// log the adwaita version
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
adwaita.logVersion();
// Set gettext global domain to be our app so that our unqualified
// 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
// want to litter null checks everywhere, we just exit here.
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.
const app_flags: c.GApplicationFlags = app_flags: {
var flags: c.GApplicationFlags = c.G_APPLICATION_DEFAULT_FLAGS;
if (!single_instance) flags |= c.G_APPLICATION_NON_UNIQUE;
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;
};
@ -321,91 +313,87 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// Using an AdwApplication lets us use Adwaita widgets and access things
// 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_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);
c.adw_style_manager_set_color_scheme(
style_manager,
const style_manager = adw_app.getStyleManager();
style_manager.setColorScheme(
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT
.prefer_light
else
c.ADW_COLOR_SCHEME_PREFER_DARK;
.prefer_dark;
},
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
.system => .prefer_light,
.dark => .prefer_dark,
.light => .force_dark,
},
);
const app: *c.GtkApplication = @ptrCast(adw_app);
const gapp: *c.GApplication = @ptrCast(app);
const gio_app = adw_app.as(gio.Application);
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
c.g_application_set_resource_base_path(gapp, "/com/mitchellh/ghostty");
c.g_resources_register(c.ghostty_get_resource());
gio_app.setResourceBasePath("/com/mitchellh/ghostty");
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
// secondary Ghostty is launched and requests a new window.
_ = c.g_signal_connect_data(
app,
"activate",
c.G_CALLBACK(&gtkActivate),
_ = gio.Application.signals.activate.connect(
adw_app,
*CoreApp,
gtkActivate,
core_app,
null,
c.G_CONNECT_DEFAULT,
.{},
);
// Other signals
_ = c.g_signal_connect_data(
app,
"window-added",
c.G_CALLBACK(&gtkWindowAdded),
_ = gtk.Application.signals.window_added.connect(
adw_app,
*CoreApp,
gtkWindowAdded,
core_app,
null,
c.G_CONNECT_DEFAULT,
.{},
);
_ = c.g_signal_connect_data(
app,
"window-removed",
c.G_CALLBACK(&gtkWindowRemoved),
_ = gtk.Application.signals.window_removed.connect(
adw_app,
*CoreApp,
gtkWindowRemoved,
core_app,
null,
c.G_CONNECT_DEFAULT,
.{},
);
// 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:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
const ctx = c.g_main_context_default() orelse return error.GtkContextFailed;
if (c.g_main_context_acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer c.g_main_context_release(ctx);
const ctx = glib.MainContext.default();
if (glib.MainContext.acquire(ctx) == 0) return error.GtkContextAcquireFailed;
errdefer glib.MainContext.release(ctx);
var err_: ?*c.GError = null;
if (c.g_application_register(
gapp,
var err_: ?*glib.Error = null;
if (gio_app.register(
null,
@ptrCast(&err_),
&err_,
) == 0) {
if (err_) |err| {
log.warn("error registering application: {s}", .{err.message});
c.g_error_free(err);
log.warn("error registering application: {s}", .{err.f_message orelse "(unknown)"});
err.free();
}
return error.GtkApplicationRegisterFailed;
}
// FIXME: when App.zig is converted to zig-gobject
// Setup our windowing protocol logic
var winproto_app = try winprotopkg.App.init(
core_app.alloc,
@ptrCast(@alignCast(display)),
display,
app_id,
&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
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
// for the display.
const css_provider = c.gtk_css_provider_new();
c.gtk_style_context_add_provider_for_display(
const css_provider = gtk.CssProvider.new();
gtk.StyleContext.addProviderForDisplay(
display,
@ptrCast(css_provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
css_provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
);
return .{
.core_app = core_app,
.app = app,
.app = adw_app,
.config = config,
.ctx = ctx,
.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.
// This means that another instance of the GTK app is running and
// 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,
};
}
@ -449,16 +437,16 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// Terminate the application. The application will not be restarted after
// this so all global state can be cleaned up.
pub fn terminate(self: *App) void {
c.g_settings_sync();
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
c.g_main_context_release(self.ctx);
c.g_object_unref(self.app);
gio.Settings.sync();
while (glib.MainContext.iteration(self.ctx, 0) != 0) {}
glib.MainContext.release(self.ctx);
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);
for (self.custom_css_providers.items) |provider| {
c.g_object_unref(provider);
provider.unref();
}
self.custom_css_providers.deinit(self.core_app.alloc);
@ -922,29 +910,25 @@ fn showDesktopNotification(
else => n.title,
};
const notification = c.g_notification_new(t.ptr);
defer c.g_object_unref(notification);
c.g_notification_set_body(notification, n.body.ptr);
const notification = gio.Notification.new(t);
defer notification.unref();
notification.setBody(n.body);
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
defer c.g_object_unref(icon);
c.g_notification_set_icon(notification, icon);
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
defer icon.unref();
notification.setIcon(icon.as(gio.Icon));
const pointer = c.g_variant_new_uint64(switch (target) {
const pointer = glib.Variant.newUint64(switch (target) {
.app => 0,
.surface => |v| @intFromPtr(v),
});
c.g_notification_set_default_action_and_target_value(
notification,
"app.present-surface",
pointer,
);
notification.setDefaultActionAndTargetValue("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
// 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(
@ -1076,29 +1060,29 @@ fn syncActionAccelerator(
gtk_action: [:0]const u8,
action: input.Binding.Action,
) !void {
const gtk_app = self.app.as(gtk.Application);
// Reset it initially
const zero = [_]?[*:0]const u8{null};
c.gtk_application_set_accels_for_action(@ptrCast(self.app), gtk_action.ptr, &zero);
const zero = [_:null]?[*:0]const u8{};
gtk_app.setAccelsForAction(gtk_action, &zero);
const trigger = self.config.keybind.set.getTrigger(action) orelse return;
var buf: [256]u8 = undefined;
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(
@ptrCast(self.app),
gtk_action.ptr,
&accels,
);
gtk_app.setAccelsForAction(gtk_action, &accels);
}
fn loadRuntimeCss(
self: *const App,
) Allocator.Error!void {
var stack_alloc = std.heap.stackFallback(4096, self.core_app.alloc);
var buf = std.ArrayList(u8).init(stack_alloc.get());
defer buf.deinit();
const writer = buf.writer();
const alloc = self.core_app.alloc;
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config: *const Config = &self.config;
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
loadCssProviderFromData(self.css_provider, buf.items);
loadCssProviderFromData(self.css_provider, data);
}
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
for (self.custom_css_providers.items) |provider| {
c.gtk_style_context_remove_provider_for_display(
gtk.StyleContext.removeProviderForDisplay(
display,
@ptrCast(provider),
provider.as(gtk.StyleProvider),
);
c.g_object_unref(provider);
provider.unref();
}
self.custom_css_providers.clearRetainingCapacity();
@ -1214,49 +1206,51 @@ fn loadCustomCss(self: *App) !void {
};
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
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;
};
defer file.close();
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();
c.gtk_style_context_add_provider_for_display(
const data = try alloc.dupeZ(u8, contents);
defer alloc.free(data);
const provider = gtk.CssProvider.new();
loadCssProviderFromData(provider, data);
gtk.StyleContext.addProviderForDisplay(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_USER,
provider.as(gtk.StyleProvider),
gtk.STYLE_PROVIDER_PRIORITY_USER,
);
loadCssProviderFromData(provider, contents);
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)) {
const g_bytes = c.g_bytes_new(data.ptr, data.len);
defer c.g_bytes_unref(g_bytes);
const g_bytes = glib.Bytes.new(data.ptr, data.len);
defer g_bytes.unref();
c.gtk_css_provider_load_from_bytes(provider, g_bytes);
provider.loadFromBytes(g_bytes);
} else {
c.gtk_css_provider_load_from_data(
provider,
data.ptr,
@intCast(data.len),
);
provider.loadFromData(data, @intCast(data.len));
}
}
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: App) void {
_ = self;
c.g_main_context_wakeup(null);
pub fn wakeup(_: App) void {
glib.MainContext.wakeup(null);
}
/// 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"});
// Setup color scheme notifications
const adw_app: *adw.Application = @ptrCast(@alignCast(self.app));
const style_manager: *adw.StyleManager = adw_app.getStyleManager();
const style_manager: *adw.StyleManager = self.app.getStyleManager();
_ = gobject.Object.signals.notify.connect(
style_manager,
*App,
@ -1324,7 +1317,7 @@ pub fn run(self: *App) !void {
};
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.
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 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 {
// If no delay is configured, treat it as expired.
self.quit_timer = .{ .expired = {} };
@ -1453,14 +1452,13 @@ fn quit(self: *App) void {
/// This immediately destroys all windows, forcing the application to quit.
pub fn quitNow(self: *App) void {
const list = c.gtk_window_list_toplevels();
defer c.g_list_free(list);
c.g_list_foreach(list, struct {
fn callback(data: c.gpointer, _: c.gpointer) callconv(.C) void {
const list = gtk.Window.listToplevels();
defer list.free();
list.foreach(struct {
fn callback(data: ?*anyopaque, _: ?*anyopaque) callconv(.c) void {
const ptr = data orelse return;
const widget: *c.GtkWidget = @ptrCast(@alignCast(ptr));
const window: *c.GtkWindow = @ptrCast(widget);
c.gtk_window_destroy(window);
const window: *gtk.Window = @ptrCast(@alignCast(ptr));
window.destroy();
}
}.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
/// also when a secondary instance launches and requests a new window.
fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
_ = app;
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {
// Queue a new window
_ = core_app.mailbox.push(.{
.new_window = .{},
@ -1481,46 +1475,41 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
}
fn gtkWindowAdded(
_: *c.GtkApplication,
window: *c.GtkWindow,
ud: ?*anyopaque,
) callconv(.C) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
_: *adw.Application,
window: *gtk.Window,
core_app: *CoreApp,
) callconv(.c) void {
// Request the is-active property change so we can detect
// when our app loses focus.
_ = c.g_signal_connect_data(
_ = gobject.Object.signals.notify.connect(
window,
"notify::is-active",
c.G_CALLBACK(&gtkWindowIsActive),
*CoreApp,
gtkWindowIsActive,
core_app,
null,
c.G_CONNECT_DEFAULT,
.{
.detail = "is-active",
},
);
}
fn gtkWindowRemoved(
_: *c.GtkApplication,
_: *c.GtkWindow,
ud: ?*anyopaque,
) callconv(.C) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
_: *adw.Application,
_: *gtk.Window,
core_app: *CoreApp,
) callconv(.c) void {
// Recheck if we are focused
gtkWindowIsActive(null, undefined, core_app);
}
fn gtkWindowIsActive(
window: ?*c.GtkWindow,
_: *c.GParamSpec,
ud: ?*anyopaque,
) callconv(.C) void {
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));
window: ?*gtk.Window,
_: *gobject.ParamSpec,
core_app: *CoreApp,
) callconv(.c) void {
// If our window is active, then we can tell the app
// that we are focused.
if (window) |w| {
if (c.gtk_window_is_active(w) == 1) {
if (w.isActive() != 0) {
core_app.focusEvent(true);
return;
}
@ -1529,16 +1518,17 @@ fn gtkWindowIsActive(
// If the window becomes inactive, we need to check if any
// other windows are active. If not, then we are no longer
// focused.
if (c.gtk_window_list_toplevels()) |list| {
defer c.g_list_free(list);
var current: ?*c.GList = list;
while (current) |elem| : (current = elem.next) {
{
const list = gtk.Window.listToplevels();
defer list.free();
var current: ?*glib.List = list;
while (current) |elem| : (current = elem.f_next) {
// If the window is active then we are still focused.
// This is another window since we did our check above.
// That window should trigger its own is-active
// callback so we don't need to call it here.
const w: *c.GtkWindow = @alignCast(@ptrCast(elem.data));
if (c.gtk_window_is_active(w) == 1) return;
const w: *gtk.Window = @alignCast(@ptrCast(elem.f_data));
if (w.isActive() == 1) return;
}
}
@ -1575,33 +1565,30 @@ fn colorSchemeEvent(
}
fn gtkActionOpenConfig(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
_ = self.core_app.mailbox.push(.{
.open_config = {},
}, .{ .forever = {} });
}
fn gtkActionReloadConfig(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
self.reloadConfig(.app, .{}) catch |err| {
log.err("error reloading configuration: {}", .{err});
};
}
fn gtkActionQuit(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
self.core_app.performAction(self, .quit) catch |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
/// the user. Usually because the user clicked on a desktop notification.
fn gtkActionPresentSurface(
_: *c.GSimpleAction,
parameter: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *App,
) callconv(.c) void {
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.
if (c.g_variant_is_of_type(parameter, c.G_VARIANT_TYPE("t")) == 0) {
if (glib.Variant.isOfType(parameter, t) == 0) {
return;
}
// Convert that u64 to pointer to a core surface. A value of zero
// means that there was no target surface for the notification so
// 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;
const surface: *CoreSurface = @ptrFromInt(ptr_int);
@ -1654,25 +1644,27 @@ fn initActions(self: *App) void {
//
// For action names:
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
const actions = .{
.{ "quit", &gtkActionQuit, null },
.{ "open-config", &gtkActionOpenConfig, null },
.{ "reload-config", &gtkActionReloadConfig, null },
.{ "present-surface", &gtkActionPresentSurface, c.G_VARIANT_TYPE("t") },
};
const t = glib.ext.VariantType.newFor(u64);
defer glib.VariantType.free(t);
const actions = .{
.{ "quit", gtkActionQuit, null },
.{ "open-config", gtkActionOpenConfig, null },
.{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t },
};
inline for (actions) |entry| {
const action = c.g_simple_action_new(entry[0], entry[2]);
defer c.g_object_unref(action);
_ = c.g_signal_connect_data(
const action = gio.SimpleAction.new(entry[0], entry[2]);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
"activate",
c.G_CALLBACK(entry[1]),
*App,
entry[1],
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));
notification.setDefaultActionAndTargetValue("app.present-surface", pointer);
// FIXME: when App.zig gets converted to zig-gobject
const app: gio.Application = @ptrCast(@alignCast(self.app.app));
const app = self.app.app.as(gio.Application);
// We set the notification ID to the body content. If the content is the
// same, this notification may replace a previous notification

View File

@ -141,9 +141,8 @@ pub fn init(self: *Window, app: *App) !void {
.winproto = .none,
};
// FIXME: when App.zig is converted to zig-gobject
// 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_widget = self.window.as(gtk.Widget);
errdefer gtk_window.destroy();

View File

@ -1,27 +1,45 @@
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
/// version. This will return false if Ghostty is configured to
/// not build with libadwaita.
/// 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 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.
/// This is inlined so that the comptime checks will disable the runtime checks
/// if the comptime checks fail.
pub inline fn versionAtLeast(
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 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 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 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 (@inComptime()) return true;
// We use the functions instead of the constants such as
// c.ADW_MINOR_VERSION because the function gets the actual
// runtime version.
if (c.adw_get_major_version() >= major) {
if (c.adw_get_major_version() > major) return true;
if (c.adw_get_minor_version() >= minor) {
if (c.adw_get_minor_version() > minor) return true;
return c.adw_get_micro_version() >= micro;
// We use the functions instead of the constants such as c.ADW_MINOR_VERSION
// because the function gets the actual runtime version.
if (adw.getMajorVersion() >= major) {
if (adw.getMajorVersion() > major) return true;
if (adw.getMinorVersion() >= minor) {
if (adw.getMinorVersion() > minor) return true;
return adw.getMicroVersion() >= micro;
}
}

View File

@ -3,9 +3,6 @@ pub const c = @cImport({
@cInclude("gtk/gtk.h");
@cInclude("adwaita.h");
// generated header files
@cInclude("ghostty_resources.h");
// compatibility
@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
/// cgroup prefix, allocated with the given allocator.
fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
// FIXME: when app.app gets converted to gobject
const g_app: *gio.Application = @ptrCast(@alignCast(app.app));
const connection = g_app.getDbusConnection() orelse
const gio_app = app.app.as(gio.Application);
const connection = gio_app.getDbusConnection() orelse
return error.DbusConnectionRequired;
const pid: u32 = @intCast(pid_);

View File

@ -136,7 +136,7 @@ const Window = struct {
};
// 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);
errdefer c.gtk_window_destroy(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.
///
/// 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 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 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.
/// 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,
@ -34,25 +56,23 @@ pub inline fn atLeast(
return runtimeAtLeast(major, minor, micro);
}
/// Verifies that the GTK version at runtime is at least the given
/// version.
/// 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`.
/// 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.
if (c.gtk_get_major_version() >= major) {
if (c.gtk_get_major_version() > major) return true;
if (c.gtk_get_minor_version() >= minor) {
if (c.gtk_get_minor_version() > minor) return true;
return c.gtk_get_micro_version() >= micro;
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
// because the function gets the actual runtime version.
if (gtk.getMajorVersion() >= major) {
if (gtk.getMajorVersion() > major) return true;
if (gtk.getMinorVersion() >= minor) {
if (gtk.getMinorVersion() > minor) return true;
return gtk.getMicroVersion() >= micro;
}
}
@ -60,7 +80,6 @@ pub inline fn runtimeAtLeast(
}
test "atLeast" {
const std = @import("std");
const testing = std.testing;
const funs = &.{ atLeast, runtimeAtLeast };