Commit Diff


commit - 0d587e8bfd989d8c425430d9832ce55186f26831
commit + 9ca3db0820a063e58fc45a2bcbdffa061ea311ec
blob - ae040fcb2af5620698ec753e51a7106c713b9fad
blob + 3b72907378186aea4b7cef3e8272441751a3001f
--- .builds/alpine.yml
+++ .builds/alpine.yml
@@ -37,10 +37,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.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
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
+      tar xf zig-x86_64-linux-0.16.0.tar.xz
+      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
   - build: |
       cd river-classic
       zig build --summary all
blob - be19c56893a7a4681624b3c9e0559b71f1460b5e
blob + 28bc349e3041befb38f09e19f10bb5999d0366d0
--- .builds/archlinux.yml
+++ .builds/archlinux.yml
@@ -34,16 +34,18 @@ 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.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
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
+      tar xf zig-x86_64-linux-0.16.0.tar.xz
+      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
   - build: |
       cd river-classic
-      zig build --summary all
+      # Arch's glibc version has SFrame sections in crt objects, force llvm/lld usage as a workaround
+      # https://codeberg.org/ziglang/zig/issues/31272
+      zig build -Dllvm --summary all
   - build_xwayland: |
       cd river-classic
-      zig build --summary all -Dxwayland
+      zig build -Dllvm --summary all -Dxwayland
   - fmt: |
       cd river-classic
       zig fmt --check river/
blob - 319157185845a3fb650c1d681f6ee74f1e3a6d02
blob + 220978579418c077b13a7e3f74f1adc6b528d53f
--- .builds/freebsd.yml
+++ .builds/freebsd.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.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
+      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-freebsd-0.16.0.tar.xz
+      tar xf zig-x86_64-freebsd-0.16.0.tar.xz
+      sudo mv zig-x86_64-freebsd-0.16.0/zig /usr/bin/
+      sudo mv zig-x86_64-freebsd-0.16.0/lib /usr/lib/zig
   - build: |
       cd river-classic
       zig build --summary all
blob - 3389c86c99467f36e872462555659b4a6bcb399a
blob + 03cb27d540eacf456abb8d4e94196d3456ea6ea2
--- .gitignore
+++ .gitignore
@@ -1,2 +1,3 @@
 .zig-cache/
 zig-out/
+zig-pkg/
blob - 0f84f01e6712159f4dcb605775b5ebc1e1e831aa
blob + 90e06fcb683f84198268e36cc2bfa5dd21612749
--- README.md
+++ README.md
@@ -50,7 +50,7 @@ To compile river-classic first ensure that you have th
 installed. The "development" versions are required if applicable to your
 distribution.
 
-- [zig](https://ziglang.org/download/) 0.15
+- [zig](https://ziglang.org/download/) 0.16
 - wayland
 - wayland-protocols
 - [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.20
blob - 81128dc1e29081f8fc72c6aeb477aa151054cd71
blob + 0ba03fb1ec9c74142ad64a7eff226c71bc80160b
--- build.zig
+++ build.zig
@@ -4,6 +4,9 @@ const Build = std.Build;
 const fs = std.fs;
 const mem = std.mem;
 
+const manifest = @import("build.zig.zon");
+const version = manifest.version;
+
 const Scanner = @import("wayland").Scanner;
 
 pub fn build(b: *Build) !void {
@@ -12,6 +15,7 @@ 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 use_llvm = b.option(bool, "llvm", "Force use of Zig's LLVM backend and the lld linker") orelse false;
 
     const omit_frame_pointer = switch (optimize) {
         .Debug, .ReleaseSafe => false,
@@ -25,7 +29,6 @@ pub fn build(b: *Build) !void {
     ) orelse scdoc_found: {
         _ = b.findProgram(&.{"scdoc"}, &.{}) catch |err| switch (err) {
             error.FileNotFound => break :scdoc_found false,
-            else => return err,
         };
         break :scdoc_found true;
     };
@@ -61,7 +64,7 @@ pub fn build(b: *Build) !void {
             const git_describe_long = b.runAllowFail(
                 &.{ "git", "-C", b.build_root.path orelse ".", "describe", "--long" },
                 &ret,
-                .Inherit,
+                .inherit,
             ) catch break :blk version;
 
             var it = mem.splitScalar(u8, mem.trim(u8, git_describe_long, &std.ascii.whitespace), '-');
@@ -162,17 +165,19 @@ pub fn build(b: *Build) !void {
                 .target = target,
                 .optimize = optimize,
                 .strip = strip,
+                .link_libc = true,
             }),
+            .use_llvm = use_llvm,
+            .use_lld = use_llvm,
         });
         river.root_module.addOptions("build_options", options);
 
-        river.linkLibC();
-        river.linkSystemLibrary("libevdev");
-        river.linkSystemLibrary("libinput");
-        river.linkSystemLibrary("wayland-server");
-        river.linkSystemLibrary("wlroots-0.20");
-        river.linkSystemLibrary("xkbcommon");
-        river.linkSystemLibrary("pixman-1");
+        river.root_module.linkSystemLibrary("libevdev", .{});
+        river.root_module.linkSystemLibrary("libinput", .{});
+        river.root_module.linkSystemLibrary("wayland-server", .{});
+        river.root_module.linkSystemLibrary("wlroots-0.20", .{});
+        river.root_module.linkSystemLibrary("xkbcommon", .{});
+        river.root_module.linkSystemLibrary("pixman-1", .{});
 
         river.root_module.addImport("wayland", wayland);
         river.root_module.addImport("xkbcommon", xkbcommon);
@@ -181,7 +186,7 @@ pub fn build(b: *Build) !void {
         river.root_module.addImport("flags", flags);
         river.root_module.addImport("globber", globber);
 
-        river.addCSourceFile(.{
+        river.root_module.addCSourceFile(.{
             .file = b.path("river/wlroots_log_wrapper.c"),
             .flags = &.{ "-std=c99", "-O2" },
         });
@@ -200,14 +205,16 @@ pub fn build(b: *Build) !void {
                 .target = target,
                 .optimize = optimize,
                 .strip = strip,
+                .link_libc = true,
             }),
+            .use_llvm = use_llvm,
+            .use_lld = use_llvm,
         });
         riverctl.root_module.addOptions("build_options", options);
 
         riverctl.root_module.addImport("flags", flags);
         riverctl.root_module.addImport("wayland", wayland);
-        riverctl.linkLibC();
-        riverctl.linkSystemLibrary("wayland-client");
+        riverctl.root_module.linkSystemLibrary("wayland-client", .{});
 
         riverctl.pie = pie;
         riverctl.root_module.omit_frame_pointer = omit_frame_pointer;
@@ -223,14 +230,16 @@ pub fn build(b: *Build) !void {
                 .target = target,
                 .optimize = optimize,
                 .strip = strip,
+                .link_libc = true,
             }),
+            .use_llvm = use_llvm,
+            .use_lld = use_llvm,
         });
         rivertile.root_module.addOptions("build_options", options);
 
         rivertile.root_module.addImport("flags", flags);
         rivertile.root_module.addImport("wayland", wayland);
