Commit Diff


commit - 208223b916c59db30ee10b33e5f9bef9374d2a3e
commit + 42bf945818946a95a481c92fb101235a2945396a
blob - 9835bd6d90b8c248671f30b6f1f10cc11ff51a26
blob + 4c1239df39138e842c816ee72b7a464de534b9a3
--- src/cfg.zig
+++ src/cfg.zig
@@ -51,7 +51,7 @@ pub fn loadConfig(allocator: std.mem.Allocator) ?std.j
     const path = std.fs.path.join(allocator, &.{ home, ".config", "magdalena", "config.json" }) catch return null;
     defer allocator.free(path);
 
-    const content = std.Io.Dir.cwd().readFileAlloc(io.rt, path, allocator, .unlimited) catch return null;
+    const content = std.Io.Dir.cwd().readFileAlloc(io.rt(), path, allocator, .unlimited) catch return null;
     defer allocator.free(content);
 
     return std.json.parseFromSlice(Config, allocator, content, .{
blob - e141be3f513d25791527ee842ca5ff8b98bfb363
blob + 5cbedb8b32b318127fec7b9a26ca37794ea5e8cd
--- src/db.zig
+++ src/db.zig
@@ -42,20 +42,20 @@ pub const Db = struct {
             .files_path = files_path,
         };
 
-        std.Io.Dir.cwd().createDirPath(io.rt, base_path) catch |err| {
+        std.Io.Dir.cwd().createDirPath(io.rt(), base_path) catch |err| {
             if (err != error.PathAlreadyExists) return err;
         };
 
         for (&[_][]const u8{ dirs_path, files_path }) |p| {
-            var f = std.Io.Dir.cwd().openFile(io.rt, p, .{ .mode = .read_write }) catch |err| {
+            var f = std.Io.Dir.cwd().openFile(io.rt(), p, .{ .mode = .read_write }) catch |err| {
                 if (err == error.FileNotFound) {
-                    const created = try std.Io.Dir.cwd().createFile(io.rt, p, .{ .truncate = false });
-                    created.close(io.rt);
+                    const created = try std.Io.Dir.cwd().createFile(io.rt(), p, .{ .truncate = false });
+                    created.close(io.rt());
                     continue;
                 }
                 return err;
             };
-            f.close(io.rt);
+            f.close(io.rt());
         }
 
         return res;
@@ -68,7 +68,7 @@ pub const Db = struct {
     }
 
     pub fn recentDirs(self: *Db) !zul.Managed([]DirectoryEntry) {
-        const content = std.Io.Dir.cwd().readFileAlloc(io.rt, self.dirs_path, self.allocator, .limited(64 * 1024 * 1024)) catch |err| {
+        const content = std.Io.Dir.cwd().readFileAlloc(io.rt(), self.dirs_path, self.allocator, .limited(64 * 1024 * 1024)) catch |err| {
             if (err == error.FileNotFound) return zul.Managed([]DirectoryEntry).fromJson(try std.json.parseFromSlice([]DirectoryEntry, self.allocator, "[]", .{}));
             return err;
         };
@@ -88,7 +88,7 @@ pub const Db = struct {
     }
 
     pub fn recentFiles(self: *Db) !zul.Managed([]FileEntry) {
-        const content = std.Io.Dir.cwd().readFileAlloc(io.rt, self.files_path, self.allocator, .limited(64 * 1024 * 1024)) catch |err| {
+        const content = std.Io.Dir.cwd().readFileAlloc(io.rt(), self.files_path, self.allocator, .limited(64 * 1024 * 1024)) catch |err| {
             if (err == error.FileNotFound) return zul.Managed([]FileEntry).fromJson(try std.json.parseFromSlice([]FileEntry, self.allocator, "[]", .{}));
             return err;
         };
@@ -153,7 +153,7 @@ pub const Db = struct {
     }
 
     fn resolvePath(self: *Db, path: []const u8) ![]const u8 {
-        return std.Io.Dir.cwd().realPathFileAlloc(io.rt, path, self.allocator) catch {
+        return std.Io.Dir.cwd().realPathFileAlloc(io.rt(), path, self.allocator) catch {
             return try std.fs.path.resolve(self.allocator, &[_][]const u8{path});
         };
     }
@@ -162,21 +162,21 @@ pub const Db = struct {
         const abs_path = try self.resolvePath(dir_path);
         defer self.allocator.free(abs_path);
 
-        const file = std.Io.Dir.cwd().openFile(io.rt, self.dirs_path, .{ .mode = .write_only }) catch |err| switch (err) {
-            error.FileNotFound => try std.Io.Dir.cwd().createFile(io.rt, self.dirs_path, .{ .truncate = false }),
+        const file = std.Io.Dir.cwd().openFile(io.rt(), self.dirs_path, .{ .mode = .write_only }) catch |err| switch (err) {
+            error.FileNotFound => try std.Io.Dir.cwd().createFile(io.rt(), self.dirs_path, .{ .truncate = false }),
             else => return err,
         };
-        defer file.close(io.rt);
+        defer file.close(io.rt());
 
-        const now = zul.DateTime.now(io.rt);
+        const now = zul.DateTime.now(io.rt());
         var ts_buf: [64]u8 = undefined;
         var ts_writer = std.Io.Writer.fixed(&ts_buf);
         try now.format(&ts_writer);
         const ts = ts_buf[0..ts_writer.end];
 
         var fbuf: [4096]u8 = undefined;
-        var fw = file.writer(io.rt, &fbuf);
-        try fw.seekTo(try file.length(io.rt));
+        var fw = file.writer(io.rt(), &fbuf);
+        try fw.seekTo(try file.length(io.rt()));
         try fw.interface.print("{s}|{s}\n", .{ ts, abs_path });
         try fw.interface.flush();
     }
@@ -185,21 +185,21 @@ pub const Db = struct {
         const abs_path = try self.resolvePath(file_path);
         defer self.allocator.free(abs_path);
 
-        const file = std.Io.Dir.cwd().openFile(io.rt, self.files_path, .{ .mode = .write_only }) catch |err| switch (err) {
-            error.FileNotFound => try std.Io.Dir.cwd().createFile(io.rt, self.files_path, .{ .truncate = false }),
+        const file = std.Io.Dir.cwd().openFile(io.rt(), self.files_path, .{ .mode = .write_only }) catch |err| switch (err) {
+            error.FileNotFound => try std.Io.Dir.cwd().createFile(io.rt(), self.files_path, .{ .truncate = false }),
             else => return err,
         };
-        defer file.close(io.rt);
+        defer file.close(io.rt());
 
-        const now = zul.DateTime.now(io.rt);
+        const now = zul.DateTime.now(io.rt());
         var ts_buf: [64]u8 = undefined;
         var ts_writer = std.Io.Writer.fixed(&ts_buf);
         try now.format(&ts_writer);
         const ts = ts_buf[0..ts_writer.end];
 
         var fbuf: [4096]u8 = undefined;
-        var fw = file.writer(io.rt, &fbuf);
-        try fw.seekTo(try file.length(io.rt));
+        var fw = file.writer(io.rt(), &fbuf);
+        try fw.seekTo(try file.length(io.rt()));
         try fw.interface.print("{s}|{s}|{s}|{s}\n", .{ ts, file_type, action, abs_path });
         try fw.interface.flush();
     }
@@ -212,7 +212,7 @@ pub const Db = struct {
     fn cleanupFile(self: *Db, path: []const u8, path_col_idx: usize) !void {
         io.warn("Cleaning up {s}...\n", .{path});
 
-        const content = try std.Io.Dir.cwd().readFileAlloc(io.rt, path, self.allocator, .limited(64 * 1024 * 1024));
+        const content = try std.Io.Dir.cwd().readFileAlloc(io.rt(), path, self.allocator, .limited(64 * 1024 * 1024));
         defer self.allocator.free(content);
 
         var seen = std.StringHashMap(void).init(self.allocator);
@@ -243,7 +243,7 @@ pub const Db = struct {
 
             if (seen.contains(p)) continue;
 
-            std.Io.Dir.cwd().access(io.rt, p, .{}) catch |err| {
+            std.Io.Dir.cwd().access(io.rt(), p, .{}) catch |err| {
                 if (err == error.FileNotFound) {
                     missing_entries += 1;
                     continue;
@@ -258,11 +258,11 @@ pub const Db = struct {
         const duplicates = total_entries - lines.items.len - missing_entries;
         io.warn("  Entries: {d} total, {d} unique, {d} missing, {d} duplicates removed\n", .{ total_entries, lines.items.len, missing_entries, duplicates });
 
-        const write_file = try std.Io.Dir.cwd().createFile(io.rt, path, .{ .truncate = true });
-        defer write_file.close(io.rt);
+        const write_file = try std.Io.Dir.cwd().createFile(io.rt(), path, .{ .truncate = true });
+        defer write_file.close(io.rt());
 
         var write_buf: [4096]u8 = undefined;
-        var writer = write_file.writer(io.rt, &write_buf);
+        var writer = write_file.writer(io.rt(), &write_buf);
 
         var i: usize = lines.items.len;
         while (i > 0) {
blob - 127706ad4c3cb551cb5ef20cb1b65fc4445e7ec6
blob + e07175224c5d0aa9c9fcd4fc19818ebcebaf561c
--- src/fzf.zig
+++ src/fzf.zig
@@ -26,32 +26,31 @@ fn gotoFile(allocator: std.mem.Allocator, db: ?*hist.D
         d.logFile(file_path, ext, action) catch |err| io.warn("failed to log file: {}\n", .{err});
     }
 
-    if (std.Io.Dir.openFileAbsolute(io.rt, "/dev/tty", .{ .mode = .read_write })) |tty| {
+    if (std.Io.Dir.openFileAbsolute(io.rt(), "/dev/tty", .{ .mode = .read_write })) |tty| {
         if (std.c.dup2(tty.handle, std.posix.STDIN_FILENO) < 0) io.warn("failed to redirect stdin to tty\n", .{});
         if (std.c.dup2(tty.handle, std.posix.STDOUT_FILENO) < 0) io.warn("failed to redirect stdout to tty\n", .{});
-        tty.close(io.rt);
+        tty.close(io.rt());
     } else |err| {
         io.warn("failed to open /dev/tty: {}\n", .{err});
     }
 
     if (command) |cmd| {
-        return std.process.replace(io.rt, .{ .argv = &.{ cmd, file_path } });
+        return std.process.replace(io.rt(), .{ .argv = &.{ cmd, file_path } });
     }
 
     const editor = config.editor orelse (io.getenv("EDITOR") orelse return error.EditorNotSet);
 
     if (line) |l| {
         const line_arg = try std.fmt.allocPrint(allocator, "+{s}", .{l});
-        return std.process.replace(io.rt, .{ .argv = &.{ editor, line_arg, file_path } });
+        return std.process.replace(io.rt(), .{ .argv = &.{ editor, line_arg, file_path } });
     }
 
-    return std.process.replace(io.rt, .{ .argv = &.{ editor, file_path } });
+    return std.process.replace(io.rt(), .{ .argv = &.{ editor, file_path } });
 }
 
 const StdIo = std.process.SpawnOptions.StdIo;
 
 const Fzf = struct {
-    child: std.process.Child = undefined,
     argv: [][]const u8,
     allocator: std.mem.Allocator,
     stdin: StdIo = .pipe,
@@ -80,8 +79,8 @@ const Fzf = struct {
         };
     }
 
-    pub fn spawn(self: *Fzf) !void {
-        self.child = try std.process.spawn(io.rt, .{
+    pub fn spawn(self: *Fzf) !std.process.Child {
+        return std.process.spawn(io.rt(), .{
             .argv = self.argv,
             .stdin = self.stdin,
             .stdout = self.stdout,
@@ -110,21 +109,20 @@ fn shouldSkip(path: []const u8, ignored_patterns: []co
 fn runFzfPicker(allocator: std.mem.Allocator, fzf_opts: []const []const u8, items: []const []const u8) ![]u8 {
     var fzf = try Fzf.init(allocator, fzf_opts, &[_][]const u8{});
     defer fzf.deinit();
-    try fzf.spawn();
-    const child = &fzf.child;
+    var child = try fzf.spawn();
 
     if (child.stdin) |stdin| {
         for (items) |item| {
-            stdin.writeStreamingAll(io.rt, item) catch |err| {
+            stdin.writeStreamingAll(io.rt(), item) catch |err| {
                 if (err == error.BrokenPipe) break;
                 return err;
             };
-            stdin.writeStreamingAll(io.rt, "\n") catch |err| {
+            stdin.writeStreamingAll(io.rt(), "\n") catch |err| {
                 if (err == error.BrokenPipe) break;
                 return err;
             };
         }
-        stdin.close(io.rt);
+        stdin.close(io.rt());
         child.stdin = null;
     }
 
@@ -133,11 +131,11 @@ fn runFzfPicker(allocator: std.mem.Allocator, fzf_opts
 
     if (child.stdout) |stdout| {
         var buffer: [4096]u8 = undefined;
-        var sr = stdout.reader(io.rt, &buffer);
+        var sr = stdout.reader(io.rt(), &buffer);
         stdout_data = try sr.interface.allocRemaining(allocator, .unlimited);
     }
 
-    const term = try child.wait(io.rt);
+    const term = try child.wait(io.rt());
     if (term != .exited or term.exited != 0) return error.UserAbort;
     const selected = std.mem.trim(u8, stdout_data orelse "", " \n\r\t");
     if (selected.len == 0) return error.UserAbort;
@@ -166,7 +164,7 @@ pub fn recentDir(allocator: std.mem.Allocator, db: *hi
     const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
     defer allocator.free(selected);
 
-    std.Io.Dir.accessAbsolute(io.rt, selected, .{}) catch |err| {
+    std.Io.Dir.accessAbsolute(io.rt(), selected, .{}) catch |err| {
         io.warn("Selected directory no longer exists: {s} ({})\n", .{ selected, err });
         return error.UserAbort;
     };
@@ -198,8 +196,8 @@ pub fn favorites(allocator: std.mem.Allocator, db: *hi
     defer allocator.free(selected);
 
     const is_dir = blk: {
-        var dir = std.Io.Dir.openDirAbsolute(io.rt, selected, .{}) catch break :blk false;
-        dir.close(io.rt);
+        var dir = std.Io.Dir.openDirAbsolute(io.rt(), selected, .{}) catch break :blk false;
+        dir.close(io.rt());
         break :blk true;
     };
 
@@ -254,7 +252,7 @@ pub fn recentFile(allocator: std.mem.Allocator, db: *h
     const selected = try runFzfPicker(allocator, config.fzf_opts, items.items);
     defer allocator.free(selected);
 
-    try std.Io.Dir.accessAbsolute(io.rt, selected, .{});
+    try std.Io.Dir.accessAbsolute(io.rt(), selected, .{});
 
     try gotoFile(allocator, db, selected, null, config);
 }
@@ -294,19 +292,18 @@ pub fn grep(allocator: std.mem.Allocator, db: *hist.Db
     fzf.stdout = .pipe;
     fzf.stderr = .inherit;
 
-    try fzf.spawn();
-    const child = &fzf.child;
+    var child = try fzf.spawn();
 
     var stdout_data: ?[]u8 = null;
     defer if (stdout_data) |d| allocator.free(d);
 
     if (child.stdout) |stdout| {
         var buffer: [4096]u8 = undefined;
-        var sr = stdout.reader(io.rt, &buffer);
+        var sr = stdout.reader(io.rt(), &buffer);
         stdout_data = try sr.interface.allocRemaining(allocator, .unlimited);
     }
 
-    const term = try child.wait(io.rt);
+    const term = try child.wait(io.rt());
     if (term != .exited or term.exited != 0) return error.UserAbort;
     const selected = std.mem.trim(u8, stdout_data orelse "", " \n\r\t");
     if (selected.len == 0) return error.UserAbort;
@@ -334,12 +331,12 @@ const Entry = struct {
 fn getMaxDirMtime(ignored_patterns: []const []const u8, max_depth: usize) !i128 {
     var max_mtime: i128 = 0;
 
-    if (std.Io.Dir.cwd().statFile(io.rt, ".", .{})) |stat| {
+    if (std.Io.Dir.cwd().statFile(io.rt(), ".", .{})) |stat| {
         max_mtime = stat.mtime.nanoseconds;
     } else |_| {}
 
-    var root = try std.Io.Dir.cwd().openDir(io.rt, ".", .{ .iterate = true });
-    defer root.close(io.rt);
+    var root = try std.Io.Dir.cwd().openDir(io.rt(), ".", .{ .iterate = true });
+    defer root.close(io.rt());
     try scanMaxMtime(&root, 0, max_depth, ignored_patterns, &max_mtime);
 
     return max_mtime;
@@ -349,7 +346,7 @@ fn getMaxDirMtime(ignored_patterns: []const []const u8
 // once by its own `defer` as the recursion unwinds.
 fn scanMaxMtime(dir: *std.Io.Dir, depth: usize, max_depth: usize, ignored_patterns: []const []const u8, max_mtime: *i128) !void {
     var it = dir.iterate();
-    while (try it.next(io.rt)) |entry| {
+    while (try it.next(io.rt())) |entry| {
         var skip = false;
         for (ignored_patterns) |pattern| {
             if (std.mem.eql(u8, entry.name, pattern)) {
@@ -361,7 +358,7 @@ fn scanMaxMtime(dir: *std.Io.Dir, depth: usize, max_de
 
         const is_dir = entry.kind == .directory;
         if (is_dir or entry.kind == .sym_link) {
-            const stat = dir.statFile(io.rt, entry.name, .{}) catch |err| {
+            const stat = dir.statFile(io.rt(), entry.name, .{}) catch |err| {
                 if (err != error.FileNotFound and err != error.AccessDenied) {
                     io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
                 }
@@ -371,11 +368,11 @@ fn scanMaxMtime(dir: *std.Io.Dir, depth: usize, max_de
                 if (stat.mtime.nanoseconds > max_mtime.*) max_mtime.* = stat.mtime.nanoseconds;
 
                 if (is_dir and depth + 1 < max_depth) {
-                    var sub_dir = dir.openDir(io.rt, entry.name, .{ .iterate = true }) catch |err| {
+                    var sub_dir = dir.openDir(io.rt(), entry.name, .{ .iterate = true }) catch |err| {
                         io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
                         continue;
                     };
-                    defer sub_dir.close(io.rt);
+                    defer sub_dir.close(io.rt());
                     try scanMaxMtime(&sub_dir, depth + 1, max_depth, ignored_patterns, max_mtime);
                 }
             }
@@ -396,7 +393,7 @@ fn collectEntries(
     entries: *std.ArrayListUnmanaged(Entry),
 ) !void {
     var it = dir.iterate();
-    while (try it.next(io.rt)) |entry| {
+    while (try it.next(io.rt())) |entry| {
         const entry_path = if (prefix) |p|
             try std.fs.path.join(allocator, &.{ p, entry.name })
         else
@@ -406,7 +403,7 @@ fn collectEntries(
         if (shouldSkip(entry_path, ignored_patterns)) continue;
 
         if (entry.kind == kind or entry.kind == .sym_link) {
-            if (dir.statFile(io.rt, entry.name, .{})) |stat| {
+            if (dir.statFile(io.rt(), entry.name, .{})) |stat| {
                 if (stat.kind == kind) {
                     try entries.append(allocator, .{
                         .path = try allocator.dupe(u8, entry_path),
@@ -421,11 +418,11 @@ fn collectEntries(
         }
 
         if (entry.kind == .directory and depth + 1 < max_depth) {
-            var sub_dir = dir.openDir(io.rt, entry.name, .{ .iterate = true }) catch |err| {
+            var sub_dir = dir.openDir(io.rt(), entry.name, .{ .iterate = true }) catch |err| {
                 io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
                 continue;
             };
-            defer sub_dir.close(io.rt);
+            defer sub_dir.close(io.rt());
             try collectEntries(allocator, &sub_dir, entry_path, depth + 1, max_depth, kind, ignored_patterns, entries);
         }
     }
@@ -455,7 +452,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
     var cache_hit = false;
     if (mc) |*m| {
         const cwd = blk: {
-            break :blk std.process.currentPathAlloc(io.rt, allocator) catch |err| {
+            break :blk std.process.currentPathAlloc(io.rt(), allocator) catch |err| {
                 io.warn("failed to get cwd for cache: {}\n", .{err});
                 break :blk null;
             };
@@ -488,8 +485,8 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
     }
 
     if (!cache_hit) {
-        var root = try std.Io.Dir.cwd().openDir(io.rt, ".", .{ .iterate = true });
-        defer root.close(io.rt);
+        var root = try std.Io.Dir.cwd().openDir(io.rt(), ".", .{ .iterate = true });
+        defer root.close(io.rt());
         try collectEntries(allocator, &root, null, 0, max_depth, kind, config.ignored_patterns, &entries);
 
         if (mc) |*m| {
@@ -519,7 +516,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
     const clean_path = if (std.mem.startsWith(u8, selected, "./")) selected[2..] else selected;
 
     if (clean_path.len > 0) {
-        std.Io.Dir.cwd().access(io.rt, clean_path, .{}) catch |err| {
+        std.Io.Dir.cwd().access(io.rt(), clean_path, .{}) catch |err| {
             io.warn("Selected item no longer exists or is inaccessible: {s} ({})\n", .{ clean_path, err });
             return error.UserAbort;
         };
blob - 83df8579424a713c60e70582016467554ec1f780
blob + afdb924d98de999b342f0f16c57e0be5b140a1b0
--- src/io.zig
+++ src/io.zig
@@ -1,13 +1,24 @@
 const std = @import("std");
 
-// Process-wide handles, set once from `std.process.Init` in `main`. The app is
-// a short-lived single-threaded CLI, so threading these through every call site
-// would be pure noise; a couple of globals keep the IO surface small.
-pub var rt: std.Io = undefined;
-pub var env: *std.process.Environ.Map = undefined;
+// Process-wide handles, populated once from `std.process.Init` in `main`. The
+// app is a short-lived single-threaded CLI, so threading these through every
+// call site would be pure noise; ambient state keeps the IO surface small.
+// They are optionals (not `undefined`) so that any use before `init` is a
+// checked unwrap panic rather than undefined behavior.
+var rt_handle: ?std.Io = null;
+var env_map: ?*std.process.Environ.Map = null;
 
+pub fn init(io_handle: std.Io, environ: *std.process.Environ.Map) void {
+    rt_handle = io_handle;
+    env_map = environ;
+}
+
+pub fn rt() std.Io {
+    return rt_handle.?;
+}
+
 pub fn getenv(key: []const u8) ?[]const u8 {
-    return env.get(key);
+    return env_map.?.get(key);
 }
 
 pub const Writer = struct {
@@ -38,12 +49,12 @@ pub const Writer = struct {
 };
 
 pub fn getStdout(buf: []u8) Writer {
-    return .{ .inner = std.Io.File.stdout().writerStreaming(rt, buf) };
+    return .{ .inner = std.Io.File.stdout().writerStreaming(rt(), buf) };
 }
 
 pub fn warn(comptime fmt: []const u8, args: anytype) void {
     var buf: [4096]u8 = undefined;
-    var writer = std.Io.File.stderr().writerStreaming(rt, &buf);
+    var writer = std.Io.File.stderr().writerStreaming(rt(), &buf);
     writer.interface.print(fmt, args) catch |err| {
         std.log.warn("failed to write to stderr: {}", .{err});
     };
blob - c80f3a526bf0097fc1a42aaeff6962b341e4f133
blob + 649121766a95937a1654f74083b5709f43ce17e7
--- src/main.zig
+++ src/main.zig
@@ -13,8 +13,7 @@ test {
 }
 
 pub fn main(init: std.process.Init) void {
-    io.rt = init.io;
-    io.env = init.environ_map;
+    io.init(init.io, init.environ_map);
     const allocator = init.gpa;
 
     const args = cli.parseArgs(allocator, init.minimal.args) catch |err| {
@@ -36,7 +35,7 @@ pub fn main(init: std.process.Init) void {
 
     var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
     const cwd = blk: {
-        const n = std.process.currentPath(io.rt, &cwd_buf) catch |err| {
+        const n = std.process.currentPath(io.rt(), &cwd_buf) catch |err| {
             io.warn("failed to get cwd for folder overrides: {}\n", .{err});
             break :blk "";
         };
blob - 9d9d89d2f11c3c6f75ef45da2e7c10843ce77b81
blob + fbdd49d0015baeb3e9eccea8e7c4ee40535834ea
--- src/memcached.zig
+++ src/memcached.zig
@@ -4,15 +4,17 @@ const io = @import("io.zig");
 pub const Memcached = struct {
     allocator: std.mem.Allocator,
     stream: std.Io.net.Stream,
-    read_buf: [65536]u8 = undefined,
-    write_buf: [4096]u8 = undefined,
-    reader: std.Io.net.Stream.Reader = undefined,
-    writer: std.Io.net.Stream.Writer = undefined,
-    started: bool = false,
+    read_buf: [65536]u8 = @splat(0),
+    write_buf: [4096]u8 = @splat(0),
+    // `null` until first use; the Stream's buffered Reader/Writer hold pointers
+    // into this struct's own buffers, so they can only be created once the
+    // struct sits at its final address (i.e. after `init` returns by value).
+    reader: ?std.Io.net.Stream.Reader = null,
+    writer: ?std.Io.net.Stream.Writer = null,
 
     pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !Memcached {
         const address = try std.Io.net.IpAddress.parseIp4(host, port);
-        const stream = try address.connect(io.rt, .{ .mode = .stream });
+        const stream = try address.connect(io.rt(), .{ .mode = .stream });
         return .{
             .allocator = allocator,
             .stream = stream,
@@ -20,32 +22,28 @@ pub const Memcached = struct {
     }
 
     pub fn deinit(self: *Memcached) void {
-        if (self.started) {
-            self.writer.interface.flush() catch |err| {
+        if (self.writer) |*writer| {
+            writer.interface.flush() catch |err| {
                 io.warn("failed to flush memcached on deinit: {}\n", .{err});
             };
         }
-        self.stream.close(io.rt);
+        self.stream.close(io.rt());
     }
 
-    // The Stream's buffered Reader/Writer hold pointers into this struct's own
-    // buffers, so they must be initialized only once the struct sits at its
-    // final address (i.e. after `init` returns by value).
     fn ensureStarted(self: *Memcached) void {
-        if (self.started) return;
-        self.reader = self.stream.reader(io.rt, &self.read_buf);
-        self.writer = self.stream.writer(io.rt, &self.write_buf);
-        self.started = true;
+        if (self.reader != null) return;
+        self.reader = self.stream.reader(io.rt(), &self.read_buf);
+        self.writer = self.stream.writer(io.rt(), &self.write_buf);
     }
 
     fn r(self: *Memcached) *std.Io.Reader {
         self.ensureStarted();
-        return &self.reader.interface;
+        return &self.reader.?.interface;
     }
 
     fn w(self: *Memcached) *std.Io.Writer {
         self.ensureStarted();
-        return &self.writer.interface;
+        return &self.writer.?.interface;
     }
 
     fn flushWrite(self: *Memcached) !void {