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);