a73x

1f3f2cdf

feat(wayland): connect + bind globals

a73x   2026-04-08 08:40

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>

diff --git a/build.zig b/build.zig
index 591d705..29b5596 100644
--- a/build.zig
+++ b/build.zig
@@ -8,6 +8,32 @@ pub fn build(b: *std.Build) void {
    // materializes the package; subsequent builds use the local cache.
    const ghostty_dep = b.lazyDependency("ghostty", .{});

    // zig-wayland scanner — generates protocol bindings at build time
    const wayland_dep = b.dependency("wayland", .{});
    const Scanner = @import("wayland").Scanner;
    const scanner = Scanner.create(b, .{});
    scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
    scanner.generate("wl_compositor", 6);
    scanner.generate("wl_seat", 9);
    scanner.generate("xdg_wm_base", 6);

    // wayland module — generated bindings + our Connection wrapper
    const wayland_generated_mod = b.createModule(.{
        .root_source_file = scanner.result,
        .target = target,
        .optimize = optimize,
    });

    const wayland_mod = b.createModule(.{
        .root_source_file = b.path("src/wayland.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    wayland_mod.addImport("wayland", wayland_generated_mod);
    wayland_mod.linkSystemLibrary("wayland-client", .{});
    _ = wayland_dep; // referenced via Scanner

    // vt module — wraps ghostty-vt
    const vt_mod = b.createModule(.{
        .root_source_file = b.path("src/vt.zig"),
@@ -25,6 +51,7 @@ pub fn build(b: *std.Build) void {
        .optimize = optimize,
    });
    exe_mod.addImport("vt", vt_mod);
    exe_mod.addImport("wayland-client", wayland_mod);

    const exe = b.addExecutable(.{
        .name = "waystty",
@@ -72,6 +99,7 @@ pub fn build(b: *std.Build) void {
        .optimize = optimize,
    });
    main_test_mod.addImport("vt", vt_mod);
    main_test_mod.addImport("wayland-client", wayland_mod);
    const main_tests = b.addTest(.{
        .root_module = main_test_mod,
    });
diff --git a/src/main.zig b/src/main.zig
index 3fc8430..8f617a3 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const vt = @import("vt");
const pty = @import("pty");
const wayland_client = @import("wayland-client");

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
@@ -14,9 +15,19 @@ pub fn main() !void {
        return runHeadless(alloc);
    }

    if (args.len >= 2 and std.mem.eql(u8, args[1], "--wayland-smoke-test")) {
        return runWaylandSmokeTest();
    }

    std.debug.print("waystty (run with --headless for CLI dump mode)\n", .{});
}

fn runWaylandSmokeTest() !void {
    var conn = try wayland_client.Connection.init();
    defer conn.deinit();
    std.debug.print("connected\n", .{});
}

fn runHeadless(alloc: std.mem.Allocator) !void {
    const shell: [:0]const u8 = "/bin/sh";

diff --git a/src/wayland.zig b/src/wayland.zig
new file mode 100644
index 0000000..fe93e39
--- /dev/null
+++ b/src/wayland.zig
@@ -0,0 +1,64 @@
const std = @import("std");
const posix = std.posix;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const xdg = wayland.client.xdg;

pub const Globals = struct {
    compositor: ?*wl.Compositor = null,
    wm_base: ?*xdg.WmBase = null,
    seat: ?*wl.Seat = null,
};

pub const Connection = struct {
    display: *wl.Display,
    registry: *wl.Registry,
    globals: Globals,

    pub fn init() !Connection {
        const display = try wl.Display.connect(null);
        errdefer display.disconnect();

        const registry = try display.getRegistry();
        errdefer registry.destroy();

        var globals = Globals{};
        registry.setListener(*Globals, registryListener, &globals);

        if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;

        if (globals.compositor == null) return error.NoCompositor;
        if (globals.wm_base == null) return error.NoXdgWmBase;
        if (globals.seat == null) return error.NoSeat;

        return .{
            .display = display,
            .registry = registry,
            .globals = globals,
        };
    }

    pub fn deinit(self: *Connection) void {
        self.display.disconnect();
    }
};

fn registryListener(
    registry: *wl.Registry,
    event: wl.Registry.Event,
    globals: *Globals,
) void {
    switch (event) {
        .global => |g| {
            const iface = std.mem.span(g.interface);
            if (std.mem.eql(u8, iface, std.mem.span(wl.Compositor.interface.name))) {
                globals.compositor = registry.bind(g.name, wl.Compositor, 6) catch return;
            } else if (std.mem.eql(u8, iface, std.mem.span(xdg.WmBase.interface.name))) {
                globals.wm_base = registry.bind(g.name, xdg.WmBase, 6) catch return;
            } else if (std.mem.eql(u8, iface, std.mem.span(wl.Seat.interface.name))) {
                globals.seat = registry.bind(g.name, wl.Seat, 9) catch return;
            }
        },
        .global_remove => {},
    }
}