const std = @import("std"); const wayland = @import("zig-wayland/wayland.zig"); const zig_args = @import("zig-args"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const zxdg = wayland.client.zxdg; const zwlr = wayland.client.zwlr; const list = wayland.server.wl.list; const xkb = @cImport({ @cInclude("xkbcommon/xkbcommon.h"); }); const Cairo = @cImport({ @cInclude("cairo/cairo.h"); }); const Point = struct { const Self = @This(); x: i32 = 0, y: i32 = 0, fn inverted(self: *const Self) Self { return Self{ .x = self.x * -1, .y = self.y * -1, }; } fn add(self: *Self, other: Self) void { self.x += other.x; self.y += other.x; } fn added(self: *const Self, other: Self) Self { var new = self.*; new.add(other); return new; } }; const Size = struct { const Self = @This(); width: u32 = 0, height: u32 = 0, fn size(self: *Self) usize { return self.width * self.height; } }; const Selection = struct { const Self = @This(); start: Point, end: Point, fn fromPoint(point: Point) Self { return .{ .start = point, .end = point, }; } fn asBox(self: Self) Box { return Box.fromCorners(self.start, self.end); } }; const Buffer = struct { const Self = @This(); buffer: *wl.Buffer = undefined, cairo: struct { surface: *Cairo.cairo_surface_t = undefined, ctx: *Cairo.cairo_t = undefined, } = .{}, size: Size = .{}, data: []u8 = undefined, in_use: std.atomic.Atomic(bool) = std.atomic.Atomic(bool).init(false), fn randomName() [13]u8 { var name = [_]u8{'/', 'z', 'l', 'u','r','p','-','0','0','0','0','0','0'}; var rng = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp())); for (name[7..]) |*c| { c.* = rng.random().intRangeAtMost(u8, 'A', 'Z'); } return name; } fn init(self: *Self, shm: *wl.Shm, size: Size) !void { const name = randomName(); const stride = size.width * 4; const len = stride * size.height; const fd = try std.os.memfd_create(&name, 0); defer std.os.close(fd); try std.os.ftruncate(fd, len); const data = try std.os.mmap( null, len, std.os.PROT.READ | std.os.PROT.WRITE, std.os.MAP.SHARED, fd, 0, ); const pool = try shm.createPool(fd, @intCast(i32, len)); defer pool.destroy(); const buffer = try pool.createBuffer( 0, @intCast(i32, size.width), @intCast(i32, size.height), @intCast(i32, stride), .argb8888, ); const surface = Cairo.cairo_image_surface_create_for_data( @ptrCast([*c]u8, data), Cairo.CAIRO_FORMAT_ARGB32, @intCast(c_int, size.width), @intCast(c_int, size.height), @intCast(c_int, stride), ) orelse return error.NoCairoSurface; const cairo_ctx = Cairo.cairo_create(surface) orelse return error.NoCairoContext; self.* = .{ .buffer = buffer, .size = size, .data = data, .cairo = .{ .surface = surface, .ctx = cairo_ctx, }, }; self.buffer.setListener(*Self, bufferListener, self); } fn deinit(self: *Self) void { std.os.munmap(@alignCast(0x1000, self.data)); self.buffer.destroy(); Cairo.cairo_destroy(self.cairo.ctx); Cairo.cairo_surface_destroy(self.cairo.surface); } fn bufferListener(buffer: *wl.Buffer, event: wl.Buffer.Event, self: *Self) void { _ = buffer; switch (event) { .release => { self.in_use.store(false, .Monotonic); }, } } }; const Buffers = struct { const Self = @This(); state: *State.Init, buffers: [2]?Buffer = undefined, fn init(self: *Self, state: *State.Init) !void { self.* = Self{ .state = state, }; for (self.buffers) |*buffer| { buffer.* = null; } } // returns any already initialized and not currently used buffer fn getAvailableBuffer(self: *Self, size: Size) ?*Buffer { for (self.buffers) |*buffer| { if (buffer.*) |*buf| { if (!buf.in_use.load(.Monotonic)) { if (buf.size.width != size.width or buf.size.height != size.height) { buf.deinit(); buf.* = .{}; buf.init(self.state.shm, size) catch { buffer.* = null; continue; }; } return buf; } } } for (self.buffers) |*buffer| { if (buffer.* == null) { buffer.* = .{}; buffer.*.?.init(self.state.shm, size) catch { buffer.* = null; continue; }; return &buffer.*.?; } } return null; } }; test "buffers" { std.testing.refAllDecls(Buffers); } const Box = struct { const Self = @This(); position: Point = .{.x = 0, .y = 0,}, extents: Size = .{.width = 0, .height = 0,}, fn default() Self { return Self { .position = .{.x = 0, .y = 0,}, .extents = .{.width = 0, .height = 0,}, }; } fn intersects(self: *const Self, other: *const Self) bool { const x1 = self.position.x; const y1 = self.position.y; const w1 = @intCast(isize, self.extents.width); const h1 = @intCast(isize, self.extents.height); const x2 = other.position.x; const y2 = other.position.y; const w2 = @intCast(isize, other.extents.width); const h2 = @intCast(isize, other.extents.height); // return self.x < other.x + other.width and // self.x + self.width > other.x and // self.y < other.y + other.height and // self.height + self.y > other.y; return x1 < x2 + w2 and x1 + w1 > x2 and y1 < y1 + h2 and y1 + h1 > y2; } fn fromCorners(a: Point, b: Point) Self { const top = @min(a.y, b.y); const bot = @max(a.y, b.y); const left = @min(a.x, b.x); const right = @max(a.x, b.x); const pos = Point{.x = left, .y = top,}; const extents = Size{ .width = @intCast(u32, right - left), .height = @intCast(u32, bot - top), }; return Self { .position = pos, .extents = extents, }; } fn getInner(comptime T: anytype, v: anytype) T { switch (@typeInfo(T)) { .Float => { return @intToFloat(T, v); }, .Int => { return @intCast(T, v); }, else => { @compileError("can only cast positional scalar to integer or floating point types"); }, } } fn getX(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.x); } fn getY(self: *const Self, comptime T: anytype) T { return getInner(T, self.position.y); } fn getWidth(self: *const Self, comptime T: anytype) T { return getInner(T, self.extents.width); } fn getHeight(self: *const Self, comptime T: anytype) T { return getInner(T, self.extents.height); } fn translate(self: *Self, delta: Point) void { self.position.add(delta); } fn translated(self: Self, delta: Point) Self { var new = self; new.translate(delta); return new; } fn contains(self: *const Self, point: Point) bool { const x1 = self.position.x; const y1 = self.position.y; const w1 = @intCast(isize, self.extents.width); const h1 = @intCast(isize, self.extents.height); const x2 = point.x; const y2 = point.y; // return self.x <= point.x and self.x + self.width > point.x and // self.y <= point.y and self.y + self.height > point.y; return x1 <= x2 and x1 + w1 > x2 and y1 <= y2 and y1 + h1 > y2; } fn size(self: *Self) usize { return self.extents.size(); } }; test "box" { const a = Box.default(); const a2 = Box{.extents = .{.width = 1, .height = 1,}}; const b = Box.default(); const b2 = Box{.extents = .{.width = 1, .height = 1,}}; std.debug.assert(!a.contains(.{.x = 0, .y = 0})); std.debug.assert(!a.intersects(&b)); std.debug.assert(a2.contains(.{.x = 0, .y = 0})); std.debug.assert(a2.intersects(&b2)); const c1 = Box{ .position = .{.x = 1, .y = 1,}, .extents = .{.width = 2, .height = 2,}, }; const c2 = Box{ .position = .{.x = 1, .y = 1,}, .extents = .{.width = 1, .height = 1,}, }; std.debug.assert(!c1.intersects(&a2)); std.debug.assert(c1.intersects(&c2)); } fn ListItem(comptime T: type) type { return struct { const Self = @This(); link: list.Link, value: T = undefined, fn create(ally: std.mem.Allocator, value: T) !*Self { const self = try ally.create(Self); self.init(value); return self; } fn init(self: *Self, value: T) void { self.value = value; } fn Head() type { return list.Head(Self, "link"); } fn fromLink(link: *list.Link) *Self { return @fieldParentPtr(Self, "link", link); } fn fromValue(value: *T) *Self { return @fieldParentPtr(Self, "value", value); } /// automatically calls `deinit()` on `data` if a function with that name exists fn remove(self: *Self) void { if (@hasDecl(T, "deinit")) { self.value.deinit(); } self.link.remove(); } fn next(self: *Self) ?*list.Link { self.link.next(); } fn prev(self: *Self) ?*list.Link { self.link.prev(); } fn getLink(self: *Self) *list.Link { return &self.link; } fn get(self: *Self) *T { return &self.value; } fn getConst(self: *const Self) *const T { return &self.value; } }; } const Options = struct { const Self = @This(); help: bool = false, @"display-dimensions": bool = false, background: ?u32 = null, border: ?u32 = null, selection: ?u32 = null, choice: ?u32 = null, format: ?[]const u8 = null, @"font-family": ?[]const u8 = null, @"border-weight": ?u32 = null, @"single-point": bool = false, @"output-boxes": bool = false, @"restrict-selection": bool = false, // TODO: parse this in the format of W:H instead of a fraction @"aspect-ratio": ?f64 = null, pub const shorthands = .{ .h = "help", .b = "background", .c = "border", .s = "selection", .B = "choice", .f = "format", .F = "font-family", .w = "border-weight", .p = "single-point", .o = "output-boxes", .r = "restrict-selection", .a = "aspect-ratio", }; const usage = @embedFile("usage"); fn parse() !zig_args.ParseArgsResult(Self, null) { const self = try zig_args.parseForCurrentProcess(Self, std.heap.page_allocator, .print); // TODO: print help return self; } }; const Output = struct { const Self = @This(); state: *State.Init = undefined, output: *wl.Output, surface: *wl.Surface = undefined, layer_surface: *zwlr.LayerSurfaceV1 = undefined, xdg_output: *zxdg.OutputV1 = undefined, geometry: Box = Box.default(), logical_geometry: Box = Box.default(), scale: i32 = 1, size: Size = Size{}, configured: bool = false, needs_redraw: bool = false, buffers: Buffers = undefined, frame_callback: ?*wl.Callback = null, fn init(self: *Self, state: *State.Init) !void { std.debug.print("output.init()\n", .{}); self.state = state; try self.buffers.init(state); self.output.setListener(*Self, outputListener, self); self.surface = try state.compositor.createSurface(); self.layer_surface = try state.layer_shell.getLayerSurface( self.surface, self.output, .overlay, "selection", ); self.layer_surface.setListener(*Self, layerSurfaceListener, self); self.xdg_output = try state.xdg_output_manager.getXdgOutput(self.output); self.xdg_output.setListener(*Self, xdgOutputListener, self); self.layer_surface.setAnchor(.{.top = true, .left = true, .right = true, .bottom = true,}); self.layer_surface.setKeyboardInteractivity(1); self.layer_surface.setExclusiveZone(-1); self.surface.commit(); } fn deinit(self: *Self) void { self.output.destroy(); } fn setSourceU32(c: *Cairo.cairo_t, color: u32) void { Cairo.cairo_set_source_rgba( c, @intToFloat(f32, color >> (3 * 8) & 0xff) / 255.0, @intToFloat(f32, color >> (2 * 8) & 0xff) / 255.0, @intToFloat(f32, color >> (1 * 8) & 0xff) / 255.0, @intToFloat(f32, color >> (0 * 8) & 0xff) / 255.0, ); } fn drawRect(buffer: *Buffer, box: Box, color: u32) void { const cairo = buffer.cairo.ctx; setSourceU32(cairo, color); Cairo.cairo_rectangle( cairo, box.getX(f64), box.getY(f64), box.getWidth(f64), box.getHeight(f64), ); } fn render(self: *Self, buffer: *Buffer) void { const cairo = &buffer.cairo; Cairo.cairo_set_operator(cairo.ctx, Cairo.CAIRO_OPERATOR_SOURCE); setSourceU32(cairo.ctx, 0xFFFFFF40); Cairo.cairo_paint(cairo.ctx); var seat_iter = self.state.seats.iterator(.forward); while (seat_iter.next()) |link| { const seat = link.get(); if (seat.pointer == null) { continue; } if (seat.pointer.?.selection.getSelectionBox()) |box| { // draw inner box const corrected_box = box.translated(self.logical_geometry.position.inverted()); std.debug.print("selection: {}\n", .{corrected_box}); drawRect(buffer, corrected_box, 0x0); Cairo.cairo_fill(cairo.ctx); Cairo.cairo_set_line_width(cairo.ctx, 2.0); drawRect(buffer, corrected_box, 0x000000FF); Cairo.cairo_stroke(cairo.ctx); // TODO: draw dimensions } } } fn commitFrame(self: *Self) void { const size = Size{ .width = self.size.width * @intCast(u32, self.scale), .height = self.size.height * @intCast(u32, self.scale), }; // FIXME: maybe somehow make this retry to get another buffer if the one // that was returned has just become in-use? if (self.buffers.getAvailableBuffer(size)) |buffer| { if (buffer.in_use.compareAndSwap(false, true, .Release, .Monotonic) == null) { Cairo.cairo_identity_matrix(buffer.cairo.ctx); Cairo.cairo_scale( buffer.cairo.ctx, @intToFloat(f64, self.scale), @intToFloat(f64, self.scale), ); // RENDER self.render(buffer); if (self.frame_callback == null) { self.frame_callback = self.surface.frame() catch null; if (self.frame_callback) |frame| { frame.setListener(*Self, frameCallbackListener, self); } } self.surface.attach(buffer.buffer, 0, 0); self.surface.damage(0, 0, @intCast(i32, self.size.width), @intCast(i32, self.size.height), ); self.surface.setBufferScale(self.scale); self.surface.commit(); self.needs_redraw = false; } else { std.debug.print("failed to CAS in_use.\n", .{}); } } else { std.debug.print("No buffer available!\n", .{}); } } fn requestRedraw(self: *Self) !void { self.needs_redraw = true; if (self.frame_callback == null) { self.frame_callback = try self.surface.frame(); self.frame_callback.?.setListener(*Self, frameCallbackListener, self); self.surface.commit(); } } fn frameCallbackListener(frame: *wl.Callback, event: wl.Callback.Event, self: *Self) void { switch (event) { .done => { frame.destroy(); self.frame_callback = null; if (self.needs_redraw) { self.commitFrame(); } std.debug.print("frame_callback done\n", .{}); }, } } fn xdgOutputListener(output: *zxdg.OutputV1, event: zxdg.OutputV1.Event, self: *Self) void { _ = output; std.debug.print("zxdg_output listener: {}\n", .{event}); switch (event) { .logical_position => |pos| { self.logical_geometry.position = .{.x = pos.x, .y = pos.y,}; }, .logical_size => |size| { self.logical_geometry.extents = .{ .width = @intCast(u32, size.width), .height = @intCast(u32, size.height), }; }, else => {}, } } fn layerSurfaceListener( layer: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, self: *Self, ) void { std.debug.print("zwlr_layer_surface listener: {}\n", .{event}); switch (event) { .configure => |cfg|{ self.configured = true; self.size = .{ .width = cfg.width, .height = cfg.height, }; layer.ackConfigure(cfg.serial); self.commitFrame(); }, .closed => { self.state.removeOutput(self); }, } } fn outputListener(output: *wl.Output, event: wl.Output.Event, self: *Self) void { _ = output; std.debug.print("wl_output listener: {}\n", .{event}); switch (event) { .geometry => |geom| { self.geometry.position = .{.x = geom.x, .y = geom.y}; }, .mode => |mode| { if (!mode.flags.current) { self.geometry.extents = .{ .width = @intCast(u32, mode.width), .height = @intCast(u32, mode.height), }; } }, .scale => |scale| { self.scale = scale.factor; }, else => { std.debug.print("wl_output listener: unhandled\n", .{}); }, } } }; const Seat = struct { const Self = @This(); state: *State.Init = undefined, seat: *wl.Seat, keyboard: ?struct { keyboard: *wl.Keyboard, xkb: ?struct { keymap: *xkb.xkb_keymap, state: *xkb.xkb_state, } = null, } = null, pointer: ?struct { pointer: *wl.Pointer, current_surface_offset: ?Point = null, selection: SelectionState = SelectionState.None(), } = null, const SelectionTag = enum { none, pre, updating, post, }; const SelectionState = union(SelectionTag) { none: void, pre: Point, updating: Selection, post: Box, fn None() SelectionState { return SelectionState.none; } fn translate(self: SelectionState, delta: Point) void { switch (self) { .pre => |*point| { point.add(delta); }, .updating => |*selection| { selection.start.add(delta); selection.end.add(delta); }, .post => |*box| { box.translate(delta); }, else => {return null;}, } } fn getSelectionBox(self: SelectionState) ?Box { switch (self) { .updating => |selection| { return selection.asBox(); }, .post => |box| { return box; }, else => {return null;}, } } }; fn init(self: *Self, state: *State.Init) void { self.state = state; std.debug.print("seat.init()\n", .{}); self.seat.setListener(*Self, seatListener, self); } fn calculateRedraws(self: *Self) void { if (self.pointer) |ptr| { const box = ptr.selection.getSelectionBox(); var output_iter = self.state.outputs.iterator(.forward); while (output_iter.next()) |link| { const output = link.get(); if (box == null or output.logical_geometry.intersects(&box.?)) { output.requestRedraw() catch {}; } } } } fn pointerListener(pointer: *wl.Pointer, event: wl.Pointer.Event, self: *Self) void { _ = pointer; switch (event) { .enter => |enter| { if (self.state.getOutputForSurface(enter.surface.?)) |output| { self.pointer.?.current_surface_offset = output.logical_geometry.position; } std.debug.print("surface offset: {?}\n", .{self.pointer.?.current_surface_offset}); }, .leave => { self.pointer.?.current_surface_offset = null; }, .motion => |motion| { const x = motion.surface_x.toInt(); const y = motion.surface_y.toInt(); const xy = Point{.x = x, .y = y,}; if (self.pointer.?.current_surface_offset) |offset| { const point = xy.added(offset); switch (self.pointer.?.selection) { .pre => { self.pointer.?.selection = .{.updating = .{.start = point, .end = point}}; return self.calculateRedraws(); }, .updating => |*selection| { selection.end = point; return self.calculateRedraws(); }, else => {}, } } }, .button => |button| { switch (button.state) { .pressed => { self.pointer.?.selection = SelectionState{.pre = .{}}; }, .released => { switch (self.pointer.?.selection) { .updating => |selection| { const box = selection.asBox(); self.pointer.?.selection = .{.post = box}; return self.calculateRedraws(); }, else => {}, } }, else => {}, } }, else => {}, } } fn keyboardListener(keyboard: *wl.Keyboard, event: wl.Keyboard.Event, self: *Self) void { _ = keyboard; switch (event) { .keymap => |keymap_event| { const keymap = blk: { switch (keymap_event.format) { .no_keymap => { break :blk xkb.xkb_keymap_new_from_names( self.state.xkb_context, null, xkb.XKB_KEYMAP_COMPILE_NO_FLAGS, ); }, .xkb_v1 => { const buffer = std.os.mmap( null, keymap_event.size, std.os.PROT.READ, std.os.MAP.PRIVATE, keymap_event.fd, 0, ) catch break :blk null; defer std.os.munmap(buffer); defer std.os.close(keymap_event.fd); break :blk xkb.xkb_keymap_new_from_buffer( self.state.xkb_context, @ptrCast([*c]const u8, buffer), keymap_event.size - 1, xkb.XKB_KEYMAP_FORMAT_TEXT_V1, xkb.XKB_KEYMAP_COMPILE_NO_FLAGS, ); }, else => unreachable, } }; if (keymap) |map| { const state = xkb.xkb_state_new(map); if (state) |state_| { // SAFETY: keyboard cant be null because we are in the keyboard listener self.keyboard.?.xkb = .{ .keymap = map, .state = state_, }; } } }, .key => |key| { if (self.keyboard.?.xkb) |xkb_| { const keysym = xkb.xkb_state_key_get_one_sym(xkb_.state, key.key + 8); // const buf_len = 32; // const buf = self.state.ally.alloc(u8, buf_len) catch null; // if (buf) |buffer| { // _ = xkb.xkb_keysym_get_name(keysym, @ptrCast([*c]u8, buffer), buf_len); // std.debug.print("key: {s} {s}\n", .{buffer, if (key.state == .pressed) "pressed" else "released"}); // } switch (key.state) { .pressed => { switch (keysym) { xkb.XKB_KEY_Escape, xkb.XKB_KEY_q => { if (self.pointer) |*ptr| { switch (ptr.selection) { .none => { self.state.running = false; return; }, else => { ptr.selection = .none; return self.calculateRedraws(); } } } }, else => {}, } }, .released => {}, else => {}, } } }, else => {}, } } fn seatListener(seat: *wl.Seat, event: wl.Seat.Event, self: *Self) void { switch (event) { .capabilities => |value| { std.debug.print("seat capabilities: {}\n", .{value.capabilities}); const capabilities = value.capabilities; if (capabilities.keyboard) { const keyboard = seat.getKeyboard() catch return; self.keyboard = .{ .keyboard = keyboard, }; keyboard.setListener(*Self, keyboardListener, self); } if (capabilities.pointer) { const pointer = seat.getPointer() catch return; self.pointer = .{ .pointer = pointer, }; pointer.setListener(*Self, pointerListener, self); } }, else => {}, } } fn deinit(self: *Self) void { if (self.keyboard) |keyboard| { keyboard.keyboard.destroy(); if (keyboard.xkb) |kb| { xkb.xkb_state_unref(kb.state); xkb.xkb_keymap_unref(kb.keymap); } } if (self.pointer) |pointer| { pointer.pointer.destroy(); } self.seat.destroy(); } }; /// helper type to handle dynamic dispatch listeners whatever they're called fn Listener(comptime T: type, comptime D: type) type { return struct { const Self = @This(); const Fn = *const fn (obj: *T, event: T.Event, data: *D) void; data: *D, callback: Fn, fn create(data: *D, callback: Fn) Self { return Self{ .data = data, .callback = callback, }; } fn listener(obj: *T, event: T.Event, self: *Self) void { return self.callback(obj, event, self.data); } fn set(self: *Self, obj: *T) void { obj.setListener(*Self, listener, self); } fn cast( self: *Self, comptime D2: type, new_data: *D2, new_callback: Listener(T, D2).Fn, ) *Listener(T, D2) { const other = @ptrCast(*Listener(T, D2), self); other.data = new_data; other.callback = new_callback; return other; } }; } const State = struct { const Uninit = struct { const Self = @This(); ally: std.mem.Allocator = std.heap.c_allocator, dpy: *wl.Display = undefined, registry: *wl.Registry = undefined, registry_listener: *Listener(wl.Registry, Self) = undefined, shm: ?*wl.Shm = null, compositor: ?*wl.Compositor = null, layer_shell: ?*zwlr.LayerShellV1 = null, xdg_output_manager: ?*zxdg.OutputManagerV1 = null, seats: *ListItem(Seat).Head() = undefined, outputs: *ListItem(Output).Head() = undefined, fn create() !*Self { const ally = std.heap.c_allocator; const dpy = try wl.Display.connect(null); const registry = try dpy.getRegistry(); const self = try ally.create(Self); const listener = try ally.create(Listener(wl.Registry, Self)); listener.* = .{ .data = self, .callback = registryListener, }; self.* = .{ .ally = ally, .dpy = dpy, .registry = registry, .registry_listener = listener, .seats = try ally.create(ListItem(Seat).Head()), .outputs = try ally.create(ListItem(Output).Head()), }; self.seats.init(); self.outputs.init(); self.registry_listener.set(self.registry); if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed; return self; } fn deinit(self: *Self) void { self.ally.destroy(self.seats); self.ally.destroy(self.outputs); self.ally.destroy(self); // TODO: clean up wayland state } fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { switch (event) { .global => |global| { std.debug.print("global: {s}\n", .{global.interface}); if (std.cstr.cmp(global.interface, wl.Compositor.getInterface().name) == 0) { self.compositor = registry.bind(global.name, wl.Compositor, 4) catch return; } else if (std.cstr.cmp(global.interface, wl.Shm.getInterface().name) == 0) { self.shm = registry.bind(global.name, wl.Shm, 1) catch return; } else if (std.cstr.cmp(global.interface, zwlr.LayerShellV1.getInterface().name) == 0) { self.layer_shell = registry.bind(global.name, zwlr.LayerShellV1, 1) catch return; } else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0) { self.xdg_output_manager = registry.bind(global.name, zxdg.OutputManagerV1, 2) catch return; } else if (std.cstr.cmp(global.interface, wl.Seat.getInterface().name) == 0) { const seat = registry.bind(global.name, wl.Seat, 1) catch return; if (ListItem(Seat).create(self.ally, Seat{.seat = seat}) catch null) |ele| { self.seats.append(ele); } } else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) { const output = registry.bind(global.name, wl.Output, 3) catch return; if (ListItem(Output).create(self.ally, Output{.output = output}) catch null) |ele| { self.outputs.append(ele); } } }, .global_remove => {}, } } fn intoInit(self: *Self) !*Init { // free self at end of this function defer self.ally.destroy(self); const init = try self.ally.create(Init); // free allocated memory if not returning it errdefer self.ally.destroy(init); try init.init(self); return init; } }; const Init = struct { const Self = @This(); ally: std.mem.Allocator = std.heap.c_allocator, running: bool = true, dpy: *wl.Display = undefined, registry: *wl.Registry = undefined, registry_listener: *Listener(wl.Registry, Self) = undefined, shm: *wl.Shm = undefined, compositor: *wl.Compositor = undefined, layer_shell: *zwlr.LayerShellV1 = undefined, xdg_output_manager: *zxdg.OutputManagerV1 = undefined, xkb_context: *xkb.xkb_context = undefined, cursor_theme: []const u8 = undefined, cursor_size: u32 = undefined, seats: *ListItem(Seat).Head() = undefined, outputs: *ListItem(Output).Head() = undefined, fn init(self: *Self, uninit: *Uninit) !void { const xkb_context = xkb.xkb_context_new(xkb.XKB_CONTEXT_NO_FLAGS) orelse return error.NoXkbContext; // free xkb_context if we fail to initialize the state errdefer xkb.xkb_context_unref(xkb_context); const cursor_theme = std.process.getEnvVarOwned(uninit.ally, "XCURSOR_THEME") catch ""; // only free cursor_theme if bailing errdefer uninit.ally.free(cursor_theme); const buf = try std.process.getEnvVarOwned(uninit.ally, "XCURSOR_SIZE"); defer uninit.ally.free(buf); const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize; self.* = Self{ .ally = uninit.ally, .dpy = uninit.dpy, .registry = uninit.registry, // these 4 fields are set by the registry listener so they may be null at this point // treat that as a hard error tho. .shm = uninit.shm orelse return error.NoWlShm, .compositor = uninit.compositor orelse return error.NoWlCompositor, .layer_shell = uninit.layer_shell orelse return error.NoWlrLayerShell, .xdg_output_manager = uninit.xdg_output_manager orelse return error.NoXdgOutputManager, // these 3 fields should never be null because init() will bail if these functions fail .xkb_context = xkb_context, .cursor_theme = cursor_theme, .cursor_size = cursor_size, .seats = uninit.seats, .outputs = uninit.outputs, }; // i hope this is fine but appears to be requried to reset the listener on the registry self.registry_listener = uninit.registry_listener.cast(Self, self, Self.registryListener); // init seats var seats_iter = self.seats.safeIterator(.forward); while (seats_iter.next()) |item| { const seat = item.get(); seat.init(self); } // init outputs var output_iter = self.outputs.safeIterator(.forward); while (output_iter.next()) |item| { const output = item.get(); try output.init(self); } if (self.dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed; } fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { _ = registry; _ = self; std.debug.print("registryListener called after init", .{}); switch (event) { .global => {}, .global_remove => {}, } } fn stuff(self: *Self) !void { while (self.running) { if (self.dpy.dispatch() != .SUCCESS) return error.DispatchFailed; } } fn finish(self: *Self, box: Box) void { _ = self; _ = box; } fn removeOutput(self: *Self, output: *Output) void { const link = ListItem(Output).fromValue(output); link.remove(); self.ally.destroy(link); } fn getOutputForSurface(self: *Self, surface: *wl.Surface) ?*Output { var output_iter = self.outputs.iterator(.forward); while (output_iter.next()) |next| { const output = next.get(); if (output.surface == surface) { return output; } } return null; } fn deinit(self: *Self) void { defer self.ally.destroy(self); var seats_iter = self.seats.safeIterator(.forward); while (seats_iter.next()) |next| { next.remove(); self.ally.destroy(next); } self.ally.destroy(self.seats); var outputs_iter = self.outputs.safeIterator(.forward); while (outputs_iter.next()) |next| { next.remove(); self.ally.destroy(next); } self.ally.destroy(self.outputs); self.shm.destroy(); self.compositor.destroy(); self.layer_shell.destroy(); self.xdg_output_manager.destroy(); xkb.xkb_context_unref(self.xkb_context); self.registry.destroy(); self.ally.free(self.cursor_theme); } }; }; pub fn main() !void { // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); const state = try State.Uninit.create(); const init = try state.intoInit(); defer init.deinit(); try init.stuff(); }