a73x

1f3f2cdf

feat(wayland): connect + bind globals

a73x   2026-04-08 08:40

Commit message
feat(wayland): connect + bind globals

Wire zig-wayland scanner into build.zig, create src/wayland.zig with a
Connection that connects to wl_display and binds wl_compositor,
xdg_wm_base, and wl_seat. Add --wayland-smoke-test branch to main.zig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

build.zig
Old New
@@ -8,6 +8,32 @@ pub fn build(b: *std.Build) void {
8 // materializes the package; subsequent builds use the local cache. 8 // materializes the package; subsequent builds use the local cache.
9 const ghostty_dep = b.lazyDependency("ghostty", .{}); 9 const ghostty_dep = b.lazyDependency("ghostty", .{});
10 10
11 // zig-wayland scanner — generates protocol bindings at build time
12 const wayland_dep = b.dependency("wayland", .{});
13 const Scanner = @import("wayland").Scanner;
14 const scanner = Scanner.create(b, .{});
15 scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
16 scanner.generate("wl_compositor", 6);
17 scanner.generate("wl_seat", 9);
18 scanner.generate("xdg_wm_base", 6);
19
20 // wayland module — generated bindings + our Connection wrapper
21 const wayland_generated_mod = b.createModule(.{
22 .root_source_file = scanner.result,
23 .target = target,
24 .optimize = optimize,
25 });
26
27 const wayland_mod = b.createModule(.{
28 .root_source_file = b.path("src/wayland.zig"),
29 .target = target,
30 .optimize = optimize,
31 .link_libc = true,
32 });
33 wayland_mod.addImport("wayland", wayland_generated_mod);
34 wayland_mod.linkSystemLibrary("wayland-client", .{});
35 _ = wayland_dep; // referenced via Scanner
36
11 // vt module — wraps ghostty-vt 37 // vt module — wraps ghostty-vt
12 const vt_mod = b.createModule(.{ 38 const vt_mod = b.createModule(.{
13 .root_source_file = b.path("src/vt.zig"), 39 .root_source_file = b.path("src/vt.zig"),
@@ -25,6 +51,7 @@ pub fn build(b: *std.Build) void {
25 .optimize = optimize, 51 .optimize = optimize,
26 }); 52 });
27 exe_mod.addImport("vt", vt_mod); 53 exe_mod.addImport("vt", vt_mod);
54 exe_mod.addImport("wayland-client", wayland_mod);
28 55
29 const exe = b.addExecutable(.{ 56 const exe = b.addExecutable(.{
30 .name = "waystty", 57 .name = "waystty",
@@ -72,6 +99,7 @@ pub fn build(b: *std.Build) void {
72 .optimize = optimize, 99 .optimize = optimize,
73 }); 100 });
74 main_test_mod.addImport("vt", vt_mod); 101 main_test_mod.addImport("vt", vt_mod);
102 main_test_mod.addImport("wayland-client", wayland_mod);
75 const main_tests = b.addTest(.{ 103 const main_tests = b.addTest(.{
76 .root_module = main_test_mod, 104 .root_module = main_test_mod,
77 }); 105 });
src/main.zig
Old New
@@ -1,6 +1,7 @@
1 const std = @import("std"); 1 const std = @import("std");
2 const vt = @import("vt"); 2 const vt = @import("vt");
3 const pty = @import("pty"); 3 const pty = @import("pty");
4 const wayland_client = @import("wayland-client");
4 5
5 pub fn main() !void { 6 pub fn main() !void {
6 var gpa: std.heap.DebugAllocator(.{}) = .init; 7 var gpa: std.heap.DebugAllocator(.{}) = .init;
@@ -14,9 +15,19 @@ pub fn main() !void {
14 return runHeadless(alloc); 15 return runHeadless(alloc);
15 } 16 }
16 17
18 if (args.len >= 2 and std.mem.eql(u8, args[1], "--wayland-smoke-test")) {
19 return runWaylandSmokeTest();
20 }
21
17 std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{}); 22 std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{});
18 } 23 }
19 24
25 fn runWaylandSmokeTest() !void {
26 var conn = try wayland_client.Connection.init();
27 defer conn.deinit();
28 std.debug.print("connected\n", .{});
29 }
30
20 fn runHeadless(alloc: std.mem.Allocator) !void { 31 fn runHeadless(alloc: std.mem.Allocator) !void {
21 const shell: [:0]const u8 = "/bin/sh"; 32 const shell: [:0]const u8 = "/bin/sh";
22 33
src/wayland.zig
Old New
@@ -0,0 +1,64 @@
1 const std = @import("std");
2 const posix = std.posix;
3 const wayland = @import("wayland");
4 const wl = wayland.client.wl;
5 const xdg = wayland.client.xdg;
6
7 pub const Globals = struct {
8 compositor: ?*wl.Compositor = null,
9 wm_base: ?*xdg.WmBase = null,
10 seat: ?*wl.Seat = null,
11 };
12
13 pub const Connection = struct {
14 display: *wl.Display,
15 registry: *wl.Registry,
16 globals: Globals,
17
18 pub fn init() !Connection {
19 const display = try wl.Display.connect(null);
20 errdefer display.disconnect();
21
22 const registry = try display.getRegistry();
23 errdefer registry.destroy();
24
25 var globals = Globals{};
26 registry.setListener(*Globals, registryListener, &globals);
27
28 if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
29
30 if (globals.compositor == null) return error.NoCompositor;
31 if (globals.wm_base == null) return error.NoXdgWmBase;
32 if (globals.seat == null) return error.NoSeat;
33
34 return .{
35 .display = display,
36 .registry = registry,
37 .globals = globals,
38 };
39 }
40
41 pub fn deinit(self: *Connection) void {
42 self.display.disconnect();
43 }
44 };
45
46 fn registryListener(
47 registry: *wl.Registry,
48 event: wl.Registry.Event,
49 globals: *Globals,
50 ) void {
51 switch (event) {
52 .global => |g| {
53 const iface = std.mem.span(g.interface);
54 if (std.mem.eql(u8, iface, std.mem.span(wl.Compositor.interface.name))) {
55 globals.compositor = registry.bind(g.name, wl.Compositor, 6) catch return;
56 } else if (std.mem.eql(u8, iface, std.mem.span(xdg.WmBase.interface.name))) {
57 globals.wm_base = registry.bind(g.name, xdg.WmBase, 6) catch return;
58 } else if (std.mem.eql(u8, iface, std.mem.span(wl.Seat.interface.name))) {
59 globals.seat = registry.bind(g.name, wl.Seat, 9) catch return;
60 }
61 },
62 .global_remove => {},
63 }
64 }