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

View File

@ -25,6 +25,10 @@
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25", .hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
}, },
.zig_wayland = .{
.url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
.hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
},
// C libs // C libs
.cimgui = .{ .path = "./pkg/cimgui" }, .cimgui = .{ .path = "./pkg/cimgui" },
@ -64,5 +68,9 @@
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a", .url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a", .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, pandoc,
hyperfine, hyperfine,
typos, typos,
wayland,
wayland-scanner,
wayland-protocols,
}: let }: let
# See package.nix. Keep in sync. # See package.nix. Keep in sync.
rpathLibs = rpathLibs =
@ -80,6 +83,7 @@
libadwaita libadwaita
gtk4 gtk4
glib glib
wayland
]; ];
in in
mkShell { mkShell {
@ -153,6 +157,9 @@ in
libadwaita libadwaita
gtk4 gtk4
glib glib
wayland
wayland-scanner
wayland-protocols
]; ];
# This should be set onto the rpath of the ghostty binary if you want # This should be set onto the rpath of the ghostty binary if you want

View File

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

View File

@ -37,6 +37,7 @@ 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");
const x11 = @import("x11.zig"); const x11 = @import("x11.zig");
const wayland = @import("wayland.zig");
const testing = std.testing; const testing = std.testing;
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -73,6 +74,9 @@ running: bool = true,
/// Xkb state (X11 only). Will be null on Wayland. /// Xkb state (X11 only). Will be null on Wayland.
x11_xkb: ?x11.Xkb = null, 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 /// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled /// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful. /// 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); 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 // 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 // routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening // for launching Ghostty in the "background" without immediately opening
@ -422,6 +430,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
.ctx = ctx, .ctx = ctx,
.cursor_none = cursor_none, .cursor_none = cursor_none,
.x11_xkb = x11_xkb, .x11_xkb = x11_xkb,
.wayland = wl,
.single_instance = single_instance, .single_instance = single_instance,
// 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

View File

@ -25,6 +25,7 @@ const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook; const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar; const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig"); const version = @import("version.zig");
const wayland = @import("wayland.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -55,6 +56,8 @@ toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this. /// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null, adw_tab_overview_focus_timer: ?c.guint = null,
wayland: ?wayland.SurfaceState,
pub fn create(alloc: Allocator, app: *App) !*Window { pub fn create(alloc: Allocator, app: *App) !*Window {
// Allocate a fixed pointer for our window. We try to minimize // Allocate a fixed pointer for our window. We try to minimize
// allocations but windows and other GUI requirements are so minimal // allocations but windows and other GUI requirements are so minimal
@ -79,6 +82,7 @@ pub fn init(self: *Window, app: *App) !void {
.notebook = undefined, .notebook = undefined,
.context_menu = undefined, .context_menu = undefined,
.toast_overlay = undefined, .toast_overlay = undefined,
.wayland = null,
}; };
// Create the window // Create the window
@ -290,6 +294,7 @@ pub fn init(self: *Window, app: *App) !void {
// All of our events // 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(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, "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(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); _ = 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 { pub fn deinit(self: *Window) void {
c.gtk_widget_unparent(@ptrCast(self.context_menu)); c.gtk_widget_unparent(@ptrCast(self.context_menu));
if (self.wayland) |*wl| wl.deinit();
if (self.adw_tab_overview_focus_timer) |timer| { if (self.adw_tab_overview_focus_timer) |timer| {
_ = c.g_source_remove(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); 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 // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
// sends an undefined value. // sends an undefined value.
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {

View File

@ -14,6 +14,9 @@ pub const c = @cImport({
// Xkb for X11 state handling // Xkb for X11 state handling
@cInclude("X11/XKBlib.h"); @cInclude("X11/XKBlib.h");
} }
if (build_options.wayland) {
@cInclude("gdk/wayland/gdkwayland.h");
}
// generated header files // generated header files
@cInclude("ghostty_resources.h"); @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, flatpak: bool = false,
adwaita: bool = false, adwaita: bool = false,
x11: bool = false, x11: bool = false,
wayland: bool = false,
sentry: bool = true, sentry: bool = true,
app_runtime: apprt.Runtime = .none, app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl, renderer: rendererpkg.Impl = .opengl,
@ -44,6 +45,7 @@ pub const BuildConfig = struct {
step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita); step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11); step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry); step.addOption(bool, "sentry", self.sentry);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime); step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend); step.addOption(font.Backend, "font_backend", self.font_backend);

View File

@ -68,6 +68,14 @@ pub fn run(alloc: Allocator) !u8 {
} else { } else {
try stdout.print(" - libX11 : disabled\n", .{}); 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; return 0;
} }