Commit 4232d74

Eric Bower  ·  2026-07-23 11:42:37 -0400 EDT
parent 93cd148
refactor: upgrade to zig v0.16.0

The biggest challenge was the deprecation of std.posix.  In an effort to upgrade
to 0.16 without a major rewrite I decided to port the functions from std.posix
that we were heavily using.  We can continue to chip away at some of the posix
functions but it's likely many will remain until std.Io matures and we can migrate
off of using `poll(2)` directly.

Build is passing, tests are passing.
10 files changed,  +1640, -433
+1, -1
1@@ -2,7 +2,7 @@ FROM debian:12
2 
3 RUN apt-get update && apt-get install -y curl git bats coreutils && rm -rf /var/lib/apt/lists/*
4 
5-ARG ZIG_VERSION=0.15.2
6+ARG ZIG_VERSION=0.16.0
7 RUN curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/${ZIG_VERSION}/zig-x86_64-linux-${ZIG_VERSION}.tar.xz && \
8 	cd /tmp && \
9 	tar -xvf zig.tar.xz && \
+1, -1
1@@ -80,7 +80,7 @@ nix run github:neurosnap/zmx
2 
3 ### src
4 
5-- Requires zig `v0.15`
6+- Requires zig `v0.16`
7 - Clone the repo
8 - Run build cmd
9 
+6, -5
 1@@ -27,6 +27,7 @@ pub fn build(b: *std.Build) void {
 2         .root_source_file = b.path("src/main.zig"),
 3         .target = target,
 4         .optimize = optimize,
 5+        .link_libc = true,
 6     });
 7     exe_mod.addOptions("build_options", options);
 8 
 9@@ -54,7 +55,7 @@ pub fn build(b: *std.Build) void {
10             .use_lld = !is_macos,
11             .root_module = exe_mod,
12         });
13-        exe.linkLibC();
14+
15         b.installArtifact(exe);
16         const run_cmd = b.addRunArtifact(exe);
17         run_cmd.step.dependOn(b.getInstallStep());
18@@ -69,6 +70,7 @@ pub fn build(b: *std.Build) void {
19             .root_source_file = b.path("src/test.zig"),
20             .target = target,
21             .optimize = optimize,
22+            .link_libc = true,
23         });
24         const test_dep = b.dependency("ghostty", .{
25             .target = target,
26@@ -86,7 +88,7 @@ pub fn build(b: *std.Build) void {
27             .use_llvm = true,
28             .use_lld = !is_macos,
29         });
30-        exe_unit_tests.linkLibC();
31+
32         const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
33         test_step.dependOn(&run_exe_unit_tests.step);
34     }
35@@ -100,7 +102,6 @@ pub fn build(b: *std.Build) void {
36             .use_lld = !is_macos,
37             .root_module = exe_mod,
38         });
39-        exe_check.linkLibC();
40 
41         // Finally we add the "check" step which will be detected
42         // by ZLS and automatically enable Build-On-Save.
43@@ -121,6 +122,7 @@ pub fn build(b: *std.Build) void {
44                 .root_source_file = b.path("src/main.zig"),
45                 .target = resolved,
46                 .optimize = .ReleaseSafe,
47+                .link_libc = true,
48             });
49             release_mod.addOptions("build_options", options);
50 
51@@ -141,7 +143,6 @@ pub fn build(b: *std.Build) void {
52                 .use_lld = !is_local_macos,
53                 .root_module = release_mod,
54             });
55-            release_exe.linkLibC();
56 
57             const os_name = @tagName(release_target.os_tag orelse .linux);
58             const arch_name = @tagName(release_target.cpu_arch orelse .x86_64);
59@@ -156,7 +157,7 @@ pub fn build(b: *std.Build) void {
60 
61             const shasum = b.addSystemCommand(&.{"sha256sum"});
62             shasum.addFileArg(tarball);
63-            const shasum_output = shasum.captureStdOut();
64+            const shasum_output = shasum.captureStdOut(.{});
65 
66             const install_tar = b.addInstallFile(tarball, b.fmt("dist/{s}", .{tarball_name}));
67             const install_sha = b.addInstallFile(
+3, -3
 1@@ -2,11 +2,11 @@
 2     .name = .zmx,
 3     .version = "0.7.0",
 4     .fingerprint = 0x28aad87005052b4e, // Changing this has security and trust implications.
 5-    .minimum_zig_version = "0.15.2",
 6+    .minimum_zig_version = "0.16.0",
 7     .dependencies = .{
 8         .ghostty = .{
 9-            .url = "git+https://github.com/ghostty-org/ghostty#30e1f3bb8c3d2949e9ae4aefc1c2b76142569cfb",
10-            .hash = "ghostty-1.3.2-dev-5UdBC5gHIgWG-_Voonf5F76Vt2I2KljI5lIuvI-7eyBI",
11+            .url = "git+https://github.com/ghostty-org/ghostty.git/#15484b607eb5a518dedf1548247c923b8abaae7c",
12+            .hash = "ghostty-1.3.2-dev-5UdBCwfRJAX2W3lpOWUOrkLvsqJckxucSrjECoDjq9kL",
13         },
14     },
15     .paths = .{
+5, -4
 1@@ -2,6 +2,7 @@ const std = @import("std");
 2 const posix = std.posix;
 3 const cross = @import("cross.zig");
 4 const socket = @import("socket.zig");
 5+const lib_posix = @import("posix.zig");
 6 
 7 pub const Tag = enum(u8) {
 8     Input = 0,
 9@@ -115,7 +116,7 @@ pub fn appendMessage(
10 fn writeAll(fd: i32, data: []const u8) !void {
11     var index: usize = 0;
12     while (index < data.len) {
13-        const n = try posix.write(fd, data[index..]);
14+        const n = try lib_posix.write(fd, data[index..]);
15         if (n == 0) return error.DiskQuota;
16         index += n;
17     }
18@@ -223,7 +224,7 @@ const SessionProbeResult = struct {
19 
20     pub fn deinit(self: *const SessionProbeResult) void {
21         if (self.labels) |lbl| self.alloc.free(lbl);
22-        posix.close(self.fd);
23+        lib_posix.close(self.fd);
24     }
25 };
26 
27@@ -233,7 +234,7 @@ pub fn probeSession(
28 ) SessionProbeError!SessionProbeResult {
29     const timeout_ms = 1000;
30     const fd = try connectSession(socket_path);
31-    errdefer posix.close(fd);
32+    errdefer lib_posix.close(fd);
33 
34     send(fd, .Info, "") catch return error.Unexpected;
35     send(fd, .LabelGet, "") catch {};
36@@ -320,7 +321,7 @@ pub fn roundTripForTag(
37 ) SessionProbeError![]u8 {
38     const timeout_ms = 1000;
39     const fd = try connectSession(socket_path);
40-    defer posix.close(fd);
41+    defer lib_posix.close(fd);
42 
43     send(fd, request_tag, payload) catch return error.Unexpected;
44 
+31, -24
  1@@ -1,50 +1,52 @@
  2 const std = @import("std");
  3-const posix = std.posix;
  4 
  5 pub const LogSystem = struct {
  6-    file: ?std.fs.File = null,
  7-    mutex: std.Thread.Mutex = .{},
  8+    file: ?std.Io.File = null,
  9+    mutex: std.Io.Mutex = .init,
 10     current_size: u64 = 0,
 11     max_size: u64 = 5 * 1024 * 1024, // 5MB
 12     path: []const u8 = "",
 13     alloc: std.mem.Allocator = undefined,
 14-    mode: u32 = 0o640,
 15+    io: std.Io = undefined,
 16+    mode: std.Io.File.Permissions = std.Io.File.Permissions.fromMode(0o640),
 17 
 18-    pub fn init(self: *LogSystem, alloc: std.mem.Allocator, path: []const u8, mode: u32) !void {
 19+    pub fn init(self: *LogSystem, alloc: std.mem.Allocator, io: std.Io, path: []const u8, mode: std.Io.File.Permissions) !void {
 20         self.alloc = alloc;
 21+        self.io = io;
 22         self.path = try alloc.dupe(u8, path);
 23         self.mode = mode;
 24 
 25-        const file = std.fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) {
 26-            error.FileNotFound => try std.fs.createFileAbsolute(
 27+        const file = std.Io.Dir.openFileAbsolute(self.io, path, .{ .mode = .read_write }) catch |err| switch (err) {
 28+            error.FileNotFound => try std.Io.Dir.createFileAbsolute(
 29+                self.io,
 30                 path,
 31-                .{ .read = true, .mode = @intCast(self.mode) },
 32+                .{ .read = true, .permissions = self.mode },
 33             ),
 34             else => return err,
 35         };
 36 
 37-        // fstat (not getEndPos) to avoid the statx syscall; see #186.
 38-        const st = try posix.fstat(file.handle);
 39-        const end_pos: u64 = @intCast(st.size);
 40-        try file.seekTo(end_pos);
 41+        const end_pos = try std.Io.File.length(file, self.io);
 42+        var buf: [1]u8 = undefined;
 43+        var w = std.Io.File.writer(file, self.io, &buf);
 44+        try w.seekTo(end_pos);
 45         self.current_size = end_pos;
 46         self.file = file;
 47     }
 48 
 49     pub fn deinit(self: *LogSystem) void {
 50-        if (self.file) |f| f.close();
 51+        if (self.file) |f| std.Io.File.close(f, self.io);
 52         if (self.path.len > 0) self.alloc.free(self.path);
 53     }
 54 
 55     pub fn log(
 56         self: *LogSystem,
 57         comptime level: std.log.Level,
 58-        comptime scope: @Type(.enum_literal),
 59+        comptime scope: anytype,
 60         comptime format: []const u8,
 61         args: anytype,
 62-    ) void {
 63-        self.mutex.lock();
 64-        defer self.mutex.unlock();
 65+    ) !void {
 66+        try self.mutex.lock(self.io);
 67+        defer self.mutex.unlock(self.io);
 68 
 69         if (self.file == null) {
 70             std.log.defaultLog(level, scope, format, args);
 71@@ -57,7 +59,7 @@ pub const LogSystem = struct {
 72             };
 73         }
 74 
 75-        const now = std.time.milliTimestamp();
 76+        const now: i64 = @intCast(@divTrunc(std.Io.Timestamp.now(self.io, .real).nanoseconds, std.time.ns_per_ms));
 77         const prefix = "[{d}] [{s}] ({s}): ";
 78         const scope_name = @tagName(scope);
 79         const level_name = level.asText();
 80@@ -76,29 +78,34 @@ pub const LogSystem = struct {
 81             self.current_size += total_len;
 82 
 83             var buf: [4096]u8 = undefined;
 84-            var w = f.writerStreaming(&buf);
 85-            w.interface.print(prefix ++ format ++ "\n", prefix_args ++ args) catch {};
 86+            var w = f.writerStreaming(self.io, &buf);
 87+            std.Io.Writer.print(&w.interface, prefix ++ format ++ "\n", prefix_args ++ args) catch {};
 88             w.interface.flush() catch {};
 89         }
 90     }
 91 
 92     fn rotate(self: *LogSystem) !void {
 93         if (self.file) |f| {
 94-            f.close();
 95+            std.Io.File.close(f, self.io);
 96             self.file = null;
 97         }
 98 
 99         const old_path = try std.fmt.allocPrint(self.alloc, "{s}.old", .{self.path});
100         defer self.alloc.free(old_path);
101 
102-        std.fs.renameAbsolute(self.path, old_path) catch |err| switch (err) {
103+        std.Io.Dir.renameAbsolute(self.path, old_path, self.io) catch |err| switch (err) {
104             error.FileNotFound => {},
105             else => return err,
106         };
107 
108-        self.file = try std.fs.createFileAbsolute(
109+        self.file = try std.Io.Dir.createFileAbsolute(
110+            self.io,
111             self.path,
112-            .{ .truncate = true, .read = true, .mode = @intCast(self.mode) },
113+            .{
114+                .truncate = true,
115+                .read = true,
116+                .permissions = self.mode,
117+            },
118         );
119         self.current_size = 0;
120     }
+363, -340
   1@@ -9,6 +9,7 @@ const util = @import("util.zig");
   2 const cross = @import("cross.zig");
   3 const socket = @import("socket.zig");
   4 const label = @import("label.zig");
   5+const lib_posix = @import("posix.zig");
   6 
   7 pub const version = build_options.version;
   8 pub const ghostty_version = build_options.ghostty_version;
   9@@ -22,11 +23,11 @@ pub const std_options: std.Options = .{
  10 
  11 fn zmxLogFn(
  12     comptime level: std.log.Level,
  13-    comptime scope: @Type(.enum_literal),
  14+    comptime scope: anytype,
  15     comptime format: []const u8,
  16     args: anytype,
  17 ) void {
  18-    log_system.log(level, scope, format, args);
  19+    log_system.log(level, scope, format, args) catch {};
  20 }
  21 
  22 /// Self-pipe woken by signal handlers. std.posix.poll loops on .INTR internally
  23@@ -47,13 +48,13 @@ const SessionMatch = struct {
  24     }
  25 };
  26 
  27-fn resolveSessionOrEnv(alloc: std.mem.Allocator, session_name: ?[]const u8) ![]const u8 {
  28+fn resolveSessionOrEnv(alloc: std.mem.Allocator, io: std.Io, session_name: ?[]const u8) ![]const u8 {
  29     const sesh_env = socket.getSeshNameFromEnv();
  30     const raw = if (session_name) |name|
  31         if (std.mem.eql(u8, name, ".")) blk: {
  32             if (sesh_env.len > 0) break :blk sesh_env;
  33             var buf: [4096]u8 = undefined;
  34-            var w = std.fs.File.stderr().writer(&buf);
  35+            var w = std.Io.File.stderr().writer(io, &buf);
  36             w.interface.print("error: \".\" requires ZMX_SESSION (are you inside a zmx session?)\n", .{}) catch {};
  37             w.interface.flush() catch {};
  38             return error.SessionNameRequired;
  39@@ -76,7 +77,7 @@ fn parseSessionArg(alloc: std.mem.Allocator, raw: []const u8) !SessionMatch {
  40 }
  41 
  42 fn openSignalPipe() !void {
  43-    sig_pipe = try posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true });
  44+    sig_pipe = try lib_posix.pipe2(.{ .CLOEXEC = true, .NONBLOCK = true });
  45 }
  46 
  47 fn drainSignalPipe() void {
  48@@ -91,9 +92,9 @@ fn detectHelp(arg: []const u8) bool {
  49     return (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h"));
  50 }
  51 
  52-pub fn main() !void {
  53-    // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
  54-    const alloc = std.heap.c_allocator;
  55+pub fn main(init: std.process.Init) !void {
  56+    const gpa = init.gpa;
  57+    const io = init.io;
  58 
  59     // Every subcommand may write to a Unix-domain socket; a peer that
  60     // disappears between probe and send would otherwise kill us before
  61@@ -101,76 +102,79 @@ pub fn main() !void {
  62     // covers the daemon.
  63     ignoreSigpipe();
  64 
  65-    var args = try std.process.argsWithAllocator(alloc);
  66+    var args = init.minimal.args.iterate();
  67     defer args.deinit();
  68-    _ = args.skip(); // skip program name
  69+    _ = args.next(); // skip program name
  70 
  71-    var cfg = try Cfg.init(alloc);
  72-    defer cfg.deinit(alloc);
  73+    var cfg = try Cfg.init(gpa, io);
  74+    defer cfg.deinit(gpa);
  75 
  76-    const log_path = try std.fs.path.join(alloc, &.{ cfg.log_dir, "zmx.log" });
  77-    defer alloc.free(log_path);
  78-    try log_system.init(alloc, log_path, cfg.log_mode);
  79+    const log_path = try std.fs.path.join(gpa, &.{ cfg.log_dir, "zmx.log" });
  80+    defer gpa.free(log_path);
  81+    const log_mode = std.Io.File.Permissions.fromMode(cfg.log_mode);
  82+    try log_system.init(gpa, io, log_path, log_mode);
  83     defer log_system.deinit();
  84 
  85+    const shell_env = init.environ_map.get("SHELL") orelse "/bin/sh";
  86+
  87     const cmd = args.next() orelse {
  88-        return list(&cfg, false);
  89+        return list(gpa, io, &cfg, false);
  90     };
  91 
  92     if (std.mem.eql(u8, cmd, "version") or std.mem.eql(u8, cmd, "v") or std.mem.eql(u8, cmd, "-v") or std.mem.eql(u8, cmd, "--version")) {
  93-        return printVersion(&cfg);
  94+        return printVersion(io, &cfg);
  95     } else if (std.mem.eql(u8, cmd, "help") or std.mem.eql(u8, cmd, "h") or std.mem.eql(u8, cmd, "-h")) {
  96-        return help();
  97+        return help(io);
  98     } else if (std.mem.eql(u8, cmd, "list") or std.mem.eql(u8, cmd, "l") or std.mem.eql(u8, cmd, "ls")) {
  99         var short = false;
 100         while (args.next()) |arg| {
 101-            if (detectHelp(arg)) return help();
 102+            if (detectHelp(arg)) return help(io);
 103             if (std.mem.eql(u8, arg, "--short")) short = true;
 104         }
 105-        return list(&cfg, short);
 106+        return list(gpa, io, &cfg, short);
 107     } else if (std.mem.eql(u8, cmd, "get") or std.mem.eql(u8, cmd, "g")) {
 108         const sesh_name = args.next() orelse return error.SessionNameRequired;
 109-        if (detectHelp(sesh_name)) return help();
 110-        const sesh = try resolveSessionOrEnv(alloc, sesh_name);
 111-        defer alloc.free(sesh);
 112+        if (detectHelp(sesh_name)) return help(io);
 113+        const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
 114+        defer gpa.free(sesh);
 115         const single_kv = args.next() orelse "";
 116-        return labelGet(&cfg, sesh, single_kv);
 117+        return labelGet(gpa, io, &cfg, sesh, single_kv);
 118     } else if (std.mem.eql(u8, cmd, "set")) {
 119         const sesh_name = args.next() orelse return error.SessionNameRequired;
 120-        if (detectHelp(sesh_name)) return help();
 121-        const sesh = try resolveSessionOrEnv(alloc, sesh_name);
 122-        defer alloc.free(sesh);
 123+        if (detectHelp(sesh_name)) return help(io);
 124+        const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
 125+        defer gpa.free(sesh);
 126 
 127         var kvs = std.ArrayList(u8).empty;
 128-        defer kvs.deinit(alloc);
 129+        defer kvs.deinit(gpa);
 130         var first = true;
 131         while (args.next()) |arg| {
 132-            if (!first) try kvs.append(alloc, ' ');
 133-            try kvs.appendSlice(alloc, arg);
 134+            if (!first) try kvs.append(gpa, ' ');
 135+            try kvs.appendSlice(gpa, arg);
 136             first = false;
 137         }
 138-        return labelSet(&cfg, sesh, kvs.items);
 139+        return labelSet(gpa, io, &cfg, sesh, kvs.items);
 140     } else if (std.mem.eql(u8, cmd, "clear")) {
 141         const sesh_name = args.next() orelse return error.SessionNameRequired;
 142-        if (detectHelp(sesh_name)) return help();
 143-        const sesh = try resolveSessionOrEnv(alloc, sesh_name);
 144-        defer alloc.free(sesh);
 145-        return labelClear(&cfg, sesh);
 146+        if (detectHelp(sesh_name)) return help(io);
 147+        const sesh = try resolveSessionOrEnv(gpa, io, sesh_name);
 148+        defer gpa.free(sesh);
 149+        return labelClear(gpa, io, &cfg, sesh);
 150     } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) {
 151         const arg = args.next() orelse return;
 152         if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
 153-            return help();
 154+            return help(io);
 155         }
 156         const shell = completions.Shell.fromString(arg) orelse return;
 157-        return printCompletions(shell);
 158+        return printCompletions(io, shell);
 159     } else if (std.mem.eql(u8, cmd, "detach") or std.mem.eql(u8, cmd, "d")) {
 160-        return detachAll(&cfg);
 161+        return detachAll(gpa, io, &cfg);
 162     } else if (std.mem.eql(u8, cmd, "history") or std.mem.eql(u8, cmd, "hi")) {
 163         var session_name: ?[]const u8 = null;
 164         var format: util.HistoryFormat = .plain;
 165         while (args.next()) |arg| {
 166             if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
 167-                return help();
 168+                return help(io);
 169             } else if (std.mem.eql(u8, arg, "--vt")) {
 170                 format = .vt;
 171             } else if (std.mem.eql(u8, arg, "--html")) {
 172@@ -180,47 +184,50 @@ pub fn main() !void {
 173             }
 174         }
 175         const sesh_env = socket.getSeshNameFromEnv();
 176-        const sesh = try socket.getSeshName(alloc, session_name orelse sesh_env);
 177-        defer alloc.free(sesh);
 178-        return history(&cfg, sesh, format);
 179+        const sesh = try socket.getSeshName(gpa, session_name orelse sesh_env);
 180+        defer gpa.free(sesh);
 181+        return history(gpa, io, &cfg, sesh, format);
 182     } else if (std.mem.eql(u8, cmd, "attach") or std.mem.eql(u8, cmd, "a")) {
 183         const session_name = args.next() orelse "";
 184         if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 185-            return help();
 186+            return help(io);
 187         }
 188 
 189         var command_args: std.ArrayList([]const u8) = .empty;
 190-        defer command_args.deinit(alloc);
 191+        defer command_args.deinit(gpa);
 192         while (args.next()) |arg| {
 193-            try command_args.append(alloc, arg);
 194+            try command_args.append(gpa, arg);
 195         }
 196 
 197-        const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
 198+        const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
 199         var command: ?[][]const u8 = null;
 200         if (command_args.items.len > 0) {
 201             command = command_args.items;
 202         }
 203 
 204         var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
 205-        const cwd = std.posix.getcwd(&cwd_buf) catch "";
 206+        const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
 207+        const cwd = cwd_buf[0..cwd_len];
 208 
 209-        const sesh = try socket.getSeshName(alloc, session_name);
 210-        defer alloc.free(sesh);
 211+        const sesh = try socket.getSeshName(gpa, session_name);
 212+        defer gpa.free(sesh);
 213         var daemon = Daemon{
 214+            .io = io,
 215             .running = true,
 216             .cfg = &cfg,
 217-            .alloc = alloc,
 218+            .alloc = std.heap.c_allocator,
 219             .clients = clients,
 220             .session_name = sesh,
 221             .socket_path = undefined,
 222             .pid = undefined,
 223             .command = command,
 224             .cwd = cwd,
 225-            .created_at = @intCast(std.time.timestamp()),
 226+            .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
 227             .leader_client_fd = null,
 228+            .shell = shell_env,
 229         };
 230-        daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
 231-            error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
 232+        daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
 233+            error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
 234             error.OutOfMemory => return err,
 235         };
 236         std.log.info("socket path={s}", .{daemon.socket_path});
 237@@ -228,42 +235,45 @@ pub fn main() !void {
 238     } else if (std.mem.eql(u8, cmd, "run") or std.mem.eql(u8, cmd, "r")) {
 239         const session_name = args.next() orelse "";
 240         if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 241-            return help();
 242+            return help(io);
 243         }
 244 
 245         var cmd_args_raw: std.ArrayList([]const u8) = .empty;
 246-        defer cmd_args_raw.deinit(alloc);
 247+        defer cmd_args_raw.deinit(gpa);
 248         var detached = false;
 249         while (args.next()) |arg| {
 250             if (std.mem.startsWith(u8, arg, "-d")) {
 251                 detached = true;
 252             } else {
 253-                try cmd_args_raw.append(alloc, arg);
 254+                try cmd_args_raw.append(gpa, arg);
 255             }
 256         }
 257-        const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
 258+        const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
 259 
 260         var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
 261-        const cwd = std.posix.getcwd(&cwd_buf) catch "";
 262+        const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
 263+        const cwd = cwd_buf[0..cwd_len];
 264 
 265-        const sesh = try socket.getSeshName(alloc, session_name);
 266-        defer alloc.free(sesh);
 267+        const sesh = try socket.getSeshName(gpa, session_name);
 268+        defer gpa.free(sesh);
 269         var daemon = Daemon{
 270+            .io = io,
 271             .running = true,
 272             .cfg = &cfg,
 273-            .alloc = alloc,
 274+            .alloc = std.heap.c_allocator,
 275             .clients = clients,
 276             .session_name = sesh,
 277             .socket_path = undefined,
 278             .pid = undefined,
 279             .command = null,
 280             .cwd = cwd,
 281-            .created_at = @intCast(std.time.timestamp()),
 282+            .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
 283             .is_task_mode = true,
 284             .leader_client_fd = null,
 285+            .shell = shell_env,
 286         };
 287-        daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
 288-            error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
 289+        daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
 290+            error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
 291             error.OutOfMemory => return err,
 292         };
 293         std.log.info("socket path={s}", .{daemon.socket_path});
 294@@ -271,76 +281,76 @@ pub fn main() !void {
 295     } else if (std.mem.eql(u8, cmd, "send") or std.mem.eql(u8, cmd, "s")) {
 296         const session_name = args.next() orelse "";
 297         if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 298-            return help();
 299+            return help(io);
 300         }
 301         if (session_name.len == 0) return error.SessionNameRequired;
 302 
 303         var text_parts: std.ArrayList([]const u8) = .empty;
 304-        defer text_parts.deinit(alloc);
 305+        defer text_parts.deinit(gpa);
 306         while (args.next()) |arg| {
 307-            try text_parts.append(alloc, arg);
 308+            try text_parts.append(gpa, arg);
 309         }
 310 
 311-        const sesh = try socket.getSeshName(alloc, session_name);
 312-        defer alloc.free(sesh);
 313-        const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
 314-            error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
 315+        const sesh = try socket.getSeshName(gpa, session_name);
 316+        defer gpa.free(sesh);
 317+        const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
 318+            error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir),
 319             error.OutOfMemory => return err,
 320         };
 321-        return send(&cfg, sesh, socket_path, text_parts.items, .Send);
 322+        return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Send);
 323     } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) {
 324         const session_name = args.next() orelse "";
 325         if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 326-            return help();
 327+            return help(io);
 328         }
 329         if (session_name.len == 0) return error.SessionNameRequired;
 330 
 331         var text_parts: std.ArrayList([]const u8) = .empty;
 332-        defer text_parts.deinit(alloc);
 333+        defer text_parts.deinit(gpa);
 334         while (args.next()) |arg| {
 335-            try text_parts.append(alloc, arg);
 336+            try text_parts.append(gpa, arg);
 337         }
 338 
 339-        const sesh = try socket.getSeshName(alloc, session_name);
 340-        defer alloc.free(sesh);
 341-        const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
 342-            error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
 343+        const sesh = try socket.getSeshName(gpa, session_name);
 344+        defer gpa.free(sesh);
 345+        const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
 346+            error.NameTooLong => return socket.printSessionNameTooLong(io, sesh, cfg.socket_dir),
 347             error.OutOfMemory => return err,
 348         };
 349-        return send(&cfg, sesh, socket_path, text_parts.items, .Output);
 350+        return send(gpa, io, &cfg, sesh, socket_path, text_parts.items, .Output);
 351     } else if (std.mem.eql(u8, cmd, "kill") or std.mem.eql(u8, cmd, "k")) {
 352         var stderr_buffer: [1024]u8 = undefined;
 353-        var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
 354+        var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
 355         const stderr = &stderr_writer.interface;
 356 
 357         var matchers: std.ArrayList(SessionMatch) = .empty;
 358         defer {
 359             for (matchers.items) |m| {
 360-                alloc.free(m.name);
 361+                gpa.free(m.name);
 362             }
 363-            matchers.deinit(alloc);
 364+            matchers.deinit(gpa);
 365         }
 366         var force = false;
 367         while (args.next()) |session_name| {
 368             if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 369-                return help();
 370+                return help(io);
 371             }
 372             if (std.mem.eql(u8, session_name, "--force")) {
 373                 force = true;
 374                 continue;
 375             }
 376-            const m = try parseSessionArg(alloc, session_name);
 377-            try matchers.append(alloc, m);
 378+            const m = try parseSessionArg(gpa, session_name);
 379+            try matchers.append(gpa, m);
 380         }
 381         if (matchers.items.len == 0) {
 382             return error.SessionNameRequired;
 383         }
 384-        var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
 385+        var sessions = try util.get_session_entries(gpa, io, cfg.socket_dir);
 386         defer {
 387             for (sessions.items) |session| {
 388-                session.deinit(alloc);
 389+                session.deinit(gpa);
 390             }
 391-            sessions.deinit(alloc);
 392+            sessions.deinit(gpa);
 393         }
 394 
 395         for (sessions.items) |session| {
 396@@ -349,7 +359,7 @@ pub fn main() !void {
 397                     continue;
 398                 }
 399 
 400-                kill(&cfg, session.name, force) catch |err| {
 401+                kill(gpa, io, &cfg, session.name, force) catch |err| {
 402                     try stderr.print(
 403                         "failed to kill session={s}: {s}\n",
 404                         .{ session.name, @errorName(err) },
 405@@ -363,35 +373,35 @@ pub fn main() !void {
 406         var matchers: std.ArrayList(SessionMatch) = .empty;
 407         defer {
 408             for (matchers.items) |m| {
 409-                alloc.free(m.name);
 410+                gpa.free(m.name);
 411             }
 412-            matchers.deinit(alloc);
 413+            matchers.deinit(gpa);
 414         }
 415         while (args.next()) |session_name| {
 416             if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 417-                return help();
 418+                return help(io);
 419             }
 420-            const m = try parseSessionArg(alloc, session_name);
 421-            try matchers.append(alloc, m);
 422+            const m = try parseSessionArg(gpa, session_name);
 423+            try matchers.append(gpa, m);
 424         }
 425         if (matchers.items.len == 0) {
 426             return error.SessionNameRequired;
 427         }
 428-        return wait(&cfg, matchers);
 429+        return wait(gpa, io, &cfg, matchers);
 430     } else if (std.mem.eql(u8, cmd, "tail") or std.mem.eql(u8, cmd, "t")) {
 431         var matchers: std.ArrayList(SessionMatch) = .empty;
 432         defer {
 433             for (matchers.items) |m| {
 434-                alloc.free(m.name);
 435+                gpa.free(m.name);
 436             }
 437-            matchers.deinit(alloc);
 438+            matchers.deinit(gpa);
 439         }
 440         while (args.next()) |session_name| {
 441             if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 442-                return help();
 443+                return help(io);
 444             }
 445-            const m = try parseSessionArg(alloc, session_name);
 446-            try matchers.append(alloc, m);
 447+            const m = try parseSessionArg(gpa, session_name);
 448+            try matchers.append(gpa, m);
 449         }
 450         if (matchers.items.len == 0) {
 451             return error.SessionNameRequired;
 452@@ -401,9 +411,9 @@ pub fn main() !void {
 453         var resolved_names: std.ArrayList([]const u8) = .empty;
 454         defer {
 455             for (resolved_names.items) |name| {
 456-                alloc.free(name);
 457+                gpa.free(name);
 458             }
 459-            resolved_names.deinit(alloc);
 460+            resolved_names.deinit(gpa);
 461         }
 462 
 463         var any_prefix = false;
 464@@ -415,17 +425,17 @@ pub fn main() !void {
 465         }
 466 
 467         if (any_prefix) {
 468-            var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
 469+            var sessions = try util.get_session_entries(gpa, io, cfg.socket_dir);
 470             defer {
 471                 for (sessions.items) |session| {
 472-                    session.deinit(alloc);
 473+                    session.deinit(gpa);
 474                 }
 475-                sessions.deinit(alloc);
 476+                sessions.deinit(gpa);
 477             }
 478             for (sessions.items) |session| {
 479                 for (matchers.items) |m| {
 480                     if (m.matches(session.name)) {
 481-                        try resolved_names.append(alloc, try alloc.dupe(u8, session.name));
 482+                        try resolved_names.append(gpa, try gpa.dupe(u8, session.name));
 483                         break;
 484                     }
 485                 }
 486@@ -434,66 +444,69 @@ pub fn main() !void {
 487         // Add exact-match names directly.
 488         for (matchers.items) |m| {
 489             if (!m.is_prefix) {
 490-                try resolved_names.append(alloc, try alloc.dupe(u8, m.name));
 491+                try resolved_names.append(gpa, try gpa.dupe(u8, m.name));
 492             }
 493         }
 494 
 495-        var client_socket_fds = try std.ArrayList(i32).initCapacity(alloc, resolved_names.items.len);
 496+        var client_socket_fds = try std.ArrayList(i32).initCapacity(gpa, resolved_names.items.len);
 497         defer {
 498             for (client_socket_fds.items) |client_fd| {
 499-                posix.close(client_fd);
 500+                lib_posix.close(client_fd);
 501             }
 502-            client_socket_fds.deinit(alloc);
 503+            client_socket_fds.deinit(gpa);
 504         }
 505 
 506         for (resolved_names.items) |session_name| {
 507-            const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
 508-                error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
 509+            const socket_path = socket.getSocketPath(gpa, cfg.socket_dir, session_name) catch |err| switch (err) {
 510+                error.NameTooLong => return socket.printSessionNameTooLong(init.io, session_name, cfg.socket_dir),
 511                 error.OutOfMemory => return err,
 512             };
 513             const client_sock = try socket.sessionConnect(socket_path);
 514-            try client_socket_fds.append(alloc, client_sock);
 515+            try client_socket_fds.append(gpa, client_sock);
 516         }
 517-        _ = try tail(client_socket_fds, false, false);
 518+        _ = try tail(gpa, client_socket_fds, false, false);
 519     } else if (std.mem.eql(u8, cmd, "write") or std.mem.eql(u8, cmd, "wr")) {
 520         const session_name = args.next() orelse "";
 521         if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
 522-            return help();
 523+            return help(io);
 524         }
 525         if (session_name.len == 0) return error.SessionNameRequired;
 526         const file_path = args.next() orelse "";
 527         if (std.mem.eql(u8, file_path, "--help") or std.mem.eql(u8, file_path, "-h")) {
 528-            return help();
 529+            return help(io);
 530         }
 531         if (file_path.len == 0) return error.FilePathRequired;
 532 
 533         var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
 534-        const cwd = std.posix.getcwd(&cwd_buf) catch "";
 535-        const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
 536-        const sesh = try socket.getSeshName(alloc, session_name);
 537-        defer alloc.free(sesh);
 538+        const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
 539+        const cwd = cwd_buf[0..cwd_len];
 540+        const clients = try std.ArrayList(*Client).initCapacity(gpa, 10);
 541+        const sesh = try socket.getSeshName(gpa, session_name);
 542+        defer gpa.free(sesh);
 543         var daemon = Daemon{
 544+            .io = io,
 545             .running = true,
 546             .cfg = &cfg,
 547-            .alloc = alloc,
 548+            .alloc = std.heap.c_allocator,
 549             .clients = clients,
 550             .session_name = sesh,
 551             .socket_path = undefined,
 552             .pid = undefined,
 553             .command = null,
 554             .cwd = cwd,
 555-            .created_at = @intCast(std.time.timestamp()),
 556+            .created_at = @intCast(std.Io.Timestamp.now(io, .real).nanoseconds),
 557             .is_task_mode = true,
 558             .leader_client_fd = null,
 559+            .shell = shell_env,
 560         };
 561-        daemon.socket_path = socket.getSocketPath(alloc, cfg.socket_dir, sesh) catch |err| switch (err) {
 562-            error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
 563+        daemon.socket_path = socket.getSocketPath(gpa, cfg.socket_dir, sesh) catch |err| switch (err) {
 564+            error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, sesh, cfg.socket_dir),
 565             error.OutOfMemory => return err,
 566         };
 567         std.log.info("socket path={s}", .{daemon.socket_path});
 568         try writeFile(&daemon, file_path);
 569     } else {
 570-        return help();
 571+        return help(io);
 572     }
 573 }
 574 
 575@@ -508,7 +521,7 @@ const Client = struct {
 576     write_buf: std.ArrayList(u8),
 577 
 578     pub fn deinit(self: *Client) void {
 579-        posix.close(self.socket_fd);
 580+        lib_posix.close(self.socket_fd);
 581         self.read_buf.deinit();
 582         self.write_buf.deinit(self.alloc);
 583     }
 584@@ -524,18 +537,18 @@ const Cfg = struct {
 585     dir_mode: u32 = 0o750,
 586     log_mode: u32 = 0o640,
 587 
 588-    pub fn init(alloc: std.mem.Allocator) !Cfg {
 589+    pub fn init(alloc: std.mem.Allocator, io: std.Io) !Cfg {
 590         const socket_dir = try socketDir(alloc);
 591         errdefer alloc.free(socket_dir);
 592         const log_dir = try logDir(alloc);
 593         errdefer alloc.free(log_dir);
 594 
 595-        const dir_mode = if (std.posix.getenv("ZMX_DIR_MODE")) |m|
 596+        const dir_mode = if (lib_posix.getenv("ZMX_DIR_MODE")) |m|
 597             std.fmt.parseInt(u32, m, 8) catch 0o750
 598         else
 599             0o750;
 600 
 601-        const log_mode = if (std.posix.getenv("ZMX_LOG_MODE")) |m|
 602+        const log_mode = if (lib_posix.getenv("ZMX_LOG_MODE")) |m|
 603             std.fmt.parseInt(u32, m, 8) catch 0o640
 604         else
 605             0o640;
 606@@ -547,18 +560,18 @@ const Cfg = struct {
 607             .log_mode = log_mode,
 608         };
 609 
 610-        try cfg.mkdir();
 611+        try cfg.mkdir(io);
 612 
 613         return cfg;
 614     }
 615 
 616     fn socketDir(alloc: std.mem.Allocator) ![]const u8 {
 617-        const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
 618-        const uid = posix.getuid();
 619+        const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
 620+        const uid = lib_posix.getuid();
 621 
 622-        const socket_dir: []const u8 = if (posix.getenv("ZMX_DIR")) |zmxdir|
 623+        const socket_dir: []const u8 = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
 624             try alloc.dupe(u8, zmxdir)
 625-        else if (posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
 626+        else if (lib_posix.getenv("XDG_RUNTIME_DIR")) |xdg_runtime|
 627             try std.fmt.allocPrint(alloc, "{s}/zmx", .{xdg_runtime})
 628         else
 629             try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
 630@@ -567,16 +580,16 @@ const Cfg = struct {
 631     }
 632 
 633     fn logDir(alloc: std.mem.Allocator) ![]const u8 {
 634-        const log_dir = if (posix.getenv("ZMX_DIR")) |zmxdir|
 635+        const log_dir = if (lib_posix.getenv("ZMX_DIR")) |zmxdir|
 636             try std.fmt.allocPrint(alloc, "{s}/logs", .{zmxdir})
 637-        else if (posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
 638+        else if (lib_posix.getenv("XDG_STATE_HOME")) |xdg_state_home|
 639             try std.fmt.allocPrint(alloc, "{s}/zmx/logs", .{xdg_state_home})
 640-        else if (posix.getenv("HOME")) |home_dir|
 641+        else if (lib_posix.getenv("HOME")) |home_dir|
 642             try std.fmt.allocPrint(alloc, "{s}/.local/state/zmx/logs", .{home_dir})
 643         else fallback: {
 644             // This is the last resort: falling back to /tmp/$UID if HOME is unset.
 645-            const tmpdir = std.mem.trimRight(u8, posix.getenv("TMPDIR") orelse "/tmp", "/");
 646-            const uid = posix.getuid();
 647+            const tmpdir = std.mem.trimEnd(u8, lib_posix.getenv("TMPDIR") orelse "/tmp", "/");
 648+            const uid = lib_posix.getuid();
 649             break :fallback try std.fmt.allocPrint(alloc, "{s}/zmx-{d}", .{ tmpdir, uid });
 650         };
 651 
 652@@ -588,16 +601,18 @@ const Cfg = struct {
 653         if (self.log_dir.len > 0) alloc.free(self.log_dir);
 654     }
 655 
 656-    pub fn mkdir(self: *Cfg) !void {
 657-        try mkdirAll(self.socket_dir, @intCast(self.dir_mode));
 658-        try mkdirAll(self.log_dir, @intCast(self.dir_mode));
 659+    pub fn mkdir(self: *Cfg, io: std.Io) !void {
 660+        const sock_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
 661+        try mkdirAll(io, self.socket_dir, sock_perms);
 662+        const log_perms = std.Io.Dir.Permissions.fromMode(@intCast(self.dir_mode));
 663+        try mkdirAll(io, self.log_dir, log_perms);
 664     }
 665 
 666-    fn mkdirAll(sub_dir_path: []const u8, mode: posix.mode_t) !void {
 667-        var it = try std.fs.path.componentIterator(sub_dir_path);
 668+    fn mkdirAll(io: std.Io, sub_dir_path: []const u8, permissions: std.Io.Dir.Permissions) !void {
 669+        var it = std.fs.path.componentIterator(sub_dir_path);
 670         var component = it.last() orelse return error.BadPathName;
 671         while (true) {
 672-            posix.mkdirat(posix.AT.FDCWD, component.path, mode) catch |err| switch (err) {
 673+            std.Io.Dir.createDirAbsolute(io, component.path, permissions) catch |err| switch (err) {
 674                 error.PathAlreadyExists => {},
 675                 error.FileNotFound => |e| {
 676                     component = it.previous() orelse return e;
 677@@ -617,7 +632,7 @@ test "Cfg.init uses default modes when env vars are not set" {
 678     _ = cross.c.unsetenv("ZMX_DIR_MODE");
 679     _ = cross.c.unsetenv("ZMX_LOG_MODE");
 680 
 681-    var cfg = try Cfg.init(alloc);
 682+    var cfg = try Cfg.init(alloc, std.testing.io);
 683     defer cfg.deinit(alloc);
 684 
 685     try std.testing.expectEqual(@as(u32, 0o750), cfg.dir_mode);
 686@@ -635,7 +650,7 @@ test "Cfg.init uses custom modes from env vars" {
 687         _ = cross.c.unsetenv("ZMX_LOG_MODE");
 688     }
 689 
 690-    var cfg = try Cfg.init(alloc);
 691+    var cfg = try Cfg.init(alloc, std.testing.io);
 692     defer cfg.deinit(alloc);
 693 
 694     try std.testing.expectEqual(@as(u32, 0o770), cfg.dir_mode);
 695@@ -651,6 +666,7 @@ test "Cfg.init uses custom modes from env vars" {
 696 ///
 697 /// Conceptually it's also much simpler to reason about.
 698 const Daemon = struct {
 699+    io: std.Io,
 700     cfg: *Cfg,
 701     alloc: std.mem.Allocator,
 702     clients: std.ArrayList(*Client),
 703@@ -673,6 +689,7 @@ const Daemon = struct {
 704     task_ended_at: ?u64 = null, // timestamp when task exited
 705     pty_fd: i32 = -1, // set by daemonLoop so handleRun can probe the foreground process
 706     pty_write_buf: std.ArrayList(u8) = .empty,
 707+    shell: []const u8 = "/bin/sh",
 708 
 709     const EnsureSessionResult = struct {
 710         created: bool,
 711@@ -808,12 +825,14 @@ const Daemon = struct {
 712             for (cmd_args, 0..) |arg, i| {
 713                 argv[i] = try alloc.dupeZ(u8, arg);
 714             }
 715-            const err = std.posix.execvpeZ(argv[0].?, argv.ptr, std.c.environ);
 716+            const err = lib_posix.execvpeZ(argv[0].?, argv.ptr, std.c.environ);
 717             std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) });
 718-            std.posix.exit(1);
 719+            lib_posix.exit(1);
 720         }
 721 
 722-        const shell: [:0]const u8 = if (self.is_task_mode) "bash" else util.detectShell();
 723+        var buf: [256]u8 = undefined;
 724+        const z = try std.fmt.bufPrintZ(&buf, "{s}", .{self.shell});
 725+        const shell: [:0]const u8 = if (self.is_task_mode) "bash" else z;
 726         // Use "-shellname" as argv[0] to signal login shell (traditional method)
 727         const login_shell = try std.fmt.allocPrintSentinel(
 728             alloc,
 729@@ -822,9 +841,9 @@ const Daemon = struct {
 730             0,
 731         );
 732         const argv = [_:null]?[*:0]const u8{ login_shell, null };
 733-        const err = std.posix.execvpeZ(shell, &argv, std.c.environ);
 734+        const err = lib_posix.execvpeZ(shell, &argv, std.c.environ);
 735         std.log.err("execvpe failed: shell={s} err={s}", .{ shell, @errorName(err) });
 736-        std.posix.exit(1);
 737+        lib_posix.exit(1);
 738     }
 739 
 740     /// spawnPty runs forkpty() and executes the shell or shell command the user provides.
 741@@ -850,7 +869,7 @@ const Daemon = struct {
 742             // errdefers that delete the parent's socket file).
 743             execChild(self) catch |err| {
 744                 std.log.err("child setup failed: {s}", .{@errorName(err)});
 745-                std.posix.exit(1);
 746+                lib_posix.exit(1);
 747             };
 748             unreachable; // execChild either execs or exits, never returns ok
 749         }
 750@@ -859,8 +878,8 @@ const Daemon = struct {
 751         std.log.info("pty spawned session={s} pid={d}", .{ self.session_name, pid });
 752 
 753         // make pty non-blocking
 754-        const flags = try posix.fcntl(master_fd, posix.F.GETFL, 0);
 755-        _ = try posix.fcntl(master_fd, posix.F.SETFL, flags | O_NONBLOCK);
 756+        const flags = try lib_posix.fcntl(master_fd, posix.F.GETFL, 0);
 757+        _ = try lib_posix.fcntl(master_fd, posix.F.SETFL, flags | O_NONBLOCK);
 758         return master_fd;
 759     }
 760 
 761@@ -868,15 +887,15 @@ const Daemon = struct {
 762     /// If not it creates one and spawns the daemon.
 763     fn ensureSession(self: *Daemon) !EnsureSessionResult {
 764         std.log.info("ensure session session={s}", .{self.session_name});
 765-        var dir = try std.fs.openDirAbsolute(self.cfg.socket_dir, .{});
 766-        defer dir.close();
 767+        var dir = try std.Io.Dir.openDirAbsolute(self.io, self.cfg.socket_dir, .{});
 768+        defer dir.close(self.io);
 769 
 770-        const exists = try socket.sessionExists(dir, self.session_name);
 771+        const exists = try socket.sessionExists(self.io, dir, self.session_name);
 772         var should_create = !exists;
 773 
 774         if (exists) {
 775             if (ipc.connectSession(self.socket_path)) |fd| {
 776-                posix.close(fd);
 777+                lib_posix.close(fd);
 778                 if (self.command != null) {
 779                     std.log.warn(
 780                         "session already exists, ignoring command session={s}",
 781@@ -886,7 +905,7 @@ const Daemon = struct {
 782             } else |err| switch (err) {
 783                 // Daemon is definitively gone: safe to replace.
 784                 error.ConnectionRefused => {
 785-                    socket.cleanupStaleSocket(dir, self.session_name);
 786+                    socket.cleanupStaleSocket(self.io, dir, self.session_name);
 787                     should_create = true;
 788                 },
 789                 // Connect failed for an unusual reason. The check is only to
 790@@ -906,10 +925,10 @@ const Daemon = struct {
 791             const server_sock_fd = try socket.createSocket(self.socket_path);
 792 
 793             // creates the daemon
 794-            const pid = try posix.fork();
 795+            const pid = try lib_posix.fork();
 796             if (pid == 0) { // child (daemon)
 797                 // becomes the session leader and detaches process from its controlling terminal
 798-                _ = try posix.setsid();
 799+                _ = try lib_posix.setsid();
 800 
 801                 log_system.deinit();
 802 
 803@@ -919,7 +938,7 @@ const Daemon = struct {
 804                 // keyword) stays open for the daemon's lifetime, causing
 805                 // the caller to hang waiting for EOF.
 806                 {
 807-                    const devnull = std.posix.open(
 808+                    const devnull = lib_posix.open(
 809                         "/dev/null",
 810                         .{ .ACCMODE = .RDWR },
 811                         0,
 812@@ -928,12 +947,12 @@ const Daemon = struct {
 813                         return err;
 814                     };
 815                     inline for (.{ posix.STDIN_FILENO, posix.STDOUT_FILENO, posix.STDERR_FILENO }) |fd| {
 816-                        _ = posix.dup2(devnull, fd) catch |err| {
 817+                        _ = lib_posix.dup2(devnull, fd) catch |err| {
 818                             std.log.warn("dup2 /dev/null -> {d}: {s}", .{ fd, @errorName(err) });
 819                             return err;
 820                         };
 821                     }
 822-                    if (devnull > 2) posix.close(devnull);
 823+                    if (devnull > 2) lib_posix.close(devnull);
 824                 }
 825 
 826                 // Close file descriptors inherited from the parent that the
 827@@ -948,7 +967,7 @@ const Daemon = struct {
 828                 // Skip server_sock_fd (needed for IPC) and dir.fd (needed to
 829                 // delete the socket file on shutdown).
 830                 {
 831-                    const dir_fd = @as(i32, @intCast(dir.fd));
 832+                    const dir_fd = @as(i32, @intCast(dir.handle));
 833                     var fd: i32 = 3;
 834                     while (fd < 64) : (fd += 1) {
 835                         if (fd == server_sock_fd or fd == dir_fd) continue;
 836@@ -967,14 +986,15 @@ const Daemon = struct {
 837                     &.{ self.cfg.log_dir, session_log_name },
 838                 );
 839                 defer self.alloc.free(session_log_path);
 840-                try log_system.init(self.alloc, session_log_path, self.cfg.log_mode);
 841+                const log_mode = std.Io.File.Permissions.fromMode(self.cfg.log_mode);
 842+                try log_system.init(self.alloc, self.io, session_log_path, log_mode);
 843 
 844                 // If spawnPty fails, clean up here. Once it succeeds,
 845                 // the inner block's defer takes ownership of cleanup to
 846                 // avoid double-closing server_sock_fd on daemonLoop error.
 847                 const pty_fd = self.spawnPty() catch |err| {
 848-                    posix.close(server_sock_fd);
 849-                    dir.deleteFile(self.session_name) catch {};
 850+                    lib_posix.close(server_sock_fd);
 851+                    dir.deleteFile(self.io, self.session_name) catch {};
 852                     return err;
 853                 };
 854 
 855@@ -983,23 +1003,23 @@ const Daemon = struct {
 856                     // 500ms SIGHUP->SIGKILL grace sleep. Otherwise a `zmx run`
 857                     // for the same name issued in that window will hang waiting
 858                     // for a connect.
 859-                    posix.close(server_sock_fd);
 860+                    lib_posix.close(server_sock_fd);
 861                     std.log.info("deleting socket file session={s}", .{self.session_name});
 862-                    dir.deleteFile(self.session_name) catch |err| {
 863+                    dir.deleteFile(self.io, self.session_name) catch |err| {
 864                         std.log.warn("failed to delete socket file err={s}", .{@errorName(err)});
 865                     };
 866                     self.handleKill();
 867                     self.deinit();
 868-                    posix.close(pty_fd);
 869-                    _ = posix.waitpid(self.pid, 0);
 870+                    lib_posix.close(pty_fd);
 871+                    _ = lib_posix.waitpid(self.pid, 0);
 872                 }
 873 
 874                 try daemonLoop(self, server_sock_fd, pty_fd);
 875                 std.log.info("daemon loop shutdown", .{});
 876                 return .{ .created = true, .is_daemon = true };
 877             }
 878-            posix.close(server_sock_fd);
 879-            std.Thread.sleep(10 * std.time.ns_per_ms);
 880+            lib_posix.close(server_sock_fd);
 881+            std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(10), .real) catch unreachable;
 882             return .{ .created = true, .is_daemon = false };
 883         }
 884 
 885@@ -1204,7 +1224,7 @@ const Daemon = struct {
 886         posix.kill(-self.pid, posix.SIG.HUP) catch |err| {
 887             std.log.warn("failed to send SIGHUP to pty child err={s}", .{@errorName(err)});
 888         };
 889-        std.Thread.sleep(500 * std.time.ns_per_ms);
 890+        std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(500), .real) catch unreachable;
 891         posix.kill(-self.pid, posix.SIG.KILL) catch |err| {
 892             std.log.warn("failed to send SIGKILL to pty child err={s}", .{@errorName(err)});
 893         };
 894@@ -1265,7 +1285,7 @@ const Daemon = struct {
 895         payload: []const u8,
 896     ) !void {
 897         const format: util.HistoryFormat = if (payload.len > 0)
 898-            std.meta.intToEnum(util.HistoryFormat, payload[0]) catch .plain
 899+            @enumFromInt(payload[0])
 900         else
 901             .plain;
 902         if (util.serializeTerminal(self.alloc, term, format)) |output| {
 903@@ -1385,6 +1405,7 @@ test "send queues PTY input without changing leader" {
 904         .leader_client_fd = 42,
 905         .session_name = "test",
 906         .socket_path = "",
 907+        .io = std.testing.io,
 908         .running = true,
 909         .pid = 0,
 910         .created_at = 0,
 911@@ -1397,9 +1418,9 @@ test "send queues PTY input without changing leader" {
 912     try std.testing.expectEqualStrings("hello", daemon.pty_write_buf.items);
 913 }
 914 
 915-fn printVersion(cfg: *Cfg) !void {
 916+fn printVersion(io: std.Io, cfg: *Cfg) !void {
 917     var buf: [256]u8 = undefined;
 918-    var w = std.fs.File.stdout().writer(&buf);
 919+    var w = std.Io.File.stdout().writer(io, &buf);
 920     try w.interface.print(
 921         "zmx\t\t{s}\nghostty_vt\t{s}\nsocket_dir\t{s}\nlog_dir\t\t{s}\n",
 922         .{ version, ghostty_version, cfg.socket_dir, cfg.log_dir },
 923@@ -1407,15 +1428,15 @@ fn printVersion(cfg: *Cfg) !void {
 924     try w.interface.flush();
 925 }
 926 
 927-fn printCompletions(shell: completions.Shell) !void {
 928+fn printCompletions(io: std.Io, shell: completions.Shell) !void {
 929     const script = shell.getCompletionScript();
 930     var buf: [8192]u8 = undefined;
 931-    var w = std.fs.File.stdout().writer(&buf);
 932+    var w = std.Io.File.stdout().writer(io, &buf);
 933     try w.interface.print("{s}\n", .{script});
 934     try w.interface.flush();
 935 }
 936 
 937-fn help() !void {
 938+fn help(io: std.Io) !void {
 939     const help_text =
 940         \\zmx - session persistence for terminal processes
 941         \\
 942@@ -1553,16 +1574,12 @@ fn help() !void {
 943         \\
 944     ;
 945     var buf: [8192]u8 = undefined;
 946-    var w = std.fs.File.stdout().writer(&buf);
 947+    var w = std.Io.File.stdout().writer(io, &buf);
 948     try w.interface.print(help_text, .{});
 949     try w.interface.flush();
 950 }
 951 
 952-fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) !u8 {
 953-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 954-    defer _ = gpa.deinit();
 955-    const alloc = gpa.allocator();
 956-
 957+fn tail(alloc: std.mem.Allocator, client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool) !u8 {
 958     var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(alloc, 4);
 959     defer poll_fds.deinit(alloc);
 960 
 961@@ -1625,7 +1642,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
 962                     switch (msg.header.tag) {
 963                         .Ack => {
 964                             if (detached) {
 965-                                _ = posix.write(posix.STDOUT_FILENO, "command sent!\n") catch |err| blk: {
 966+                                _ = lib_posix.write(posix.STDOUT_FILENO, "command sent!\n") catch |err| blk: {
 967                                     if (err == error.WouldBlock) break :blk 0;
 968                                     return err;
 969                                 };
 970@@ -1687,7 +1704,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
 971         if (task_complete_code) |exit_code| {
 972             // Flush any remaining output before returning
 973             flush_loop: while (stdout_buf.items.len > 0) {
 974-                const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| {
 975+                const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| {
 976                     if (err == error.WouldBlock) break :flush_loop;
 977                     return err;
 978                 };
 979@@ -1697,7 +1714,7 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
 980         }
 981 
 982         if (stdout_buf.items.len > 0) {
 983-            const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
 984+            const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
 985                 if (err == error.WouldBlock) break :blk 0;
 986                 return err;
 987             };
 988@@ -1715,17 +1732,13 @@ fn tail(client_socket_fds: std.ArrayList(i32), detached: bool, is_run_cmd: bool)
 989     }
 990 }
 991 
 992-fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
 993-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 994-    defer _ = gpa.deinit();
 995-    const alloc = gpa.allocator();
 996-
 997+fn wait(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
 998     var stdout_buffer: [1024]u8 = undefined;
 999-    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
1000+    var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
1001     const stdout = &stdout_writer.interface;
1002 
1003     var stderr_buffer: [1024]u8 = undefined;
1004-    var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
1005+    var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
1006     const stderr = &stderr_writer.interface;
1007 
1008     // Highest match count seen so far. Lets us distinguish "sessions haven't
1009@@ -1735,11 +1748,11 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1010     var zero_match_iters: u32 = 0;
1011 
1012     var agg_exit_code: u8 = 0;
1013-    var last_print: i64 = 0;
1014+    var last_print: i96 = 0;
1015     var prev_done: i32 = 0;
1016     while (true) {
1017         agg_exit_code = 0;
1018-        var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1019+        var sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
1020         var total: i32 = 0;
1021         var done: i32 = 0;
1022 
1023@@ -1763,7 +1776,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1024                 // waiting". Count it as done+failed so wait terminates.
1025                 try stderr.print(
1026                     "[{d}] task unreachable: {s} ({s})\n",
1027-                    .{ std.time.timestamp(), session.name, session.error_name orelse "unknown" },
1028+                    .{ std.Io.Timestamp.now(io, .real).nanoseconds, session.name, session.error_name orelse "unknown" },
1029                 );
1030                 try stderr.flush();
1031                 agg_exit_code = 1;
1032@@ -1771,7 +1784,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1033                 continue;
1034             }
1035             if (session.task_ended_at == 0) {
1036-                const now = std.time.timestamp();
1037+                const now = std.Io.Timestamp.now(io, .real).nanoseconds;
1038                 if (now - last_print >= 5) {
1039                     try stdout.print(
1040                         "[{d}] waiting task={s}\n",
1041@@ -1834,7 +1847,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1042         }
1043 
1044         prev_done = done;
1045-        std.Thread.sleep(1000 * std.time.ns_per_ms);
1046+        std.Io.sleep(io, std.Io.Duration.fromMilliseconds(1000), .real) catch unreachable;
1047     }
1048 
1049     if (agg_exit_code == 0) {
1050@@ -1844,7 +1857,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1051     }
1052     try stdout.flush();
1053 
1054-    const sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1055+    const sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
1056     for (sessions.items) |session| {
1057         var found = false;
1058         for (matchers.items) |m| {
1059@@ -1866,7 +1879,7 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1060 
1061             // Fetch and print the last 20 lines of history for debugging
1062             const history_lines: usize = 20;
1063-            const history_text = fetchHistory(alloc, cfg, session.name) catch null;
1064+            const history_text = fetchHistory(alloc, io, cfg, session.name) catch null;
1065             if (history_text) |text| {
1066                 defer alloc.free(text);
1067                 try stdout.print("\nLast {d} lines of {s} history:\n", .{ history_lines, session.name });
1068@@ -1897,15 +1910,11 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
1069     std.process.exit(agg_exit_code);
1070 }
1071 
1072-fn list(cfg: *Cfg, short: bool) !void {
1073-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1074-    defer _ = gpa.deinit();
1075-    const alloc = gpa.allocator();
1076-
1077+fn list(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, short: bool) !void {
1078     const current_session = socket.getSeshNameFromEnv();
1079     var buf: [4096]u8 = undefined;
1080-    var stdout = std.fs.File.stdout().writer(&buf);
1081-    var sessions = try util.get_session_entries(alloc, cfg.socket_dir);
1082+    var stdout = std.Io.File.stdout().writer(io, &buf);
1083+    var sessions = try util.get_session_entries(alloc, io, cfg.socket_dir);
1084     defer {
1085         for (sessions.items) |session| {
1086             session.deinit(alloc);
1087@@ -1916,7 +1925,7 @@ fn list(cfg: *Cfg, short: bool) !void {
1088     if (sessions.items.len == 0) {
1089         if (short) return;
1090         var errbuf: [4096]u8 = undefined;
1091-        var stderr = std.fs.File.stderr().writer(&errbuf);
1092+        var stderr = std.Io.File.stderr().writer(io, &errbuf);
1093         try stderr.interface.print("no sessions found in {s}\n", .{cfg.socket_dir});
1094         try stderr.interface.flush();
1095         return;
1096@@ -1936,10 +1945,7 @@ fn list(cfg: *Cfg, short: bool) !void {
1097     }
1098 }
1099 
1100-fn detachAll(cfg: *Cfg) !void {
1101-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1102-    defer _ = gpa.deinit();
1103-    const alloc = gpa.allocator();
1104+fn detachAll(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg) !void {
1105     const session_name = socket.getSeshNameFromEnv();
1106     if (session_name.len == 0) {
1107         std.log.err("ZMX_SESSION env var not found: are you inside a zmx session?", .{});
1108@@ -1947,45 +1953,41 @@ fn detachAll(cfg: *Cfg) !void {
1109     }
1110     std.log.info("detach all session={s}", .{session_name});
1111 
1112-    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1113-    defer dir.close();
1114+    var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1115+    defer dir.close(io);
1116 
1117     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1118-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1119+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1120         error.OutOfMemory => return err,
1121     };
1122     defer alloc.free(socket_path);
1123     const fd = ipc.connectSession(socket_path) catch |err| {
1124         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1125-        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
1126+        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
1127         return;
1128     };
1129-    defer posix.close(fd);
1130+    defer lib_posix.close(fd);
1131     ipc.send(fd, .DetachAll, "") catch |err| switch (err) {
1132         error.BrokenPipe, error.ConnectionResetByPeer => return,
1133         else => return err,
1134     };
1135 }
1136 
1137-fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
1138+fn kill(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, force: bool) !void {
1139     std.log.info("kill session={s}", .{session_name});
1140-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1141-    defer _ = gpa.deinit();
1142-    const alloc = gpa.allocator();
1143-
1144     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1145-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1146+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1147         error.OutOfMemory => return err,
1148     };
1149     defer alloc.free(socket_path);
1150 
1151-    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1152-    defer dir.close();
1153+    var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1154+    defer dir.close(io);
1155 
1156-    const exists = try socket.sessionExists(dir, session_name);
1157+    const exists = try socket.sessionExists(io, dir, session_name);
1158     if (!exists) {
1159         var buf: [4096]u8 = undefined;
1160-        var w = std.fs.File.stderr().writer(&buf);
1161+        var w = std.Io.File.stderr().writer(io, &buf);
1162         w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {};
1163         w.interface.flush() catch {};
1164         return error.SessionNotFound;
1165@@ -1993,9 +1995,9 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
1166     const fd = ipc.connectSession(socket_path) catch |err| {
1167         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1168         var buf: [4096]u8 = undefined;
1169-        var w = std.fs.File.stdout().writer(&buf);
1170+        var w = std.Io.File.stdout().writer(io, &buf);
1171         if (force or err == error.ConnectionRefused) {
1172-            socket.cleanupStaleSocket(dir, session_name);
1173+            socket.cleanupStaleSocket(io, dir, session_name);
1174             w.interface.print("cleaned up stale session {s}\n", .{session_name}) catch {};
1175         } else {
1176             w.interface.print(
1177@@ -2007,7 +2009,7 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
1178         return;
1179     };
1180 
1181-    defer posix.close(fd);
1182+    defer lib_posix.close(fd);
1183     ipc.send(fd, .Kill, "") catch |err| switch (err) {
1184         error.BrokenPipe, error.ConnectionResetByPeer => return,
1185         else => return err,
1186@@ -2025,14 +2027,14 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
1187     }
1188 
1189     var buf: [100]u8 = undefined;
1190-    var w = std.fs.File.stdout().writer(&buf);
1191+    var w = std.Io.File.stdout().writer(io, &buf);
1192     try w.interface.print("killed session {s}\n", .{session_name});
1193     try w.interface.flush();
1194 }
1195 
1196-fn printLabelError(session_name: []const u8, err: anyerror) noreturn {
1197+fn printLabelError(io: std.Io, session_name: []const u8, err: anyerror) noreturn {
1198     var buf: [4096]u8 = undefined;
1199-    var w = std.fs.File.stderr().writer(&buf);
1200+    var w = std.Io.File.stderr().writer(io, &buf);
1201     switch (err) {
1202         error.Timeout => w.interface.print(
1203             "error: session \"{s}\" does not support labels (daemon too old?)\n",
1204@@ -2051,25 +2053,22 @@ fn printLabelError(session_name: []const u8, err: anyerror) noreturn {
1205     std.process.exit(1);
1206 }
1207 
1208-fn labelGet(cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
1209+fn labelGet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
1210     std.log.info("label get session={s}", .{session_name});
1211-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1212-    defer _ = gpa.deinit();
1213-    const alloc = gpa.allocator();
1214 
1215     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1216-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1217+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1218         error.OutOfMemory => return err,
1219     };
1220     defer alloc.free(socket_path);
1221 
1222     const payload = ipc.roundTripForTag(alloc, socket_path, .LabelGet, "", .LabelData) catch |err| {
1223-        printLabelError(session_name, err);
1224+        printLabelError(io, session_name, err);
1225     };
1226     defer alloc.free(payload);
1227 
1228     var buf: [4096]u8 = undefined;
1229-    var stdout = std.fs.File.stdout().writer(&buf);
1230+    var stdout = std.Io.File.stdout().writer(io, &buf);
1231     if (single_kv.len == 0) {
1232         try stdout.interface.print("{s}", .{payload});
1233         try stdout.interface.flush();
1234@@ -2081,17 +2080,14 @@ fn labelGet(cfg: *Cfg, session_name: []const u8, single_kv: []const u8) !void {
1235     try stdout.interface.flush();
1236 }
1237 
1238-fn labelSet(cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
1239+fn labelSet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
1240     std.log.info("label set session={s}", .{session_name});
1241-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1242-    defer _ = gpa.deinit();
1243-    const alloc = gpa.allocator();
1244 
1245     var kvs = label.LabelIterator.init(labels);
1246     while (kvs.next()) |kv| {
1247         label.assertLabel(kv.key, kv.value) catch |err| {
1248             var buf: [4096]u8 = undefined;
1249-            var w = std.fs.File.stderr().writer(&buf);
1250+            var w = std.Io.File.stderr().writer(io, &buf);
1251             const msg = "error: key-value kvs can only contain [a-z, A-Z, 0-9, -_.] characters";
1252             switch (err) {
1253                 error.LabelKeyEmpty => {
1254@@ -2113,30 +2109,27 @@ fn labelSet(cfg: *Cfg, session_name: []const u8, labels: []const u8) !void {
1255     }
1256 
1257     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1258-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1259+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1260         error.OutOfMemory => return err,
1261     };
1262     defer alloc.free(socket_path);
1263 
1264     _ = ipc.roundTripForTag(alloc, socket_path, .LabelSet, labels, .Ack) catch |err| {
1265-        printLabelError(session_name, err);
1266+        printLabelError(io, session_name, err);
1267     };
1268 }
1269 
1270-fn labelClear(cfg: *Cfg, session_name: []const u8) !void {
1271+fn labelClear(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8) !void {
1272     std.log.info("label clear session={s}", .{session_name});
1273-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1274-    defer _ = gpa.deinit();
1275-    const alloc = gpa.allocator();
1276 
1277     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1278-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1279+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1280         error.OutOfMemory => return err,
1281     };
1282     defer alloc.free(socket_path);
1283 
1284     _ = ipc.roundTripForTag(alloc, socket_path, .LabelClear, "", .Ack) catch |err| {
1285-        printLabelError(session_name, err);
1286+        printLabelError(io, session_name, err);
1287     };
1288 }
1289 
1290@@ -2144,32 +2137,33 @@ fn labelClear(cfg: *Cfg, session_name: []const u8) !void {
1291 /// string. Caller owns the returned memory and must free it.
1292 fn fetchHistory(
1293     alloc: std.mem.Allocator,
1294+    io: std.Io,
1295     cfg: *Cfg,
1296     session_name: []const u8,
1297 ) ![]const u8 {
1298     std.log.info("fetch history session={s}", .{session_name});
1299     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1300         error.NameTooLong => {
1301-            socket.printSessionNameTooLong(session_name, cfg.socket_dir);
1302+            socket.printSessionNameTooLong(io, session_name, cfg.socket_dir);
1303             return error.NameTooLong;
1304         },
1305         error.OutOfMemory => return err,
1306     };
1307     defer alloc.free(socket_path);
1308 
1309-    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1310-    defer dir.close();
1311+    var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1312+    defer dir.close(io);
1313 
1314-    const exists = try socket.sessionExists(dir, session_name);
1315+    const exists = try socket.sessionExists(io, dir, session_name);
1316     if (!exists) {
1317         return error.SessionNotFound;
1318     }
1319 
1320     const fd = ipc.connectSession(socket_path) catch |err| {
1321-        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
1322+        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
1323         return err;
1324     };
1325-    defer posix.close(fd);
1326+    defer lib_posix.close(fd);
1327 
1328     const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain);
1329     const payload = [_]u8{format_byte};
1330@@ -2205,35 +2199,32 @@ fn fetchHistory(
1331     return error.NoHistoryResponse;
1332 }
1333 
1334-fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
1335+fn history(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
1336     std.log.info("history session={s}", .{session_name});
1337-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
1338-    defer _ = gpa.deinit();
1339-    const alloc = gpa.allocator();
1340 
1341     const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1342-        error.NameTooLong => return socket.printSessionNameTooLong(session_name, cfg.socket_dir),
1343+        error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1344         error.OutOfMemory => return err,
1345     };
1346     defer alloc.free(socket_path);
1347 
1348-    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1349-    defer dir.close();
1350+    var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1351+    defer dir.close(io);
1352 
1353-    const exists = try socket.sessionExists(dir, session_name);
1354+    const exists = try socket.sessionExists(io, dir, session_name);
1355     if (!exists) {
1356         var buf: [4096]u8 = undefined;
1357-        var w = std.fs.File.stderr().writer(&buf);
1358+        var w = std.Io.File.stderr().writer(io, &buf);
1359         w.interface.print("error: session \"{s}\" does not exist\n", .{session_name}) catch {};
1360         w.interface.flush() catch {};
1361         return error.SessionNotFound;
1362     }
1363     const fd = ipc.connectSession(socket_path) catch |err| {
1364         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1365-        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
1366+        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(io, dir, session_name);
1367         return;
1368     };
1369-    defer posix.close(fd);
1370+    defer lib_posix.close(fd);
1371 
1372     const format_byte = [_]u8{@intFromEnum(format)};
1373     ipc.send(fd, .History, &format_byte) catch |err| switch (err) {
1374@@ -2257,7 +2248,7 @@ fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !voi
1375 
1376         while (sb.next()) |msg| {
1377             if (msg.header.tag == .History) {
1378-                _ = posix.write(posix.STDOUT_FILENO, msg.payload) catch return;
1379+                _ = lib_posix.write(posix.STDOUT_FILENO, msg.payload) catch return;
1380                 return;
1381             }
1382         }
1383@@ -2271,28 +2262,28 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void {
1384     std.log.info("switch session cur={s} next={s}", .{ current_sesh, next_session });
1385 
1386     const socket_path = socket.getSocketPath(daemon.alloc, daemon.cfg.socket_dir, current_sesh) catch |err| switch (err) {
1387-        error.NameTooLong => return socket.printSessionNameTooLong(current_sesh, daemon.cfg.socket_dir),
1388+        error.NameTooLong => return socket.printSessionNameTooLong(daemon.io, current_sesh, daemon.cfg.socket_dir),
1389         error.OutOfMemory => return err,
1390     };
1391     defer daemon.alloc.free(socket_path);
1392 
1393-    var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
1394-    defer dir.close();
1395+    var dir = try std.Io.Dir.openDirAbsolute(daemon.io, daemon.cfg.socket_dir, .{});
1396+    defer dir.close(daemon.io);
1397 
1398-    const exists = try socket.sessionExists(dir, current_sesh);
1399+    const exists = try socket.sessionExists(daemon.io, dir, current_sesh);
1400     if (!exists) {
1401         var buf: [4096]u8 = undefined;
1402-        var w = std.fs.File.stderr().writer(&buf);
1403+        var w = std.Io.File.stderr().writer(daemon.io, &buf);
1404         w.interface.print("error: session \"{s}\" does not exist\n", .{current_sesh}) catch {};
1405         w.interface.flush() catch {};
1406         return error.SessionNotFound;
1407     }
1408     const fd = ipc.connectSession(socket_path) catch |err| {
1409         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1410-        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh);
1411+        if (err == error.ConnectionRefused) socket.cleanupStaleSocket(daemon.io, dir, current_sesh);
1412         return;
1413     };
1414-    defer posix.close(fd);
1415+    defer lib_posix.close(fd);
1416 
1417     ipc.send(fd, .Switch, next_session) catch |err| switch (err) {
1418         error.BrokenPipe, error.ConnectionResetByPeer => return,
1419@@ -2331,7 +2322,7 @@ fn attach(daemon: *Daemon) !void {
1420         }
1421         // Reset terminal modes on detach
1422         const restore_seq = "\x1bc";
1423-        _ = posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
1424+        _ = lib_posix.write(posix.STDOUT_FILENO, restore_seq) catch {};
1425     }
1426 
1427     if (stdin_is_tty) {
1428@@ -2355,7 +2346,7 @@ fn attach(daemon: *Daemon) !void {
1429     // Clear screen before attaching. This provides a clean slate before
1430     // the session restore.
1431     const clear_seq = "\x1b[2J\x1b[H";
1432-    _ = try posix.write(posix.STDOUT_FILENO, clear_seq);
1433+    _ = try lib_posix.write(posix.STDOUT_FILENO, clear_seq);
1434 
1435     const looper = try clientLoop(client_sock);
1436     switch (looper.kind) {
1437@@ -2363,13 +2354,15 @@ fn attach(daemon: *Daemon) !void {
1438         .switch_session => {
1439             if (looper.session_name) |session_name| {
1440                 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
1441-                const cwd = std.posix.getcwd(&cwd_buf) catch "";
1442+                const cwd_len = std.process.currentPath(daemon.io, &cwd_buf) catch 0;
1443+                const cwd = cwd_buf[0..cwd_len];
1444                 const target_path = socket.getSocketPath(
1445                     daemon.alloc,
1446                     daemon.cfg.socket_dir,
1447                     session_name,
1448                 ) catch |err| switch (err) {
1449                     error.NameTooLong => return socket.printSessionNameTooLong(
1450+                        daemon.io,
1451                         session_name,
1452                         daemon.cfg.socket_dir,
1453                     ),
1454@@ -2378,6 +2371,7 @@ fn attach(daemon: *Daemon) !void {
1455 
1456                 const clients = try std.ArrayList(*Client).initCapacity(daemon.alloc, 10);
1457                 var target_daemon = Daemon{
1458+                    .io = daemon.io,
1459                     .running = true,
1460                     .cfg = daemon.cfg,
1461                     .alloc = daemon.alloc,
1462@@ -2386,7 +2380,7 @@ fn attach(daemon: *Daemon) !void {
1463                     .socket_path = target_path,
1464                     .pid = undefined,
1465                     .cwd = cwd,
1466-                    .created_at = @intCast(std.time.timestamp()),
1467+                    .created_at = @intCast(std.Io.Timestamp.now(daemon.io, .real).nanoseconds),
1468                     .leader_client_fd = null,
1469                 };
1470                 return attach(&target_daemon);
1471@@ -2397,7 +2391,7 @@ fn attach(daemon: *Daemon) !void {
1472 
1473 fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
1474     var buf: [4096]u8 = undefined;
1475-    var w = std.fs.File.stdout().writer(&buf);
1476+    var w = std.Io.File.stdout().writer(daemon.io, &buf);
1477     const sesh_result = try daemon.ensureSession();
1478     if (sesh_result.is_daemon) return;
1479 
1480@@ -2425,18 +2419,19 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
1481         daemon.session_name,
1482     ) catch |err| switch (err) {
1483         error.NameTooLong => return socket.printSessionNameTooLong(
1484+            daemon.io,
1485             daemon.session_name,
1486             daemon.cfg.socket_dir,
1487         ),
1488         error.OutOfMemory => return err,
1489     };
1490-    var dir = try std.fs.openDirAbsolute(daemon.cfg.socket_dir, .{});
1491-    defer dir.close();
1492+    var dir = try std.Io.Dir.openDirAbsolute(daemon.io, daemon.cfg.socket_dir, .{});
1493+    defer dir.close(daemon.io);
1494 
1495     const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| {
1496         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1497         if (err == error.ConnectionRefused) {
1498-            socket.cleanupStaleSocket(dir, daemon.session_name);
1499+            socket.cleanupStaleSocket(daemon.io, dir, daemon.session_name);
1500             w.interface.print("cleaned up stale session {s}\n", .{daemon.session_name}) catch {};
1501         } else {
1502             w.interface.print(
1503@@ -2483,11 +2478,10 @@ fn writeFile(daemon: *Daemon, file_path: []const u8) !void {
1504     return error.NoAckReceived;
1505 }
1506 
1507-fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts: [][]const u8, tag: ipc.Tag) !void {
1508+fn send(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts: [][]const u8, tag: ipc.Tag) !void {
1509     std.log.info("send session={s}", .{session_name});
1510-    const alloc = std.heap.c_allocator;
1511     var buf: [4096]u8 = undefined;
1512-    var w = std.fs.File.stdout().writer(&buf);
1513+    var w = std.Io.File.stdout().writer(io, &buf);
1514 
1515     var payload = std.ArrayList(u8).empty;
1516     defer payload.deinit(alloc);
1517@@ -2499,16 +2493,16 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
1518         }
1519     } else {
1520         // Read from stdin when no text arguments provided.
1521-        const stdin_fd = posix.STDIN_FILENO;
1522-        if (!std.posix.isatty(stdin_fd)) {
1523+        const stdin_file = std.Io.File.stdin();
1524+        defer stdin_file.close(io);
1525+        var stdin_buf: [4096]u8 = undefined;
1526+        var reader = stdin_file.reader(io, &stdin_buf);
1527+        if (!try stdin_file.isTty(io)) {
1528             while (true) {
1529-                var tmp: [4096]u8 = undefined;
1530-                const n = posix.read(stdin_fd, &tmp) catch |err| {
1531-                    if (err == error.WouldBlock) break;
1532-                    return err;
1533-                };
1534-                if (n == 0) break;
1535-                try payload.appendSlice(alloc, tmp[0..n]);
1536+                var dest: [1024]u8 = undefined;
1537+                const n = try reader.interface.readSliceShort(&dest);
1538+                if (n == 0) break; // EOF
1539+                try payload.appendSlice(alloc, dest[0..n]);
1540             }
1541             // Strip trailing newline from piped input; the caller is
1542             // responsible for including \r when submission is desired.
1543@@ -2521,13 +2515,13 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
1544 
1545     if (payload.items.len == 0) return error.TextRequired;
1546 
1547-    var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
1548-    defer dir.close();
1549+    var dir = try std.Io.Dir.openDirAbsolute(io, cfg.socket_dir, .{});
1550+    defer dir.close(io);
1551 
1552     const probe_result = ipc.probeSession(alloc, socket_path) catch |err| {
1553         std.log.err("session unresponsive: {s}", .{@errorName(err)});
1554         if (err == error.ConnectionRefused) {
1555-            socket.cleanupStaleSocket(dir, session_name);
1556+            socket.cleanupStaleSocket(io, dir, session_name);
1557             try w.interface.print("cleaned up stale session {s}\n", .{session_name});
1558         } else {
1559             try w.interface.print(
1560@@ -2549,7 +2543,7 @@ fn send(cfg: *Cfg, session_name: []const u8, socket_path: []const u8, text_parts
1561 fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
1562     const alloc = daemon.alloc;
1563     var buf: [4096]u8 = undefined;
1564-    var w = std.fs.File.stdout().writer(&buf);
1565+    var w = std.Io.File.stdout().writer(daemon.io, &buf);
1566 
1567     var cmd_to_send: ?[]const u8 = null;
1568     var allocated_cmd: ?[]u8 = null;
1569@@ -2587,19 +2581,19 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
1570         cmd_to_send = try cmd_list.toOwnedSlice(alloc);
1571         allocated_cmd = @constCast(cmd_to_send.?);
1572     } else {
1573-        const stdin_fd = posix.STDIN_FILENO;
1574-        if (!std.posix.isatty(stdin_fd)) {
1575-            var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
1576-            defer stdin_buf.deinit(alloc);
1577-
1578+        // Read from stdin when no text arguments provided.
1579+        const stdin_file = std.Io.File.stdin();
1580+        defer stdin_file.close(daemon.io);
1581+        var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
1582+        defer stdin_buf.deinit(alloc);
1583+        var stdbuf: [4096]u8 = undefined;
1584+        var reader = stdin_file.reader(daemon.io, &stdbuf);
1585+        if (!try stdin_file.isTty(daemon.io)) {
1586             while (true) {
1587-                var tmp: [4096]u8 = undefined;
1588-                const n = posix.read(stdin_fd, &tmp) catch |err| {
1589-                    if (err == error.WouldBlock) break;
1590-                    return err;
1591-                };
1592-                if (n == 0) break;
1593-                try stdin_buf.appendSlice(alloc, tmp[0..n]);
1594+                var dest: [1024]u8 = undefined;
1595+                const n = try reader.interface.readSliceShort(&dest);
1596+                if (n == 0) break; // EOF
1597+                try stdin_buf.appendSlice(alloc, dest[0..n]);
1598             }
1599 
1600             if (stdin_buf.items.len > 0) {
1601@@ -2615,6 +2609,35 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
1602                 allocated_cmd = @constCast(cmd_to_send.?);
1603             }
1604         }
1605+
1606+        // const stdin_fd = posix.STDIN_FILENO;
1607+        // if (!lib_posix.isatty(stdin_fd)) {
1608+        //     var stdin_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
1609+        //     defer stdin_buf.deinit(alloc);
1610+
1611+        //     while (true) {
1612+        //         var tmp: [4096]u8 = undefined;
1613+        //         const n = posix.read(stdin_fd, &tmp) catch |err| {
1614+        //             if (err == error.WouldBlock) break;
1615+        //             return err;
1616+        //         };
1617+        //         if (n == 0) break;
1618+        //         try stdin_buf.appendSlice(alloc, tmp[0..n]);
1619+        //     }
1620+
1621+        //     if (stdin_buf.items.len > 0) {
1622+        //         // Normalize any trailing newline to CR so readline (raw mode)
1623+        //         // accepts each line.
1624+        //         if (stdin_buf.items[stdin_buf.items.len - 1] == '\n') {
1625+        //             stdin_buf.items[stdin_buf.items.len - 1] = '\r';
1626+        //         } else {
1627+        //             try stdin_buf.append(alloc, '\r');
1628+        //         }
1629+
1630+        //         cmd_to_send = try alloc.dupe(u8, stdin_buf.items);
1631+        //         allocated_cmd = @constCast(cmd_to_send.?);
1632+        //     }
1633+        // }
1634     }
1635 
1636     if (cmd_to_send == null) {
1637@@ -2625,7 +2648,7 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
1638         std.log.err("session not ready: {s}", .{@errorName(err)});
1639         return error.SessionNotReady;
1640     };
1641-    defer posix.close(client_sock);
1642+    defer lib_posix.close(client_sock);
1643 
1644     var fds = try std.ArrayList(i32).initCapacity(alloc, 1);
1645     defer fds.deinit(alloc);
1646@@ -2636,8 +2659,8 @@ fn run(daemon: *Daemon, detached: bool, command_args: [][]const u8) !void {
1647         else => return err,
1648     };
1649 
1650-    const exit_code = try tail(fds, detached, true);
1651-    posix.exit(exit_code);
1652+    const exit_code = try tail(daemon.alloc, fds, detached, true);
1653+    lib_posix.exit(exit_code);
1654 }
1655 
1656 const ClientResult = struct {
1657@@ -2654,15 +2677,15 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
1658     std.log.info("client loop fd={d}", .{client_sock_fd});
1659     // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
1660     const alloc = std.heap.c_allocator;
1661-    defer posix.close(client_sock_fd);
1662+    defer lib_posix.close(client_sock_fd);
1663 
1664     try openSignalPipe();
1665-    installWakeHandler(posix.SIG.WINCH);
1666+    installWakeHandler(@intFromEnum(posix.SIG.WINCH));
1667 
1668     // Make socket non-blocking to avoid blocking on writes
1669-    var sock_flags = try posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
1670+    var sock_flags = try lib_posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
1671     sock_flags |= O_NONBLOCK;
1672-    _ = try posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags);
1673+    _ = try lib_posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags);
1674 
1675     // Buffer for outgoing socket writes
1676     var sock_write_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
1677@@ -2686,9 +2709,9 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
1678     // Make stdin non-blocking. O_NONBLOCK is set on the open file description,
1679     // which is shared with the parent shell; restore on exit to avoid
1680     // corrupting the parent's stdin.
1681-    const stdin_orig_flags = try posix.fcntl(stdin_fd, posix.F.GETFL, 0);
1682-    _ = try posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags | O_NONBLOCK);
1683-    defer _ = posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags) catch {};
1684+    const stdin_orig_flags = try lib_posix.fcntl(stdin_fd, posix.F.GETFL, 0);
1685+    _ = try lib_posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags | O_NONBLOCK);
1686+    defer _ = lib_posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags) catch {};
1687 
1688     while (true) {
1689         poll_fds.clearRetainingCapacity();
1690@@ -2800,7 +2823,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
1691         // Handle socket write (flush buffered messages to daemon)
1692         if (poll_fds.items[1].revents & posix.POLL.OUT != 0) {
1693             if (sock_write_buf.items.len > 0) {
1694-                const n = posix.write(client_sock_fd, sock_write_buf.items) catch |err| blk: {
1695+                const n = lib_posix.write(client_sock_fd, sock_write_buf.items) catch |err| blk: {
1696                     if (err == error.WouldBlock) break :blk 0;
1697                     if (err == error.ConnectionResetByPeer or err == error.BrokenPipe) {
1698                         std.log.info("connection reset or broken pipe", .{});
1699@@ -2815,7 +2838,7 @@ fn clientLoop(client_sock_fd: i32) !ClientResult {
1700         }
1701 
1702         if (stdout_buf.items.len > 0) {
1703-            const n = posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
1704+            const n = lib_posix.write(posix.STDOUT_FILENO, stdout_buf.items) catch |err| blk: {
1705                 if (err == error.WouldBlock) break :blk 0;
1706                 return err;
1707             };
1708@@ -2837,12 +2860,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1709     std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
1710     daemon.pty_fd = pty_fd;
1711     try openSignalPipe();
1712-    installWakeHandler(posix.SIG.TERM);
1713+    installWakeHandler(@intFromEnum(lib_posix.SIG.TERM));
1714     var poll_fds = try std.ArrayList(posix.pollfd).initCapacity(daemon.alloc, 8);
1715     defer poll_fds.deinit(daemon.alloc);
1716 
1717     const init_size = ipc.getTerminalSize(pty_fd);
1718-    var term = try ghostty_vt.Terminal.init(daemon.alloc, .{
1719+    var term = try ghostty_vt.Terminal.init(daemon.io, daemon.alloc, .{
1720         .cols = init_size.cols,
1721         .rows = init_size.rows,
1722         .max_scrollback = daemon.cfg.max_scrollback,
1723@@ -2905,7 +2928,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1724             std.log.err("server socket error revents={d}", .{poll_fds.items[0].revents});
1725             break :daemon_loop;
1726         } else if (poll_fds.items[0].revents & posix.POLL.IN != 0) {
1727-            const client_fd = try posix.accept(
1728+            const client_fd = try lib_posix.accept(
1729                 server_sock_fd,
1730                 null,
1731                 null,
1732@@ -2979,7 +3002,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1733 
1734                         if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| {
1735                             daemon.task_exit_code = exit_code;
1736-                            daemon.task_ended_at = @intCast(std.time.timestamp());
1737+                            daemon.task_ended_at = @intCast(std.Io.Timestamp.now(daemon.io, .real).nanoseconds);
1738 
1739                             std.log.info("task completed exit_code={d}", .{exit_code});
1740 
1741@@ -3018,7 +3041,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1742 
1743         if (poll_fds.items[1].revents & posix.POLL.OUT != 0) {
1744             while (daemon.pty_write_buf.items.len > 0) {
1745-                const n = posix.write(pty_fd, daemon.pty_write_buf.items) catch |err| {
1746+                const n = lib_posix.write(pty_fd, daemon.pty_write_buf.items) catch |err| {
1747                     if (err != error.WouldBlock) {
1748                         std.log.warn("pty write failed: {s}", .{@errorName(err)});
1749                         daemon.pty_write_buf.clearRetainingCapacity();
1750@@ -3102,7 +3125,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1751 
1752             if (revents & posix.POLL.OUT != 0) {
1753                 // Flush pending output buffers
1754-                const n = posix.write(client.socket_fd, client.write_buf.items) catch |err| blk: {
1755+                const n = lib_posix.write(client.socket_fd, client.write_buf.items) catch |err| blk: {
1756                     if (err == error.WouldBlock) break :blk 0;
1757                     // Error on write, close client
1758                     const last = daemon.closeClient(client, i, false);
1759@@ -3127,7 +3150,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
1760     }
1761 }
1762 
1763-fn wakeSignalPipe(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
1764+fn wakeSignalPipe(_: std.os.linux.SIG, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void {
1765     const saved = std.c._errno().*;
1766     _ = std.c.write(sig_pipe[1], "x", 1);
1767     std.c._errno().* = saved;
1768@@ -3142,7 +3165,7 @@ fn installWakeHandler(sig: u6) void {
1769         .mask = posix.sigemptyset(),
1770         .flags = posix.SA.SIGINFO,
1771     };
1772-    posix.sigaction(sig, &act, null);
1773+    posix.sigaction(@as(posix.SIG, @enumFromInt(sig)), &act, null);
1774 }
1775 
1776 fn ignoreSigpipe() void {
+1170, -0
   1@@ -0,0 +1,1170 @@
   2+const builtin = @import("builtin");
   3+const std = @import("std");
   4+const maxInt = std.math.maxInt;
   5+const assert = std.debug.assert;
   6+const mem = std.mem;
   7+const native_os = builtin.os.tag;
   8+const use_libc = builtin.link_libc;
   9+const linux = std.os.linux;
  10+
  11+/// A libc-compatible API layer.
  12+const system = if (use_libc)
  13+    std.c
  14+else switch (native_os) {
  15+    .linux => linux,
  16+    .plan9 => std.os.plan9,
  17+    else => struct {
  18+        pub const ucontext_t = void;
  19+        pub const pid_t = void;
  20+        pub const pollfd = void;
  21+        pub const fd_t = void;
  22+        pub const uid_t = void;
  23+        pub const gid_t = void;
  24+    },
  25+};
  26+
  27+pub const SIG = system.SIG;
  28+const E = system.E;
  29+const PATH_MAX = system.PATH_MAX;
  30+const pid_t = system.pid_t;
  31+const AT = system.AT;
  32+const lfs64_abi = native_os == .linux and builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid());
  33+const uid_t = system.uid_t;
  34+const fd_t = system.fd_t;
  35+const mode_t = system.mode_t;
  36+const socket_t = fd_t;
  37+const SOCK = system.SOCK;
  38+const F = system.F;
  39+const O = system.O;
  40+const AF = system.AF;
  41+const FD_CLOEXEC = system.FD_CLOEXEC;
  42+const sockaddr = system.sockaddr;
  43+pub const socklen_t = system.socklen_t;
  44+
  45+pub fn getuid() uid_t {
  46+    return system.getuid();
  47+}
  48+
  49+/// Get an environment variable.
  50+/// See also `getenvZ`.
  51+pub fn getenv(key: []const u8) ?[:0]const u8 {
  52+    if (mem.indexOfScalar(u8, key, '=') != null) {
  53+        return null;
  54+    }
  55+    if (builtin.link_libc) {
  56+        var ptr = std.c.environ;
  57+        while (ptr[0]) |line| : (ptr += 1) {
  58+            var line_i: usize = 0;
  59+            while (line[line_i] != 0) : (line_i += 1) {
  60+                if (line_i == key.len) break;
  61+                if (line[line_i] != key[line_i]) break;
  62+            }
  63+            if ((line_i != key.len) or (line[line_i] != '=')) continue;
  64+
  65+            return mem.sliceTo(line + line_i + 1, 0);
  66+        }
  67+        return null;
  68+    }
  69+    // The simplified start logic doesn't populate environ.
  70+    if (std.start.simplified_logic) return null;
  71+    // TODO see https://github.com/ziglang/zig/issues/4524
  72+    for (std.os.environ) |ptr| {
  73+        var line_i: usize = 0;
  74+        while (ptr[line_i] != 0) : (line_i += 1) {
  75+            if (line_i == key.len) break;
  76+            if (ptr[line_i] != key[line_i]) break;
  77+        }
  78+        if ((line_i != key.len) or (ptr[line_i] != '=')) continue;
  79+
  80+        return mem.sliceTo(ptr + line_i + 1, 0);
  81+    }
  82+    return null;
  83+}
  84+
  85+const UnexpectedError = error{
  86+    /// The Operating System returned an undocumented error code.
  87+    ///
  88+    /// This error is in theory not possible, but it would be better
  89+    /// to handle this error than to invoke undefined behavior.
  90+    ///
  91+    /// When this error code is observed, it usually means the Zig Standard
  92+    /// Library needs a small patch to add the error code to the error set for
  93+    /// the respective function.
  94+    Unexpected,
  95+};
  96+
  97+const SocketError = error{
  98+    /// Permission to create a socket of the specified type and/or
  99+    /// pro‐tocol is denied.
 100+    AccessDenied,
 101+
 102+    /// The implementation does not support the specified address family.
 103+    AddressFamilyNotSupported,
 104+
 105+    /// Unknown protocol, or protocol family not available.
 106+    ProtocolFamilyNotAvailable,
 107+
 108+    /// The per-process limit on the number of open file descriptors has been reached.
 109+    ProcessFdQuotaExceeded,
 110+
 111+    /// The system-wide limit on the total number of open files has been reached.
 112+    SystemFdQuotaExceeded,
 113+
 114+    /// Insufficient memory is available. The socket cannot be created until sufficient
 115+    /// resources are freed.
 116+    SystemResources,
 117+
 118+    /// The protocol type or the specified protocol is not supported within this domain.
 119+    ProtocolNotSupported,
 120+
 121+    /// The socket type is not supported by the protocol.
 122+    SocketTypeNotSupported,
 123+} || UnexpectedError;
 124+
 125+pub fn socket(domain: u32, socket_type: u32, protocol: u32) SocketError!socket_t {
 126+    const have_sock_flags = !builtin.target.os.tag.isDarwin() and native_os != .haiku;
 127+    const filtered_sock_type = if (!have_sock_flags)
 128+        socket_type & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC)
 129+    else
 130+        socket_type;
 131+    const rc = system.socket(domain, filtered_sock_type, protocol);
 132+    switch (errno(rc)) {
 133+        .SUCCESS => {
 134+            const fd: fd_t = @intCast(rc);
 135+            errdefer close(fd);
 136+            if (!have_sock_flags) {
 137+                try setSockFlags(fd, socket_type);
 138+            }
 139+            return fd;
 140+        },
 141+        .ACCES => return error.AccessDenied,
 142+        .AFNOSUPPORT => return error.AddressFamilyNotSupported,
 143+        .INVAL => return error.ProtocolFamilyNotAvailable,
 144+        .MFILE => return error.ProcessFdQuotaExceeded,
 145+        .NFILE => return error.SystemFdQuotaExceeded,
 146+        .NOBUFS => return error.SystemResources,
 147+        .NOMEM => return error.SystemResources,
 148+        .PROTONOSUPPORT => return error.ProtocolNotSupported,
 149+        .PROTOTYPE => return error.SocketTypeNotSupported,
 150+        else => |err| return unexpectedErrno(err),
 151+    }
 152+}
 153+
 154+pub fn close(fd: fd_t) void {
 155+    return std.Io.Threaded.closeFd(fd);
 156+    // switch (errno(system.close(fd))) {
 157+    //     .BADF => unreachable, // Always a race condition.
 158+    //     .SUCCESS, .INTR => return, // This is still a success. See https://github.com/ziglang/zig/issues/2425
 159+    //     else => return,
 160+    // }
 161+}
 162+
 163+const ConnectError = error{
 164+    /// For UNIX domain sockets, which are identified by pathname: Write permission is denied on  the  socket
 165+    /// file,  or  search  permission  is  denied  for  one of the directories in the path prefix.
 166+    /// or
 167+    /// The user tried to connect to a broadcast address without having the socket broadcast flag enabled  or
 168+    /// the connection request failed because of a local firewall rule.
 169+    AccessDenied,
 170+
 171+    /// See AccessDenied
 172+    PermissionDenied,
 173+
 174+    /// Local address is already in use.
 175+    AddressInUse,
 176+
 177+    /// (Internet  domain  sockets)  The  socket  referred  to  by sockfd had not previously been bound to an
 178+    /// address and, upon attempting to bind it to an ephemeral port, it was determined that all port numbers
 179+    /// in    the    ephemeral    port    range    are   currently   in   use.    See   the   discussion   of
 180+    /// /proc/sys/net/ipv4/ip_local_port_range in ip(7).
 181+    AddressNotAvailable,
 182+
 183+    /// The passed address didn't have the correct address family in its sa_family field.
 184+    AddressFamilyNotSupported,
 185+
 186+    /// Insufficient entries in the routing cache.
 187+    SystemResources,
 188+
 189+    /// A connect() on a stream socket found no one listening on the remote address.
 190+    ConnectionRefused,
 191+
 192+    /// Network is unreachable.
 193+    NetworkUnreachable,
 194+
 195+    /// Timeout  while  attempting  connection.   The server may be too busy to accept new connections.  Note
 196+    /// that for IP sockets the timeout may be very long when syncookies are enabled on the server.
 197+    ConnectionTimedOut,
 198+
 199+    /// This error occurs when no global event loop is configured,
 200+    /// and connecting to the socket would block.
 201+    WouldBlock,
 202+
 203+    /// The given path for the unix socket does not exist.
 204+    FileNotFound,
 205+
 206+    /// Connection was reset by peer before connect could complete.
 207+    ConnectionResetByPeer,
 208+
 209+    /// Socket is non-blocking and already has a pending connection in progress.
 210+    ConnectionPending,
 211+} || UnexpectedError;
 212+
 213+/// Initiate a connection on a socket.
 214+/// If `sockfd` is opened in non blocking mode, the function will
 215+/// return error.WouldBlock when EAGAIN or EINPROGRESS is received.
 216+pub fn connect(sock: socket_t, sock_addr: *const sockaddr, len: socklen_t) ConnectError!void {
 217+    while (true) {
 218+        switch (errno(system.connect(sock, sock_addr, len))) {
 219+            .SUCCESS => return,
 220+            .ACCES => return error.AccessDenied,
 221+            .PERM => return error.PermissionDenied,
 222+            .ADDRINUSE => return error.AddressInUse,
 223+            .ADDRNOTAVAIL => return error.AddressNotAvailable,
 224+            .AFNOSUPPORT => return error.AddressFamilyNotSupported,
 225+            .AGAIN, .INPROGRESS => return error.WouldBlock,
 226+            .ALREADY => return error.ConnectionPending,
 227+            .BADF => unreachable, // sockfd is not a valid open file descriptor.
 228+            .CONNREFUSED => return error.ConnectionRefused,
 229+            .CONNRESET => return error.ConnectionResetByPeer,
 230+            .FAULT => unreachable, // The socket structure address is outside the user's address space.
 231+            .INTR => continue,
 232+            .ISCONN => unreachable, // The socket is already connected.
 233+            .HOSTUNREACH => return error.NetworkUnreachable,
 234+            .NETUNREACH => return error.NetworkUnreachable,
 235+            .NOTSOCK => unreachable, // The file descriptor sockfd does not refer to a socket.
 236+            .PROTOTYPE => unreachable, // The socket type does not support the requested communications protocol.
 237+            .TIMEDOUT => return error.ConnectionTimedOut,
 238+            .NOENT => return error.FileNotFound, // Returned when socket is AF.UNIX and the given path does not exist.
 239+            .CONNABORTED => unreachable, // Tried to reuse socket that previously received error.ConnectionRefused.
 240+            else => |err| return unexpectedErrno(err),
 241+        }
 242+    }
 243+}
 244+
 245+const BindError = error{
 246+    /// The address is protected, and the user is not the superuser.
 247+    /// For UNIX domain sockets: Search permission is denied on  a  component
 248+    /// of  the  path  prefix.
 249+    AccessDenied,
 250+
 251+    /// The given address is already in use, or in the case of Internet domain sockets,
 252+    /// The  port number was specified as zero in the socket
 253+    /// address structure, but, upon attempting to bind to  an  ephemeral  port,  it  was
 254+    /// determined  that  all  port  numbers in the ephemeral port range are currently in
 255+    /// use.  See the discussion of /proc/sys/net/ipv4/ip_local_port_range ip(7).
 256+    AddressInUse,
 257+
 258+    /// A nonexistent interface was requested or the requested address was not local.
 259+    AddressNotAvailable,
 260+
 261+    /// The address is not valid for the address family of socket.
 262+    AddressFamilyNotSupported,
 263+
 264+    /// Too many symbolic links were encountered in resolving addr.
 265+    SymLinkLoop,
 266+
 267+    /// addr is too long.
 268+    NameTooLong,
 269+
 270+    /// A component in the directory prefix of the socket pathname does not exist.
 271+    FileNotFound,
 272+
 273+    /// Insufficient kernel memory was available.
 274+    SystemResources,
 275+
 276+    /// A component of the path prefix is not a directory.
 277+    NotDir,
 278+
 279+    /// The socket inode would reside on a read-only filesystem.
 280+    ReadOnlyFileSystem,
 281+
 282+    /// The network subsystem has failed.
 283+    NetworkSubsystemFailed,
 284+
 285+    FileDescriptorNotASocket,
 286+
 287+    AlreadyBound,
 288+} || UnexpectedError;
 289+
 290+/// addr is `*const T` where T is one of the sockaddr
 291+pub fn bind(sock: socket_t, addr: *const sockaddr, len: socklen_t) BindError!void {
 292+    const rc = system.bind(sock, addr, len);
 293+    switch (errno(rc)) {
 294+        .SUCCESS => return,
 295+        .ACCES, .PERM => return error.AccessDenied,
 296+        .ADDRINUSE => return error.AddressInUse,
 297+        .BADF => unreachable, // always a race condition if this error is returned
 298+        .INVAL => unreachable, // invalid parameters
 299+        .NOTSOCK => unreachable, // invalid `sockfd`
 300+        .AFNOSUPPORT => return error.AddressFamilyNotSupported,
 301+        .ADDRNOTAVAIL => return error.AddressNotAvailable,
 302+        .FAULT => unreachable, // invalid `addr` pointer
 303+        .LOOP => return error.SymLinkLoop,
 304+        .NAMETOOLONG => return error.NameTooLong,
 305+        .NOENT => return error.FileNotFound,
 306+        .NOMEM => return error.SystemResources,
 307+        .NOTDIR => return error.NotDir,
 308+        .ROFS => return error.ReadOnlyFileSystem,
 309+        else => |err| return unexpectedErrno(err),
 310+    }
 311+}
 312+
 313+const ListenError = error{
 314+    /// Another socket is already listening on the same port.
 315+    /// For Internet domain sockets, the  socket referred to by sockfd had not previously
 316+    /// been bound to an address and, upon attempting to bind it to an ephemeral port, it
 317+    /// was determined that all port numbers in the ephemeral port range are currently in
 318+    /// use.  See the discussion of /proc/sys/net/ipv4/ip_local_port_range in ip(7).
 319+    AddressInUse,
 320+
 321+    /// The file descriptor sockfd does not refer to a socket.
 322+    FileDescriptorNotASocket,
 323+
 324+    /// The socket is not of a type that supports the listen() operation.
 325+    OperationNotSupported,
 326+
 327+    /// The network subsystem has failed.
 328+    NetworkSubsystemFailed,
 329+
 330+    /// Ran out of system resources
 331+    /// On Windows it can either run out of socket descriptors or buffer space
 332+    SystemResources,
 333+
 334+    /// Already connected
 335+    AlreadyConnected,
 336+
 337+    /// Socket has not been bound yet
 338+    SocketNotBound,
 339+} || UnexpectedError;
 340+
 341+pub fn listen(sock: socket_t, backlog: u31) ListenError!void {
 342+    const rc = system.listen(sock, backlog);
 343+    switch (errno(rc)) {
 344+        .SUCCESS => return,
 345+        .ADDRINUSE => return error.AddressInUse,
 346+        .BADF => unreachable,
 347+        .NOTSOCK => return error.FileDescriptorNotASocket,
 348+        .OPNOTSUPP => return error.OperationNotSupported,
 349+        else => |err| return unexpectedErrno(err),
 350+    }
 351+}
 352+
 353+/// Obtains errno from the return value of a system function call.
 354+///
 355+/// For some systems this will obtain the value directly from the syscall return value;
 356+/// for others it will use a thread-local errno variable. Therefore, this
 357+/// function only returns a well-defined value when it is called directly after
 358+/// the system function call whose errno value is intended to be observed.
 359+fn errno(rc: anytype) E {
 360+    if (use_libc) {
 361+        return if (rc == -1) @enumFromInt(std.c._errno().*) else .SUCCESS;
 362+    }
 363+    const signed: isize = @bitCast(rc);
 364+    const int = if (signed > -4096 and signed < 0) -signed else 0;
 365+    return @enumFromInt(int);
 366+}
 367+
 368+fn setSockFlags(sock: socket_t, flags: u32) !void {
 369+    if ((flags & SOCK.CLOEXEC) != 0) {
 370+        var fd_flags = fcntl(sock, F.GETFD, 0) catch |err| switch (err) {
 371+            error.FileBusy => unreachable,
 372+            error.Locked => unreachable,
 373+            error.PermissionDenied => unreachable,
 374+            error.DeadLock => unreachable,
 375+            error.LockedRegionLimitExceeded => unreachable,
 376+            else => |e| return e,
 377+        };
 378+        fd_flags |= FD_CLOEXEC;
 379+        _ = fcntl(sock, F.SETFD, fd_flags) catch |err| switch (err) {
 380+            error.FileBusy => unreachable,
 381+            error.Locked => unreachable,
 382+            error.PermissionDenied => unreachable,
 383+            error.DeadLock => unreachable,
 384+            error.LockedRegionLimitExceeded => unreachable,
 385+            else => |e| return e,
 386+        };
 387+    }
 388+    if ((flags & SOCK.NONBLOCK) != 0) {
 389+        var fl_flags = fcntl(sock, F.GETFL, 0) catch |err| switch (err) {
 390+            error.FileBusy => unreachable,
 391+            error.Locked => unreachable,
 392+            error.PermissionDenied => unreachable,
 393+            error.DeadLock => unreachable,
 394+            error.LockedRegionLimitExceeded => unreachable,
 395+            else => |e| return e,
 396+        };
 397+        fl_flags |= 1 << @bitOffsetOf(O, "NONBLOCK");
 398+        _ = fcntl(sock, F.SETFL, fl_flags) catch |err| switch (err) {
 399+            error.FileBusy => unreachable,
 400+            error.Locked => unreachable,
 401+            error.PermissionDenied => unreachable,
 402+            error.DeadLock => unreachable,
 403+            error.LockedRegionLimitExceeded => unreachable,
 404+            else => |e| return e,
 405+        };
 406+    }
 407+}
 408+
 409+const FcntlError = error{
 410+    PermissionDenied,
 411+    FileBusy,
 412+    ProcessFdQuotaExceeded,
 413+    Locked,
 414+    DeadLock,
 415+    LockedRegionLimitExceeded,
 416+} || UnexpectedError;
 417+
 418+pub fn fcntl(fd: fd_t, cmd: i32, arg: usize) FcntlError!usize {
 419+    while (true) {
 420+        const rc = system.fcntl(fd, cmd, arg);
 421+        switch (errno(rc)) {
 422+            .SUCCESS => return @intCast(rc),
 423+            .INTR => continue,
 424+            .AGAIN, .ACCES => return error.Locked,
 425+            .BADF => unreachable,
 426+            .BUSY => return error.FileBusy,
 427+            .INVAL => unreachable, // invalid parameters
 428+            .PERM => return error.PermissionDenied,
 429+            .MFILE => return error.ProcessFdQuotaExceeded,
 430+            .NOTDIR => unreachable, // invalid parameter
 431+            .DEADLK => return error.DeadLock,
 432+            .NOLCK => return error.LockedRegionLimitExceeded,
 433+            else => |err| return unexpectedErrno(err),
 434+        }
 435+    }
 436+}
 437+
 438+const WriteError = error{
 439+    DiskQuota,
 440+    FileTooBig,
 441+    InputOutput,
 442+    NoSpaceLeft,
 443+    DeviceBusy,
 444+    InvalidArgument,
 445+
 446+    /// File descriptor does not hold the required rights to write to it.
 447+    AccessDenied,
 448+    PermissionDenied,
 449+    BrokenPipe,
 450+    SystemResources,
 451+    OperationAborted,
 452+    NotOpenForWriting,
 453+
 454+    /// The process cannot access the file because another process has locked
 455+    /// a portion of the file. Windows-only.
 456+    LockViolation,
 457+
 458+    /// This error occurs when no global event loop is configured,
 459+    /// and reading from the file descriptor would block.
 460+    WouldBlock,
 461+
 462+    /// Connection reset by peer.
 463+    ConnectionResetByPeer,
 464+
 465+    /// This error occurs in Linux if the process being written to
 466+    /// no longer exists.
 467+    ProcessNotFound,
 468+    /// This error occurs when a device gets disconnected before or mid-flush
 469+    /// while it's being written to - errno(6): No such device or address.
 470+    NoDevice,
 471+
 472+    /// The socket type requires that message be sent atomically, and the size of the message
 473+    /// to be sent made this impossible. The message is not transmitted.
 474+    MessageTooBig,
 475+} || UnexpectedError;
 476+
 477+/// Write to a file descriptor.
 478+/// Retries when interrupted by a signal.
 479+/// Returns the number of bytes written. If nonzero bytes were supplied, this will be nonzero.
 480+///
 481+/// Note that a successful write() may transfer fewer than count bytes.  Such partial  writes  can
 482+/// occur  for  various reasons; for example, because there was insufficient space on the disk
 483+/// device to write all of the requested bytes, or because a blocked write() to a socket,  pipe,  or
 484+/// similar  was  interrupted by a signal handler after it had transferred some, but before it had
 485+/// transferred all of the requested bytes.  In the event of a partial write, the caller can  make
 486+/// another  write() call to transfer the remaining bytes.  The subsequent call will either
 487+/// transfer further bytes or may result in an error (e.g., if the disk is now full).
 488+///
 489+/// For POSIX systems, if `fd` is opened in non blocking mode, the function will
 490+/// return error.WouldBlock when EAGAIN is received.
 491+/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are
 492+/// used to perform the I/O. `error.WouldBlock` is not possible on Windows.
 493+///
 494+/// Linux has a limit on how many bytes may be transferred in one `write` call, which is `0x7ffff000`
 495+/// on both 64-bit and 32-bit systems. This is due to using a signed C int as the return value, as
 496+/// well as stuffing the errno codes into the last `4096` values. This is noted on the `write` man page.
 497+/// The limit on Darwin is `0x7fffffff`, trying to read more than that returns EINVAL.
 498+/// The corresponding POSIX limit is `maxInt(isize)`.
 499+pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize {
 500+    if (bytes.len == 0) return 0;
 501+    const max_count = switch (native_os) {
 502+        .linux => 0x7ffff000,
 503+        .macos, .ios, .watchos, .tvos, .visionos => maxInt(i32),
 504+        else => maxInt(isize),
 505+    };
 506+    while (true) {
 507+        const rc = system.write(fd, bytes.ptr, @min(bytes.len, max_count));
 508+        switch (errno(rc)) {
 509+            .SUCCESS => return @intCast(rc),
 510+            .INTR => continue,
 511+            .INVAL => return error.InvalidArgument,
 512+            .FAULT => unreachable,
 513+            .SRCH => return error.ProcessNotFound,
 514+            .AGAIN => return error.WouldBlock,
 515+            .BADF => return error.NotOpenForWriting, // can be a race condition.
 516+            .DESTADDRREQ => unreachable, // `connect` was never called.
 517+            .DQUOT => return error.DiskQuota,
 518+            .FBIG => return error.FileTooBig,
 519+            .IO => return error.InputOutput,
 520+            .NOSPC => return error.NoSpaceLeft,
 521+            .ACCES => return error.AccessDenied,
 522+            .PERM => return error.PermissionDenied,
 523+            .PIPE => return error.BrokenPipe,
 524+            .CONNRESET => return error.ConnectionResetByPeer,
 525+            .BUSY => return error.DeviceBusy,
 526+            .NXIO => return error.NoDevice,
 527+            .MSGSIZE => return error.MessageTooBig,
 528+            else => |err| return unexpectedErrno(err),
 529+        }
 530+    }
 531+}
 532+
 533+pub const ForkError = error{SystemResources} || UnexpectedError;
 534+
 535+pub fn fork() ForkError!pid_t {
 536+    const rc = system.fork();
 537+    switch (errno(rc)) {
 538+        .SUCCESS => return @intCast(rc),
 539+        .AGAIN => return error.SystemResources,
 540+        .NOMEM => return error.SystemResources,
 541+        else => |err| return unexpectedErrno(err),
 542+    }
 543+}
 544+
 545+const SetSidError = error{
 546+    /// The calling process is already a process group leader, or the process group ID of a process other than the calling process matches the process ID of the calling process.
 547+    PermissionDenied,
 548+} || UnexpectedError;
 549+
 550+pub fn setsid() SetSidError!pid_t {
 551+    const rc = system.setsid();
 552+    switch (errno(rc)) {
 553+        .SUCCESS => return rc,
 554+        .PERM => return error.PermissionDenied,
 555+        else => |err| return unexpectedErrno(err),
 556+    }
 557+}
 558+
 559+/// Open and possibly create a file. Keeps trying if it gets interrupted.
 560+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
 561+/// On WASI, `file_path` should be encoded as valid UTF-8.
 562+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
 563+/// See also `open`.
 564+fn openZ(file_path: [*:0]const u8, flags: O, perm: mode_t) OpenError!fd_t {
 565+    const open_sym = if (lfs64_abi) system.open64 else system.open;
 566+    while (true) {
 567+        const rc = open_sym(file_path, flags, perm);
 568+        switch (errno(rc)) {
 569+            .SUCCESS => return @intCast(rc),
 570+            .INTR => continue,
 571+
 572+            .FAULT => unreachable,
 573+            .INVAL => return error.BadPathName,
 574+            .ACCES => return error.AccessDenied,
 575+            .FBIG => return error.FileTooBig,
 576+            .OVERFLOW => return error.FileTooBig,
 577+            .ISDIR => return error.IsDir,
 578+            .LOOP => return error.SymLinkLoop,
 579+            .MFILE => return error.ProcessFdQuotaExceeded,
 580+            .NAMETOOLONG => return error.NameTooLong,
 581+            .NFILE => return error.SystemFdQuotaExceeded,
 582+            .NODEV => return error.NoDevice,
 583+            .NOENT => return error.FileNotFound,
 584+            .SRCH => return error.ProcessNotFound,
 585+            .NOMEM => return error.SystemResources,
 586+            .NOSPC => return error.NoSpaceLeft,
 587+            .NOTDIR => return error.NotDir,
 588+            .PERM => return error.PermissionDenied,
 589+            .EXIST => return error.PathAlreadyExists,
 590+            .BUSY => return error.DeviceBusy,
 591+            .ILSEQ => |err| if (native_os == .wasi)
 592+                return error.InvalidUtf8
 593+            else
 594+                return unexpectedErrno(err),
 595+            else => |err| return unexpectedErrno(err),
 596+        }
 597+    }
 598+}
 599+
 600+/// Open and possibly create a file. Keeps trying if it gets interrupted.
 601+/// `file_path` is relative to the open directory handle `dir_fd`.
 602+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
 603+/// On WASI, `file_path` should be encoded as valid UTF-8.
 604+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
 605+/// See also `openat`.
 606+fn openatZ(dir_fd: fd_t, file_path: [*:0]const u8, flags: O, mode: mode_t) OpenError!fd_t {
 607+    const openat_sym = if (lfs64_abi) system.openat64 else system.openat;
 608+    while (true) {
 609+        const rc = openat_sym(dir_fd, file_path, flags, mode);
 610+        switch (errno(rc)) {
 611+            .SUCCESS => return @intCast(rc),
 612+            .INTR => continue,
 613+
 614+            .FAULT => unreachable,
 615+            .INVAL => return error.BadPathName,
 616+            .BADF => unreachable,
 617+            .ACCES => return error.AccessDenied,
 618+            .FBIG => return error.FileTooBig,
 619+            .OVERFLOW => return error.FileTooBig,
 620+            .ISDIR => return error.IsDir,
 621+            .LOOP => return error.SymLinkLoop,
 622+            .MFILE => return error.ProcessFdQuotaExceeded,
 623+            .NAMETOOLONG => return error.NameTooLong,
 624+            .NFILE => return error.SystemFdQuotaExceeded,
 625+            .NODEV => return error.NoDevice,
 626+            .NOENT => return error.FileNotFound,
 627+            .SRCH => return error.ProcessNotFound,
 628+            .NOMEM => return error.SystemResources,
 629+            .NOSPC => return error.NoSpaceLeft,
 630+            .NOTDIR => return error.NotDir,
 631+            .PERM => return error.PermissionDenied,
 632+            .EXIST => return error.PathAlreadyExists,
 633+            .BUSY => return error.DeviceBusy,
 634+            .OPNOTSUPP => return error.FileLocksNotSupported,
 635+            .AGAIN => return error.WouldBlock,
 636+            .TXTBSY => return error.FileBusy,
 637+            .NXIO => return error.NoDevice,
 638+            .ILSEQ => |err| if (native_os == .wasi)
 639+                return error.InvalidUtf8
 640+            else
 641+                return unexpectedErrno(err),
 642+            else => |err| return unexpectedErrno(err),
 643+        }
 644+    }
 645+}
 646+
 647+/// Open and possibly create a file. Keeps trying if it gets interrupted.
 648+/// `file_path` is relative to the open directory handle `dir_fd`.
 649+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
 650+/// On WASI, `file_path` should be encoded as valid UTF-8.
 651+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
 652+/// See also `openatZ`.
 653+fn openat(dir_fd: fd_t, file_path: []const u8, flags: O, mode: mode_t) OpenError!fd_t {
 654+    const file_path_c = try toPosixPath(file_path);
 655+    return openatZ(dir_fd, &file_path_c, flags, mode);
 656+}
 657+
 658+const OpenError = error{
 659+    /// In WASI, this error may occur when the file descriptor does
 660+    /// not hold the required rights to open a new resource relative to it.
 661+    AccessDenied,
 662+    PermissionDenied,
 663+    SymLinkLoop,
 664+    ProcessFdQuotaExceeded,
 665+    SystemFdQuotaExceeded,
 666+    NoDevice,
 667+    /// Either:
 668+    /// * One of the path components does not exist.
 669+    /// * Cwd was used, but cwd has been deleted.
 670+    /// * The path associated with the open directory handle has been deleted.
 671+    /// * On macOS, multiple processes or threads raced to create the same file
 672+    ///   with `O.EXCL` set to `false`.
 673+    FileNotFound,
 674+
 675+    /// The path exceeded `max_path_bytes` bytes.
 676+    NameTooLong,
 677+
 678+    /// Insufficient kernel memory was available, or
 679+    /// the named file is a FIFO and per-user hard limit on
 680+    /// memory allocation for pipes has been reached.
 681+    SystemResources,
 682+
 683+    /// The file is too large to be opened. This error is unreachable
 684+    /// for 64-bit targets, as well as when opening directories.
 685+    FileTooBig,
 686+
 687+    /// The path refers to directory but the `DIRECTORY` flag was not provided.
 688+    IsDir,
 689+
 690+    /// A new path cannot be created because the device has no room for the new file.
 691+    /// This error is only reachable when the `CREAT` flag is provided.
 692+    NoSpaceLeft,
 693+
 694+    /// A component used as a directory in the path was not, in fact, a directory, or
 695+    /// `DIRECTORY` was specified and the path was not a directory.
 696+    NotDir,
 697+
 698+    /// The path already exists and the `CREAT` and `EXCL` flags were provided.
 699+    PathAlreadyExists,
 700+    DeviceBusy,
 701+
 702+    /// The underlying filesystem does not support file locks
 703+    FileLocksNotSupported,
 704+
 705+    /// Path contains characters that are disallowed by the underlying filesystem.
 706+    BadPathName,
 707+
 708+    /// WASI-only; file paths must be valid UTF-8.
 709+    InvalidUtf8,
 710+
 711+    /// Windows-only; file paths provided by the user must be valid WTF-8.
 712+    /// https://simonsapin.github.io/wtf-8/
 713+    InvalidWtf8,
 714+
 715+    /// On Windows, `\\server` or `\\server\share` was not found.
 716+    NetworkNotFound,
 717+
 718+    /// This error occurs in Linux if the process to be open was not found.
 719+    ProcessNotFound,
 720+
 721+    /// One of these three things:
 722+    /// * pathname  refers to an executable image which is currently being
 723+    ///   executed and write access was requested.
 724+    /// * pathname refers to a file that is currently in  use  as  a  swap
 725+    ///   file, and the O_TRUNC flag was specified.
 726+    /// * pathname  refers  to  a file that is currently being read by the
 727+    ///   kernel (e.g., for module/firmware loading), and write access was
 728+    ///   requested.
 729+    FileBusy,
 730+
 731+    WouldBlock,
 732+} || UnexpectedError;
 733+
 734+/// Open and possibly create a file. Keeps trying if it gets interrupted.
 735+/// On Windows, `file_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
 736+/// On WASI, `file_path` should be encoded as valid UTF-8.
 737+/// On other platforms, `file_path` is an opaque sequence of bytes with no particular encoding.
 738+/// See also `openZ`.
 739+pub fn open(file_path: []const u8, flags: O, perm: mode_t) OpenError!fd_t {
 740+    const file_path_c = try toPosixPath(file_path);
 741+    return openZ(&file_path_c, flags, perm);
 742+}
 743+
 744+pub fn dup2(old_fd: fd_t, new_fd: fd_t) !void {
 745+    while (true) {
 746+        switch (errno(system.dup2(old_fd, new_fd))) {
 747+            .SUCCESS => return,
 748+            .BUSY, .INTR => continue,
 749+            .MFILE => return error.ProcessFdQuotaExceeded,
 750+            .INVAL => unreachable, // invalid parameters passed to dup2
 751+            .BADF => unreachable, // invalid file descriptor
 752+            else => |err| return unexpectedErrno(err),
 753+        }
 754+    }
 755+}
 756+
 757+/// This function ignores PATH environment variable. See `execvpeZ` for that.
 758+fn execveZ(
 759+    path: [*:0]const u8,
 760+    child_argv: [*:null]const ?[*:0]const u8,
 761+    envp: [*:null]const ?[*:0]const u8,
 762+) ExecveError {
 763+    switch (errno(system.execve(path, child_argv, envp))) {
 764+        .SUCCESS => unreachable,
 765+        .FAULT => unreachable,
 766+        .@"2BIG" => return error.SystemResources,
 767+        .MFILE => return error.ProcessFdQuotaExceeded,
 768+        .NAMETOOLONG => return error.NameTooLong,
 769+        .NFILE => return error.SystemFdQuotaExceeded,
 770+        .NOMEM => return error.SystemResources,
 771+        .ACCES => return error.AccessDenied,
 772+        .PERM => return error.PermissionDenied,
 773+        .INVAL => return error.InvalidExe,
 774+        .NOEXEC => return error.InvalidExe,
 775+        .IO => return error.FileSystem,
 776+        .LOOP => return error.FileSystem,
 777+        .ISDIR => return error.IsDir,
 778+        .NOENT => return error.FileNotFound,
 779+        .NOTDIR => return error.NotDir,
 780+        .TXTBSY => return error.FileBusy,
 781+        else => |err| switch (native_os) {
 782+            .macos, .ios, .tvos, .watchos, .visionos => switch (err) {
 783+                .BADEXEC => return error.InvalidExe,
 784+                .BADARCH => return error.InvalidExe,
 785+                else => return unexpectedErrno(err),
 786+            },
 787+            .linux => switch (err) {
 788+                .LIBBAD => return error.InvalidExe,
 789+                else => return unexpectedErrno(err),
 790+            },
 791+            else => return unexpectedErrno(err),
 792+        },
 793+    }
 794+}
 795+
 796+/// Get an environment variable with a null-terminated name.
 797+/// See also `getenv`.
 798+fn getenvZ(key: [*:0]const u8) ?[:0]const u8 {
 799+    if (builtin.link_libc) {
 800+        const value = system.getenv(key) orelse return null;
 801+        return mem.sliceTo(value, 0);
 802+    }
 803+    return getenv(mem.sliceTo(key, 0));
 804+}
 805+
 806+const Arg0Expand = enum {
 807+    expand,
 808+    no_expand,
 809+};
 810+
 811+/// Like `execvpeZ` except if `arg0_expand` is `.expand`, then `argv` is mutable,
 812+/// and `argv[0]` is expanded to be the same absolute path that is passed to the execve syscall.
 813+/// If this function returns with an error, `argv[0]` will be restored to the value it was when it was passed in.
 814+fn execvpeZ_expandArg0(
 815+    comptime arg0_expand: Arg0Expand,
 816+    file: [*:0]const u8,
 817+    child_argv: switch (arg0_expand) {
 818+        .expand => [*:null]?[*:0]const u8,
 819+        .no_expand => [*:null]const ?[*:0]const u8,
 820+    },
 821+    envp: [*:null]const ?[*:0]const u8,
 822+) ExecveError {
 823+    const file_slice = mem.sliceTo(file, 0);
 824+    if (mem.indexOfScalar(u8, file_slice, '/') != null) return execveZ(file, child_argv, envp);
 825+
 826+    const PATH = getenvZ("PATH") orelse "/usr/local/bin:/bin/:/usr/bin";
 827+    // Use of PATH_MAX here is valid as the path_buf will be passed
 828+    // directly to the operating system in execveZ.
 829+    var path_buf: [PATH_MAX]u8 = undefined;
 830+    var it = mem.tokenizeScalar(u8, PATH, ':');
 831+    var seen_eacces = false;
 832+    var err: ExecveError = error.FileNotFound;
 833+
 834+    // In case of expanding arg0 we must put it back if we return with an error.
 835+    const prev_arg0 = child_argv[0];
 836+    defer switch (arg0_expand) {
 837+        .expand => child_argv[0] = prev_arg0,
 838+        .no_expand => {},
 839+    };
 840+
 841+    while (it.next()) |search_path| {
 842+        const path_len = search_path.len + file_slice.len + 1;
 843+        if (path_buf.len < path_len + 1) return error.NameTooLong;
 844+        @memcpy(path_buf[0..search_path.len], search_path);
 845+        path_buf[search_path.len] = '/';
 846+        @memcpy(path_buf[search_path.len + 1 ..][0..file_slice.len], file_slice);
 847+        path_buf[path_len] = 0;
 848+        const full_path = path_buf[0..path_len :0].ptr;
 849+        switch (arg0_expand) {
 850+            .expand => child_argv[0] = full_path,
 851+            .no_expand => {},
 852+        }
 853+        err = execveZ(full_path, child_argv, envp);
 854+        switch (err) {
 855+            error.AccessDenied => seen_eacces = true,
 856+            error.FileNotFound, error.NotDir => {},
 857+            else => |e| return e,
 858+        }
 859+    }
 860+    if (seen_eacces) return error.AccessDenied;
 861+    return err;
 862+}
 863+
 864+const ExecveError = error{
 865+    SystemResources,
 866+    AccessDenied,
 867+    PermissionDenied,
 868+    InvalidExe,
 869+    FileSystem,
 870+    IsDir,
 871+    FileNotFound,
 872+    NotDir,
 873+    FileBusy,
 874+    ProcessFdQuotaExceeded,
 875+    SystemFdQuotaExceeded,
 876+    NameTooLong,
 877+} || UnexpectedError;
 878+
 879+/// This function also uses the PATH environment variable to get the full path to the executable.
 880+/// If `file` is an absolute path, this is the same as `execveZ`.
 881+pub fn execvpeZ(
 882+    file: [*:0]const u8,
 883+    argv_ptr: [*:null]const ?[*:0]const u8,
 884+    envp: [*:null]const ?[*:0]const u8,
 885+) ExecveError {
 886+    return execvpeZ_expandArg0(.no_expand, file, argv_ptr, envp);
 887+}
 888+
 889+/// Exits all threads of the program with the specified status code.
 890+pub fn exit(status: u8) noreturn {
 891+    if (builtin.link_libc) {
 892+        std.c.exit(status);
 893+    }
 894+    if (native_os == .linux and !builtin.single_threaded) {
 895+        linux.exit_group(status);
 896+    }
 897+    if (native_os == .uefi) {
 898+        const uefi = std.os.uefi;
 899+        // exit() is only available if exitBootServices() has not been called yet.
 900+        // This call to exit should not fail, so we catch-ignore errors.
 901+        if (uefi.system_table.boot_services) |bs| {
 902+            bs.exit(uefi.handle, @enumFromInt(status), null) catch {};
 903+        }
 904+        // If we can't exit, reboot the system instead.
 905+        uefi.system_table.runtime_services.resetSystem(.cold, @enumFromInt(status), null);
 906+    }
 907+    system.exit(status);
 908+}
 909+
 910+/// Creates a unidirectional data channel that can be used for interprocess communication.
 911+fn pipe() PipeError![2]fd_t {
 912+    var fds: [2]fd_t = undefined;
 913+    switch (errno(system.pipe(&fds))) {
 914+        .SUCCESS => return fds,
 915+        .INVAL => unreachable, // Invalid parameters to pipe()
 916+        .FAULT => unreachable, // Invalid fds pointer
 917+        .NFILE => return error.SystemFdQuotaExceeded,
 918+        .MFILE => return error.ProcessFdQuotaExceeded,
 919+        else => |err| return unexpectedErrno(err),
 920+    }
 921+}
 922+
 923+const PipeError = error{
 924+    SystemFdQuotaExceeded,
 925+    ProcessFdQuotaExceeded,
 926+} || UnexpectedError;
 927+
 928+pub fn pipe2(flags: O) PipeError![2]fd_t {
 929+    if (@TypeOf(system.pipe2) != void) {
 930+        var fds: [2]fd_t = undefined;
 931+        switch (errno(system.pipe2(&fds, flags))) {
 932+            .SUCCESS => return fds,
 933+            .INVAL => unreachable, // Invalid flags
 934+            .FAULT => unreachable, // Invalid fds pointer
 935+            .NFILE => return error.SystemFdQuotaExceeded,
 936+            .MFILE => return error.ProcessFdQuotaExceeded,
 937+            else => |err| return unexpectedErrno(err),
 938+        }
 939+    }
 940+
 941+    const fds: [2]fd_t = try pipe();
 942+    errdefer {
 943+        close(fds[0]);
 944+        close(fds[1]);
 945+    }
 946+
 947+    // https://github.com/ziglang/zig/issues/18882
 948+    if (@as(u32, @bitCast(flags)) == 0)
 949+        return fds;
 950+
 951+    // CLOEXEC is special, it's a file descriptor flag and must be set using
 952+    // F.SETFD.
 953+    if (flags.CLOEXEC) {
 954+        for (fds) |fd| {
 955+            switch (errno(system.fcntl(fd, F.SETFD, @as(u32, FD_CLOEXEC)))) {
 956+                .SUCCESS => {},
 957+                .INVAL => unreachable, // Invalid flags
 958+                .BADF => unreachable, // Always a race condition
 959+                else => |err| return unexpectedErrno(err),
 960+            }
 961+        }
 962+    }
 963+
 964+    const new_flags: u32 = f: {
 965+        var new_flags = flags;
 966+        new_flags.CLOEXEC = false;
 967+        break :f @bitCast(new_flags);
 968+    };
 969+    // Set every other flag affecting the file status using F.SETFL.
 970+    if (new_flags != 0) {
 971+        for (fds) |fd| {
 972+            switch (errno(system.fcntl(fd, F.SETFL, new_flags))) {
 973+                .SUCCESS => {},
 974+                .INVAL => unreachable, // Invalid flags
 975+                .BADF => unreachable, // Always a race condition
 976+                else => |err| return unexpectedErrno(err),
 977+            }
 978+        }
 979+    }
 980+
 981+    return fds;
 982+}
 983+
 984+const AcceptError = error{
 985+    ConnectionAborted,
 986+
 987+    /// The file descriptor sockfd does not refer to a socket.
 988+    FileDescriptorNotASocket,
 989+
 990+    /// The per-process limit on the number of open file descriptors has been reached.
 991+    ProcessFdQuotaExceeded,
 992+
 993+    /// The system-wide limit on the total number of open files has been reached.
 994+    SystemFdQuotaExceeded,
 995+
 996+    /// Not enough free memory.  This often means that the memory allocation  is  limited
 997+    /// by the socket buffer limits, not by the system memory.
 998+    SystemResources,
 999+
1000+    /// Socket is not listening for new connections.
1001+    SocketNotListening,
1002+
1003+    ProtocolFailure,
1004+
1005+    /// Firewall rules forbid connection.
1006+    BlockedByFirewall,
1007+
1008+    /// This error occurs when no global event loop is configured,
1009+    /// and accepting from the socket would block.
1010+    WouldBlock,
1011+
1012+    /// An incoming connection was indicated, but was subsequently terminated by the
1013+    /// remote peer prior to accepting the call.
1014+    ConnectionResetByPeer,
1015+
1016+    /// The network subsystem has failed.
1017+    NetworkSubsystemFailed,
1018+
1019+    /// The referenced socket is not a type that supports connection-oriented service.
1020+    OperationNotSupported,
1021+} || UnexpectedError;
1022+
1023+/// Accept a connection on a socket.
1024+/// If `sockfd` is opened in non blocking mode, the function will
1025+/// return error.WouldBlock when EAGAIN is received.
1026+pub fn accept(
1027+    /// This argument is a socket that has been created with `socket`, bound to a local address
1028+    /// with `bind`, and is listening for connections after a `listen`.
1029+    sock: socket_t,
1030+    /// This argument is a pointer to a sockaddr structure.  This structure is filled in with  the
1031+    /// address  of  the  peer  socket, as known to the communications layer.  The exact format of the
1032+    /// address returned addr is determined by the socket's address  family  (see  `socket`  and  the
1033+    /// respective  protocol  man  pages).
1034+    addr: ?*sockaddr,
1035+    /// This argument is a value-result argument: the caller must initialize it to contain  the
1036+    /// size (in bytes) of the structure pointed to by addr; on return it will contain the actual size
1037+    /// of the peer address.
1038+    ///
1039+    /// The returned address is truncated if the buffer provided is too small; in this  case,  `addr_size`
1040+    /// will return a value greater than was supplied to the call.
1041+    addr_size: ?*socklen_t,
1042+    /// The following values can be bitwise ORed in flags to obtain different behavior:
1043+    /// * `SOCK.NONBLOCK` - Set the `NONBLOCK` file status flag on the open file description (see `open`)
1044+    ///   referred  to by the new file descriptor.  Using this flag saves extra calls to `fcntl` to achieve
1045+    ///   the same result.
1046+    /// * `SOCK.CLOEXEC`  - Set the close-on-exec (`FD_CLOEXEC`) flag on the new file descriptor.   See  the
1047+    ///   description  of the `CLOEXEC` flag in `open` for reasons why this may be useful.
1048+    flags: u32,
1049+) AcceptError!socket_t {
1050+    const have_accept4 = !builtin.target.os.tag.isDarwin();
1051+    assert(0 == (flags & ~@as(u32, SOCK.NONBLOCK | SOCK.CLOEXEC))); // Unsupported flag(s)
1052+
1053+    const accepted_sock: socket_t = while (true) {
1054+        const rc = if (have_accept4)
1055+            system.accept4(sock, addr, addr_size, flags)
1056+        else
1057+            system.accept(sock, addr, addr_size);
1058+
1059+        switch (errno(rc)) {
1060+            .SUCCESS => break @intCast(rc),
1061+            .INTR => continue,
1062+            .AGAIN => return error.WouldBlock,
1063+            .BADF => unreachable, // always a race condition
1064+            .CONNABORTED => return error.ConnectionAborted,
1065+            .FAULT => unreachable,
1066+            .INVAL => return error.SocketNotListening,
1067+            .NOTSOCK => unreachable,
1068+            .MFILE => return error.ProcessFdQuotaExceeded,
1069+            .NFILE => return error.SystemFdQuotaExceeded,
1070+            .NOBUFS => return error.SystemResources,
1071+            .NOMEM => return error.SystemResources,
1072+            .OPNOTSUPP => unreachable,
1073+            .PROTO => return error.ProtocolFailure,
1074+            .PERM => return error.BlockedByFirewall,
1075+            else => |err| return unexpectedErrno(err),
1076+        }
1077+    };
1078+
1079+    errdefer close(accepted_sock);
1080+    if (!have_accept4) {
1081+        try setSockFlags(accepted_sock, flags);
1082+    }
1083+    return accepted_sock;
1084+}
1085+
1086+const WaitPidResult = struct {
1087+    pid: pid_t,
1088+    status: u32,
1089+};
1090+
1091+/// Use this version of the `waitpid` wrapper if you spawned your child process using explicit
1092+/// `fork` and `execve` method.
1093+pub fn waitpid(pid: pid_t, flags: u32) WaitPidResult {
1094+    var status: if (builtin.link_libc) c_int else u32 = undefined;
1095+    while (true) {
1096+        const rc = system.waitpid(pid, &status, @intCast(flags));
1097+        switch (errno(rc)) {
1098+            .SUCCESS => return .{
1099+                .pid = @intCast(rc),
1100+                .status = @bitCast(status),
1101+            },
1102+            .INTR => continue,
1103+            .CHILD => unreachable, // The process specified does not exist. It would be a race condition to handle this error.
1104+            .INVAL => unreachable, // Invalid flags.
1105+            else => unreachable,
1106+        }
1107+    }
1108+}
1109+
1110+/// Call this when you made a syscall or something that sets errno
1111+/// and you get an unexpected error.
1112+fn unexpectedErrno(err: E) UnexpectedError {
1113+    if (unexpected_error_tracing) {
1114+        std.debug.print("unexpected errno: {d}\n", .{@intFromEnum(err)});
1115+        std.debug.dumpCurrentStackTrace(std.debug.StackUnwindOptions{});
1116+    }
1117+    return error.Unexpected;
1118+}
1119+
1120+/// Whether or not `error.Unexpected` will print its value and a stack trace.
1121+///
1122+/// If this happens the fix is to add the error code to the corresponding
1123+/// switch expression, possibly introduce a new error in the error set, and
1124+/// send a patch to Zig.
1125+const unexpected_error_tracing = builtin.mode == .Debug and switch (builtin.zig_backend) {
1126+    .stage2_llvm, .stage2_x86_64 => true,
1127+    else => false,
1128+};
1129+
1130+/// Used to convert a slice to a null terminated slice on the stack.
1131+fn toPosixPath(file_path: []const u8) error{NameTooLong}![PATH_MAX - 1:0]u8 {
1132+    if (std.debug.runtime_safety) assert(mem.indexOfScalar(u8, file_path, 0) == null);
1133+    var path_with_null: [PATH_MAX - 1:0]u8 = undefined;
1134+    // >= rather than > to make room for the null byte
1135+    if (file_path.len >= PATH_MAX) return error.NameTooLong;
1136+    @memcpy(path_with_null[0..file_path.len], file_path);
1137+    path_with_null[file_path.len] = 0;
1138+    return path_with_null;
1139+}
1140+
1141+const Address = extern union {
1142+    any: sockaddr,
1143+    un: sockaddr.un,
1144+
1145+    pub fn getOsSockLen(_: Address) socklen_t {
1146+        // Using the full length of the structure here is more portable than returning
1147+        // the number of bytes actually used by the currently stored path.
1148+        // This also is correct regardless if we are passing a socket address to the kernel
1149+        // (e.g. in bind, connect, sendto) since we ensure the path is 0 terminated in
1150+        // initUnix() or if we are receiving a socket address from the kernel and must
1151+        // provide the full buffer size (e.g. getsockname, getpeername, recvfrom, accept).
1152+        //
1153+        // To access the path, std.mem.sliceTo(&address.un.path, 0) should be used.
1154+        return @as(socklen_t, @intCast(@sizeOf(sockaddr.un)));
1155+    }
1156+};
1157+
1158+pub fn initUnix(path: []const u8) !Address {
1159+    var sock_addr = sockaddr.un{
1160+        .family = AF.UNIX,
1161+        .path = undefined,
1162+    };
1163+
1164+    // Add 1 to ensure a terminating 0 is present in the path array for maximum portability.
1165+    if (path.len + 1 > sock_addr.path.len) return error.NameTooLong;
1166+
1167+    @memset(&sock_addr.path, 0);
1168+    @memcpy(sock_addr.path[0..path.len], path);
1169+
1170+    return Address{ .un = sock_addr };
1171+}
+24, -24
 1@@ -1,12 +1,13 @@
 2 const std = @import("std");
 3 const posix = std.posix;
 4+const lib_posix = @import("posix.zig");
 5 
 6 pub fn getSeshPrefix() []const u8 {
 7-    return std.posix.getenv("ZMX_SESSION_PREFIX") orelse "";
 8+    return lib_posix.getenv("ZMX_SESSION_PREFIX") orelse "";
 9 }
10 
11 pub fn getSeshNameFromEnv() []const u8 {
12-    return std.posix.getenv("ZMX_SESSION") orelse "";
13+    return lib_posix.getenv("ZMX_SESSION") orelse "";
14 }
15 
16 pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
17@@ -29,48 +30,47 @@ pub fn getSeshName(alloc: std.mem.Allocator, sesh: []const u8) ![]const u8 {
18 }
19 
20 pub fn sessionConnect(sesh: []const u8) !i32 {
21-    var unix_addr = try std.net.Address.initUnix(sesh);
22-    const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
23-    errdefer posix.close(socket_fd);
24-    try posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
25+    var unix_addr = try lib_posix.initUnix(sesh);
26+    const socket_fd = try lib_posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
27+    errdefer lib_posix.close(socket_fd);
28+    try lib_posix.connect(socket_fd, &unix_addr.any, unix_addr.getOsSockLen());
29     return socket_fd;
30 }
31 
32-pub fn cleanupStaleSocket(dir: std.fs.Dir, session_name: []const u8) void {
33+pub fn cleanupStaleSocket(io: std.Io, dir: std.Io.Dir, session_name: []const u8) void {
34     std.log.warn("stale socket found, cleaning up session={s}", .{session_name});
35-    dir.deleteFile(session_name) catch |err| {
36+    dir.deleteFile(io, session_name) catch |err| {
37         std.log.warn("failed to delete stale socket err={s}", .{@errorName(err)});
38     };
39 }
40 
41-pub fn sessionExists(dir: std.fs.Dir, name: []const u8) !bool {
42-    // fstatatZ (not statFile) to avoid the statx syscall
43-    // https://github.com/neurosnap/zmx/issues/186
44-    const name_c = try posix.toPosixPath(name);
45-    const stat = posix.fstatatZ(dir.fd, &name_c, posix.AT.SYMLINK_NOFOLLOW) catch |err| switch (err) {
46-        error.FileNotFound => return false,
47-        else => return err,
48+pub fn sessionExists(io: std.Io, dir: std.Io.Dir, name: []const u8) !bool {
49+    const stat = dir.statFile(io, name, std.Io.Dir.StatFileOptions{}) catch |err| {
50+        switch (err) {
51+            error.FileNotFound => return false,
52+            else => return err,
53+        }
54     };
55-    if (!posix.S.ISSOCK(stat.mode)) {
56+    if (stat.kind != .unix_domain_socket) {
57         return error.FileNotUnixSocket;
58     }
59     return true;
60 }
61 
62-pub fn createSocket(fname: []const u8) !i32 {
63+pub fn createSocket(sesh: []const u8) !i32 {
64     // AF.UNIX: Unix domain socket for local IPC with client processes
65     // SOCK.STREAM: Reliable, bidirectional communication
66     // SOCK.NONBLOCK: Set socket to non-blocking
67-    const fd = try posix.socket(
68+    const fd = try lib_posix.socket(
69         posix.AF.UNIX,
70         posix.SOCK.STREAM | posix.SOCK.NONBLOCK | posix.SOCK.CLOEXEC,
71         0,
72     );
73-    errdefer posix.close(fd);
74+    errdefer lib_posix.close(fd);
75 
76-    var unix_addr = try std.net.Address.initUnix(fname);
77-    try posix.bind(fd, &unix_addr.any, unix_addr.getOsSockLen());
78-    try posix.listen(fd, 128);
79+    var unix_addr = try lib_posix.initUnix(sesh);
80+    try lib_posix.bind(fd, &unix_addr.any, unix_addr.getOsSockLen());
81+    try lib_posix.listen(fd, 128);
82     return fd;
83 }
84 
85@@ -96,9 +96,9 @@ pub fn getSocketPath(
86     return fname;
87 }
88 
89-pub fn printSessionNameTooLong(session_name: []const u8, socket_dir: []const u8) void {
90+pub fn printSessionNameTooLong(io: std.Io, session_name: []const u8, socket_dir: []const u8) void {
91     var buf: [4096]u8 = undefined;
92-    var w = std.fs.File.stderr().writer(&buf);
93+    var w = std.Io.File.stderr().writer(io, &buf);
94     if (maxSessionNameLen(socket_dir)) |max_len| {
95         w.interface.print(
96             "error: session name is too long ({d} bytes, max {d} for socket directory \"{s}\")\n",
+36, -31
  1@@ -33,17 +33,18 @@ pub const SessionEntry = struct {
  2 
  3 pub fn get_session_entries(
  4     alloc: std.mem.Allocator,
  5+    io: std.Io,
  6     socket_dir: []const u8,
  7 ) !std.ArrayList(SessionEntry) {
  8     std.log.info("get session entries socket_dir={s}", .{socket_dir});
  9-    var dir = try std.fs.openDirAbsolute(socket_dir, .{ .iterate = true });
 10-    defer dir.close();
 11+    var dir = try std.Io.Dir.openDirAbsolute(io, socket_dir, .{ .iterate = true });
 12+    defer dir.close(io);
 13     var iter = dir.iterate();
 14 
 15     var sessions = try std.ArrayList(SessionEntry).initCapacity(alloc, 30);
 16 
 17-    while (try iter.next()) |entry| {
 18-        const exists = socket.sessionExists(dir, entry.name) catch continue;
 19+    while (try iter.next(io)) |entry| {
 20+        const exists = socket.sessionExists(io, dir, entry.name) catch continue;
 21         if (exists) {
 22             const name = try alloc.dupe(u8, entry.name);
 23             errdefer alloc.free(name);
 24@@ -70,7 +71,7 @@ pub fn get_session_entries(
 25                 // daemon can miss the probe timeout; deleting its socket
 26                 // orphans it permanently.
 27                 if (err == error.ConnectionRefused) {
 28-                    socket.cleanupStaleSocket(dir, entry.name);
 29+                    socket.cleanupStaleSocket(io, dir, entry.name);
 30                 }
 31                 continue;
 32             };
 33@@ -404,7 +405,7 @@ pub fn stripAnsi(alloc: std.mem.Allocator, data: []const u8) ![]const u8 {
 34     return result.toOwnedSlice(alloc);
 35 }
 36 
 37-/// Detects Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings.
 38+/// Dcts Ctrl+\ across raw, Kitty CSI u, and xterm modifyOtherKeys encodings.
 39 pub fn isCtrlBackslash(buf: []const u8) bool {
 40     if (buf.len == 0) return false;
 41     return buf[0] == 0x1C or isKeyPressed(buf, 0x5c, 0b100) or isModifyOtherKey(buf, 0x5c, 0b100);
 42@@ -765,10 +766,6 @@ pub fn serializeTerminal(
 43     };
 44 }
 45 
 46-pub fn detectShell() [:0]const u8 {
 47-    return std.posix.getenv("SHELL") orelse "/bin/sh";
 48-}
 49-
 50 /// Formats a session entry for list output (only the name when `short` is
 51 /// true), adding a prefix to indicate the current session, if there is one.
 52 pub fn writeSessionLine(
 53@@ -1168,8 +1165,9 @@ test "isCtrlBackslash xterm modifyOtherKeys" {
 54 
 55 test "serializeTerminalState excludes synchronized output replay" {
 56     const alloc = testing.allocator;
 57+    const io = testing.io;
 58 
 59-    var term = try ghostty_vt.Terminal.init(alloc, .{
 60+    var term = try ghostty_vt.Terminal.init(io, alloc, .{
 61         .cols = 80,
 62         .rows = 24,
 63     });
 64@@ -1194,8 +1192,8 @@ test "serializeTerminalState excludes synchronized output replay" {
 65     try testing.expect(std.mem.indexOf(u8, output, "\x1b[?2026h") == null);
 66 }
 67 
 68-fn testCreateTerminal(alloc: std.mem.Allocator, cols: u16, rows: u16, vt_data: []const u8) !ghostty_vt.Terminal {
 69-    var term = try ghostty_vt.Terminal.init(alloc, .{
 70+fn testCreateTerminal(alloc: std.mem.Allocator, io: std.Io, cols: u16, rows: u16, vt_data: []const u8) !ghostty_vt.Terminal {
 71+    var term = try ghostty_vt.Terminal.init(io, alloc, .{
 72         .cols = cols,
 73         .rows = rows,
 74         .max_scrollback = 10_000_000,
 75@@ -1222,12 +1220,12 @@ fn expectCursorAt(term: *ghostty_vt.Terminal, row: usize, col: usize) !void {
 76     try testing.expectEqual(row, cursor.y);
 77 }
 78 
 79-fn serializeRoundtrip(alloc: std.mem.Allocator, source: *ghostty_vt.Terminal) !ghostty_vt.Terminal {
 80+fn serializeRoundtrip(alloc: std.mem.Allocator, io: std.Io, source: *ghostty_vt.Terminal) !ghostty_vt.Terminal {
 81     const serialized = serializeTerminalState(alloc, source) orelse
 82         return error.SerializationFailed;
 83     defer alloc.free(serialized);
 84 
 85-    var dest = try ghostty_vt.Terminal.init(alloc, .{
 86+    var dest = try ghostty_vt.Terminal.init(io, alloc, .{
 87         .cols = source.screens.active.pages.cols,
 88         .rows = source.screens.active.pages.rows,
 89         .max_scrollback = 10_000_000,
 90@@ -1256,15 +1254,16 @@ fn expectMarkerAtRow(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, marke
 91 
 92 test "serializeTerminalState roundtrip preserves cursor position" {
 93     const alloc = testing.allocator;
 94+    const io = testing.io;
 95 
 96-    var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++ // clear
 97+    var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++ // clear
 98         "\x1b[10;20H" // cursor at row 10, col 20 (1-indexed)
 99     );
100     defer term.deinit(alloc);
101 
102     try expectCursorAt(&term, 9, 19); // 0-indexed
103 
104-    var client = try serializeRoundtrip(alloc, &term);
105+    var client = try serializeRoundtrip(alloc, io, &term);
106     defer client.deinit(alloc);
107 
108     try expectCursorAt(&client, 9, 19);
109@@ -1272,8 +1271,9 @@ test "serializeTerminalState roundtrip preserves cursor position" {
110 
111 test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
112     const alloc = testing.allocator;
113+    const io = testing.io;
114 
115-    var term = try testCreateTerminal(alloc, 80, 24, "\x1b[2J" ++
116+    var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[2J" ++
117         "\x1b[2;5HMARK_A" ++
118         "\x1b[6;15HMARK_B" ++
119         "\x1b[10;30HMARK_C" ++
120@@ -1281,7 +1281,7 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
121         "\x1b[16;20H");
122     defer term.deinit(alloc);
123 
124-    var client = try serializeRoundtrip(alloc, &term);
125+    var client = try serializeRoundtrip(alloc, io, &term);
126     defer client.deinit(alloc);
127 
128     try expectScreensMatch(alloc, &term, &client);
129@@ -1294,8 +1294,9 @@ test "serializeTerminalState roundtrip preserves CUP-positioned markers" {
130 
131 test "serializeTerminalState with scrollback preserves visible content" {
132     const alloc = testing.allocator;
133+    const io = testing.io;
134 
135-    var term = try testCreateTerminal(alloc, 80, 24, "");
136+    var term = try testCreateTerminal(alloc, io, 80, 24, "");
137     defer term.deinit(alloc);
138 
139     var stream = term.vtStream();
140@@ -1321,7 +1322,7 @@ test "serializeTerminalState with scrollback preserves visible content" {
141     try testing.expect(has_scrollback);
142 
143     // Roundtrip: serialize → feed into fresh terminal
144-    var client = try serializeRoundtrip(alloc, &term);
145+    var client = try serializeRoundtrip(alloc, io, &term);
146     defer client.deinit(alloc);
147 
148     // Visible content must match (this is the core cursor corruption test)
149@@ -1336,9 +1337,10 @@ test "serializeTerminalState nested roundtrip preserves content" {
150     // Simulates: inner zmx → serialized state → outer ghostty-vt → serialized again → client
151     // This is the exact nested session scenario (zmx → SSH → zmx).
152     const alloc = testing.allocator;
153+    const io = testing.io;
154 
155     // "Inner" terminal with scrollback + markers
156-    var inner = try testCreateTerminal(alloc, 80, 24, "");
157+    var inner = try testCreateTerminal(alloc, io, 80, 24, "");
158     defer inner.deinit(alloc);
159 
160     {
161@@ -1365,7 +1367,7 @@ test "serializeTerminalState nested roundtrip preserves content" {
162     defer alloc.free(inner_serialized);
163 
164     // "Outer" terminal processes inner's serialized output
165-    var outer = try testCreateTerminal(alloc, 80, 24, "");
166+    var outer = try testCreateTerminal(alloc, io, 80, 24, "");
167     defer outer.deinit(alloc);
168 
169     {
170@@ -1375,7 +1377,7 @@ test "serializeTerminalState nested roundtrip preserves content" {
171     }
172 
173     // Serialize outer (simulates outer daemon re-attach after detach)
174-    var client = try serializeRoundtrip(alloc, &outer);
175+    var client = try serializeRoundtrip(alloc, io, &outer);
176     defer client.deinit(alloc);
177 
178     // Client must see the same content as inner's visible screen
179@@ -1387,15 +1389,16 @@ test "serializeTerminalState nested roundtrip preserves content" {
180 
181 test "serializeTerminalState alternate screen not leaked" {
182     const alloc = testing.allocator;
183+    const io = testing.io;
184 
185-    var term = try testCreateTerminal(alloc, 80, 24, "\x1b[?1049h" ++ // enter alt screen
186+    var term = try testCreateTerminal(alloc, io, 80, 24, "\x1b[?1049h" ++ // enter alt screen
187         "\x1b[2J\x1b[3;10HALT_MARK" ++ // write on alt screen
188         "\x1b[?1049l" ++ // exit alt screen
189         "\x1b[2J\x1b[2;5HMAIN_MARK\x1b[8;20H" // write on main screen
190     );
191     defer term.deinit(alloc);
192 
193-    var client = try serializeRoundtrip(alloc, &term);
194+    var client = try serializeRoundtrip(alloc, io, &term);
195     defer client.deinit(alloc);
196 
197     try expectScreensMatch(alloc, &term, &client);
198@@ -1408,8 +1411,9 @@ test "serializeTerminalState alternate screen not leaked" {
199 
200 test "serializeTerminalState size mismatch roundtrip" {
201     const alloc = testing.allocator;
202+    const io = testing.io;
203 
204-    var term = try testCreateTerminal(alloc, 80, 30, "\x1b[2J" ++
205+    var term = try testCreateTerminal(alloc, io, 80, 30, "\x1b[2J" ++
206         "\x1b[3;10HSIZE_A" ++
207         "\x1b[12;20HSIZE_B" ++
208         "\x1b[20;40HSIZE_C" ++
209@@ -1419,7 +1423,7 @@ test "serializeTerminalState size mismatch roundtrip" {
210     // Resize to 24 rows (simulates outer terminal being smaller)
211     try term.resize(alloc, ghostty_vt.Terminal.Resize{ .cols = 80, .rows = 24 });
212 
213-    var client = try serializeRoundtrip(alloc, &term);
214+    var client = try serializeRoundtrip(alloc, io, &term);
215     defer client.deinit(alloc);
216 
217     try expectScreensMatch(alloc, &term, &client);
218@@ -1428,8 +1432,9 @@ test "serializeTerminalState size mismatch roundtrip" {
219 
220 test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
221     const alloc = testing.allocator;
222+    const io = testing.io;
223 
224-    var inner = try testCreateTerminal(alloc, 80, 30, "");
225+    var inner = try testCreateTerminal(alloc, io, 80, 30, "");
226     defer inner.deinit(alloc);
227 
228     {
229@@ -1457,7 +1462,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
230         return error.SerializationFailed;
231     defer alloc.free(inner_ser);
232 
233-    var outer = try testCreateTerminal(alloc, 80, 24, "");
234+    var outer = try testCreateTerminal(alloc, io, 80, 24, "");
235     defer outer.deinit(alloc);
236     {
237         var outer_stream = outer.vtStream();
238@@ -1465,7 +1470,7 @@ test "serializeTerminalState scrollback + size mismatch nested roundtrip" {
239         outer_stream.nextSlice(inner_ser);
240     }
241 
242-    var client = try serializeRoundtrip(alloc, &outer);
243+    var client = try serializeRoundtrip(alloc, io, &outer);
244     defer client.deinit(alloc);
245 
246     try expectScreensMatch(alloc, &inner, &client);