git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit06aadc3472b144be944a1d9c0bba8ee42aaf8161
parent73c78cc858
authorIsaac Freund <[email protected]>
date2025-08-25 11:26
build: update to Zig 0.15

 .builds/alpine.yml           |   8 +-
 .builds/archlinux.yml        |   8 +-
 .builds/freebsd.yml          |   8 +-
 README.md                    |   2 +-
 build.zig                    |  41 ++--
 build.zig.zon                |   8 +-
 common/deque.zig             | 431 +++++++++++++++++++++++++++++++++++++++++++
 river/Cursor.zig             |   8 +-
 river/Decoration.zig         |   4 +-
 river/IdleInhibitManager.zig |  25 ++-
 river/IdleInhibitor.zig      |  35 ++--
 river/InputConfig.zig        |   2 +-
 river/InputDevice.zig        |   2 +-
 river/InputManager.zig       |   5 +-
 river/KeyboardGroup.zig      |  84 ++++-----
 river/Output.zig             |   2 +-
 river/OutputManager.zig      |   6 +-
 river/PointerBinding.zig     |   4 +-
 river/Seat.zig               |  25 ++-
 river/Server.zig             |   2 +-
 river/ShellSurface.zig       |   4 +-
 river/XkbBinding.zig         |   6 +-
 river/c.zig                  |   2 +-
 river/main.zig               |  27 ++-
 river/process.zig            |   8 +-
 25 files changed, 600 insertions(+), 157 deletions(-)

diff --git a/.builds/alpine.yml b/.builds/alpine.yml
index e03784a..b2a8892 100644
--- a/.builds/alpine.yml
+++ b/.builds/alpine.yml
@@ -43,10 +43,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz
-      tar xf zig-linux-x86_64-0.14.0.tar.xz
-      sudo mv zig-linux-x86_64-0.14.0/zig /usr/bin/
-      sudo mv zig-linux-x86_64-0.14.0/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz
+      tar xf zig-x86_64-linux-0.15.1.tar.xz
+      sudo mv zig-x86_64-linux-0.15.1/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.15.1/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml
index 414ee76..a6c25ab 100644
--- a/.builds/archlinux.yml
+++ b/.builds/archlinux.yml
@@ -41,10 +41,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz
-      tar xf zig-linux-x86_64-0.14.0.tar.xz
-      sudo mv zig-linux-x86_64-0.14.0/zig /usr/bin/
-      sudo mv zig-linux-x86_64-0.14.0/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz
+      tar xf zig-x86_64-linux-0.15.1.tar.xz
+      sudo mv zig-x86_64-linux-0.15.1/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.15.1/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
index 0756d14..d49fe20 100644
--- a/.builds/freebsd.yml
+++ b/.builds/freebsd.yml
@@ -46,10 +46,10 @@ tasks:
       cd ..
 
       # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.14.0/zig-freebsd-x86_64-0.14.0.tar.xz
-      tar xf zig-freebsd-x86_64-0.14.0.tar.xz
-      sudo mv zig-freebsd-x86_64-0.14.0/zig /usr/bin/
-      sudo mv zig-freebsd-x86_64-0.14.0/lib /usr/lib/zig
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.15.1/zig-x86_64-freebsd-0.15.1.tar.xz
+      tar xf zig-x86_64-freebsd-0.15.1.tar.xz
+      sudo mv zig-x86_64-freebsd-0.15.1/zig /usr/bin/
+      sudo mv zig-x86_64-freebsd-0.15.1/lib /usr/lib/zig
   - build: |
       cd river
       zig build --summary all
diff --git a/README.md b/README.md
index 3b0bf28..8c94d05 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ To compile river first ensure that you have the following dependencies
 installed. The "development" versions are required if applicable to your
 distribution.
 
