1f3f2cdf
feat(wayland): connect + bind globals
a73x 2026-04-08 08:40
Commit message
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 | } | ||