a73x

2c843d0d

feat(wayland): create surface + xdg_toplevel

a73x   2026-04-08 08:42

Add Window struct with wl_surface/xdg_surface/xdg_toplevel, listeners
for ping/configure/close, and extend smoke test to create and destroy a
titled window.

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

diff --git a/src/main.zig b/src/main.zig
index 8f617a3..f413a5e 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -16,16 +16,21 @@ pub fn main() !void {
    }

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

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

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

    const window = try conn.createWindow(alloc, "waystty");
    defer window.deinit();

    std.debug.print("window created (w={d} h={d})\n", .{ window.width, window.height });
}

fn runHeadless(alloc: std.mem.Allocator) !void {
diff --git a/src/wayland.zig b/src/wayland.zig
index fe93e39..6affd67 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -10,6 +10,24 @@ pub const Globals = struct {
    seat: ?*wl.Seat = null,
};

pub const Window = struct {
    alloc: std.mem.Allocator,
    surface: *wl.Surface,
    xdg_surface: *xdg.Surface,
    xdg_toplevel: *xdg.Toplevel,
    configured: bool = false,
    should_close: bool = false,
    width: u32 = 800,
    height: u32 = 600,

    pub fn deinit(self: *Window) void {
        self.xdg_toplevel.destroy();
        self.xdg_surface.destroy();
        self.surface.destroy();
        self.alloc.destroy(self);
    }
};

pub const Connection = struct {
    display: *wl.Display,
    registry: *wl.Registry,
@@ -41,8 +59,68 @@ pub const Connection = struct {
    pub fn deinit(self: *Connection) void {
        self.display.disconnect();
    }

    pub fn createWindow(self: *Connection, alloc: std.mem.Allocator, title: [*:0]const u8) !*Window {
        const compositor = self.globals.compositor orelse return error.NoCompositor;
        const wm_base = self.globals.wm_base orelse return error.NoXdgWmBase;

        const window = try alloc.create(Window);
        errdefer alloc.destroy(window);

        window.* = .{
            .alloc = alloc,
            .surface = try compositor.createSurface(),
            .xdg_surface = undefined,
            .xdg_toplevel = undefined,
        };
        errdefer window.surface.destroy();

        window.xdg_surface = try wm_base.getXdgSurface(window.surface);
        errdefer window.xdg_surface.destroy();

        window.xdg_toplevel = try window.xdg_surface.getToplevel();

        window.xdg_toplevel.setTitle(title);
        window.xdg_toplevel.setAppId("waystty");

        window.xdg_surface.setListener(*Window, xdgSurfaceListener, window);
        window.xdg_toplevel.setListener(*Window, xdgToplevelListener, window);
        wm_base.setListener(*xdg.WmBase, wmBaseListener, wm_base);

        window.surface.commit();
        _ = self.display.roundtrip();

        return window;
    }
};

fn wmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: *xdg.WmBase) void {
    switch (event) {
        .ping => |p| wm_base.pong(p.serial),
    }
}

fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void {
    switch (event) {
        .configure => |c| {
            surface.ackConfigure(c.serial);
            window.configured = true;
        },
    }
}

fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void {
    switch (event) {
        .configure => |c| {
            if (c.width > 0) window.width = @intCast(c.width);
            if (c.height > 0) window.height = @intCast(c.height);
        },
        .close => window.should_close = true,
        .configure_bounds => {},
        .wm_capabilities => {},
    }
}

fn registryListener(
    registry: *wl.Registry,
    event: wl.Registry.Event,
@@ -54,7 +132,7 @@ fn registryListener(
            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;
                globals.wm_base = registry.bind(g.name, xdg.WmBase, 5) 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;
            }