-- [zig](https://ziglang.org/download/) 0.14
+- [zig](https://ziglang.org/download/) 0.15
 - wayland
 - wayland-protocols
 - [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.19
diff --git a/build.zig b/build.zig
index ad87479..3b56e34 100644
--- a/build.zig
+++ b/build.zig
@@ -12,7 +12,6 @@ pub fn build(b: *Build) !void {
 
     const strip = b.option(bool, "strip", "Omit debug information") orelse false;
     const pie = b.option(bool, "pie", "Build a Position Independent Executable") orelse false;
-    const llvm = !(b.option(bool, "no-llvm", "(expirimental) Use non-LLVM x86 Zig backend") orelse false);
 
     const omit_frame_pointer = switch (optimize) {
         .Debug, .ReleaseSafe => false,
@@ -128,16 +127,17 @@ pub fn build(b: *Build) !void {
     const flags = b.createModule(.{ .root_source_file = b.path("common/flags.zig") });
     const globber = b.createModule(.{ .root_source_file = b.path("common/globber.zig") });
     const slotmap = b.createModule(.{ .root_source_file = b.path("common/slotmap.zig") });
+    const deque = b.createModule(.{ .root_source_file = b.path("common/deque.zig") });
 
     {
         const river = b.addExecutable(.{
             .name = "river",
-            .root_source_file = b.path("river/main.zig"),
-            .target = target,
-            .optimize = optimize,
-            .strip = strip,
-            .use_llvm = llvm,
-            .use_lld = llvm,
+            .root_module = b.createModule(.{
+                .root_source_file = b.path("river/main.zig"),
+                .target = target,
+                .optimize = optimize,
+                .strip = strip,
+            }),
         });
         river.root_module.addOptions("build_options", options);
 
@@ -156,6 +156,7 @@ pub fn build(b: *Build) !void {
         river.root_module.addImport("flags", flags);
         river.root_module.addImport("globber", globber);
         river.root_module.addImport("slotmap", slotmap);
+        river.root_module.addImport("deque", deque);
 
         river.addCSourceFile(.{
             .file = b.path("river/wlroots_log_wrapper.c"),
@@ -200,22 +201,36 @@ pub fn build(b: *Build) !void {
 
     {
         const globber_test = b.addTest(.{
-            .root_source_file = b.path("common/globber.zig"),
-            .target = target,
-            .optimize = optimize,
+            .root_module = b.createModule(.{
+                .root_source_file = b.path("common/globber.zig"),
+                .target = target,
+                .optimize = optimize,
+            }),
         });
         const run_globber_test = b.addRunArtifact(globber_test);
 
         const slotmap_test = b.addTest(.{
-            .root_source_file = b.path("common/slotmap.zig"),
-            .target = target,
-            .optimize = optimize,
+            .root_module = b.createModule(.{
+                .root_source_file = b.path("common/slotmap.zig"),
+                .target = target,
+                .optimize = optimize,
+            }),
         });
         const run_slotmap_test = b.addRunArtifact(slotmap_test);
 
+        const deque_test = b.addTest(.{
+            .root_module = b.createModule(.{
+                .root_source_file = b.path("common/deque.zig"),
+                .target = target,
+                .optimize = optimize,
+            }),
+        });
+        const run_deque_test = b.addRunArtifact(deque_test);
+
         const test_step = b.step("test", "Run the tests");
         test_step.dependOn(&run_globber_test.step);
         test_step.dependOn(&run_slotmap_test.step);
+        test_step.dependOn(&run_deque_test.step);
     }
 }
 
diff --git a/build.zig.zon b/build.zig.zon
index 0a9a658..e27b6b6 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -14,12 +14,12 @@
             .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
         },
         .wayland = .{
-            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.3.0.tar.gz",
-            .hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl",
+            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz",
+            .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27",
         },
         .wlroots = .{
-            .url = "git+https://codeberg.org/ifreund/zig-wlroots#e96d90ce224d10278fc31ca7c3150cbd1919ef1c",
-            .hash = "wlroots-0.19.3-dev-jmOlcrcBBAB4-xzzlJBEPEIJwD9Fnzk2VSPHPCWIgTjV",
+            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz",
+            .hash = "wlroots-0.19.3-jmOlcuL_AwBHhLCwpFsXbTizE3q9BugFmGX-XIxqcPMc",
         },
         .xkbcommon = .{
             .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz",
diff --git a/common/deque.zig b/common/deque.zig
new file mode 100644
index 0000000..012bd1a
--- /dev/null
+++ b/common/deque.zig
@@ -0,0 +1,431 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+
+/// A contiguous, growable, double-ended queue.
+///
+/// Pushing/popping items from either end of the queue is O(1).
+pub fn Deque(comptime T: type) type {
+    return struct {
+        const Self = @This();
+
+        /// A ring buffer.
+        buffer: []T,
+        /// The index in buffer where the first item in the logical deque is stored.
+        head: usize,
+        /// The number of items stored in the logical deque.
+        len: usize,
+
+        /// A Deque containing no elements.
+        pub const empty: Self = .{
+            .buffer = &.{},
+            .head = 0,
+            .len = 0,
+        };
+
+        /// Initialize with capacity to hold `capacity` elements.
+        /// The resulting capacity will equal `capacity` exactly.
+        /// Deinitialize with `deinit`.
+        pub fn initCapacity(gpa: Allocator, capacity: usize) Allocator.Error!Self {
+            var deque: Self = .empty;
+            try deque.ensureTotalCapacityPrecise(gpa, capacity);
+            return deque;
+        }
+
+        /// Initialize with externally-managed memory. The buffer determines the
+        /// capacity and the deque is initially empty.
+        ///
+        /// When initialized this way, all functions that accept an Allocator
+        /// argument cause illegal behavior.
+        pub fn initBuffer(buffer: []T) Self {
+            return .{
+                .buffer = buffer,
+                .head = 0,
+                .len = 0,
+            };
+        }
+
+        /// Release all allocated memory.
+        pub fn deinit(deque: *Self, gpa: Allocator) void {
+            gpa.free(deque.buffer);
+            deque.* = undefined;
+        }
+
+        /// Modify the deque so that it can hold at least `new_capacity` items.
+        /// Implements super-linear growth to achieve amortized O(1) push/pop operations.
+        /// Invalidates element pointers if additional memory is needed.
+        pub fn ensureTotalCapacity(deque: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
+            if (deque.buffer.len >= new_capacity) return;
+            return deque.ensureTotalCapacityPrecise(gpa, growCapacity(deque.buffer.len, new_capacity));
+        }
+
+        /// If the current capacity is less than `new_capacity`, this function will
+        /// modify the deque so that it can hold exactly `new_capacity` items.
+        /// Invalidates element pointers if additional memory is needed.
+        pub fn ensureTotalCapacityPrecise(deque: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
+            if (deque.buffer.len >= new_capacity) return;
+            const old_buffer = deque.buffer;
+            if (gpa.remap(old_buffer, new_capacity)) |new_buffer| {
+                // If the items wrap around the end of the buffer we need to do
+                // a memcpy to prevent a gap after resizing the buffer.
+                if (deque.head > old_buffer.len - deque.len) {
+                    // The gap splits the items in the deque into head and tail parts.
+                    // Choose the shorter part to copy.
+                    const head = new_buffer[deque.head..old_buffer.len];
+                    const tail = new_buffer[0 .. deque.len - head.len];
+                    if (head.len > tail.len and new_buffer.len - old_buffer.len > tail.len) {
+                        @memcpy(new_buffer[old_buffer.len..][0..tail.len], tail);
+                    } else {
+                        // In this case overlap is possible if e.g. the capacity increase is 1
+                        // and head.len is greater than 1.
+                        deque.head = new_buffer.len - head.len;
+                        @memmove(new_buffer[deque.head..][0..head.len], head);
+                    }
+                }
+                deque.buffer = new_buffer;
+            } else {
+                const new_buffer = try gpa.alloc(T, new_capacity);
+                if (deque.head < old_buffer.len - deque.len) {
+                    @memcpy(new_buffer[0..deque.len], old_buffer[deque.head..][0..deque.len]);
+                } else {
+                    const head = old_buffer[deque.head..];
+                    const tail = old_buffer[0 .. deque.len - head.len];
+                    @memcpy(new_buffer[0..head.len], head);
+                    @memcpy(new_buffer[head.len..][0..tail.len], tail);
+                }
+                deque.head = 0;
+                deque.buffer = new_buffer;
+                gpa.free(old_buffer);
+            }
+        }
+
+        /// Modify the deque so that it can hold at least `additional_count` **more** items.
+        /// Invalidates element pointers if additional memory is needed.
+        pub fn ensureUnusedCapacity(
+            deque: *Self,
+            gpa: Allocator,
+            additional_count: usize,
+        ) Allocator.Error!void {
+            return deque.ensureTotalCapacity(gpa, try addOrOom(deque.len, additional_count));
+        }
+
+        /// Add one item to the front of the deque.
+        ///
+        /// Invalidates element pointers if additional memory is needed.
+        pub fn pushFront(deque: *Self, gpa: Allocator, item: T) error{OutOfMemory}!void {
+            try deque.ensureUnusedCapacity(gpa, 1);
+            deque.pushFrontAssumeCapacity(item);
+        }
+
+        /// Add one item to the front of the deque.
+        ///
+        /// Never invalidates element pointers.
+        ///
+        /// If the deque lacks unused capacity for the additional item, returns
+        /// `error.OutOfMemory`.
+        pub fn pushFrontBounded(deque: *Self, item: T) error{OutOfMemory}!void {
+            if (deque.buffer.len - deque.len == 0) return error.OutOfMemory;
+            return deque.pushFrontAssumeCapacity(item);
+        }
+
+        /// Add one item to the front of the deque.
+        ///
+        /// Never invalidates element pointers.
+        ///
+        /// Asserts that the deque can hold one additional item.
+        pub fn pushFrontAssumeCapacity(deque: *Self, item: T) void {
+            assert(deque.len < deque.buffer.len);
+            if (deque.head == 0) {
+                deque.head = deque.buffer.len;
+            }
+            deque.head -= 1;
+            deque.buffer[deque.head] = item;
+            deque.len += 1;
+        }
+
+        /// Add one item to the front of the deque.
+        ///
+        /// Invalidates element pointers if additional memory is needed.
+        pub fn pushBack(deque: *Self, gpa: Allocator, item: T) error{OutOfMemory}!void {
+            try deque.ensureUnusedCapacity(gpa, 1);
+            deque.pushBackAssumeCapacity(item);
+        }
+
+        /// Add one item to the back of the deque.
+        ///
+        /// Never invalidates element pointers.
+        ///
+        /// If the deque lacks unused capacity for the additional item, returns
+        /// `error.OutOfMemory`.
+        pub fn pushBackBounded(deque: *Self, item: T) error{OutOfMemory}!void {
+            if (deque.buffer.len - deque.len == 0) return error.OutOfMemory;
+            deque.pushBackAssumeCapacity(item);
+        }
+
+        /// Add one item to the back of the deque.
+        ///
+        /// Never invalidates element pointers.
+        ///
+        /// Asserts that the deque can hold one additional item.
+        pub fn pushBackAssumeCapacity(deque: *Self, item: T) void {
+            assert(deque.len < deque.buffer.len);
+            const buffer_index = deque.bufferIndex(deque.len);
+            deque.buffer[buffer_index] = item;
+            deque.len += 1;
+        }
+
+        /// Return the first item in the deque or null if empty.
+        pub fn front(deque: *const Self) ?T {
+            if (deque.len == 0) return null;
+            return deque.buffer[deque.head];
+        }
+
+        /// Return the last item in the deque or null if empty.
+        pub fn back(deque: *const Self) ?T {
+            if (deque.len == 0) return null;
+            return deque.buffer[deque.bufferIndex(deque.len - 1)];
+        }
+
+        /// Return the item at the given index in the deque.
+        ///
+        /// The first item in the queue is at index 0.
+        ///
+        /// Asserts that the index is in-bounds.
+        pub fn at(deque: *const Self, index: usize) T {
+            assert(index < deque.len);
+            return deque.buffer[deque.bufferIndex(index)];
+        }
+
+        /// Remove and return the first item in the deque or null if empty.
+        pub fn popFront(deque: *Self) ?T {
+            if (deque.len == 0) return null;
+            const pop_index = deque.head;
+            deque.head = deque.bufferIndex(1);
+            deque.len -= 1;
+            return deque.buffer[pop_index];
+        }
+
+        /// Remove and return the last item in the deque or null if empty.
+        pub fn popBack(deque: *Self) ?T {
+            if (deque.len == 0) return null;
+            deque.len -= 1;
+            return deque.buffer[deque.bufferIndex(deque.len)];
+        }
+
+        pub const Iterator = struct {
+            deque: *const Self,
+            index: usize,
+
+            pub fn next(it: *Iterator) ?T {
+                if (it.index < it.deque.len) {
+                    defer it.index += 1;
+                    return it.deque.at(it.index);
+                } else {
+                    return null;
+                }
+            }
+        };
+
+        /// Iterates over all items in the deque in order from front to back.
+        pub fn iterator(deque: *const Self) Iterator {
+            return .{ .deque = deque, .index = 0 };
+        }
+
+        /// Returns the index in `buffer` where the element at the given
+        /// index in the logical deque is stored.
+        fn bufferIndex(deque: *const Self, index: usize) usize {
+            // This is written with this confusing style to prevent overflow.
+            if (deque.head >= deque.buffer.len - index) {
+                return index - (deque.buffer.len - deque.head);
+            } else {
+                return deque.head + index;
+            }
+        }
+
+        const init_capacity: comptime_int = @max(1, std.atomic.cache_line / @sizeOf(T));
+
+        /// Called when memory growth is necessary. Returns a capacity larger than
+        /// minimum that grows super-linearly.
+        fn growCapacity(current: usize, minimum: usize) usize {
+            var new = current;
+            while (true) {
+                new +|= new / 2 + init_capacity;
+                if (new >= minimum) return new;
+            }
+        }
+    };
+}
+
+/// Integer addition returning `error.OutOfMemory` on overflow.
+fn addOrOom(a: usize, b: usize) error{OutOfMemory}!usize {
+    const result, const overflow = @addWithOverflow(a, b);
+    if (overflow != 0) return error.OutOfMemory;
+    return result;
+}
+
+test "basic" {
+    const testing = std.testing;
+    const gpa = testing.allocator;
+
+    var q: Deque(u32) = .empty;
+    defer q.deinit(gpa);
+
+    try testing.expectEqual(null, q.popFront());
+    try testing.expectEqual(null, q.popBack());
+
+    try q.pushBack(gpa, 1);
+    try q.pushBack(gpa, 2);
+    try q.pushBack(gpa, 3);
+    try q.pushFront(gpa, 0);
+
+    try testing.expectEqual(0, q.popFront());
+    try testing.expectEqual(1, q.popFront());
+    try testing.expectEqual(3, q.popBack());
+    try testing.expectEqual(2, q.popFront());
+    try testing.expectEqual(null, q.popFront());
+    try testing.expectEqual(null, q.popBack());
+}
+
+test "buffer" {
+    const testing = std.testing;
+
+    var buffer: [4]u32 = undefined;
+    var q: Deque(u32) = .initBuffer(&buffer);
+
+    try testing.expectEqual(null, q.popFront());
+    try testing.expectEqual(null, q.popBack());
+
+    try q.pushBackBounded(1);
+    try q.pushBackBounded(2);
+    try q.pushBackBounded(3);
+    try q.pushFrontBounded(0);
+    try testing.expectError(error.OutOfMemory, q.pushBackBounded(4));
+
+    try testing.expectEqual(0, q.popFront());
+    try testing.expectEqual(1, q.popFront());
+    try testing.expectEqual(3, q.popBack());
+    try testing.expectEqual(2, q.popFront());
+    try testing.expectEqual(null, q.popFront());
+    try testing.expectEqual(null, q.popBack());
+}
+
+test "slow growth" {
+    const testing = std.testing;
+    const gpa = testing.allocator;
+
+    var q: Deque(i32) = .empty;
+    defer q.deinit(gpa);
+
+    try q.ensureTotalCapacityPrecise(gpa, 1);
+    q.pushBackAssumeCapacity(1);
+    try q.ensureTotalCapacityPrecise(gpa, 2);
+    q.pushFrontAssumeCapacity(0);
+    try q.ensureTotalCapacityPrecise(gpa, 3);
+    q.pushBackAssumeCapacity(2);
+    try q.ensureTotalCapacityPrecise(gpa, 5);
+    q.pushBackAssumeCapacity(3);
+    q.pushFrontAssumeCapacity(-1);
+    try q.ensureTotalCapacityPrecise(gpa, 6);
+    q.pushFrontAssumeCapacity(-2);
+
+    try testing.expectEqual(-2, q.popFront());
+    try testing.expectEqual(-1, q.popFront());
+    try testing.expectEqual(3, q.popBack());
+    try testing.expectEqual(0, q.popFront());
+    try testing.expectEqual(2, q.popBack());
+    try testing.expectEqual(1, q.popBack());
+    try testing.expectEqual(null, q.popFront());
+    try testing.expectEqual(null, q.popBack());
+}
+
+test "fuzz against ArrayList oracle" {
+    try std.testing.fuzz({}, fuzzAgainstArrayList, .{});
+}
+
+test "dumb fuzz against ArrayList oracle" {
+    const testing = std.testing;
+    const gpa = testing.allocator;
+
+    const input = try gpa.alloc(u8, 1024);
+    defer gpa.free(input);
+
+    var prng = std.Random.DefaultPrng.init(testing.random_seed);
+    prng.random().bytes(input);
+
+    try fuzzAgainstArrayList({}, input);
+}
+
+fn fuzzAgainstArrayList(_: void, input: []const u8) anyerror!void {
+    const testing = std.testing;
+    const gpa = testing.allocator;
+
+    var q: Deque(u32) = .empty;
+    defer q.deinit(gpa);
+    var l: std.ArrayList(u32) = .empty;
+    defer l.deinit(gpa);
+
+    if (input.len < 2) return;
+
+    var prng = std.Random.DefaultPrng.init(input[0]);
+    const random = prng.random();
+
+    const Action = enum {
+        push_back,
+        push_front,
+        pop_back,
+        pop_front,
+        grow,
+        /// Sentinel to avoid hardcoding the cast below
+        max,
+    };
+    for (input[1..]) |byte| {
+        switch (@as(Action, @enumFromInt(byte % (@intFromEnum(Action.max))))) {
+            .push_back => {
+                const item = random.int(u8);
+                try testing.expectEqual(
+                    l.appendBounded(item),
+                    q.pushBackBounded(item),
+                );
+            },
+            .push_front => {
+                const item = random.int(u8);
+                try testing.expectEqual(
+                    l.insertBounded(0, item),
+                    q.pushFrontBounded(item),
+                );
+            },
+            .pop_back => {
+                try testing.expectEqual(l.pop(), q.popBack());
+            },
+            .pop_front => {
+                try testing.expectEqual(
+                    if (l.items.len > 0) l.orderedRemove(0) else null,
+                    q.popFront(),
+                );
+            },
+            // Growing by small, random, linear amounts seems to better test
+            // ensureTotalCapacityPrecise(), which is the most complex part
+            // of the Deque implementation.
+            .grow => {
+                const growth = random.int(u3);
+                try l.ensureTotalCapacityPrecise(gpa, l.items.len + growth);
+                try q.ensureTotalCapacityPrecise(gpa, q.len + growth);
+            },
+            .max => unreachable,
+        }
+        try testing.expectEqual(l.getLastOrNull(), q.back());
+        try testing.expectEqual(
+            if (l.items.len > 0) l.items[0] else null,
+            q.front(),
+        );
+        try testing.expectEqual(l.items.len, q.len);
+        try testing.expectEqual(l.capacity, q.buffer.len);
+        {
+            var it = q.iterator();
+            for (l.items) |item| {
+                try testing.expectEqual(item, it.next());
+            }
+            try testing.expectEqual(null, it.next());
+        }
+    }
+}
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 997eef8..1a6ec15 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -26,7 +26,7 @@ const wayland = @import("wayland");
 const wl = wayland.server.wl;
 const zwlr = wayland.server.zwlr;
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
@@ -225,7 +225,7 @@ pub fn setTheme(cursor: *Cursor, theme: ?[*:0]const u8, _size: ?u32) !void {
     // If this cursor belongs to the default seat, set the xcursor environment
     // variables as well as the xwayland cursor theme.
     if (cursor.seat == server.input_manager.defaultSeat()) {
-        const size_str = try std.fmt.allocPrintZ(util.gpa, "{}", .{size});
+        const size_str = try std.fmt.allocPrintSentinel(util.gpa, "{}", .{size}, 0);
         defer util.gpa.free(size_str);
         if (c.setenv("XCURSOR_SIZE", size_str.ptr, 1) < 0) return error.OutOfMemory;
         if (theme) |t| if (c.setenv("XCURSOR_THEME", t, 1) < 0) return error.OutOfMemory;
@@ -532,8 +532,8 @@ pub fn processAxis(cursor: *Cursor, event: *const wlr.Pointer.event.Axis) void {
             // @intFromFloat() call safe due to the max/min i32 not being exactly representable
             // by an f32. Dividing by 2 is a low effort way to ensure the value is in bounds and
             // allow users to set their scroll-factor to inf without crashing river.
-            math.minInt(i32) / 2,
-            math.maxInt(i32) / 2,
+            @as(f32, @floatFromInt(math.minInt(i32) / 2)),
+            @as(f32, @floatFromInt(math.maxInt(i32) / 2)),
         )),
         event.source,
         event.relative_direction,
diff --git a/river/Decoration.zig b/river/Decoration.zig
index a47817d..2bf1a80 100644
--- a/river/Decoration.zig
+++ b/river/Decoration.zig
@@ -135,7 +135,7 @@ fn handleRequest(
     }
 }
 
-fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
+fn clientCommit(wlr_surface: *wlr.Surface) callconv(.c) void {
     if (wlr_surface.role != &role) return;
     const resource = wlr_surface.role_resource orelse return;
     const decoration: *Decoration = @ptrCast(@alignCast(resource.getUserData()));
@@ -144,7 +144,7 @@ fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
     }
 }
 
-fn commit(wlr_surface: *wlr.Surface) callconv(.C) void {
+fn commit(wlr_surface: *wlr.Surface) callconv(.c) void {
     if (wlr_surface.hasBuffer()) {
         wlr_surface.map();
     }
diff --git a/river/IdleInhibitManager.zig b/river/IdleInhibitManager.zig
index 44785cc..ca7e01f 100644
--- a/river/IdleInhibitManager.zig
+++ b/river/IdleInhibitManager.zig
@@ -29,28 +29,30 @@ const Window = @import("Window.zig");
 
 wlr_manager: *wlr.IdleInhibitManagerV1,
 new_idle_inhibitor: wl.Listener(*wlr.IdleInhibitorV1) = .init(handleNewIdleInhibitor),
-inhibitors: std.DoublyLinkedList(IdleInhibitor) = .{},
+inhibitors: wl.list.Head(IdleInhibitor, .link),
 
 pub fn init(inhibit_manager: *IdleInhibitManager) !void {
     inhibit_manager.* = .{
         .wlr_manager = try wlr.IdleInhibitManagerV1.create(server.wl_server),
+        .inhibitors = undefined,
     };
+    inhibit_manager.inhibitors.init();
+
     inhibit_manager.wlr_manager.events.new_inhibitor.add(&inhibit_manager.new_idle_inhibitor);
 }
 
 pub fn deinit(inhibit_manager: *IdleInhibitManager) void {
-    while (inhibit_manager.inhibitors.pop()) |inhibitor| {
-        inhibitor.data.destroy.link.remove();
-        util.gpa.destroy(inhibitor);
+    while (inhibit_manager.inhibitors.first()) |inhibitor| {
+        inhibitor.destroy();
     }
     inhibit_manager.new_idle_inhibitor.link.remove();
 }
 
 pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
     var inhibited = false;
-    var it = inhibit_manager.inhibitors.first;
-    while (it) |node| : (it = node.next) {
-        const node_data = SceneNodeData.fromSurface(node.data.wlr_inhibitor.surface) orelse continue;
+    var it = inhibit_manager.inhibitors.iterator(.forward);
+    while (it.next()) |inhibitor| {
+        const node_data = SceneNodeData.fromSurface(inhibitor.wlr_inhibitor.surface) orelse continue;
         switch (node_data.data) {
             .window => {
                 inhibited = true; // XXX be strict
@@ -68,13 +70,8 @@ pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
 
 fn handleNewIdleInhibitor(listener: *wl.Listener(*wlr.IdleInhibitorV1), inhibitor: *wlr.IdleInhibitorV1) void {
     const inhibit_manager: *IdleInhibitManager = @fieldParentPtr("new_idle_inhibitor", listener);
-    const inhibitor_node = util.gpa.create(std.DoublyLinkedList(IdleInhibitor).Node) catch return;
-    inhibitor_node.data.init(inhibitor, inhibit_manager) catch {
-        util.gpa.destroy(inhibitor_node);
+    IdleInhibitor.create(inhibitor, inhibit_manager) catch {
+        std.log.err("out of memory", .{});
         return;
     };
-
-    inhibit_manager.inhibitors.append(inhibitor_node);
-
-    inhibit_manager.checkActive();
 }
diff --git a/river/IdleInhibitor.zig b/river/IdleInhibitor.zig
index 9e2ccc5..d0b9466 100644
--- a/river/IdleInhibitor.zig
+++ b/river/IdleInhibitor.zig
@@ -28,31 +28,38 @@ const IdleInhibitManager = @import("IdleInhibitManager.zig");
 inhibit_manager: *IdleInhibitManager,
 wlr_inhibitor: *wlr.IdleInhibitorV1,
 
-destroy: wl.Listener(*wlr.Surface) = .init(handleDestroy),
+listen_destroy: wl.Listener(*wlr.Surface) = .init(handleDestroy),
+
+link: wl.list.Link,
+
+pub fn create(wlr_inhibitor: *wlr.IdleInhibitorV1, inhibit_manager: *IdleInhibitManager) !void {
+    const inhibitor = try util.gpa.create(IdleInhibitor);
+    errdefer util.gpa.destroy(inhibitor);
 
-pub fn init(
-    inhibitor: *IdleInhibitor,
-    wlr_inhibitor: *wlr.IdleInhibitorV1,
-    inhibit_manager: *IdleInhibitManager,
-) !void {
     inhibitor.* = .{
         .inhibit_manager = inhibit_manager,
         .wlr_inhibitor = wlr_inhibitor,
+        .link = undefined,
     };
-    wlr_inhibitor.events.destroy.add(&inhibitor.destroy);
+    wlr_inhibitor.events.destroy.add(&inhibitor.listen_destroy);
+
+    inhibit_manager.inhibitors.append(inhibitor);
 
     inhibit_manager.checkActive();
 }
 
-fn handleDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const inhibitor: *IdleInhibitor = @fieldParentPtr("destroy", listener);
-
-    inhibitor.destroy.link.remove();
+pub fn destroy(inhibitor: *IdleInhibitor) void {
+    inhibitor.listen_destroy.link.remove();
 
-    const node: *std.DoublyLinkedList(IdleInhibitor).Node = @fieldParentPtr("data", inhibitor);
-    server.idle_inhibit_manager.inhibitors.remove(node);
+    inhibitor.link.remove();
 
     inhibitor.inhibit_manager.checkActive();
 
-    util.gpa.destroy(node);
+    util.gpa.destroy(inhibitor);
+}
+
+fn handleDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
+    const inhibitor: *IdleInhibitor = @fieldParentPtr("listen_destroy", listener);
+
+    inhibitor.destroy();
 }
diff --git a/river/InputConfig.zig b/river/InputConfig.zig
index fe7af14..59f31be 100644
--- a/river/InputConfig.zig
+++ b/river/InputConfig.zig
@@ -25,7 +25,7 @@ const wlr = @import("wlroots");
 
 const log = std.log.scoped(.input);
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
diff --git a/river/InputDevice.zig b/river/InputDevice.zig
index 09a6767..7629755 100644
--- a/river/InputDevice.zig
+++ b/river/InputDevice.zig
@@ -24,7 +24,7 @@ const wl = @import("wayland").server.wl;
 
 const globber = @import("globber");
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
diff --git a/river/InputManager.zig b/river/InputManager.zig
index c8c2102..9c6b3a5 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -55,7 +55,7 @@ tablet_manager: *wlr.TabletManagerV2,
 
 /// List of input device configurations. Ordered by glob generality, with
 /// the most general towards the start and the most specific towards the end.
-configs: std.ArrayList(InputConfig),
+configs: std.ArrayList(InputConfig) = .empty,
 
 devices: wl.list.Head(InputDevice, .link),
 seats: wl.list.Head(Seat, .link),
@@ -78,7 +78,6 @@ pub fn init(input_manager: *InputManager) !void {
         .input_method_manager = try wlr.InputMethodManagerV2.create(server.wl_server),
         .text_input_manager = try wlr.TextInputManagerV3.create(server.wl_server),
         .tablet_manager = try wlr.TabletManagerV2.create(server.wl_server),
-        .configs = std.ArrayList(InputConfig).init(util.gpa),
 
         .devices = undefined,
         .seats = undefined,
@@ -119,7 +118,7 @@ pub fn deinit(input_manager: *InputManager) void {
     for (input_manager.configs.items) |*config| {
         config.deinit();
     }
-    input_manager.configs.deinit();
+    input_manager.configs.deinit(util.gpa);
 }
 
 pub fn defaultSeat(input_manager: *InputManager) *Seat {
diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig
index 89f3f15..8dd2c4d 100644
--- a/river/KeyboardGroup.zig
+++ b/river/KeyboardGroup.zig
@@ -41,53 +41,23 @@ const KeyConsumer = union(enum) {
     focus,
 };
 
-pub const Pressed = struct {
-    const Key = struct {
-        /// The raw libinput keycode, not the xkb keycode
-        code: u32,
-        consumer: KeyConsumer,
-        count: u32,
-    };
-
-    pub const capacity = 32;
-
-    comptime {
-        // wlroots uses a buffer of length 32 to track pressed keys and does not track pressed
-        // keys beyond that limit. It seems likely that this can cause some inconsistency within
-        // wlroots in the case that someone has 32 fingers and the hardware supports N-key rollover.
-        //
-        // Furthermore, wlroots will continue to forward key press/release events to river if more
-        // than 32 keys are pressed. Therefore river chooses to ignore keypresses that would take
-        // the keyboard beyond 32 simultaneously pressed keys.
-        assert(capacity == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
-    }
-
-    keys: std.BoundedArray(Key, capacity) = .{},
-
-    fn get(pressed: *Pressed, code: u32) ?*Key {
-        for (pressed.keys.slice()) |*key| {
-            if (key.code == code) return key;
-        }
-        return null;
-    }
-
-    fn add(pressed: *Pressed, new: Key) void {
-        assert(pressed.get(new.code) == null);
-        pressed.keys.appendAssumeCapacity(new);
-    }
-
-    /// Asserts that the key is present and has count == 0.
-    fn remove(pressed: *Pressed, code: u32) KeyConsumer {
-        for (pressed.keys.constSlice(), 0..) |key, idx| {
-            if (key.code == code) {
-                assert(key.count == 0);
-                return pressed.keys.swapRemove(idx).consumer;
-            }
-        }
-        unreachable;
-    }
+const Press = struct {
+    consumer: KeyConsumer,
+    count: u32,
 };
 
+pub const pressed_count_max = 32;
+comptime {
+    // wlroots uses a buffer of length 32 to track pressed keys and does not track pressed
+    // keys beyond that limit. It seems likely that this can cause some inconsistency within
+    // wlroots in the case that someone has 32 fingers and the hardware supports N-key rollover.
+    //
+    // Furthermore, wlroots will continue to forward key press/release events to river if more
+    // than 32 keys are pressed. Therefore river chooses to ignore keypresses that would take
+    // the keyboard beyond 32 simultaneously pressed keys.
+    assert(pressed_count_max == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
+}
+
 ref_count: u32 = 1,
 
 seat: *Seat,
@@ -100,8 +70,9 @@ virtual: bool,
 /// setting keyboard focus.
 state: wlr.Keyboard,
 
-/// Pressed keys along with where their press event has been sent
-pressed: Pressed = .{},
+/// Maps from pressed libinput keycode (not xkb keycode) to information
+/// about where the press event has been sent.
+pressed: std.AutoArrayHashMapUnmanaged(u32, Press) = .empty,
 
 key: wl.Listener(*wlr.Keyboard.event.Key) = .init(handleKey),
 modifiers: wl.Listener(*wlr.Keyboard) = .init(handleModifiers),
@@ -115,6 +86,10 @@ pub fn create(seat: *Seat, keymap: ?*xkb.Keymap, virtual: bool) !*KeyboardGroup
         .state = undefined,
         .link = undefined,
     };
+
+    try group.pressed.ensureTotalCapacity(util.gpa, pressed_count_max);
+    errdefer comptime unreachable;
+
     seat.keyboard_groups.append(group);
 
     group.state.init(&.{
@@ -161,11 +136,13 @@ pub fn unref(group: *KeyboardGroup) void {
 
     group.state.finish();
 
+    group.pressed.deinit(util.gpa);
+
     util.gpa.destroy(group);
 }
 
 pub fn processKey(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) void {
-    if (group.pressed.get(event.keycode)) |key| {
+    if (group.pressed.getPtr(event.keycode)) |key| {
         assert(key.count > 0);
         if (event.state == .pressed) {
             key.count += 1;
@@ -183,7 +160,7 @@ pub fn processKey(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) v
             }
         }
     } else if (event.state == .pressed) {
-        if (group.pressed.keys.ensureUnusedCapacity(1)) {
+        if (group.pressed.count() < pressed_count_max) {
             var key_event: wlr.Keyboard.event.Key = .{
                 .time_msec = event.time_msec,
                 .keycode = event.keycode,
@@ -192,7 +169,7 @@ pub fn processKey(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) v
             };
             // Calls handleKey(), which will add to pressed
             group.state.notifyKey(&key_event);
-        } else |_| {}
+        }
     }
     // Release events without a prior press event are ignored.
 }
@@ -211,7 +188,9 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
     const consumer: KeyConsumer = blk: {
         if (event.state == .released) {
             // Decision is made on press; release only follows it
-            break :blk group.pressed.remove(event.keycode);
+            const kv = group.pressed.fetchSwapRemove(event.keycode).?;
+            assert(kv.value.count == 0);
+            break :blk kv.value.consumer;
         }
         // Translate libinput keycode -> xkbcommon
         const xkb_keycode = event.keycode + 8;
@@ -235,8 +214,7 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
     };
 
     if (event.state == .pressed) {
-        group.pressed.add(.{
-            .code = event.keycode,
+        group.pressed.putAssumeCapacityNoClobber(event.keycode, .{
             .consumer = consumer,
             .count = 1,
         });
diff --git a/river/Output.zig b/river/Output.zig
index 5f709aa..3eef3ba 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -153,7 +153,7 @@ pub fn create(wlr_output: *wlr.Output) !void {
     errdefer util.gpa.destroy(output);
 
     {
-        const title = try fmt.allocPrintZ(util.gpa, "river - {s}", .{wlr_output.name});
+        const title = try fmt.allocPrintSentinel(util.gpa, "river - {s}", .{wlr_output.name}, 0);
         defer util.gpa.free(title);
         if (wlr_output.isWl()) {
             wlr_output.wlSetTitle(title);
diff --git a/river/OutputManager.zig b/river/OutputManager.zig
index 7cbb11c..ee63d3c 100644
--- a/river/OutputManager.zig
+++ b/river/OutputManager.zig
@@ -305,15 +305,15 @@ pub fn commitOutputState(om: *OutputManager) void {
     };
 
     if (need_modeset) {
-        var states = std.ArrayList(wlr.Backend.OutputState).init(util.gpa);
-        defer states.deinit();
+        var states: std.ArrayList(wlr.Backend.OutputState) = .empty;
+        defer states.deinit(util.gpa);
         defer for (states.items) |*s| s.base.finish();
 
         {
             var it = wm.wm_sent.outputs.iterator(.forward);
             while (it.next()) |output| {
                 const wlr_output = output.wlr_output orelse continue;
-                const state = states.addOne() catch {
+                const state = states.addOne(util.gpa) catch {
                     log.err("out of memory", .{});
                     return;
                 };
diff --git a/river/PointerBinding.zig b/river/PointerBinding.zig
index bbc8816..4da340c 100644
--- a/river/PointerBinding.zig
+++ b/river/PointerBinding.zig
@@ -23,7 +23,7 @@ const wayland = @import("wayland");
 const wl = wayland.server.wl;
 const river = wayland.server.river;
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
@@ -72,7 +72,7 @@ pub fn create(
 
     log.debug("new river_pointer_binding_v1: button: {d}({?s}) modifiers: {d}", .{
         button,
-        c.libevdev_event_code_get_name(c.EV_KEY, button),
+        @as(?[*:0]const u8, c.libevdev_event_code_get_name(c.EV_KEY, button)),
         @as(u32, @bitCast(modifiers)),
     });
 
diff --git a/river/Seat.zig b/river/Seat.zig
index b2e9e0f..27c8192 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -24,6 +24,7 @@ const wayland = @import("wayland");
 const wl = wayland.server.wl;
 const river = wayland.server.river;
 const xkb = @import("xkbcommon");
+const Deque = @import("deque").Deque;
 
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
@@ -102,9 +103,6 @@ pub const Focus = union(enum) {
     }
 };
 
-/// XXX experiment with different sizes here, consider making dynamic
-const EventQueue = std.fifo.LinearFifo(Event, .{ .Static = 1024 });
-
 wlr_seat: *wlr.Seat,
 
 link: wl.list.Link,
@@ -113,7 +111,7 @@ destroying: bool = false,
 
 object: ?*river.SeatV1 = null,
 
-event_queue: EventQueue = EventQueue.init(),
+event_queue: Deque(Event),
 
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
@@ -185,9 +183,14 @@ pub fn create(name: [*:0]const u8) !void {
     const seat = try util.gpa.create(Seat);
     errdefer util.gpa.destroy(seat);
 
+    // XXX have actual reasoning for choosing this capacity.
+    var event_queue: Deque(Event) = try .initCapacity(util.gpa, 1024);
+    errdefer event_queue.deinit(util.gpa);
+
     seat.* = .{
         // This will be automatically destroyed when the display is destroyed
         .wlr_seat = try wlr.Seat.create(server.wl_server, name),
+        .event_queue = event_queue,
         .link = undefined,
         .link_sent = undefined,
         .xkb_bindings = undefined,
@@ -225,6 +228,7 @@ pub fn destroy(seat: *Seat) void {
     seat.link.remove();
     seat.link_sent.remove();
 
+    seat.event_queue.deinit(util.gpa);
     seat.cursor.deinit();
 
     seat.request_set_selection.link.remove();
@@ -237,7 +241,7 @@ pub fn destroy(seat: *Seat) void {
 pub fn queueEvent(seat: *Seat, event: Event) !void {
     seat.handleActivity();
 
-    seat.event_queue.writeItem(event) catch {
+    seat.event_queue.pushBackBounded(event) catch {
         log.err("dropping {s} event, no space in event queue", .{@tagName(event)});
         return error.QueueFull;
     };
@@ -256,7 +260,7 @@ pub fn processEvents(seat: *Seat) void {
     while (!server.wm.wm_scheduled.dirty) {
         assert(server.wm.state == .idle);
 
-        const event = seat.event_queue.readItem() orelse break;
+        const event = seat.event_queue.popFront() orelse break;
 
         const pg = server.input_manager.pointer_gestures;
         switch (event) {
@@ -573,14 +577,15 @@ fn keyboardNotifyEnter(seat: *Seat, wlr_surface: *wlr.Surface) void {
     if (seat.wlr_seat.getKeyboard()) |wlr_keyboard| {
         const group: *KeyboardGroup = @alignCast(@ptrCast(wlr_keyboard.data));
 
-        var keycodes: std.BoundedArray(u32, KeyboardGroup.Pressed.capacity) = .{};
-        for (group.pressed.keys.constSlice()) |item| {
-            if (item.consumer == .focus) keycodes.appendAssumeCapacity(item.code);
+        var buffer: [KeyboardGroup.pressed_count_max]u32 = undefined;
+        var keycodes: std.ArrayList(u32) = .initBuffer(&buffer);
+        for (group.pressed.keys(), group.pressed.values()) |keycode, press| {
+            if (press.consumer == .focus) keycodes.appendAssumeCapacity(keycode);
         }
 
         seat.wlr_seat.keyboardNotifyEnter(
             wlr_surface,
-            keycodes.constSlice(),
+            keycodes.items,
             &group.state.modifiers,
         );
     } else {
diff --git a/river/Server.zig b/river/Server.zig
index 4835695..97246e6 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -24,7 +24,7 @@ const posix = std.posix;
 const wlr = @import("wlroots");
 const wl = @import("wayland").server.wl;
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 const util = @import("util.zig");
 
 const Config = @import("Config.zig");
diff --git a/river/ShellSurface.zig b/river/ShellSurface.zig
index 6bd9fbd..3908156 100644
--- a/river/ShellSurface.zig
+++ b/river/ShellSurface.zig
@@ -135,7 +135,7 @@ fn handleRequest(
     }
 }
 
-fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
+fn clientCommit(wlr_surface: *wlr.Surface) callconv(.c) void {
     if (wlr_surface.role != &role) return;
     const resource = wlr_surface.role_resource orelse return;
 
@@ -146,7 +146,7 @@ fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
     }
 }
 
-fn commit(wlr_surface: *wlr.Surface) callconv(.C) void {
+fn commit(wlr_surface: *wlr.Surface) callconv(.c) void {
     if (wlr_surface.hasBuffer()) {
         wlr_surface.map();
     }
diff --git a/river/XkbBinding.zig b/river/XkbBinding.zig
index 3568cda..ed87c1e 100644
--- a/river/XkbBinding.zig
+++ b/river/XkbBinding.zig
@@ -100,9 +100,9 @@ fn handleDestroy(_: *river.XkbBindingV1, binding: *XkbBinding) void {
     {
         var it = binding.seat.keyboard_groups.iterator(.forward);
         while (it.next()) |group| {
-            for (group.pressed.keys.slice()) |*key| {
-                if (key.consumer == .binding and key.consumer.binding == binding) {
-                    key.consumer.binding = null;
+            for (group.pressed.values()) |*press| {
+                if (press.consumer == .binding and press.consumer.binding == binding) {
+                    press.consumer.binding = null;
                 }
             }
         }
diff --git a/river/c.zig b/river/c.zig
index 05b5c25..59dd8a0 100644
--- a/river/c.zig
+++ b/river/c.zig
@@ -14,7 +14,7 @@
 // You should have received a copy of the GNU General Public License
 // along with this program. If not, see <https://www.gnu.org/licenses/>.
 
-pub usingnamespace @cImport({
+pub const c = @cImport({
     @cDefine("_POSIX_C_SOURCE", "200809L");
 
     @cInclude("stdlib.h");
diff --git a/river/main.zig b/river/main.zig
index 2b689c2..17a68f5 100644
--- a/river/main.zig
+++ b/river/main.zig
@@ -25,7 +25,7 @@ const builtin = @import("builtin");
 const wlr = @import("wlroots");
 const flags = @import("flags");
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 const util = @import("util.zig");
 const process = @import("process.zig");
 
@@ -53,21 +53,25 @@ pub fn main() anyerror!void {
         .{ .name = "log-scopes", .kind = .arg },
         .{ .name = "no-xwayland", .kind = .boolean },
     }).parse(std.os.argv[1..]) catch {
-        try io.getStdErr().writeAll(usage);
+        try stderr.writeAll(usage);
+        try stderr.flush();
         posix.exit(1);
     };
     if (result.flags.h) {
-        try io.getStdOut().writeAll(usage);
+        try stdout.writeAll(usage);
+        try stdout.flush();
         posix.exit(0);
     }
     if (result.args.len != 0) {
         log.err("unknown option '{s}'", .{result.args[0]});
-        try io.getStdErr().writeAll(usage);
+        try stderr.writeAll(usage);
+        try stderr.flush();
         posix.exit(1);
     }
 
     if (result.flags.version) {
-        try io.getStdOut().writeAll(build_options.version ++ "\n");
+        try stdout.writeAll(build_options.version ++ "\n");
+        try stdout.flush();
         posix.exit(0);
     }
     if (result.flags.@"log-level") |level| {
@@ -81,7 +85,6 @@ pub fn main() anyerror!void {
             runtime_log_level = .debug;
         } else {
             log.err("invalid log level '{s}'", .{level});
-            try io.getStdErr().writeAll(usage);
             posix.exit(1);
         }
     }
@@ -186,6 +189,14 @@ fn defaultInitPath() !?[:0]const u8 {
     return path;
 }
 
+var stderr_buffer: [1024]u8 = undefined;
+var stderr_writer = fs.File.stderr().writer(&stderr_buffer);
+const stderr = &stderr_writer.interface;
+
+var stdout_buffer: [1024]u8 = undefined;
+var stdout_writer = fs.File.stdout().writer(&stdout_buffer);
+const stdout = &stdout_writer.interface;
+
 // Scopes should be added to this list sparingly.
 // Only add new scopes if filtering based on them would be meaningful.
 const LogScope = enum {
@@ -225,8 +236,8 @@ pub fn logFn(
 
     const scope_prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
 
-    const stderr = io.getStdErr().writer();
-    stderr.print(level.asText() ++ scope_prefix ++ format ++ "\n", args) catch {};
+    stderr.print(level.asText() ++ scope_prefix ++ format ++ "\n", args) catch return;
+    stderr.flush() catch return;
 }
 
 /// See wlroots_log_wrapper.c
diff --git a/river/process.zig b/river/process.zig
index fb160e9..7709a7d 100644
--- a/river/process.zig
+++ b/river/process.zig
@@ -17,7 +17,7 @@
 const std = @import("std");
 const posix = std.posix;
 
-const c = @import("c.zig");
+const c = @import("c.zig").c;
 
 var original_rlimit: ?posix.rlimit = null;
 
@@ -26,7 +26,7 @@ pub fn setup() void {
     // has had its read end closed by another process.
     const sig_ign = posix.Sigaction{
         .handler = .{ .handler = posix.SIG.IGN },
-        .mask = posix.empty_sigset,
+        .mask = posix.sigemptyset(),
         .flags = 0,
     };
     posix.sigaction(posix.SIG.PIPE, &sig_ign, null);
@@ -61,11 +61,11 @@ pub fn setup() void {
 
 pub fn cleanupChild() void {
     if (c.setsid() < 0) unreachable;
-    if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.empty_sigset, null) < 0) unreachable;
+    if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.sigemptyset(), null) < 0) unreachable;
 
     const sig_dfl = posix.Sigaction{
         .handler = .{ .handler = posix.SIG.DFL },
-        .mask = posix.empty_sigset,
+        .mask = posix.sigemptyset(),
         .flags = 0,
     };
     posix.sigaction(posix.SIG.PIPE, &sig_dfl, null);