-        rivertile.linkLibC();
-        rivertile.linkSystemLibrary("wayland-client");
+        rivertile.root_module.linkSystemLibrary("wayland-client", .{});
 
         rivertile.pie = pie;
         rivertile.root_module.omit_frame_pointer = omit_frame_pointer;
@@ -263,7 +272,7 @@ pub fn build(b: *Build) !void {
             // This makes the caching work for the Workaround, and the extra argument is ignored by /bin/sh.
             scdoc.addFileArg(b.path("doc/" ++ page ++ ".1.scd"));
 
-            const stdout = scdoc.captureStdOut();
+            const stdout = scdoc.captureStdOut(.{});
             b.getInstallStep().dependOn(&b.addInstallFile(stdout, "share/man/man1/" ++ page ++ ".1").step);
         }
     }
@@ -294,31 +303,3 @@ pub fn build(b: *Build) !void {
         test_step.dependOn(&run_globber_test.step);
     }
 }
-
-const version = manifest.version;
-/// Getting rid of this wart requires upstream zig improvements.
-/// See: https://github.com/ziglang/zig/issues/22775
-const manifest: struct {
-    name: @Type(.enum_literal),
-    version: []const u8,
-    paths: []const []const u8,
-    dependencies: struct {
-        pixman: struct {
-            url: []const u8,
-            hash: []const u8,
-        },
-        wayland: struct {
-            url: []const u8,
-            hash: []const u8,
-        },
-        wlroots: struct {
-            url: []const u8,
-            hash: []const u8,
-        },
-        xkbcommon: struct {
-            url: []const u8,
-            hash: []const u8,
-        },
-    },
-    fingerprint: u64,
-} = @import("build.zig.zon");
blob - ba347f5cfa48e9a57ded29b50e31dd230ec7ef4f
blob + 6c5f5ece7ff92614a359343967c7006f19bcd07d
--- build.zig.zon
+++ build.zig.zon
@@ -13,12 +13,12 @@
             .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
         },
         .wayland = .{
-            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz",
-            .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27",
+            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz",
+            .hash = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX",
         },
         .wlroots = .{
-            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.20.0.tar.gz",
-            .hash = "wlroots-0.20.0-jmOlcmtCBADS6eoJ6mkeiSNZkibrhD-c5Qwn-LiM86r1",
+            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.20.1.tar.gz",
+            .hash = "wlroots-0.20.1-jmOlcqNVBAB3uB5oqBTzpRlwu-FmMyyZMVAWCe5kmcSt",
         },
         .xkbcommon = .{
             .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz",
blob - 79adde984759ab0b66ef8430baea4b164987fca5
blob + 72f197fba7b43369df6355d671948c9b071d40d9
--- common/flags.zig
+++ common/flags.zig
@@ -1,82 +1,51 @@
-// Zero allocation argument parsing for unix-like systems.
-// Released under the Zero Clause BSD (0BSD) license:
-//
-// Copyright 2023 Isaac Freund
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+// SPDX-FileCopyrightText: © 2023 Isaac Freund
+// SPDX-License-Identifier: 0BSD
 
 const std = @import("std");
 const mem = std.mem;
 
 pub const Flag = struct {
-    name: [:0]const u8,
+    name: []const u8,
     kind: enum { boolean, arg },
 };
 
-pub fn parser(comptime Arg: type, comptime flags: []const Flag) type {
-    switch (Arg) {
-        // TODO consider allowing []const u8
-        [:0]const u8, [*:0]const u8 => {}, // ok
-        else => @compileError("invalid argument type: " ++ @typeName(Arg)),
-    }
+pub fn parser(comptime flags: []const Flag) type {
     return struct {
         pub const Result = struct {
             /// Remaining args after the recognized flags
-            args: []const Arg,
+            args: []const [:0]const u8,
             /// Data obtained from parsed flags
             flags: Flags,
 
             pub const Flags = flags_type: {
-                var fields: []const std.builtin.Type.StructField = &.{};
-                for (flags) |flag| {
-                    const field: std.builtin.Type.StructField = switch (flag.kind) {
-                        .boolean => .{
-                            .name = flag.name,
-                            .type = bool,
-                            .default_value_ptr = &false,
-                            .is_comptime = false,
-                            .alignment = @alignOf(bool),
+                const Attributes = std.builtin.Type.StructField.Attributes;
+                var names: [flags.len][]const u8 = undefined;
+                var types: [flags.len]type = undefined;
+                var attrs: [flags.len]Attributes = undefined;
+                for (flags, &names, &types, &attrs) |flag, *name, *ty, *attr| {
+                    name.* = flag.name;
+                    switch (flag.kind) {
+                        .boolean => {
+                            ty.* = bool;
+                            attr.* = .{ .default_value_ptr = &false };
                         },
-                        .arg => .{
-                            .name = flag.name,
-                            .type = ?[:0]const u8,
-                            .default_value_ptr = &@as(?[:0]const u8, null),
-                            .is_comptime = false,
-                            .alignment = @alignOf(?[:0]const u8),
+                        .arg => {
+                            ty.* = ?[:0]const u8;
+                            attr.* = .{ .default_value_ptr = &@as(ty.*, null) };
                         },
-                    };
-                    fields = fields ++ [_]std.builtin.Type.StructField{field};
+                    }
                 }
-                break :flags_type @Type(.{ .@"struct" = .{
-                    .layout = .auto,
-                    .fields = fields,
-                    .decls = &.{},
-                    .is_tuple = false,
-                } });
+                break :flags_type @Struct(.auto, null, &names, &types, &attrs);
             };
         };
 
-        pub fn parse(args: []const Arg) !Result {
+        pub fn parse(args: []const [:0]const u8) error{MissingFlagArgument}!Result {
             var result_flags: Result.Flags = .{};
 
             var i: usize = 0;
             outer: while (i < args.len) : (i += 1) {
-                const arg = switch (Arg) {
-                    [*:0]const u8 => mem.sliceTo(args[i], 0),
-                    [:0]const u8 => args[i],
-                    else => unreachable,
-                };
                 inline for (flags) |flag| {
-                    if (mem.eql(u8, "-" ++ flag.name, arg)) {
+                    if (mem.eql(u8, "-" ++ flag.name, args[i])) {
                         switch (flag.kind) {
                             .boolean => @field(result_flags, flag.name) = true,
                             .arg => {
@@ -86,11 +55,7 @@ pub fn parser(comptime Arg: type, comptime flags: []co
                                         "' requires an argument but none was provided!", .{});
                                     return error.MissingFlagArgument;
                                 }
-                                @field(result_flags, flag.name) = switch (Arg) {
-                                    [*:0]const u8 => mem.sliceTo(args[i], 0),
-                                    [:0]const u8 => args[i],
-                                    else => unreachable,
-                                };
+                                @field(result_flags, flag.name) = args[i];
                             },
                         }
                         continue :outer;
blob - 06e30d8291349bf132dce025603df0e674102174
blob + f55ea14f8c29d082be69fe50325d1f45761c2e76
--- river/Control.zig
+++ river/Control.zig
@@ -59,7 +59,7 @@ fn bind(client: *wl.Client, control: *Control, version
         client.postNoMemory();
         return;
     };
-    control.args_map.putNoClobber(.{ .client = client, .id = id }, .{}) catch {
+    control.args_map.putNoClobber(.{ .client = client, .id = id }, .empty) catch {
         control_v1.destroy();
         client.postNoMemory();
         return;
blob - 0d5c47246b1963a09b28e8e08ac6a81f7319f817
blob + feb19d2b2f7fcd9fc1fd1ed4bf781a640b474dd1
--- river/Cursor.zig
+++ river/Cursor.zig
@@ -158,7 +158,7 @@ focus_follows_cursor_target: ?*View = null,
 
 /// Keeps track of the last known location of all touch points in layout coordinates.
 /// This information is necessary for proper touch dnd support if there are multiple touch points.
-touch_points: std.AutoHashMapUnmanaged(i32, LayoutPoint) = .{},
+touch_points: std.AutoHashMapUnmanaged(i32, LayoutPoint) = .empty,
 
 axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(handleAxis),
 frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(handleFrame),
@@ -1151,15 +1151,7 @@ pub fn updateState(cursor: *Cursor) void {
         .passthrough => {
             cursor.updateFocusFollowsCursorTarget();
             if (!cursor.hidden) {
-                const now = posix.clock_gettime(.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
-                // 2^32-1 milliseconds is ~50 days, which is a realistic uptime.
-                // This means that we must wrap if the monotonic time is greater than
-                // 2^32-1 milliseconds and hope that clients don't get too confused.
-                const msec: u32 = @intCast(@rem(
-                    now.sec *% std.time.ms_per_s +% @divTrunc(now.nsec, std.time.ns_per_ms),
-                    math.maxInt(u32),
-                ));
-                cursor.passthrough(msec);
+                cursor.passthrough(util.msecTimestamp());
             }
         },
         // TODO: Leave down mode if the target surface is no longer visible.
blob - 96df75296813758781f771c062383f0dd082a191
blob + b5232766665dd99c3c883256953c1fa258da24d7
--- river/InputManager.zig
+++ 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),
blob - d0f70d2e4489650491157466d44d9ae88c34eaed
blob + 1cd4f98f8700edf79a2ed2afa6bf4f82b024fa86
--- river/Mode.zig
+++ river/Mode.zig
@@ -24,9 +24,9 @@ const PointerMapping = @import("PointerMapping.zig");
 const SwitchMapping = @import("SwitchMapping.zig");
 
 name: [:0]const u8,
-mappings: std.ArrayListUnmanaged(Mapping) = .{},
-pointer_mappings: std.ArrayListUnmanaged(PointerMapping) = .{},
-switch_mappings: std.ArrayListUnmanaged(SwitchMapping) = .{},
+mappings: std.ArrayList(Mapping) = .empty,
+pointer_mappings: std.ArrayList(PointerMapping) = .empty,
+switch_mappings: std.ArrayList(SwitchMapping) = .empty,
 
 pub fn deinit(mode: *Mode) void {
     util.gpa.free(mode.name);
blob - 7a8228c9ed651c4ec2dd447134eca1905409e6ea
blob + 5b20a4104891e8b6e16b16ec97c807a98004055b
--- river/Output.zig
+++ river/Output.zig
@@ -537,7 +537,7 @@ fn handleFrame(listener: *wl.Listener(*wlr.Output), _:
         error.CommitFailed => log.err("output commit failed for {s}", .{output.wlr_output.name}),
     };
 
-    var now = posix.clock_gettime(.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
+    var now = util.timestamp();
     scene_output.sendFrameDone(&now);
 }
 
blob - 7e05cf8750e58e9b74ddd33c05aa091b8f732eb9
blob + a6a41573961f71590d7f226e753d514d514b6893
--- river/OutputStatus.zig
+++ river/OutputStatus.zig
@@ -31,7 +31,7 @@ const View = @import("View.zig");
 const log = std.log.scoped(.river_status);
 
 resources: wl.list.Head(zriver.OutputStatusV1, null),
-view_tags: std.ArrayListUnmanaged(u32) = .{},
+view_tags: std.ArrayList(u32) = .empty,
 focused_tags: u32 = 0,
 urgent_tags: u32 = 0,
 
blob - ae7e298cdeac2c5e09ae5c836c990eb78286cb68
blob + 20ab1f7a34e8b22a6b2ac5945a06fc1444fc3607
--- river/Seat.zig
+++ river/Seat.zig
@@ -471,10 +471,7 @@ pub fn runCommand(seat: *Seat, args: []const [:0]const
         return;
     };
     if (out) |s| {
-        var stdout = std.fs.File.stdout().writer(&.{});
-        stdout.interface.print("{s}", .{s}) catch |err| {
-            std.log.scoped(.command).err("{s}: write to stdout failed {}", .{ args[0], err });
-        };
+        std.log.scoped(.command).info("mapped command output: {s}", .{s});
     }
 }
 
blob - c7b566323698f5ea60c0343b1de05ad3ef6ac9ba
blob + dc277c538fa6659f438712612f53a3ce1013c454
--- river/Server.zig
+++ river/Server.zig
@@ -148,8 +148,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !
 
     server.* = .{
         .wl_server = wl_server,
-        .sigint_source = try loop.addSignal(*wl.Server, posix.SIG.INT, terminate, wl_server),
-        .sigterm_source = try loop.addSignal(*wl.Server, posix.SIG.TERM, terminate, wl_server),
+        .sigint_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.INT), terminate, wl_server),
+        .sigterm_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.TERM), terminate, wl_server),
 
         .fixes = try wlr.Fixes.create(wl_server, 1),
 
blob - 8cba7d0e9ab5371a213a477a38d87eb33754ae7b
blob + 5e4bf7410b50bfaccd1dfa3cdf1e49029afaa645
--- river/View.zig
+++ river/View.zig
@@ -498,7 +498,7 @@ pub fn rootSurface(view: View) ?*wlr.Surface {
 pub fn sendFrameDone(view: View) void {
     assert(view.mapped and !view.destroying);
 
-    const now = posix.clock_gettime(.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
+    const now = util.timestamp();
     view.rootSurface().?.sendFrameDone(&now);
 }
 
blob - 59dd8a004a0a4f5a67c2f955455094f50f1a6e34
blob + 9ef5b9796ba04f9e7610714da98d1b34c8fc8736
--- river/c.zig
+++ river/c.zig
@@ -15,11 +15,6 @@
 // along with this program. If not, see <https://www.gnu.org/licenses/>.
 
 pub const c = @cImport({
-    @cDefine("_POSIX_C_SOURCE", "200809L");
-
-    @cInclude("stdlib.h");
-    @cInclude("unistd.h");
-
     @cInclude("linux/input-event-codes.h");
     @cInclude("libevdev/libevdev.h");
 
blob - c09db1fa660ebf98cdf47dcc86c50d2082eb3180
blob + 52e0a16616649c7dedb6ed5c320712060ac81491
--- river/command/keyboard.zig
+++ river/command/keyboard.zig
@@ -31,7 +31,7 @@ pub fn keyboardLayout(
     args: []const [:0]const u8,
     _: *?[]const u8,
 ) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "rules", .kind = .arg },
         .{ .name = "model", .kind = .arg },
         .{ .name = "variant", .kind = .arg },
@@ -69,13 +69,15 @@ pub fn keyboardLayoutFile(
     if (args.len < 2) return Error.NotEnoughArguments;
     if (args.len > 2) return Error.TooManyArguments;
 
-    const file = std.fs.cwd().openFile(args[1], .{}) catch return error.CannotReadFile;
-    defer file.close();
+    const io = std.Io.Threaded.global_single_threaded.io();
+    const file = std.Io.Dir.cwd().openFile(io, args[1], .{}) catch return error.CannotReadFile;
+    defer file.close(io);
+    var reader = file.reader(io, &.{});
 
     // 1 GiB is arbitrarily chosen as an exceedingly large but not infinite upper bound.
-    const file_bytes = file.readToEndAlloc(util.gpa, 1024 * 1024 * 1024) catch |err| {
+    const file_bytes = reader.interface.allocRemaining(util.gpa, .limited(1024 * 1024 * 1024)) catch |err| {
         switch (err) {
-            error.FileTooBig, error.OutOfMemory => return error.OutOfMemory,
+            error.OutOfMemory => return error.OutOfMemory,
             else => return error.CannotReadFile,
         }
     };
blob - 2778fa64a4787f9d1188cddb6f854ec35d96aae4
blob + cd966f4b984ee1e8a53308b9c8663e7fdf5cba5a
--- river/command/map.zig
+++ river/command/map.zig
@@ -42,7 +42,7 @@ pub fn map(
     args: []const [:0]const u8,
     out: *?[]const u8,
 ) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "release", .kind = .boolean },
         .{ .name = "repeat", .kind = .boolean },
         .{ .name = "layout", .kind = .arg },
@@ -366,7 +366,7 @@ fn parseSwitchState(
 /// Example:
 /// unmap normal Mod4+Shift Return
 pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "release", .kind = .boolean },
     }).parse(args[1..]) catch {
         return error.InvalidValue;
blob - 6dccf8002c894899281acc9db34444b1efa21c47
blob + d9bf1b39a0d02de1cc3fa61aab875a7ba9557ac8
--- river/command/output.zig
+++ river/command/output.zig
@@ -54,10 +54,10 @@ pub fn sendToOutput(
     _: *?[]const u8,
 ) Error!void {
     if (args.len < 2) return Error.NotEnoughArguments;
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "current-tags", .kind = .boolean },
     }).parse(args[1..]) catch {
-        return error.InvalidOption;
+        return error.InvalidValue;
     };
     if (result.args.len < 1) return Error.NotEnoughArguments;
     if (result.args.len > 1) return Error.TooManyArguments;
blob - 082783a2bb4e178dfaf467eb6380d6a2a3f76b6e
blob + ff6b0a47d5b0c15e2a590ac05e898ebd6910a14e
--- river/command/rule.zig
+++ river/command/rule.zig
@@ -45,7 +45,7 @@ const Action = enum {
 };
 
 pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "app-id", .kind = .arg },
         .{ .name = "title", .kind = .arg },
     }).parse(args[1..]) catch {
@@ -147,7 +147,7 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _
 }
 
 pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "app-id", .kind = .arg },
         .{ .name = "title", .kind = .arg },
     }).parse(args[1..]) catch {
@@ -218,7 +218,7 @@ fn apply_tearing_rules() void {
     }
 }
 
-fn alignLeft(buf: []const u8, width: usize, writer: *std.io.Writer) Error!void {
+fn alignLeft(buf: []const u8, width: usize, writer: *std.Io.Writer) Error!void {
     assert(buf.len <= width);
     try writer.writeAll(buf);
     try writer.splatByteAll(' ', width - buf.len);
@@ -244,7 +244,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8,
     const app_id_column_max = 2 + @max("app-id".len, max_glob_len.app_id);
     const title_column_max = 2 + @max("title".len, max_glob_len.title);
 
-    var buffer = std.io.Writer.Allocating.init(util.gpa);
+    var buffer = std.Io.Writer.Allocating.init(util.gpa);
     defer buffer.deinit();
     const writer = &buffer.writer;
 
blob - 0462abea2f09bb5069b0398f5b32c0c1b34fdf8b
blob + ba63731a1a7b67c29ba6c8276baaa22df902962d
--- river/command/spawn.zig
+++ river/command/spawn.zig
@@ -15,9 +15,8 @@
 // along with this program. If not, see <https://www.gnu.org/licenses/>.
 
 const std = @import("std");
-const posix = std.posix;
+const c = std.c;
 
-const c = @import("../c.zig").c;
 const util = @import("../util.zig");
 const process = @import("../process.zig");
 
@@ -35,26 +34,46 @@ pub fn spawn(
 
     const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", args[1], null };
 
-    const pid = posix.fork() catch {
-        out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
-        return Error.Other;
+    const pid: c.pid_t = blk: {
+        const rc = c.fork();
+        if (c.errno(rc) != .SUCCESS) {
+            out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
+            return Error.Other;
+        }
+        break :blk @intCast(rc);
     };
 
     if (pid == 0) {
         process.cleanupChild();
 
-        const pid2 = posix.fork() catch c._exit(1);
+        const pid2: c.pid_t = blk: {
+            const rc = c.fork();
+            if (c.errno(rc) != .SUCCESS) {
+                c._exit(1);
+            }
+            break :blk @intCast(rc);
+        };
+
         if (pid2 == 0) {
-            posix.execveZ("/bin/sh", &child_args, std.c.environ) catch c._exit(1);
+            _ = c.execve("/bin/sh", &child_args, c.environ);
+            c._exit(1); // only reachable if execve fails
         }
 
         c._exit(0);
     }
 
     // Wait the intermediate child.
-    const ret = posix.waitpid(pid, 0);
-    if (!posix.W.IFEXITED(ret.status) or
-        (posix.W.IFEXITED(ret.status) and posix.W.EXITSTATUS(ret.status) != 0))
+    const status: u32 = while (true) {
+        var status: c_int = 0;
+        switch (c.errno(c.waitpid(pid, &status, 0))) {
+            .SUCCESS => break @bitCast(status),
+            .INTR => continue,
+            else => return Error.Unexpected, // should never happen, but don't trust the kernel
+        }
+    };
+
+    if (!c.W.IFEXITED(status) or
+        (c.W.IFEXITED(status) and c.W.EXITSTATUS(status) != 0))
     {
         out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
         return Error.Other;
blob - 3f8a2143bf2fcdd4d78fe66c80654f294aaef50a
blob + eb8020b6e28355bcc84729cdca534525aebdaeb5
--- river/command/view_operations.zig
+++ river/command/view_operations.zig
@@ -35,7 +35,7 @@ pub fn focusView(
     args: []const [:0]const u8,
     _: *?[]const u8,
 ) Error!void {
-    const result = flags.parser([:0]const u8, &.{
+    const result = flags.parser(&.{
         .{ .name = "skip-floating", .kind = .boolean },
     }).parse(args[1..]) catch {
         return error.InvalidValue;
blob - c21c0b312f3bcebc931647bc9b0a88b4e25a5084
blob + ad4b194ffff8fdf80a1167d80abbd7bf39aa8eef
--- river/command.zig
+++ river/command.zig
@@ -124,6 +124,7 @@ pub const Error = error{
     ConflictingOptions,
     WriteFailed,
     OutOfMemory,
+    Unexpected,
     Other,
 };
 
@@ -167,6 +168,7 @@ pub fn errToMsg(err: Error) [:0]const u8 {
         Error.CannotReadFile => "cannot read file",
         Error.CannotParseFile => "cannot parse file",
         Error.WriteFailed, Error.OutOfMemory => "out of memory",
+        Error.Unexpected => "unexpected error",
         Error.Other => unreachable,
     };
 }
blob - d2f948155f02aee12a289875533c1b2a0623c696
blob + 3346b50bdd7554240b9d5aa1f2565f86b703b945
--- river/main.zig
+++ river/main.zig
@@ -16,15 +16,19 @@
 
 const build_options = @import("build_options");
 const std = @import("std");
-const mem = std.mem;
+const Io = std.Io;
 const fs = std.fs;
 const log = std.log;
+const mem = std.mem;
 const posix = std.posix;
+const exit = std.process.exit;
+const fatal = std.process.fatal;
+const c = std.c;
+
 const builtin = @import("builtin");
 const wlr = @import("wlroots");
 const flags = @import("flags");
 
-const c = @import("c.zig").c;
 const util = @import("util.zig");
 const process = @import("process.zig");
 
@@ -43,30 +47,43 @@ const usage: []const u8 =
 
 pub var server: Server = undefined;
 
-pub fn main() anyerror!void {
-    const result = flags.parser([*:0]const u8, &.{
+pub fn main(init: std.process.Init.Minimal) anyerror!void {
+    const io = std.Io.Threaded.global_single_threaded.io();
+
+    var stdout_buffer: [64]u8 = undefined;
+    var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
+    const stdout = &stdout_writer.interface;
+
+    var stderr_buffer: [64]u8 = undefined;
+    var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer);
+    const stderr = &stderr_writer.interface;
+
+    const args = try init.args.toSlice(util.gpa);
+    defer util.gpa.free(args);
+
+    const result = flags.parser(&.{
         .{ .name = "h", .kind = .boolean },
         .{ .name = "version", .kind = .boolean },
         .{ .name = "c", .kind = .arg },
         .{ .name = "log-level", .kind = .arg },
         .{ .name = "no-xwayland", .kind = .boolean },
-    }).parse(std.os.argv[1..]) catch {
-        try fs.File.stderr().writeAll(usage);
-        posix.exit(1);
+    }).parse(args[1..]) catch {
+        try stderr.writeAll(usage);
+        exit(1);
     };
     if (result.flags.h) {
-        try fs.File.stdout().writeAll(usage);
-        posix.exit(0);
+        try stdout.writeAll(usage);
+        exit(0);
     }
     if (result.args.len != 0) {
         log.err("unknown option '{s}'", .{result.args[0]});
-        try fs.File.stderr().writeAll(usage);
-        posix.exit(1);
+        try stderr.writeAll(usage);
+        exit(1);
     }
 
     if (result.flags.version) {
-        try fs.File.stdout().writeAll(build_options.version ++ "\n");
-        posix.exit(0);
+        try stdout.writeAll(build_options.version ++ "\n");
+        exit(0);
     }
     if (result.flags.@"log-level") |level| {
         if (mem.eql(u8, level, "error")) {
@@ -79,8 +96,8 @@ pub fn main() anyerror!void {
             runtime_log_level = .debug;
         } else {
             log.err("invalid log level '{s}'", .{level});
-            try fs.File.stderr().writeAll(usage);
-            posix.exit(1);
+            try stderr.writeAll(usage);
+            exit(1);
         }
     }
     const runtime_xwayland = !result.flags.@"no-xwayland";
@@ -88,7 +105,7 @@ pub fn main() anyerror!void {
         if (result.flags.c) |command| {
             break :blk try util.gpa.dupeZ(u8, command);
         } else {
-            break :blk try defaultInitPath();
+            break :blk try defaultInitPath(io, init.environ);
         }
     };
 
@@ -120,10 +137,20 @@ pub fn main() anyerror!void {
     const child_pgid = if (startup_command) |cmd| blk: {
         log.info("running init executable '{s}'", .{cmd});
         const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd, null };
-        const pid = try posix.fork();
+
+        const pid: c.pid_t = pid: {
+            const rc = c.fork();
+            switch (c.errno(rc)) {
+                .SUCCESS => {},
+                else => |err| fatal("failed to start init process: {}", .{err}),
+            }
+            break :pid @intCast(rc);
+        };
+
         if (pid == 0) {
             process.cleanupChild();
-            posix.execveZ("/bin/sh", &child_args, std.c.environ) catch c._exit(1);
+            _ = c.execve("/bin/sh", &child_args, c.environ);
+            c._exit(1); // only reachable if execve fails
         }
         util.gpa.free(cmd);
         // Since the child has called setsid, the pid is the pgid
@@ -140,22 +167,21 @@ pub fn main() anyerror!void {
     log.info("shutting down", .{});
 }
 
-fn defaultInitPath() !?[:0]const u8 {
+fn defaultInitPath(io: Io, environ: std.process.Environ) !?[:0]const u8 {
     const path = blk: {
-        if (posix.getenv("XDG_CONFIG_HOME")) |xdg_config_home| {
+        if (environ.getPosix("XDG_CONFIG_HOME")) |xdg_config_home| {
             break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ xdg_config_home, "river/init" });
-        } else if (posix.getenv("HOME")) |home| {
+        } else if (environ.getPosix("HOME")) |home| {
             break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ home, ".config/river/init" });
         } else {
             return null;
         }
     };
 
-    posix.accessZ(path, posix.X_OK) catch |err| {
+    Io.Dir.cwd().access(io, path, .{ .execute = true }) catch |err| {
         if (err == error.PermissionDenied) {
-            if (posix.accessZ(path, posix.R_OK)) {
-                log.err("failed to run init executable {s}: the file is not executable", .{path});
-                posix.exit(1);
+            if (Io.Dir.cwd().access(io, path, .{})) {
+                fatal("failed to run init executable {s}: the file is not executable", .{path});
             } else |_| {}
         }
         log.err("failed to run init executable {s}: {s}", .{ path, @errorName(err) });
@@ -186,13 +212,7 @@ pub fn logFn(
 ) void {
     if (@intFromEnum(level) > @intFromEnum(runtime_log_level)) return;
 
-    const scope_prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
-
-    var buffer: [256]u8 = undefined;
-    const stderr = std.debug.lockStderrWriter(&buffer);
-    defer std.debug.unlockStderrWriter();
-
-    stderr.print(level.asText() ++ scope_prefix ++ format ++ "\n", args) catch {};
+    std.log.defaultLog(level, scope, format, args);
 }
 
 /// See wlroots_log_wrapper.c
blob - 7709a7dc73180d5b77d54e7946037c1e8f489cbb
blob + 2189f6aecf3c3415b57734742d36c3eb5aa81108
--- river/process.zig
+++ river/process.zig
@@ -17,8 +17,6 @@
 const std = @import("std");
 const posix = std.posix;
 
-const c = @import("c.zig").c;
-
 var original_rlimit: ?posix.rlimit = null;
 
 pub fn setup() void {
@@ -60,7 +58,7 @@ pub fn setup() void {
 }
 
 pub fn cleanupChild() void {
-    if (c.setsid() < 0) unreachable;
+    if (std.c.setsid() < 0) unreachable;
     if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.sigemptyset(), null) < 0) unreachable;
 
     const sig_dfl = posix.Sigaction{
blob - 8c5edd3079717725851dfd07f0ddcb543c1e512a
blob + f4b133ffab3b449ca9e68aae85c61cdf9d69e692
--- river/rule_list.zig
+++ river/rule_list.zig
@@ -45,7 +45,7 @@ pub fn RuleList(comptime T: type) type {
 
         /// Ordered from most specific to most general.
         /// Ordered first by app-id generality then by title generality.
-        rules: std.ArrayListUnmanaged(Rule) = .{},
+        rules: std.ArrayList(Rule) = .empty,
 
         pub fn deinit(list: *List) void {
             for (list.rules.items) |rule| {
blob - efbaf34f40016cfbf95f424beefbae4dcd297af1
blob + d8c404253820abc6e905cc5fbb0eecbf76df139a
--- river/util.zig
+++ river/util.zig
@@ -18,3 +18,22 @@ const std = @import("std");
 
 /// The global general-purpose allocator used throughout river's code
 pub const gpa = std.heap.c_allocator;
+
+pub fn timestamp() std.c.timespec {
+    var timespec: std.c.timespec = undefined;
+    switch (std.c.errno(std.c.clock_gettime(std.c.CLOCK.MONOTONIC, &timespec))) {
+        .SUCCESS => return timespec,
+        else => @panic("CLOCK_MONOTONIC not supported"),
+    }
+}
+
+pub fn msecTimestamp() u32 {
+    const now = timestamp();
+    // 2^32-1 milliseconds is ~50 days, which is a realistic uptime.
+    // This means that we must wrap if the monotonic time is greater than
+    // 2^32-1 milliseconds and hope that clients don't get too confused.
+    return @intCast(@rem(
+        now.sec *% std.time.ms_per_s +% @divTrunc(now.nsec, std.time.ns_per_ms),
+        std.math.maxInt(u32),
+    ));
+}
blob - 39abacf2ca5045c3ecc9697f44f6d74467613425
blob + 840fa9f48821f70a4dad1ce95cce38b345720957
--- riverctl/main.zig
+++ riverctl/main.zig
@@ -17,8 +17,11 @@
 const std = @import("std");
 const mem = std.mem;
 const fs = std.fs;
+const Io = std.Io;
 const posix = std.posix;
 const assert = std.debug.assert;
+const process = std.process;
+const fatal = process.fatal;
 const builtin = @import("builtin");
 
 const wayland = @import("wayland");
@@ -38,6 +41,15 @@ const usage =
     \\
 ;
 
+const io = Io.Threaded.global_single_threaded.io();
+var stdout_buffer: [64]u8 = undefined;
+var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
+const stdout = &stdout_writer.interface;
+
+var stderr_buffer: [64]u8 = undefined;
+var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer);
+const stderr = &stderr_writer.interface;
+
 const gpa = std.heap.c_allocator;
 
 pub const Globals = struct {
@@ -45,8 +57,8 @@ pub const Globals = struct {
     seat: ?*wl.Seat = null,
 };
 
-pub fn main() !void {
-    _main() catch |err| {
+pub fn main(init: std.process.Init.Minimal) !void {
+    _main(init) catch |err| {
         switch (err) {
             error.RiverControlNotAdvertised => fatal(
                 \\The Wayland server does not support river-control-unstable-v1.
@@ -57,7 +69,7 @@ pub fn main() !void {
             , .{}),
             error.ConnectFailed => {
                 std.log.err("Unable to connect to the Wayland server.", .{});
-                if (posix.getenvZ("WAYLAND_DISPLAY") == null) {
+                if (init.environ.getPosix("WAYLAND_DISPLAY") == null) {
                     fatal("WAYLAND_DISPLAY is not set.", .{});
                 } else {
                     fatal("Does WAYLAND_DISPLAY contain the socket name of a running server?", .{});
@@ -68,21 +80,24 @@ pub fn main() !void {
     };
 }
 
-fn _main() !void {
-    const result = flags.parser([*:0]const u8, &.{
+fn _main(init: std.process.Init.Minimal) !void {
+    const args = try init.args.toSlice(gpa);
+    defer gpa.free(args);
+
+    const result = flags.parser(&.{
         .{ .name = "h", .kind = .boolean },
         .{ .name = "version", .kind = .boolean },
-    }).parse(std.os.argv[1..]) catch {
-        try fs.File.stderr().writeAll(usage);
-        posix.exit(1);
+    }).parse(args[1..]) catch {
+        try stderr.writeAll(usage);
+        process.exit(1);
     };
     if (result.flags.h) {
-        try fs.File.stdout().writeAll(usage);
-        posix.exit(0);
+        try stdout.writeAll(usage);
+        process.exit(0);
     }
     if (result.flags.version) {
-        try fs.File.stdout().writeAll(@import("build_options").version ++ "\n");
-        posix.exit(0);
+        try stdout.writeAll(@import("build_options").version ++ "\n");
+        process.exit(0);
     }
 
     const display = try wl.Display.connect(null);
@@ -125,24 +140,18 @@ fn callbackListener(_: *zriver.CommandCallbackV1, even
     switch (event) {
         .success => |success| {
             if (mem.len(success.output) > 0) {
-                var stdout = fs.File.stdout().writer(&.{});
-                stdout.interface.print("{s}\n", .{success.output}) catch @panic("failed to write to stdout");
+                stdout.print("{s}\n", .{success.output}) catch @panic("failed to write to stdout");
             }
-            posix.exit(0);
+            process.exit(0);
         },
         .failure => |failure| {
             // A small hack to provide usage text when river reports an unknown command.
             if (mem.orderZ(u8, failure.failure_message, "unknown command") == .eq) {
                 std.log.err("unknown command", .{});
-                fs.File.stderr().writeAll(usage) catch {};
-                posix.exit(1);
+                stderr.writeAll(usage) catch {};
+                process.exit(1);
             }
             fatal("{s}", .{failure.failure_message});
         },
     }
 }
-
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
-    std.log.err(format, args);
-    posix.exit(1);
-}
blob - 972641e0a59eea6267b06283e65e7c5467c16447
blob + 31b994a0b2c180f5438d7959c83e9a8565a64420
--- rivertile/main.zig
+++ rivertile/main.zig
@@ -40,6 +40,8 @@ const fmt = std.fmt;
 const mem = std.mem;
 const math = std.math;
 const posix = std.posix;
+const process = std.process;
+const fatal = std.process.fatal;
 const assert = std.debug.assert;
 
 const wayland = @import("wayland");
@@ -307,8 +309,20 @@ const Output = struct {
     }
 };
 
-pub fn main() !void {
-    const result = flags.parser([*:0]const u8, &[_]flags.Flag{
+pub fn main(init: std.process.Init.Minimal) !void {
+    const args = try init.args.toSlice(gpa);
+    defer gpa.free(args);
+
+    const io = std.Io.Threaded.global_single_threaded.io();
+    var stdout_buffer: [64]u8 = undefined;
+    var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
+    const stdout = &stdout_writer.interface;
+
+    var stderr_buffer: [64]u8 = undefined;
+    var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
+    const stderr = &stderr_writer.interface;
+
+    const result = flags.parser(&.{
         .{ .name = "h", .kind = .boolean },
         .{ .name = "version", .kind = .boolean },
         .{ .name = "view-padding", .kind = .arg },
@@ -316,19 +330,19 @@ pub fn main() !void {
         .{ .name = "main-location", .kind = .arg },
         .{ .name = "main-count", .kind = .arg },
         .{ .name = "main-ratio", .kind = .arg },
-    }).parse(std.os.argv[1..]) catch {
-        try std.fs.File.stderr().writeAll(usage);
-        posix.exit(1);
+    }).parse(args[1..]) catch {
+        try stderr.writeAll(usage);
+        process.exit(1);
     };
     if (result.flags.h) {
-        try std.fs.File.stdout().writeAll(usage);
-        posix.exit(0);
+        try stdout.writeAll(usage);
+        process.exit(0);
     }
     if (result.args.len != 0) fatalPrintUsage("unknown option '{s}'", .{result.args[0]});
 
     if (result.flags.version) {
-        try std.fs.File.stdout().writeAll(@import("build_options").version ++ "\n");
-        posix.exit(0);
+        try stdout.writeAll(@import("build_options").version ++ "\n");
+        process.exit(0);
     }
     if (result.flags.@"view-padding") |raw| {
         view_padding = fmt.parseUnsigned(u31, raw, 10) catch
@@ -356,8 +370,7 @@ pub fn main() !void {
     }
 
     const display = wl.Display.connect(null) catch {
-        std.debug.print("Unable to connect to Wayland server.\n", .{});
-        posix.exit(1);
+        fatal("Unable to connect to Wayland server.\n", .{});
     };
     defer display.disconnect();
 
@@ -407,15 +420,10 @@ fn registryListener(registry: *wl.Registry, event: wl.
     }
 }
 
-fn fatal(comptime format: []const u8, args: anytype) noreturn {
-    std.log.err(format, args);
-    posix.exit(1);
-}
-
 fn fatalPrintUsage(comptime format: []const u8, args: anytype) noreturn {
     std.log.err(format, args);
-    std.fs.File.stderr().writeAll(usage) catch {};
-    posix.exit(1);
+    std.debug.print(usage, .{});
+    process.exit(1);
 }
 
 fn saturatingCast(comptime T: type, x: anytype) T {