build: add wayland

This commit is contained in:
Leah Amelia Chen
2025-01-02 21:44:16 +08:00
committed by Mitchell Hashimoto
parent f4a9b65f78
commit 31439f311d
10 changed files with 211 additions and 27 deletions

View File

@ -24,6 +24,8 @@ const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig");
const Version = @import("src/build/Version.zig");
const Command = @import("src/Command.zig");
const Scanner = @import("zig_wayland").Scanner;
comptime {
// This is the required Zig version for building this project. We allow
// any patch version but the major and minor must match exactly.
@ -105,19 +107,19 @@ pub fn build(b: *std.Build) !void {
"Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true;
config.x11 = b.option(
bool,
"gtk-x11",
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse x11: {
if (target.result.os.tag != .linux) break :x11 false;
var x11 = false;
var wayland = false;
if (target.result.os.tag == .linux) pkgconfig: {
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
pkgconfig.stdout_behavior = .Pipe;
pkgconfig.stderr_behavior = .Pipe;
try pkgconfig.spawn();
pkgconfig.spawn() catch |err| {
std.log.warn("failed to spawn pkg-config - disabling X11 and Wayland integrations: {}", .{err});
break :pkgconfig;
};
const output_max_size = 50 * 1024;
@ -139,18 +141,31 @@ pub fn build(b: *std.Build) !void {
switch (term) {
.Exited => |code| {
if (code == 0) {
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true;
break :x11 false;
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| x11 = true;
if (std.mem.indexOf(u8, stdout.items, "wayland")) |_| wayland = true;
} else {
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
return error.Unexpected;
}
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
break :x11 false;
},
inline else => |code| {
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
return error.Unexpected;
},
}
};
}
config.x11 = b.option(
bool,
"gtk-x11",
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse x11;
config.wayland = b.option(
bool,
"gtk-wayland",
"Enables linking against Wayland libraries when using the GTK rendering backend.",
) orelse wayland;
config.sentry = b.option(
bool,
@ -1459,6 +1474,17 @@ fn addDeps(
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
if (config.wayland) {
const scanner = Scanner.create(b, .{});
const wayland = b.createModule(.{ .root_source_file = scanner.result });
scanner.generate("wl_compositor", 1);
step.root_module.addImport("wayland", wayland);
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
{
const gresource = @import("src/apprt/gtk/gresource.zig");

View File

@ -25,6 +25,10 @@
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
},
.zig_wayland = .{
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
},
// C libs
.cimgui = .{ .path = "./pkg/cimgui" },
@ -64,5 +68,9 @@
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
},
.plasma_wayland_protocols = .{
.url = "git+https://invent.kde.org/libraries/plasma-wayland-protocols.git?ref=master#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
},
},
}

View File

@ -51,6 +51,9 @@
pandoc,
hyperfine,
typos,
wayland,
wayland-scanner,
wayland-protocols,
}: let
# See package.nix. Keep in sync.
rpathLibs =
@ -80,6 +83,7 @@
libadwaita
gtk4
glib
wayland
];
in
mkShell {
@ -153,6 +157,9 @@ in
libadwaita
gtk4
glib
wayland
wayland-scanner
wayland-protocols
];
# This should be set onto the rpath of the ghostty binary if you want

View File

@ -10,10 +10,6 @@
oniguruma,
zlib,
libGL,
libX11,
libXcursor,
libXi,
libXrandr,
glib,
gtk4,
libadwaita,
@ -26,7 +22,17 @@
pandoc,
revision ? "dirty",
optimize ? "Debug",
x11 ? true,
enableX11 ? true,
libX11,
libXcursor,
libXi,
libXrandr,
enableWayland ? true,
wayland,
wayland-protocols,
wayland-scanner,
}: let
# The Zig hook has no way to select the release type without actual
# overriding of the default flags.
@ -114,14 +120,19 @@ in
version = "1.0.2";
inherit src;
nativeBuildInputs = [
git
ncurses
pandoc
pkg-config
zig_hook
wrapGAppsHook4
];
nativeBuildInputs =
[
git
ncurses
pandoc
pkg-config
zig_hook
wrapGAppsHook4
]
++ lib.optionals enableWayland [
wayland-scanner
wayland-protocols
];
buildInputs =
[
@ -142,16 +153,19 @@ in
glib
gsettings-desktop-schemas
]
++ lib.optionals x11 [
++ lib.optionals enableX11 [
libX11
libXcursor
libXi
libXrandr
]
++ lib.optionals enableWayland [
wayland
];
dontConfigure = true;
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}";
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
preBuild = ''
rm -rf $ZIG_GLOBAL_CACHE_DIR

View File

@ -37,6 +37,7 @@ const version = @import("version.zig");
const inspector = @import("inspector.zig");
const key = @import("key.zig");
const x11 = @import("x11.zig");
const wayland = @import("wayland.zig");
const testing = std.testing;
const log = std.log.scoped(.gtk);
@ -73,6 +74,9 @@ running: bool = true,
/// Xkb state (X11 only). Will be null on Wayland.
x11_xkb: ?x11.Xkb = null,
/// Wayland app state. Will be null on X11.
wayland: ?wayland.AppState = null,
/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful.
@ -397,6 +401,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
break :x11_xkb try x11.Xkb.init(display);
};
// Initialize Wayland state
var wl = wayland.AppState.init(display);
if (wl) |*w| try w.register();
// 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
@ -422,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.ctx = ctx,
.cursor_none = cursor_none,
.x11_xkb = x11_xkb,
.wayland = wl,
.single_instance = single_instance,
// 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

View File

@ -25,6 +25,7 @@ const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig");
const wayland = @import("wayland.zig");
const log = std.log.scoped(.gtk);
@ -55,6 +56,8 @@ toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null,
wayland: ?wayland.SurfaceState,
pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal
@ -79,6 +82,7 @@ pub fn init(self: *Window, app: *App) !void {
.notebook = undefined,
.context_menu = undefined,
.toast_overlay = undefined,
.wayland = null,
};
// Create the window
@ -290,6 +294,7 @@ pub fn init(self: *Window, app: *App) !void {
// All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(&gtkKeyPressed), self, null, c.G_CONNECT_DEFAULT);
@ -424,6 +429,8 @@ fn initActions(self: *Window) void {
pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu));
if (self.wayland) |*wl| wl.deinit();
if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(timer);
}
@ -551,6 +558,16 @@ pub fn sendToast(self: *Window, title: [:0]const u8) void {
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
}
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
const self = userdataSelf(ud.?);
if (self.app.wayland) |*wl| {
self.wayland = wayland.SurfaceState.init(v, wl);
}
return true;
}
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value.
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {

View File

@ -14,6 +14,9 @@ pub const c = @cImport({
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
}
if (build_options.wayland) {
@cInclude("gdk/wayland/gdkwayland.h");
}
// generated header files
@cInclude("ghostty_resources.h");

90
src/apprt/gtk/wayland.zig Normal file
View File

@ -0,0 +1,90 @@
const std = @import("std");
const c = @import("c.zig").c;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const build_options = @import("build_options");
const log = std.log.scoped(.gtk_wayland);
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
pub const AppState = struct {
display: *wl.Display,
pub fn init(display: ?*c.GdkDisplay) ?AppState {
if (comptime !build_options.wayland) return null;
// It should really never be null
const display_ = display orelse return null;
// Check if we're actually on Wayland
if (c.g_type_check_instance_is_a(
@ptrCast(@alignCast(display_)),
c.gdk_wayland_display_get_type(),
) == 0)
return null;
const wl_display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(display_) orelse return null);
return .{
.display = wl_display,
};
}
pub fn register(self: *AppState) !void {
const registry = try self.display.getRegistry();
registry.setListener(*AppState, registryListener, self);
if (self.display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
log.debug("app wayland init={}", .{self});
}
};
/// Wayland state that contains Wayland objects associated with a window (e.g. wl_surface).
pub const SurfaceState = struct {
app_state: *AppState,
surface: *wl.Surface,
pub fn init(window: *c.GtkWindow, app_state: *AppState) ?SurfaceState {
if (comptime !build_options.wayland) return null;
const surface = c.gtk_native_get_surface(@ptrCast(window)) orelse return null;
// Check if we're actually on Wayland
if (c.g_type_check_instance_is_a(
@ptrCast(@alignCast(surface)),
c.gdk_wayland_surface_get_type(),
) == 0)
return null;
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(surface) orelse return null);
return .{
.app_state = app_state,
.surface = wl_surface,
};
}
pub fn deinit(self: *SurfaceState) void {
}
};
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *AppState) void {
switch (event) {
.global => |global| {
log.debug("got global interface={s}", .{global.interface});
},
.global_remove => {},
}
}
fn bindInterface(comptime T: type, registry: *wl.Registry, global: anytype, version: u32) ?*T {
if (std.mem.orderZ(u8, global.interface, T.interface.name) == .eq) {
return registry.bind(global.name, T, version) catch |err| {
log.warn("encountered error={} while binding interface {s}", .{ err, global.interface });
return null;
};
} else {
return null;
}
}

View File

@ -23,6 +23,7 @@ pub const BuildConfig = struct {
flatpak: bool = false,
adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl,
@ -44,6 +45,7 @@ pub const BuildConfig = struct {
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend);

View File

@ -68,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 {
} else {
try stdout.print(" - libX11 : disabled\n", .{});
}
// We say `libwayland` since it is possible to build Ghostty without
// Wayland integration but with Wayland-enabled GTK
if (comptime build_options.wayland) {
try stdout.print(" - libwayland : enabled\n", .{});
} else {
try stdout.print(" - libwayland : disabled\n", .{});
}
}
return 0;
}