aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore2
-rw-r--r--build.zig116
-rw-r--r--build.zig.zon86
-rw-r--r--default.nix6
-rw-r--r--src/context.zig6
-rw-r--r--src/event-set.zig83
-rw-r--r--src/main.zig15
-rw-r--r--src/object.zig41
-rw-r--r--src/root.zig10
-rw-r--r--src/types.zig18
-rw-r--r--src/wl/callback.zig33
-rw-r--r--src/wl/display.zig265
-rw-r--r--src/wl/output.zig171
-rw-r--r--src/wl/registry.zig96
-rw-r--r--src/wl/root.zig4
16 files changed, 953 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..1d953f4
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use nix
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..941c29f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.zig-cache
+zig-out/
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..ca6a90e
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,116 @@
+const std = @import("std");
+
+// Although this function looks imperative, note that its job is to
+// declaratively construct a build graph that will be executed by an external
+// runner.
+pub fn build(b: *std.Build) void {
+ // Standard target options allows the person running `zig build` to choose
+ // what target to build for. Here we do not override the defaults, which
+ // means any target is allowed, and the default is native. Other options
+ // for restricting supported target set are available.
+ const target = b.standardTargetOptions(.{});
+
+ // Standard optimization options allow the person running `zig build` to select
+ // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
+ // set a preferred release mode, allowing the user to decide how to optimize.
+ const optimize = b.standardOptimizeOption(.{});
+
+ // This creates a "module", which represents a collection of source files alongside
+ // some compilation options, such as optimization mode and linked system libraries.
+ // Every executable or library we compile will be based on one or more modules.
+ const lib_mod = b.createModule(.{
+ // `root_source_file` is the Zig "entry point" of the module. If a module
+ // only contains e.g. external object files, you can make this `null`.
+ // In this case the main source file is merely a path, however, in more
+ // complicated build scripts, this could be a generated file.
+ .root_source_file = b.path("src/root.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // We will also create a module for our other entry point, 'main.zig'.
+ const exe_mod = b.createModule(.{
+ // `root_source_file` is the Zig "entry point" of the module. If a module
+ // only contains e.g. external object files, you can make this `null`.
+ // In this case the main source file is merely a path, however, in more
+ // complicated build scripts, this could be a generated file.
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ // Modules can depend on one another using the `std.Build.Module.addImport` function.
+ // This is what allows Zig source code to use `@import("foo")` where 'foo' is not a
+ // file path. In this case, we set up `exe_mod` to import `lib_mod`.
+ exe_mod.addImport("wayland", lib_mod);
+
+ // Now, we will create a static library based on the module we created above.
+ // This creates a `std.Build.Step.Compile`, which is the build step responsible
+ // for actually invoking the compiler.
+ const lib = b.addLibrary(.{
+ .linkage = .static,
+ .name = "wayland",
+ .root_module = lib_mod,
+ });
+
+ // This declares intent for the library to be installed into the standard
+ // location when the user invokes the "install" step (the default step when
+ // running `zig build`).
+ b.installArtifact(lib);
+
+ // This creates another `std.Build.Step.Compile`, but this one builds an executable
+ // rather than a static library.
+ const exe = b.addExecutable(.{
+ .name = "wayland",
+ .root_module = exe_mod,
+ });
+
+ // This declares intent for the executable to be installed into the
+ // standard location when the user invokes the "install" step (the default
+ // step when running `zig build`).
+ b.installArtifact(exe);
+
+ // This *creates* a Run step in the build graph, to be executed when another
+ // step is evaluated that depends on it. The next line below will establish
+ // such a dependency.
+ const run_cmd = b.addRunArtifact(exe);
+
+ // By making the run step depend on the install step, it will be run from the
+ // installation directory rather than directly from within the cache directory.
+ // This is not necessary, however, if the application depends on other installed
+ // files, this ensures they will be present and in the expected location.
+ run_cmd.step.dependOn(b.getInstallStep());
+
+ // This allows the user to pass arguments to the application in the build
+ // command itself, like this: `zig build run -- arg1 arg2 etc`
+ if (b.args) |args| {
+ run_cmd.addArgs(args);
+ }
+
+ // This creates a build step. It will be visible in the `zig build --help` menu,
+ // and can be selected like this: `zig build run`
+ // This will evaluate the `run` step rather than the default, which is "install".
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run_cmd.step);
+
+ // Creates a step for unit testing. This only builds the test executable
+ // but does not run it.
+ const lib_unit_tests = b.addTest(.{
+ .root_module = lib_mod,
+ });
+
+ const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
+
+ const exe_unit_tests = b.addTest(.{
+ .root_module = exe_mod,
+ });
+
+ const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+
+ // Similar to creating the run step earlier, this exposes a `test` step to
+ // the `zig build --help` menu, providing a way for the user to request
+ // running the unit tests.
+ const test_step = b.step("test", "Run unit tests");
+ test_step.dependOn(&run_lib_unit_tests.step);
+ test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..d057b90
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,86 @@
+.{
+ // This is the default name used by packages depending on this one. For
+ // example, when a user runs `zig fetch --save <url>`, this field is used
+ // as the key in the `dependencies` table. Although the user can choose a
+ // different name, most users will stick with this provided value.
+ //
+ // It is redundant to include "zig" in this name because it is already
+ // within the Zig package namespace.
+ .name = .wayland,
+
+ // This is a [Semantic Version](https://semver.org/).
+ // In a future version of Zig it will be used for package deduplication.
+ .version = "0.0.0",
+
+ // Together with name, this represents a globally unique package
+ // identifier. This field is generated by the Zig toolchain when the
+ // package is first created, and then *never changes*. This allows
+ // unambiguous detection of one package being an updated version of
+ // another.
+ //
+ // When forking a Zig project, this id should be regenerated (delete the
+ // field and run `zig build`) if the upstream project is still maintained.
+ // Otherwise, the fork is *hostile*, attempting to take control over the
+ // original project's identity. Thus it is recommended to leave the comment
+ // on the following line intact, so that it shows up in code reviews that
+ // modify the field.
+ .fingerprint = 0xa8d655e1d1844b2, // Changing this has security and trust implications.
+
+ // Tracks the earliest Zig version that the package considers to be a
+ // supported use case.
+ .minimum_zig_version = "0.14.1",
+
+ // This field is optional.
+ // Each dependency must either provide a `url` and `hash`, or a `path`.
+ // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+ // Once all dependencies are fetched, `zig build` no longer requires
+ // internet connectivity.
+ .dependencies = .{
+ // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
+ //.example = .{
+ // // When updating this field to a new URL, be sure to delete the corresponding
+ // // `hash`, otherwise you are communicating that you expect to find the old hash at
+ // // the new URL. If the contents of a URL change this will result in a hash mismatch
+ // // which will prevent zig from using it.
+ // .url = "https://example.com/foo.tar.gz",
+ //
+ // // This is computed from the file contents of the directory of files that is
+ // // obtained after fetching `url` and applying the inclusion rules given by
+ // // `paths`.
+ // //
+ // // This field is the source of truth; packages do not come from a `url`; they
+ // // come from a `hash`. `url` is just one of many possible mirrors for how to
+ // // obtain a package matching this `hash`.
+ // //
+ // // Uses the [multihash](https://multiformats.io/multihash/) format.
+ // .hash = "...",
+ //
+ // // When this is provided, the package is found in a directory relative to the
+ // // build root. In this case the package's hash is irrelevant and therefore not
+ // // computed. This field and `url` are mutually exclusive.
+ // .path = "foo",
+ //
+ // // When this is set to `true`, a package is declared to be lazily
+ // // fetched. This makes the dependency only get fetched if it is
+ // // actually used.
+ // .lazy = false,
+ //},
+ },
+
+ // Specifies the set of files and directories that are included in this package.
+ // Only files and directories listed here are included in the `hash` that
+ // is computed for this package. Only files listed here will remain on disk
+ // when using the zig package manager. As a rule of thumb, one should list
+ // files required for compilation plus any license(s).
+ // Paths are relative to the build root. Use the empty string (`""`) to refer to
+ // the build root itself.
+ // A directory listed here means that all files within, recursively, are included.
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ // For example...
+ //"LICENSE",
+ //"README.md",
+ },
+}
diff --git a/default.nix b/default.nix
new file mode 100644
index 0000000..0bac3a3
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,6 @@
+{ pkgs ? import <nixpkgs> { }, ... }:
+pkgs.mkShell {
+ packages = [
+ pkgs.zig
+ ];
+}
diff --git a/src/context.zig b/src/context.zig
new file mode 100644
index 0000000..55480ce
--- /dev/null
+++ b/src/context.zig
@@ -0,0 +1,6 @@
+const std = @import("std");
+const wayland = @import("root.zig");
+const wl = wayland.wl;
+
+allocator: std.mem.Allocator,
+display: *wl.Display,
diff --git a/src/event-set.zig b/src/event-set.zig
new file mode 100644
index 0000000..5721a4e
--- /dev/null
+++ b/src/event-set.zig
@@ -0,0 +1,83 @@
+const std = @import("std");
+const wayland = @import("root.zig");
+
+pub fn EventSet(T: type, events: anytype) type {
+ return struct {
+ pub fn on_event(ptr: *anyopaque, ctx: *wayland.Context, opcode: u16, args: []const u8) void {
+ std.debug.print("event {} {}\n", .{T, opcode});
+ var offset: usize = 0;
+ inline for (events, 0..) |event, index| {
+ if (@TypeOf(event) != @TypeOf(null) and index == opcode) {
+ const Args = std.meta.ArgsTuple(@TypeOf(event));
+ var fn_args: Args = undefined;
+ fn_args[0] = @alignCast(@ptrCast(ptr));
+ fn_args[1] = ctx;
+
+ inline for (std.meta.fields(Args)[2..], 2..) |f, arg_index| {
+ switch (f.type) {
+ void => {},
+ u32, i32 => {
+ const result = std.mem.readPackedIntNative(f.type, args, offset * 8);
+ fn_args[arg_index] = result;
+ offset += @sizeOf(f.type);
+ },
+ []const u8 => {
+ const size: u32 = std.mem.readPackedIntNative(u32, args, offset * 8);
+ const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(size)))), 4);
+ offset += @sizeOf(u32);
+ const result = args[offset..offset + size - 1];
+ offset += size + padding_size;
+ fn_args[arg_index] = result;
+ },
+ *wayland.Object => {
+ const result = std.mem.readPackedIntNative(u32, args, offset * 8);
+ fn_args[arg_index] = ctx.display.registry.objects.items[result] orelse unreachable;
+ offset += @sizeOf(u32);
+ },
+ else => |t| switch (@typeInfo(t)) {
+ .@"enum" => {
+ const result = std.mem.readPackedIntNative(u32, args, offset * 8);
+ fn_args[arg_index] = @enumFromInt(result);
+ offset += @sizeOf(u32);
+ },
+ else => @compileError(std.fmt.comptimePrint("unsupported type {}", .{t}))
+ },
+ }
+ }
+
+ switch (@typeInfo(@typeInfo(@TypeOf(event)).@"fn".return_type orelse void)) {
+ .void => @call(.auto, event, fn_args),
+ .error_union => @call(.auto, event, fn_args) catch |err| {
+ std.log.err("error on event {} ({}): {}", .{opcode, T, err});
+ },
+ else => |t| @compileError(std.fmt.comptimePrint("unsupported return type {}", .{t})),
+ }
+
+ return;
+ }
+ }
+
+ std.log.warn("unimplemented event {} for {}", .{opcode, T});
+ }
+ };
+}
+
+test {
+ const Dummy = struct {
+ a: u32 = 0,
+ b: u32 = 0,
+
+ pub fn something(self: *@This(), a: u32, b: u32) void {
+ self.a = a;
+ self.b = b;
+ }
+ };
+
+ var dummy = Dummy{};
+ const Events = EventSet(Dummy, .{null, Dummy.something });
+
+ Events.on_event(&dummy, 1, &(std.mem.toBytes(@as(u32, 1)) ++ std.mem.toBytes(@as(u32, 2))));
+
+ try std.testing.expectEqual(dummy.a, 1);
+ try std.testing.expectEqual(dummy.b, 2);
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..7e27f94
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,15 @@
+const std = @import("std");
+const wayland = @import("wayland");
+const wl = wayland.wl;
+
+pub fn main() !void {
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+ const allocator = gpa.allocator();
+
+ var display: wl.Display = try .open_default(allocator);
+ defer display.close();
+ try display.init(allocator, &.{wl.Output.handler});
+
+ try display.roundtrip(allocator);
+ try display.roundtrip(allocator);
+}
diff --git a/src/object.zig b/src/object.zig
new file mode 100644
index 0000000..0751859
--- /dev/null
+++ b/src/object.zig
@@ -0,0 +1,41 @@
+const std = @import("std");
+const wayland = @import("root.zig");
+
+const Self = @This();
+
+const VTable = struct {
+ on_event: *const fn (ptr: *anyopaque, ctx: *wayland.Context, opcode: u16, args: []const u8) void,
+};
+
+id: ?u32 = null,
+ptr: ?*anyopaque,
+vtable: VTable,
+
+pub inline fn on_event(self: *Self, ctx: *wayland.Context, opcode: u16, args: []const u8) !void {
+ if (self.ptr) |ptr| {
+ self.vtable.on_event(ptr, ctx, opcode, args);
+ } else {
+ return error.UseAfterRelease;
+ }
+}
+
+pub inline fn from_self(ptr: anytype) Self {
+ return .{
+ .ptr = ptr,
+ .vtable = VTable{
+ .on_event = @TypeOf(ptr.*).Events.on_event,
+ },
+ };
+}
+
+pub fn format(
+ self: *const Self,
+ comptime fmt: []const u8,
+ options: std.fmt.FormatOptions,
+ writer: anytype,
+) !void {
+ _ = fmt;
+ _ = options;
+
+ try writer.print("Object {{ {?} }}", .{self.id});
+}
diff --git a/src/root.zig b/src/root.zig
new file mode 100644
index 0000000..ea9357a
--- /dev/null
+++ b/src/root.zig
@@ -0,0 +1,10 @@
+const std = @import("std");
+pub const wl = @import("wl/root.zig");
+pub const Object = @import("object.zig");
+pub const EventSet = @import("event-set.zig").EventSet;
+pub const types = @import("types.zig");
+pub const Context = @import("context.zig");
+
+test {
+ std.testing.refAllDecls(@This());
+}
diff --git a/src/types.zig b/src/types.zig
new file mode 100644
index 0000000..2849af1
--- /dev/null
+++ b/src/types.zig
@@ -0,0 +1,18 @@
+const std = @import("std");
+
+pub const Fd = struct { fd: std.posix.fd_t };
+
+pub const Fixed = struct {
+ n: u24,
+ f: u8,
+
+ pub fn from_f64(float: f64) @This() {
+ const n: u24 = @intFromFloat(float);
+ const f: u8 = @intFromFloat(std.math.maxInt(u8) * (float - @as(f64, @floatFromInt(n))));
+
+ return .{
+ .n = n,
+ .f = f,
+ };
+ }
+};
diff --git a/src/wl/callback.zig b/src/wl/callback.zig
new file mode 100644
index 0000000..0e7ea5b
--- /dev/null
+++ b/src/wl/callback.zig
@@ -0,0 +1,33 @@
+const std = @import("std");
+const wayland = @import("../root.zig");
+const wl = wayland.wl;
+
+const log = std.log.scoped(.callback);
+
+const Self = @This();
+
+pub const Events = wayland.EventSet(Self, .{done});
+
+is_done: *bool,
+interface: wayland.Object,
+
+pub fn init(
+ self: *Self,
+ allocator: std.mem.Allocator,
+ display: *wl.Display,
+ is_done: *bool,
+) !void {
+ self.is_done = is_done;
+ self.interface = wayland.Object.from_self(self);
+ try display.registry.add_object(allocator, &self.interface);
+ std.debug.print("ids {any}\n", .{display.registry.objects.items});
+ std.debug.print("callback #{?}\n", .{self.interface.id});
+ try display.request(display, .{ .sync = .{&self.interface} });
+
+}
+
+fn done(self: *Self, ctx: *wayland.Context, value: u32) void {
+ _ = value;
+ _ = ctx;
+ self.is_done.* = true;
+}
diff --git a/src/wl/display.zig b/src/wl/display.zig
new file mode 100644
index 0000000..4f04746
--- /dev/null
+++ b/src/wl/display.zig
@@ -0,0 +1,265 @@
+const std = @import("std");
+const wayland = @import("../root.zig");
+const wl = wayland.wl;
+
+const log = std.log.scoped(.display);
+
+const Self = @This();
+
+pub const Error = enum(u32) {
+ invalid_object = 0,
+ invalid_method = 1,
+ no_memory = 2,
+ implementation = 3,
+};
+
+pub const Request = union(enum) {
+ sync: struct { *wayland.Object },
+ get_registry: struct { *wayland.Object },
+};
+
+pub const Events = wayland.EventSet(Self, .{
+ err,
+ delete_id,
+});
+
+stream: std.net.Stream,
+interface: wayland.Object,
+registry: wl.Registry,
+
+pub fn init(
+ self: *Self,
+ allocator: std.mem.Allocator,
+ handlers: []const wl.Registry.GlobalHandler,
+) !void {
+ self.interface = wayland.Object.from_self(self);
+ try self.registry.init(allocator, self, handlers);
+}
+
+pub fn from_fd(fd: std.posix.fd_t) void {
+ var self: Self = undefined;
+ self.stream = std.net.Stream{ .handle = fd };
+ return self;
+}
+
+pub fn open(path: []const u8) !Self {
+ var self: Self = undefined;
+ self.stream = try std.net.connectUnixSocket(path);
+ return self;
+}
+
+pub fn close(self: *Self) void {
+ self.stream.close();
+}
+
+pub fn open_default(allocator: std.mem.Allocator) !Self {
+ const path = try default_path(allocator);
+ defer allocator.free(path);
+ return open(path);
+}
+
+pub fn default_path(allocator: std.mem.Allocator) ![]const u8 {
+ const env = try std.process.getEnvMap(allocator);
+ const xdg_runtime_dir = env.get("XDG_RUNTIME_DIR") orelse return error.MissingRuntimeDir;
+ const wayland_display = env.get("WAYLAND_DISPLAY") orelse "wayland-0";
+ return std.fmt.allocPrint(allocator, "{s}/{s}", .{ xdg_runtime_dir, wayland_display });
+}
+
+pub fn request(self: *Self, object: anytype, args: @TypeOf(object.*).Request) !void {
+ comptime std.debug.assert(@FieldType(@TypeOf(object.*), "interface") == wayland.Object);
+ comptime std.debug.assert(@typeInfo(@TypeOf(object.*).Request) == .@"union");
+ const tag_type = @typeInfo(@TypeOf(args)).@"union".tag_type.?;
+ const opcode: u32 = @intFromEnum(args);
+
+ inline for (@typeInfo(tag_type).@"enum".fields) |f| {
+ if (f.value == opcode) {
+ const data = @field(args, f.name);
+ const id = object.interface.id orelse return error.UnregisteredObject;
+
+ var size: u32 = 8;
+
+ inline for (data) |value| {
+ switch (@TypeOf(value)) {
+ u32, i32 => |t| size += @sizeOf(t),
+ *wayland.Object => size += @sizeOf(u32),
+ []const u8, []u8 => {
+ const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len + 1)))), 4);
+ size += @as(u32, @intCast(@sizeOf(u32) + (value.len + 1) + padding_size));
+ },
+ else => |t| switch (@typeInfo(t)) {
+ .pointer => |ptr| if (ptr.size == .slice) {
+ const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len * @sizeOf(ptr.child))))), 4);
+ size += @intCast(@sizeOf(u32) + (value.len * @sizeOf(ptr.child)) + padding_size);
+ } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})),
+ else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})),
+ },
+ }
+ }
+
+ _ = try self.stream.writeAll(&std.mem.toBytes(id));
+ _ = try self.stream.writeAll(&std.mem.toBytes((size << 16) | opcode));
+
+ inline for (data) |value| {
+ switch (@TypeOf(value)) {
+ u32, i32 => _ = try self.stream.writeAll(std.mem.asBytes(&value)),
+ *wayland.Object => try self.stream.writeAll(std.mem.asBytes(&(value.id orelse return error.UnregisteredObject))),
+ []const u8, []u8 => {
+ const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len + 1)))), 4);
+ _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len + 1))));
+ _ = try self.stream.writeAll(value);
+ for (0..padding_size + 1) |_| {
+ _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0)));
+ }
+ },
+ else => |t| switch (@typeInfo(t)) {
+ .pointer => |ptr| if (ptr.size == .slice) {
+ const padding_size: usize = @mod(@as(usize, @bitCast(-@as(isize, @intCast(value.len * @sizeOf(ptr.child))))), 4);
+ _ = try self.stream.writeAll(std.mem.asBytes(&@as(u32, @truncate(value.len))));
+ _ = try self.stream.writeAll(std.mem.sliceAsBytes(value));
+ for (0..padding_size) |_| {
+ _ = try self.stream.writeAll(std.mem.asBytes(&@as(u8, 0)));
+ }
+ } else @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})),
+ else => @compileError(std.fmt.comptimePrint("size: unsupported type {}", .{t})),
+ },
+ }
+ }
+ return;
+ }
+ }
+
+ return error.InvalidOpcode;
+}
+
+pub fn roundtrip(self: *Self, allocator: std.mem.Allocator) !void {
+ var done = false;
+ var callback: wl.Callback = undefined;
+ try callback.init(allocator, self, &done);
+
+ var context: wayland.Context = .{
+ .display = self,
+ .allocator = allocator,
+ };
+
+ while (!done) {
+ const endian = @import("builtin").target.cpu.arch.endian();
+ const reader = self.stream.reader();
+
+ const object_id = try reader.readInt(u32, endian);
+
+ const message_info = try reader.readInt(u32, endian);
+
+ const opcode: u16 = @truncate(message_info & 0xffff);
+ const size: u16 = @truncate(((message_info >> 16) & 0xffff));
+ const body_size = size - 2 * @sizeOf(u32);
+
+ const body = try allocator.alloc(u8, body_size);
+ defer allocator.free(body);
+ _ = try reader.readAll(body);
+
+ if (object_id >= self.registry.objects.items.len) {
+ return error.UnregisteredObject;
+ }
+
+ if (self.registry.objects.items[object_id]) |object| {
+ try object.on_event(&context, opcode, body);
+ } else {
+ return error.UnregisteredObject;
+ }
+ }
+}
+
+fn err(
+ self: *Self,
+ ctx: *wayland.Context,
+ object: *wayland.Object,
+ code: u32,
+ message: []const u8,
+) void {
+ _ = self;
+ _ = ctx;
+ log.err("{} ({}): {s}", .{ object, code, message });
+}
+
+fn delete_id(
+ self: *Self,
+ ctx: *wayland.Context,
+ id: u32,
+) void {
+ _ = ctx;
+ self.registry.release_object(id);
+}
+
+test "request" {
+ const Sample = struct {
+ pub const Request = union(enum) {
+ req1: struct { u32 },
+ req2: struct { *wayland.Object },
+ req3: struct { []const u8 },
+ req4: struct { []const u16 },
+ };
+
+ pub const Events = wayland.EventSet(@This(), .{});
+
+ interface: wayland.Object,
+
+ pub fn init(self: *@This()) void {
+ self.interface = wayland.Object.from_self(self);
+ }
+ };
+
+ const in, const out = try std.posix.pipe();
+ defer std.posix.close(in);
+
+ {
+ var sample: Sample = undefined;
+ sample.init();
+ sample.interface.id = 42;
+
+ var displ: Self = .from_fd(out);
+ defer displ.close();
+ try displ.init();
+ try displ.request(sample, .{ .req1 = .{432} });
+ try displ.request(sample, .{ .req2 = .{&sample.interface} });
+ try displ.request(sample, .{ .req3 = .{"abcd"} });
+ try displ.request(sample, .{ .req4 = .{&[_]u16{ 1, 32, 4, 5, 5 }} });
+ }
+
+ const file = std.fs.File{ .handle = in };
+ var test_buffer: [64]u8 = undefined;
+ _ = try file.readAll(test_buffer[0..12]);
+ try std.testing.expectEqualSlices(
+ u8,
+ &(std.mem.toBytes(@as(u32, 42)) ++
+ std.mem.toBytes(@as(u32, (12 << 16) | 0)) ++
+ std.mem.toBytes(@as(u32, 432))),
+ test_buffer[0..12],
+ );
+
+ _ = try file.readAll(test_buffer[0..12]);
+ try std.testing.expectEqualSlices(
+ u8,
+ &(std.mem.toBytes(@as(u32, 42)) ++
+ std.mem.toBytes(@as(u32, (12 << 16) | 1)) ++
+ std.mem.toBytes(@as(u32, 42))),
+ test_buffer[0..12],
+ );
+
+ _ = try file.readAll(test_buffer[0..20]);
+ try std.testing.expectEqualSlices(
+ u8,
+ &(std.mem.toBytes(@as(u32, 42)) ++
+ std.mem.toBytes(@as(u32, (20 << 16) | 2)) ++
+ std.mem.toBytes(@as(u32, 5))) ++ "abcd\x00\x00\x00\x00",
+ test_buffer[0..20],
+ );
+
+ _ = try file.readAll(test_buffer[0..24]);
+ try std.testing.expectEqualSlices(
+ u8,
+ &(std.mem.toBytes(@as(u32, 42)) ++
+ std.mem.toBytes(@as(u32, (24 << 16) | 3)) ++
+ std.mem.toBytes(@as(u32, 5))) ++ std.mem.toBytes([_]u16{ 1, 32, 4, 5, 5, 0 }),
+ test_buffer[0..24],
+ );
+}
diff --git a/src/wl/output.zig b/src/wl/output.zig
new file mode 100644
index 0000000..99e8ca1
--- /dev/null
+++ b/src/wl/output.zig
@@ -0,0 +1,171 @@
+const std = @import("std");
+const wayland = @import("../root.zig");
+const wl = wayland.wl;
+
+const Self = @This();
+
+pub const Requests = union(enum) {
+ release: struct {},
+};
+
+pub const Events = wayland.EventSet(Self, .{
+ geometry,
+ mode,
+ done,
+ scale,
+ name,
+ description,
+});
+
+pub const Subpixel = enum(u32) {
+ unkown = 0,
+ none = 1,
+ orizontal_rgb = 2,
+ orizontal_bgr = 3,
+ vertical_rgb = 4,
+ vertical_bgr = 5,
+};
+
+pub const Transform = enum(u32) {
+ normal = 0,
+ normal_90 = 1,
+ normal_180 = 2,
+ normal_270 = 3,
+ flipped = 4,
+ flipped_90 = 5,
+ flipped_180 = 6,
+ flipped_270 = 7,
+};
+
+pub const Geometry = struct {
+ x: u32,
+ y: u32,
+ physical_width: u32,
+ physical_height: u32,
+ subpixel: Subpixel,
+ make: []const u8,
+ model: []const u8,
+ transform: Transform,
+};
+
+pub const Mode = struct {
+ pub const Kind = enum(u32) {
+ current = 1,
+ preferred = 2,
+ };
+
+ kind: Kind,
+ width: u32,
+ height: u32,
+ refresh: u32,
+};
+
+pub const Info = struct {
+ geometry: Geometry,
+ mode: Mode,
+ scale: u32,
+ name: []const u8,
+ description: []const u8,
+};
+
+var outputs: std.ArrayListUnmanaged(*Self) = .empty;
+
+staging: Info = undefined,
+info: ?Info = null,
+interface: wayland.Object,
+
+pub fn init(
+ self: *Self,
+ allocator: std.mem.Allocator,
+ display: *wl.Display,
+) !void {
+ self.* = .{ .interface = wayland.Object.from_self(self) };
+ try display.registry.add_object(allocator, &self.interface);
+}
+
+pub fn deinit(self: *Self, display: *wl.Display) !void {
+ display.request(self, .{ .release = void{} });
+}
+
+pub const handler: wl.Registry.GlobalHandler = .{
+ .interface = "wl_output",
+ .version = 4,
+ .handler = register,
+};
+
+pub fn register(ctx: *wayland.Context) ?wayland.Object {
+ const output = ctx.allocator.create(Self) catch return null;
+ output.init(ctx.allocator, ctx.display) catch return null;
+ outputs.append(ctx.allocator, output) catch return null;
+ return output.interface;
+}
+
+fn geometry(
+ self: *Self,
+ ctx: *wayland.Context,
+ x: u32,
+ y: u32,
+ physical_width: u32,
+ physical_height: u32,
+ subpixel: Subpixel,
+ make: []const u8,
+ model: []const u8,
+ transform: Transform,
+) void {
+ _ = ctx;
+ self.staging.geometry = .{
+ .x = x,
+ .y = y,
+ .physical_width = physical_width,
+ .physical_height = physical_height,
+ .subpixel = subpixel,
+ .make = make,
+ .model = model,
+ .transform = transform,
+ };
+}
+
+fn mode(
+ self: *Self,
+ ctx: *wayland.Context,
+ flags: Mode.Kind,
+ width: u32,
+ height: u32,
+ refresh: u32,
+) void {
+ _ = ctx;
+ self.staging.mode = .{
+ .kind = flags,
+ .width = width,
+ .height = height,
+ .refresh = refresh,
+ };
+}
+
+fn done(self: *Self, ctx: *wayland.Context) void {
+ if (self.info) |info| {
+ ctx.allocator.free(info.name);
+ ctx.allocator.free(info.description);
+ }
+
+ self.info = self.staging;
+}
+
+fn scale(self: *Self, ctx: *wayland.Context, factor: u32) void {
+ _ = ctx;
+ self.staging.scale = factor;
+}
+
+fn name(self: *Self, ctx: *wayland.Context, n: []const u8) !void {
+ self.staging.name = try ctx.allocator.alloc(u8, n.len);
+ @memcpy(@constCast(self.staging.name), n);
+}
+
+fn description(self: *Self, ctx: *wayland.Context, d: []const u8) !void {
+ self.staging.description = try ctx.allocator.alloc(u8, d.len);
+ @memcpy(@constCast(self.staging.description), d);
+}
+
+pub fn get(index: usize) *Self {
+ return &outputs.items[index];
+}
diff --git a/src/wl/registry.zig b/src/wl/registry.zig
new file mode 100644
index 0000000..e4eb3a8
--- /dev/null
+++ b/src/wl/registry.zig
@@ -0,0 +1,96 @@
+const std = @import("std");
+const wayland = @import("../root.zig");
+const wl = wayland.wl;
+
+const log = std.log.scoped(.registry);
+
+const Self = @This();
+
+pub const Events = wayland.EventSet(Self, .{
+ global,
+ global_remove,
+});
+
+pub const Request = union(enum) {
+ bind: struct { u32, u32 },
+};
+
+pub const GlobalHandler = struct {
+ interface: []const u8,
+ version: u32,
+ handler: *const fn (ctx: *wayland.Context) ?wayland.Object,
+};
+
+objects: std.ArrayListUnmanaged(?*wayland.Object) = .empty,
+interface: wayland.Object,
+handlers: []const GlobalHandler,
+
+pub fn init(
+ self: *Self,
+ allocator: std.mem.Allocator,
+ display: *wl.Display,
+ handlers: []const GlobalHandler,
+) !void {
+ self.* = .{
+ .interface = wayland.Object.from_self(self),
+ .handlers = handlers,
+ };
+ try self.objects.append(allocator, null);
+ try self.objects.append(allocator, &display.interface);
+ try self.objects.append(allocator, &self.interface);
+
+ display.interface.id = 1;
+ self.interface.id = 2;
+
+ try display.request(display, .{ .get_registry = .{&self.interface} });
+}
+
+pub fn add_object(
+ self: *Self,
+ allocator: std.mem.Allocator,
+ object: *wayland.Object,
+) !void {
+ for (self.objects.items[1..], 1..) |obj, index| {
+ if (obj == null) {
+ object.id = @truncate(index);
+ self.objects.items[index] = object;
+ return;
+ }
+ }
+
+ object.id = @truncate(self.objects.items.len);
+ try self.objects.append(allocator, object);
+}
+
+pub fn release_object(self: *Self, id: u32) void {
+ self.objects.items[id] = null;
+}
+
+fn global(
+ self: *Self,
+ ctx: *wayland.Context,
+ name: u32,
+ interface: []const u8,
+ version: u32,
+) !void {
+ for (self.handlers) |handler| {
+ if (version == handler.version and std.mem.eql(u8, interface, handler.interface)) {
+ if (handler.handler(ctx)) |object| {
+ try ctx.display.request(self, .{ .bind = .{
+ name,
+ object.id orelse return error.UnregisteredObject,
+ } });
+ }
+ }
+ }
+}
+
+fn global_remove(
+ self: *Self,
+ ctx: *wayland.Context,
+ name: u32,
+) void {
+ _ = self;
+ _ = ctx;
+ log.debug("remove_global: {}", .{name});
+}
diff --git a/src/wl/root.zig b/src/wl/root.zig
new file mode 100644
index 0000000..b9e149f
--- /dev/null
+++ b/src/wl/root.zig
@@ -0,0 +1,4 @@
+pub const Display = @import("display.zig");
+pub const Registry = @import("registry.zig");
+pub const Callback = @import("callback.zig");
+pub const Output = @import("output.zig");