Commit Diff


commit - f1e6f51bc60368c7032a44f5d58f451c17179178
commit + 6d4e9c975b027c7846a7133974c2ad8e8ccb4c00
blob - e901168a76ec518f8ebca61c9d1c481ccb74ce60
blob + 9714c4fd334c8949add1bef63de70bf310f4af83
--- .gitignore
+++ .gitignore
@@ -1,2 +1,3 @@
-/.zig-cache
-/zig-out
+.zig-cache
+zig-pkg
+zig-out
blob - d490ab6424f8a704fce958f59de761edacbdad0d
blob + 25708efc37795d0858a880b92fa8f52505d9f952
--- build.zig
+++ build.zig
@@ -15,12 +15,12 @@ pub fn build(b: *std.Build) void {
             .root_source_file = b.path("src/main.zig"),
             .target = target,
             .optimize = optimize,
+            .link_libc = true,
             .imports = &.{
                 .{ .name = "zul", .module = zul.module("zul") },
             },
         }),
     });
-    exe.linkLibC();
 
     b.installArtifact(exe);
 
blob - 0f90cfac722c6bba076a46c1c5281f9d98b2d8e7
blob + b1f2924e7f5f096e7d92b654496f255937bf6168
--- build.zig.zon
+++ build.zig.zon
@@ -2,11 +2,11 @@
     .name = .magdalena,
     .version = "0.1.3",
     .fingerprint = 0xd065e535ca3c8b31,
-    .minimum_zig_version = "0.15.2",
+    .minimum_zig_version = "0.16.0",
     .dependencies = .{
         .zul = .{
-            .url = "https://github.com/karlseguin/zul/archive/e770047f208cf4538fcd9fc64550c84fd5492fc4.tar.gz",
-            .hash = "1220c0fe8bdde860b533f5625b72daaf2c0c686ac7eaf72e123aaf6998d785439b93",
+            .url = "https://github.com/karlseguin/zul/archive/146f9d5b2238c3a621b96345adf03490900c2fe2.tar.gz",
+            .hash = "zul-0.0.0-1oDot2KwBwC9c43wC7V9y4-xxg0a_d9okFDcJPhqmije",
         },
     },
     .paths = .{
blob - 4b77e22df703651cdc018ffca0ae539c24ac9eb8
blob + fe23c682d753e301bd1d51e52f4b646263ce5a46
--- justfile
+++ justfile
@@ -1,5 +1,8 @@
 run opt="Debug":
-    zig build run -Doptimize={{opt}}
+    zig build run -Doptimize={{ opt }}
 
 build opt="Debug":
-    zig build -Doptimize={{opt}} -p build/{{opt}}
+    zig build -Doptimize={{ opt }} -p build/{{ opt }}
+
+install opt="ReleaseSafe" prefix="~/.local":
+    zig build -Doptimize={{ opt }} --prefix {{ prefix }} install
blob - f9a7ffd55622381c638449dab08e9e3ce4edcb6c
blob + e8e0b66cb6663ed8ebb8e7f2a1c4668ca3b967d2
--- src/cfg.zig
+++ src/cfg.zig
@@ -1,4 +1,5 @@
 const std = @import("std");
+const io = @import("io.zig");
 
 pub const Config = struct {
     openers: []Opener = &.{},
@@ -46,14 +47,11 @@ pub fn applyFolderOverride(base: Config, cwd: []const 
 }
 
 pub fn loadConfig(allocator: std.mem.Allocator) ?std.json.Parsed(Config) {
-    const home = std.posix.getenv("HOME") orelse return null;
+    const home = io.getenv("HOME") orelse return null;
     const path = std.fs.path.join(allocator, &.{ home, ".config", "magdalena", "config.json" }) catch return null;
     defer allocator.free(path);
 
-    const file = std.fs.cwd().openFile(path, .{}) catch return null;
-    defer file.close();
-
-    const content = file.readToEndAlloc(allocator, std.math.maxInt(usize)) 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 - 22f1bfde3c9ca67e9f7abd2d7e41b262bf47fa61
blob + ffe497d71bcd539209a995586a5ecf98c62e9148
--- src/cli.zig
+++ src/cli.zig
@@ -1,5 +1,4 @@
 const std = @import("std");
-const zul = @import("zul");
 const io = @import("io.zig");
 
 pub const Action = enum {
@@ -26,16 +25,122 @@ pub const Args = struct {
     file_action: ?[]const u8 = null,
     depth: ?usize = null,
 
-    _cla: zul.CommandLineArgs,
+    _cla: CommandLineArgs,
 
     pub fn deinit(self: Args) void {
         self._cla.deinit();
     }
 };
 
-pub fn parseArgs(allocator: std.mem.Allocator) !Args {
-    const cla = try zul.CommandLineArgs.parse(allocator);
+// Minimal command-line parser. Replaces zul.CommandLineArgs, whose `parse`
+// still calls the removed `std.process.argsWithAllocator`; the parsing
+// semantics (flag/value/tail handling) are preserved exactly.
+pub const CommandLineArgs = struct {
+    _arena: *std.heap.ArenaAllocator,
+    _lookup: std.StringHashMapUnmanaged([]const u8),
 
+    exe: []const u8,
+    tail: []const [:0]const u8,
+    list: []const [:0]const u8,
+
+    pub fn parse(parent: std.mem.Allocator, source: std.process.Args) !CommandLineArgs {
+        const arena = try parent.create(std.heap.ArenaAllocator);
+        errdefer parent.destroy(arena);
+
+        arena.* = std.heap.ArenaAllocator.init(parent);
+        errdefer arena.deinit();
+
+        const allocator = arena.allocator();
+        const items = try source.toSlice(allocator);
+
+        var lookup: std.StringHashMapUnmanaged([]const u8) = .{};
+
+        if (items.len == 0) {
+            return .{ .exe = "", .tail = &.{}, .list = &.{}, ._arena = arena, ._lookup = lookup };
+        }
+
+        const exe = items[0];
+
+        // 1, skip the exe
+        var i: usize = 1;
+        var tail_start: usize = 1;
+
+        while (i < items.len) {
+            const arg = items[i];
+            if (arg.len == 1 or arg[0] != '-') {
+                // can't be a valid parameter, so it must be the start of our tail
+                break;
+            }
+
+            if (arg[1] == '-') {
+                const kv = KeyValue.from(arg[2..], items, &i);
+                try lookup.put(allocator, kv.key, kv.value);
+            } else {
+                const kv = KeyValue.from(arg[1..], items, &i);
+                const key = kv.key;
+
+                // -xvf file.tar.gz parses into x=>"", v=>"", f=>"file.tar.gz"
+                for (0..key.len - 1) |j| {
+                    try lookup.put(allocator, key[j .. j + 1], "");
+                }
+                try lookup.put(allocator, key[key.len - 1 ..], kv.value);
+            }
+            tail_start = i;
+        }
+
+        return .{
+            .exe = exe,
+            .tail = items[tail_start..],
+            .list = items,
+            ._arena = arena,
+            ._lookup = lookup,
+        };
+    }
+
+    pub fn deinit(self: CommandLineArgs) void {
+        const arena = self._arena;
+        const allocator = arena.child_allocator;
+        arena.deinit();
+        allocator.destroy(arena);
+    }
+
+    pub fn contains(self: *const CommandLineArgs, name: []const u8) bool {
+        return self._lookup.contains(name);
+    }
+
+    pub fn get(self: *const CommandLineArgs, name: []const u8) ?[]const u8 {
+        return self._lookup.get(name);
+    }
+};
+
+const KeyValue = struct {
+    key: []const u8,
+    value: []const u8,
+
+    fn from(key: []const u8, items: []const [:0]const u8, i: *usize) KeyValue {
+        const item_index = i.*;
+        if (std.mem.indexOfScalarPos(u8, key, 0, '=')) |pos| {
+            // this parameter is in the form of --key=value, or -k=value
+            i.* = item_index + 1;
+            return .{ .key = key[0..pos], .value = key[pos + 1 ..] };
+        }
+
+        if (item_index == items.len - 1 or items[item_index + 1][0] == '-') {
+            // key is at the end of the arguments OR the next argument starts
+            // with a '-'. This means this key has no value.
+            i.* = item_index + 1;
+            return .{ .key = key, .value = "" };
+        }
+
+        // skip the current key, and the next arg (which is our value)
+        i.* = item_index + 2;
+        return .{ .key = key, .value = items[item_index + 1] };
+    }
+};
+
+pub fn parseArgs(allocator: std.mem.Allocator, source: std.process.Args) !Args {
+    const cla = try CommandLineArgs.parse(allocator, source);
+
     var res = Args{
         .action = .help,
         ._cla = cla,
blob - a7032b45bcb669d92a49a75a39f4b1a9ffc68d83
blob + a236d0c6ffa055b336fe82629143f4fef8bae250
--- src/db.zig
+++ src/db.zig
@@ -43,20 +43,21 @@ pub const Db = struct {
         };
 
         // Ensure directory exists
-        std.fs.cwd().makePath(base_path) catch |err| {
+        std.Io.Dir.cwd().createDirPath(io.rt, base_path) catch |err| {
             if (err != error.PathAlreadyExists) return err;
         };
 
         // Ensure files exist
         for (&[_][]const u8{ dirs_path, files_path }) |p| {
-            var f = std.fs.cwd().openFile(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) {
-                    _ = try std.fs.cwd().createFile(p, .{ .truncate = false });
+                    const created = try std.Io.Dir.cwd().createFile(io.rt, p, .{ .truncate = false });
+                    created.close(io.rt);
                     continue;
                 }
                 return err;
             };
-            f.close();
+            f.close(io.rt);
         }
 
         return res;
@@ -69,13 +70,10 @@ pub const Db = struct {
     }
 
     pub fn recentDirs(self: *Db) !zul.Managed([]DirectoryEntry) {
-        const file = std.fs.cwd().openFile(self.dirs_path, .{}) 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;
         };
-        defer file.close();
-
-        const content = try file.readToEndAlloc(self.allocator, 64 * 1024 * 1024);
         errdefer self.allocator.free(content);
 
         var arena = try self.allocator.create(std.heap.ArenaAllocator);
@@ -86,7 +84,7 @@ pub const Db = struct {
         arena.* = std.heap.ArenaAllocator.init(self.allocator);
         const allocator = arena.allocator();
 
-        var list = std.ArrayListUnmanaged(DirectoryEntry){};
+        var list = std.ArrayListUnmanaged(DirectoryEntry).empty;
 
         var seen = std.StringHashMap(void).init(allocator);
 
@@ -117,13 +115,10 @@ pub const Db = struct {
     }
 
     pub fn recentFiles(self: *Db) !zul.Managed([]FileEntry) {
-        const file = std.fs.cwd().openFile(self.files_path, .{}) 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;
         };
-        defer file.close();
-
-        const content = try file.readToEndAlloc(self.allocator, 64 * 1024 * 1024);
         errdefer self.allocator.free(content);
 
         var arena = try self.allocator.create(std.heap.ArenaAllocator);
@@ -134,7 +129,7 @@ pub const Db = struct {
         arena.* = std.heap.ArenaAllocator.init(self.allocator);
         const allocator = arena.allocator();
 
-        var list = std.ArrayListUnmanaged(FileEntry){};
+        var list = std.ArrayListUnmanaged(FileEntry).empty;
 
         var seen = std.StringHashMap(void).init(allocator);
 
@@ -181,7 +176,7 @@ pub const Db = struct {
         arena.* = std.heap.ArenaAllocator.init(self.allocator);
         const allocator = arena.allocator();
 
-        var dir_list = std.ArrayListUnmanaged(DirectoryEntry){};
+        var dir_list = std.ArrayListUnmanaged(DirectoryEntry).empty;
         for (managed_dirs.value) |d| {
             if (std.mem.indexOf(u8, d.path, query_str) != null) {
                 try dir_list.append(allocator, .{
@@ -191,7 +186,7 @@ pub const Db = struct {
             }
         }
 
-        var file_list = std.ArrayListUnmanaged(FileEntry){};
+        var file_list = std.ArrayListUnmanaged(FileEntry).empty;
         for (managed_files.value) |f| {
             if (std.mem.indexOf(u8, f.path, query_str) != null) {
                 try file_list.append(allocator, .{
@@ -213,7 +208,7 @@ pub const Db = struct {
     }
 
     fn resolvePath(self: *Db, path: []const u8) ![]const u8 {
-        return std.fs.cwd().realpathAlloc(self.allocator, path) catch {
+        return std.Io.Dir.cwd().realPathFileAlloc(io.rt, path, self.allocator) catch {
             return try std.fs.path.resolve(self.allocator, &[_][]const u8{path});
         };
     }
@@ -222,44 +217,46 @@ pub const Db = struct {
         const abs_path = try self.resolvePath(dir_path);
         defer self.allocator.free(abs_path);
 
-        const file = std.fs.cwd().openFile(self.dirs_path, .{ .mode = .write_only }) catch |err| switch (err) {
-            error.FileNotFound => try std.fs.cwd().createFile(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();
-        try file.seekFromEnd(0);
+        defer file.close(io.rt);
 
-        const now = zul.DateTime.now();
+        const now = zul.DateTime.now(io.rt);
         var ts_buf: [64]u8 = undefined;
-        var ts_writer = std.io.Writer.fixed(&ts_buf);
+        var ts_writer = std.Io.Writer.fixed(&ts_buf);
         try now.format(&ts_writer);
         const ts = ts_buf[0..ts_writer.end];
 
-        const line = try std.fmt.allocPrint(self.allocator, "{s}|{s}\n", .{ ts, abs_path });
-        defer self.allocator.free(line);
-        try file.writeAll(line);
+        var fbuf: [4096]u8 = undefined;
+        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();
     }
 
     pub fn logFile(self: *Db, file_path: []const u8, file_type: []const u8, action: []const u8) !void {
         const abs_path = try self.resolvePath(file_path);
         defer self.allocator.free(abs_path);
 
-        const file = std.fs.cwd().openFile(self.files_path, .{ .mode = .write_only }) catch |err| switch (err) {
-            error.FileNotFound => try std.fs.cwd().createFile(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();
-        try file.seekFromEnd(0);
+        defer file.close(io.rt);
 
-        const now = zul.DateTime.now();
+        const now = zul.DateTime.now(io.rt);
         var ts_buf: [64]u8 = undefined;
-        var ts_writer = std.io.Writer.fixed(&ts_buf);
+        var ts_writer = std.Io.Writer.fixed(&ts_buf);
         try now.format(&ts_writer);
         const ts = ts_buf[0..ts_writer.end];
 
-        const line = try std.fmt.allocPrint(self.allocator, "{s}|{s}|{s}|{s}\n", .{ ts, file_type, action, abs_path });
-        defer self.allocator.free(line);
-        try file.writeAll(line);
+        var fbuf: [4096]u8 = undefined;
+        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();
     }
 
     pub fn cleanup(self: *Db) !void {
@@ -270,15 +267,13 @@ pub const Db = struct {
     fn cleanupFile(self: *Db, path: []const u8, path_col_idx: usize) !void {
         io.warn("Cleaning up {s}...\n", .{path});
 
-        const file = try std.fs.cwd().openFile(path, .{});
-        defer file.close();
-        const content = try file.readToEndAlloc(self.allocator, 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);
         defer seen.deinit();
 
-        var lines = std.ArrayListUnmanaged([]const u8){};
+        var lines = std.ArrayListUnmanaged([]const u8).empty;
         defer lines.deinit(self.allocator);
 
         var total_entries: usize = 0;
@@ -303,7 +298,7 @@ pub const Db = struct {
 
             if (seen.contains(p)) continue;
 
-            std.fs.cwd().access(p, .{}) catch |err| {
+            std.Io.Dir.cwd().access(io.rt, p, .{}) catch |err| {
                 if (err == error.FileNotFound) {
                     missing_entries += 1;
                     continue;
@@ -318,11 +313,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.fs.cwd().createFile(path, .{ .truncate = true });
-        defer write_file.close();
+        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(&write_buf);
+        var writer = write_file.writer(io.rt, &write_buf);
 
         var i: usize = lines.items.len;
         while (i > 0) {
@@ -334,7 +329,7 @@ pub const Db = struct {
 };
 
 pub fn getDefaultDbPath(allocator: std.mem.Allocator) ![]const u8 {
-    if (std.posix.getenv("HOME")) |home| {
+    if (io.getenv("HOME")) |home| {
         return std.fs.path.join(allocator, &.{ home, ".magdalena" });
     }
     return error.HomeNotFound;
blob - 06a8fe641ef1dad7cda17b5030b883792760561f
blob + 6b959d9d20c88693c39eae40f829b7c39842c5e7
--- src/fzf.zig
+++ src/fzf.zig
@@ -26,36 +26,37 @@ 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.fs.openFileAbsolute("/dev/tty", .{ .mode = .read_write })) |tty| {
-        std.posix.dup2(tty.handle, std.posix.STDIN_FILENO) catch |err| io.warn("failed to redirect stdin to tty: {}\n", .{err});
-        std.posix.dup2(tty.handle, std.posix.STDOUT_FILENO) catch |err| io.warn("failed to redirect stdout to tty: {}\n", .{err});
-        tty.close();
+    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);
     } else |err| {
         io.warn("failed to open /dev/tty: {}\n", .{err});
     }
 
     if (command) |cmd| {
-        return std.process.execv(allocator, &.{ cmd, file_path });
+        return std.process.replace(io.rt, .{ .argv = &.{ cmd, file_path } });
     }
 
-    const editor = config.editor orelse
-        std.process.getEnvVarOwned(allocator, "EDITOR") catch |err| switch (err) {
-        error.EnvironmentVariableNotFound => return error.EditorNotSet,
-        else => return err,
-    };
+    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.execv(allocator, &.{ editor, line_arg, file_path });
+        return std.process.replace(io.rt, .{ .argv = &.{ editor, line_arg, file_path } });
     }
 
-    return std.process.execv(allocator, &.{ 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,
+    child: std.process.Child = undefined,
     argv: [][]const u8,
     allocator: std.mem.Allocator,
+    stdin: StdIo = .pipe,
+    stdout: StdIo = .pipe,
+    stderr: StdIo = .inherit,
 
     pub fn init(allocator: std.mem.Allocator, fzf_opts: []const []const u8, extra_opts: []const []const u8) !Fzf {
         var argv = try allocator.alloc([]const u8, 1 + fzf_opts.len + extra_opts.len);
@@ -73,18 +74,21 @@ const Fzf = struct {
             i += 1;
         }
 
-        var child = std.process.Child.init(argv, allocator);
-        child.stdin_behavior = .Pipe;
-        child.stdout_behavior = .Pipe;
-        child.stderr_behavior = .Inherit;
-
         return .{
-            .child = child,
             .argv = argv,
             .allocator = allocator,
         };
     }
 
+    pub fn spawn(self: *Fzf) !void {
+        self.child = try std.process.spawn(io.rt, .{
+            .argv = self.argv,
+            .stdin = self.stdin,
+            .stdout = self.stdout,
+            .stderr = self.stderr,
+        });
+    }
+
     pub fn deinit(self: *Fzf) void {
         self.allocator.free(self.argv);
     }
@@ -106,10 +110,10 @@ 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;
-    try child.spawn();
 
-    if (child.stdin) |*stdin| {
+    if (child.stdin) |stdin| {
         // Write items in batches to prevent excessive buffer usage
         const batch_size = 1000;
         for (0..((items.len + batch_size - 1) / batch_size)) |batch_idx| {
@@ -117,35 +121,32 @@ fn runFzfPicker(allocator: std.mem.Allocator, fzf_opts
             const end = @min(start + batch_size, items.len);
 
             for (start..end) |i| {
-                stdin.writeAll(items[i]) catch |err| {
+                stdin.writeStreamingAll(io.rt, items[i]) catch |err| {
                     if (err == error.BrokenPipe) break;
                     return err;
                 };
-                stdin.writeAll("\n") catch |err| {
+                stdin.writeStreamingAll(io.rt, "\n") catch |err| {
                     if (err == error.BrokenPipe) break;
                     return err;
                 };
             }
         }
-        stdin.close();
+        stdin.close(io.rt);
         child.stdin = null;
     }
 
-    var stdout_buf = std.ArrayListUnmanaged(u8){};
-    defer stdout_buf.deinit(allocator);
+    var stdout_data: ?[]u8 = null;
+    defer if (stdout_data) |d| allocator.free(d);
 
     if (child.stdout) |stdout| {
-        var buffer: [1024]u8 = undefined;
-        while (true) {
-            const bytes_read = try stdout.read(&buffer);
-            if (bytes_read == 0) break;
-            try stdout_buf.appendSlice(allocator, buffer[0..bytes_read]);
-        }
+        var buffer: [4096]u8 = undefined;
+        var sr = stdout.reader(io.rt, &buffer);
+        stdout_data = try sr.interface.allocRemaining(allocator, .unlimited);
     }
 
-    const term = try child.wait();
-    if (term != .Exited or term.Exited != 0) return error.UserAbort;
-    const selected = std.mem.trim(u8, stdout_buf.items, " \n\r\t");
+    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;
 
     return allocator.dupe(u8, selected);
@@ -161,7 +162,7 @@ pub fn recentDir(allocator: std.mem.Allocator, db: *hi
         return;
     }
 
-    var items = std.ArrayListUnmanaged([]const u8){};
+    var items = std.ArrayListUnmanaged([]const u8).empty;
     defer items.deinit(allocator);
 
     for (dirs) |dir| {
@@ -172,7 +173,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.fs.accessAbsolute(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;
     };
@@ -189,7 +190,7 @@ pub fn favorites(allocator: std.mem.Allocator, db: *hi
         return;
     }
 
-    var items = std.ArrayListUnmanaged([]const u8){};
+    var items = std.ArrayListUnmanaged([]const u8).empty;
     defer {
         for (items.items) |item| allocator.free(item);
         items.deinit(allocator);
@@ -204,8 +205,8 @@ pub fn favorites(allocator: std.mem.Allocator, db: *hi
     defer allocator.free(selected);
 
     const is_dir = blk: {
-        var dir = std.fs.openDirAbsolute(selected, .{}) catch break :blk false;
-        dir.close();
+        var dir = std.Io.Dir.openDirAbsolute(io.rt, selected, .{}) catch break :blk false;
+        dir.close(io.rt);
         break :blk true;
     };
 
@@ -221,13 +222,13 @@ pub fn favorites(allocator: std.mem.Allocator, db: *hi
 }
 
 fn expandPath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
-    var res = std.ArrayListUnmanaged(u8){};
+    var res = std.ArrayListUnmanaged(u8).empty;
     errdefer res.deinit(allocator);
 
     var i: usize = 0;
     while (i < path.len) {
         if (std.mem.startsWith(u8, path[i..], "$HOME")) {
-            const home = std.posix.getenv("HOME") orelse "/home/user";
+            const home = io.getenv("HOME") orelse "/home/user";
             try res.appendSlice(allocator, home);
             i += 5;
         } else {
@@ -249,7 +250,7 @@ pub fn recentFile(allocator: std.mem.Allocator, db: *h
         return;
     }
 
-    var items = std.ArrayListUnmanaged([]const u8){};
+    var items = std.ArrayListUnmanaged([]const u8).empty;
     defer items.deinit(allocator);
 
     for (files) |file| {
@@ -260,7 +261,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.fs.accessAbsolute(selected, .{});
+    try std.Io.Dir.accessAbsolute(io.rt, selected, .{});
 
     try gotoFile(allocator, db, selected, null, config);
 }
@@ -268,7 +269,7 @@ pub fn recentFile(allocator: std.mem.Allocator, db: *h
 pub fn grep(allocator: std.mem.Allocator, db: *hist.Db, config: *const cfg.Config) !void {
     const initial_query = "";
 
-    var rg_cmd = std.ArrayListUnmanaged(u8){};
+    var rg_cmd = std.ArrayListUnmanaged(u8).empty;
     defer rg_cmd.deinit(allocator);
     try rg_cmd.appendSlice(allocator, "change:reload:rg --column --line-number --no-heading --color=always --smart-case");
     for (config.ignored_patterns) |pattern| {
@@ -295,29 +296,26 @@ pub fn grep(allocator: std.mem.Allocator, db: *hist.Db
         "right,60%,border-left,+{2}+3/3,~3",
     });
     defer fzf.deinit();
+
+    fzf.stdin = .inherit;
+    fzf.stdout = .pipe;
+    fzf.stderr = .inherit;
+
+    try fzf.spawn();
     const child = &fzf.child;
 
-    child.stdin_behavior = .Inherit;
-    child.stdout_behavior = .Pipe;
-    child.stderr_behavior = .Inherit;
+    var stdout_data: ?[]u8 = null;
+    defer if (stdout_data) |d| allocator.free(d);
 
-    try child.spawn();
-
-    var stdout_buf = std.ArrayListUnmanaged(u8){};
-    defer stdout_buf.deinit(allocator);
-
     if (child.stdout) |stdout| {
-        var buffer: [1024]u8 = undefined;
-        while (true) {
-            const bytes_read = try stdout.read(&buffer);
-            if (bytes_read == 0) break;
-            try stdout_buf.appendSlice(allocator, buffer[0..bytes_read]);
-        }
+        var buffer: [4096]u8 = undefined;
+        var sr = stdout.reader(io.rt, &buffer);
+        stdout_data = try sr.interface.allocRemaining(allocator, .unlimited);
     }
 
-    const term = try child.wait();
-    if (term != .Exited or term.Exited != 0) return error.UserAbort;
-    const selected = std.mem.trim(u8, stdout_buf.items, " \n\r\t");
+    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;
 
     var it = std.mem.splitScalar(u8, selected, ':');
@@ -340,71 +338,111 @@ const Entry = struct {
     mtime: i128,
 };
 
-fn getMaxDirMtime(allocator: std.mem.Allocator, ignored_patterns: []const []const u8, max_depth: usize) !i128 {
+fn getMaxDirMtime(ignored_patterns: []const []const u8, max_depth: usize) !i128 {
     var max_mtime: i128 = 0;
 
-    if (std.fs.cwd().statFile(".")) |stat| {
-        max_mtime = stat.mtime;
+    if (std.Io.Dir.cwd().statFile(io.rt, ".", .{})) |stat| {
+        max_mtime = stat.mtime.nanoseconds;
     } else |_| {}
 
-    const StackEntry = struct {
-        dir: std.fs.Dir,
-        depth: usize,
-    };
-    var stack = std.ArrayListUnmanaged(StackEntry){};
-    defer {
-        for (stack.items) |*se| se.dir.close();
-        stack.deinit(allocator);
-    }
+    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);
 
-    try stack.append(allocator, .{ .dir = try std.fs.cwd().openDir(".", .{ .iterate = true }), .depth = 0 });
-
-    while (stack.items.len > 0) {
-        var current = stack.pop() orelse unreachable;
-        defer current.dir.close();
-
-        var it = current.dir.iterate();
-        while (try it.next()) |entry| {
-            var skip = false;
-            for (ignored_patterns) |pattern| {
-                if (std.mem.eql(u8, entry.name, pattern)) {
-                    skip = true;
-                    break;
-                }
-            }
-            if (skip) continue;
-
-            const is_dir = entry.kind == .directory;
-            if (is_dir or entry.kind == .sym_link) {
-                const stat = current.dir.statFile(entry.name) catch |err| {
-                    if (err != error.FileNotFound and err != error.AccessDenied) {
-                        io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
-                    }
-                    continue;
-                };
-                if (stat.kind == .directory) {
-                    if (stat.mtime > max_mtime) max_mtime = stat.mtime;
-
-                    if (is_dir and current.depth + 1 < max_depth) {
-                        const sub_dir = current.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
-                            io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
-                            continue;
-                        };
-                        try stack.append(allocator, .{ .dir = sub_dir, .depth = current.depth + 1 });
-                    }
-                }
-            }
-        }
-    }
-
     return max_mtime;
 }
 
-fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.fs.File.Kind, config: *const cfg.Config, depth: ?usize) !void {
+// Recurses one open directory per call frame, so each handle is closed exactly
+// 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| {
+        var skip = false;
+        for (ignored_patterns) |pattern| {
+            if (std.mem.eql(u8, entry.name, pattern)) {
+                skip = true;
+                break;
+            }
+        }
+        if (skip) continue;
+
+        const is_dir = entry.kind == .directory;
+        if (is_dir or entry.kind == .sym_link) {
+            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 });
+                }
+                continue;
+            };
+            if (stat.kind == .directory) {
+                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| {
+                        io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
+                        continue;
+                    };
+                    defer sub_dir.close(io.rt);
+                    try scanMaxMtime(&sub_dir, depth + 1, max_depth, ignored_patterns, max_mtime);
+                }
+            }
+        }
+    }
+}
+
+// Recurses one open directory per call frame (closed by its own `defer`),
+// collecting matching entries with their path relative to the walk root.
+fn collectEntries(
+    allocator: std.mem.Allocator,
+    dir: *std.Io.Dir,
+    prefix: ?[]const u8,
+    depth: usize,
+    max_depth: usize,
+    kind: std.Io.File.Kind,
+    ignored_patterns: []const []const u8,
+    entries: *std.ArrayListUnmanaged(Entry),
+) !void {
+    var it = dir.iterate();
+    while (try it.next(io.rt)) |entry| {
+        const entry_path = if (prefix) |p|
+            try std.fs.path.join(allocator, &.{ p, entry.name })
+        else
+            try allocator.dupe(u8, entry.name);
+        defer allocator.free(entry_path);
+
+        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 (stat.kind == kind) {
+                    try entries.append(allocator, .{
+                        .path = try allocator.dupe(u8, entry_path),
+                        .mtime = stat.mtime.nanoseconds,
+                    });
+                }
+            } else |err| {
+                if (err != error.FileNotFound and err != error.AccessDenied) {
+                    io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
+                }
+            }
+        }
+
+        if (entry.kind == .directory and depth + 1 < max_depth) {
+            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);
+            try collectEntries(allocator, &sub_dir, entry_path, depth + 1, max_depth, kind, ignored_patterns, entries);
+        }
+    }
+}
+
+fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.Io.File.Kind, config: *const cfg.Config, depth: ?usize) !void {
     const is_dir = kind == .directory;
     const max_depth = depth orelse config.max_depth;
 
-    var entries = std.ArrayListUnmanaged(Entry){};
+    var entries = std.ArrayListUnmanaged(Entry).empty;
     defer {
         for (entries.items) |e| allocator.free(e.path);
         entries.deinit(allocator);
@@ -424,7 +462,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
     var cache_hit = false;
     if (mc) |*m| {
         const cwd = blk: {
-            break :blk std.process.getCwdAlloc(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;
             };
@@ -433,7 +471,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
             defer allocator.free(c);
 
             const hash = std.hash.Crc32.hash(c);
-            const max_dir_mtime = getMaxDirMtime(allocator, config.ignored_patterns, max_depth) catch blk: {
+            const max_dir_mtime = getMaxDirMtime(config.ignored_patterns, max_depth) catch blk: {
                 break :blk @as(i128, 0);
             };
             const mtime = @divTrunc(max_dir_mtime, std.time.ns_per_s);
@@ -457,67 +495,10 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
     }
 
     if (!cache_hit) {
-        const StackEntry = struct {
-            dir: std.fs.Dir,
-            path: ?[]const u8,
-            depth: usize,
-        };
-        var stack = std.ArrayListUnmanaged(StackEntry){};
-        defer {
-            for (stack.items) |*se| se.dir.close();
-            stack.deinit(allocator);
-        }
+        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);
 
-        try stack.append(allocator, .{ .dir = try std.fs.cwd().openDir(".", .{ .iterate = true }), .path = null, .depth = 0 });
-
-        while (stack.items.len > 0) {
-            var current = stack.pop() orelse unreachable;
-            defer {
-                current.dir.close();
-                if (current.path) |p| allocator.free(p);
-            }
-
-            var it = current.dir.iterate();
-            while (try it.next()) |entry| {
-                const entry_path = if (current.path) |p|
-                    try std.fs.path.join(allocator, &.{ p, entry.name })
-                else
-                    try allocator.dupe(u8, entry.name);
-                errdefer allocator.free(entry_path);
-
-                if (shouldSkip(entry_path, config.ignored_patterns)) {
-                    allocator.free(entry_path);
-                    continue;
-                }
-
-                if (entry.kind == kind or entry.kind == .sym_link) {
-                    if (current.dir.statFile(entry.name)) |stat| {
-                        if (stat.kind == kind) {
-                            try entries.append(allocator, .{
-                                .path = try allocator.dupe(u8, entry_path),
-                                .mtime = stat.mtime,
-                            });
-                        }
-                    } else |err| {
-                        if (err != error.FileNotFound and err != error.AccessDenied) {
-                            io.warn("failed to stat {s}: {}\n", .{ entry.name, err });
-                        }
-                    }
-                }
-
-                if (entry.kind == .directory and current.depth + 1 < max_depth) {
-                    const sub_dir = current.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
-                        io.warn("failed to open directory {s}: {}\n", .{ entry.name, err });
-                        allocator.free(entry_path);
-                        continue;
-                    };
-                    try stack.append(allocator, .{ .dir = sub_dir, .path = entry_path, .depth = current.depth + 1 });
-                } else {
-                    allocator.free(entry_path);
-                }
-            }
-        }
-
         if (mc) |*m| {
             if (cache_key) |key| {
                 const json_data = try std.json.Stringify.valueAlloc(allocator, entries.items, .{});
@@ -533,7 +514,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: 
         }
     }.lessThan);
 
-    var items = std.ArrayListUnmanaged([]const u8){};
+    var items = std.ArrayListUnmanaged([]const u8).empty;
     defer items.deinit(allocator);
     for (entries.items) |entry| {
         try items.append(allocator, entry.path);
@@ -545,7 +526,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.fs.cwd().access(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 - 684a010edcc132405af3fb664f9de7b24a698094
blob + 83df8579424a713c60e70582016467554ec1f780
--- src/io.zig
+++ src/io.zig
@@ -1,7 +1,17 @@
 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;
+
+pub fn getenv(key: []const u8) ?[]const u8 {
+    return env.get(key);
+}
+
 pub const Writer = struct {
-    inner: std.fs.File.Writer,
+    inner: std.Io.File.Writer,
     failed: bool = false,
 
     pub fn print(self: *Writer, comptime fmt: []const u8, args: anytype) !void {
@@ -17,7 +27,7 @@ pub const Writer = struct {
 
     pub fn end(self: *Writer) !void {
         if (self.failed) return;
-        self.inner.end() catch |err| {
+        self.inner.interface.flush() catch |err| {
             if (err == error.WriteFailed) {
                 self.failed = true;
                 return;
@@ -28,17 +38,17 @@ pub const Writer = struct {
 };
 
 pub fn getStdout(buf: []u8) Writer {
-    return .{ .inner = std.fs.File.stdout().writer(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.fs.File.stderr().writer(&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});
     };
-    writer.end() catch |err| {
-        std.log.warn("failed to end stderr writer: {}", .{err});
+    writer.interface.flush() catch |err| {
+        std.log.warn("failed to flush stderr writer: {}", .{err});
     };
 }
 
blob - 84361b7c1a1a193e31e1e9d93af1a346d8fb826f
blob + f580ffd7adca82805ed440c0a28c241af68676c6
--- src/main.zig
+++ src/main.zig
@@ -5,12 +5,12 @@ const fzf = @import("fzf.zig");
 const hist = @import("db.zig");
 const io = @import("io.zig");
 
-pub fn main() void {
-    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
-    defer _ = gpa.deinit();
-    const allocator = gpa.allocator();
+pub fn main(init: std.process.Init) void {
+    io.rt = init.io;
+    io.env = init.environ_map;
+    const allocator = init.gpa;
 
-    const args = cli.parseArgs(allocator) catch |err| {
+    const args = cli.parseArgs(allocator, init.minimal.args) catch |err| {
         io.fatal("failed to parse arguments: {}", .{err});
     };
     defer args.deinit();
@@ -28,9 +28,12 @@ pub fn main() void {
     const base_config = if (parsed_config) |c| c.value else default_config;
 
     var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
-    const cwd = std.process.getCwd(&cwd_buf) catch |err| blk: {
-        io.warn("failed to get cwd for folder overrides: {}\n", .{err});
-        break :blk "";
+    const cwd = blk: {
+        const n = std.process.currentPath(io.rt, &cwd_buf) catch |err| {
+            io.warn("failed to get cwd for folder overrides: {}\n", .{err});
+            break :blk "";
+        };
+        break :blk cwd_buf[0..n];
     };
     const config = cfg.applyFolderOverride(base_config, cwd);
 
blob - a9bfd025d5140fb762317db8350bb8f2ee704197
blob + 9d9d89d2f11c3c6f75ef45da2e7c10843ce77b81
--- src/memcached.zig
+++ src/memcached.zig
@@ -3,16 +3,16 @@ const io = @import("io.zig");
 
 pub const Memcached = struct {
     allocator: std.mem.Allocator,
-    stream: std.net.Stream,
-    write_buf: [4096]u8 = undefined,
-    write_end: usize = 0,
+    stream: std.Io.net.Stream,
     read_buf: [65536]u8 = undefined,
-    read_start: usize = 0,
-    read_end: usize = 0,
+    write_buf: [4096]u8 = undefined,
+    reader: std.Io.net.Stream.Reader = undefined,
+    writer: std.Io.net.Stream.Writer = undefined,
+    started: bool = false,
 
     pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !Memcached {
-        const address = try std.net.Address.parseIp4(host, port);
-        const stream = try std.net.tcpConnectToAddress(address);
+        const address = try std.Io.net.IpAddress.parseIp4(host, port);
+        const stream = try address.connect(io.rt, .{ .mode = .stream });
         return .{
             .allocator = allocator,
             .stream = stream,
@@ -20,30 +20,40 @@ pub const Memcached = struct {
     }
 
     pub fn deinit(self: *Memcached) void {
-        self.flushWrite() catch |err| {
-            io.warn("failed to flush memcached on deinit: {}\n", .{err});
-        };
-        self.stream.close();
+        if (self.started) {
+            self.writer.interface.flush() catch |err| {
+                io.warn("failed to flush memcached on deinit: {}\n", .{err});
+            };
+        }
+        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;
+    }
+
+    fn r(self: *Memcached) *std.Io.Reader {
+        self.ensureStarted();
+        return &self.reader.interface;
+    }
+
+    fn w(self: *Memcached) *std.Io.Writer {
+        self.ensureStarted();
+        return &self.writer.interface;
+    }
+
     fn flushWrite(self: *Memcached) !void {
-        if (self.write_end == 0) return;
-        try self.stream.writeAll(self.write_buf[0..self.write_end]);
-        self.write_end = 0;
+        try self.w().flush();
     }
 
     fn bufferedWrite(self: *Memcached, data: []const u8) !void {
-        if (data.len >= self.write_buf.len) {
-            try self.flushWrite();
-            try self.stream.writeAll(data);
-            return;
-        }
-        const available = self.write_buf.len - self.write_end;
-        if (data.len > available) {
-            try self.flushWrite();
-        }
-        @memcpy(self.write_buf[self.write_end..][0..data.len], data);
-        self.write_end += data.len;
+        try self.w().writeAll(data);
     }
 
     const chunk_size = 900 * 1024;
@@ -82,80 +92,41 @@ pub const Memcached = struct {
     }
 
     fn readLine(self: *Memcached, buf: []u8) !?[]const u8 {
-        while (true) {
-            if (self.read_start < self.read_end) {
-                if (std.mem.indexOfScalar(u8, self.read_buf[self.read_start..self.read_end], '\n')) |newline_offset| {
-                    const line_start = self.read_start;
-                    const line_end = self.read_start + newline_offset;
-                    self.read_start = line_end + 1;
-                    const line = self.read_buf[line_start..line_end];
-                    const trimmed = std.mem.trim(u8, line, "\r");
-                    if (buf.len < trimmed.len) return error.BufferTooSmall;
-                    @memcpy(buf[0..trimmed.len], trimmed);
-                    return buf[0..trimmed.len];
-                }
-            }
-            if (self.read_start > 0) {
-                const remaining = self.read_end - self.read_start;
-                @memcpy(self.read_buf[0..remaining], self.read_buf[self.read_start..self.read_end]);
-                self.read_end = remaining;
-                self.read_start = 0;
-            }
-            if (self.read_end >= self.read_buf.len) return error.LineTooLong;
-            const n = try self.stream.read(self.read_buf[self.read_end..]);
-            if (n == 0) return null;
-            self.read_end += n;
-        }
+        const line = self.r().takeDelimiterExclusive('\n') catch |err| switch (err) {
+            error.EndOfStream => return null,
+            error.StreamTooLong => return error.LineTooLong,
+            else => |e| return e,
+        };
+        const trimmed = std.mem.trim(u8, line, "\r");
+        if (buf.len < trimmed.len) return error.BufferTooSmall;
+        @memcpy(buf[0..trimmed.len], trimmed);
+        return buf[0..trimmed.len];
     }
 
     fn readExact(self: *Memcached, buf: []u8) !void {
-        var offset: usize = 0;
-        while (offset < buf.len) {
-            if (self.read_start < self.read_end) {
-                const available = self.read_end - self.read_start;
-                const needed = buf.len - offset;
-                const to_copy = @min(available, needed);
-                @memcpy(buf[offset..][0..to_copy], self.read_buf[self.read_start..][0..to_copy]);
-                self.read_start += to_copy;
-                offset += to_copy;
-                continue;
-            }
-            self.read_start = 0;
-            self.read_end = 0;
-            const remaining = buf.len - offset;
-            if (remaining >= self.read_buf.len) {
-                const n = try self.stream.read(buf[offset..]);
-                if (n == 0) return error.IncompleteRead;
-                offset += n;
-            } else {
-                const n = try self.stream.read(self.read_buf[0..]);
-                if (n == 0) return error.IncompleteRead;
-                self.read_end = n;
-            }
-        }
+        self.r().readSliceAll(buf) catch |err| switch (err) {
+            error.EndOfStream => return error.IncompleteRead,
+            else => |e| return e,
+        };
     }
 
     fn consumeCrlf(self: *Memcached) void {
-        if (self.read_start < self.read_end) {
-            const available = self.read_end - self.read_start;
-            if (available >= 2 and self.read_buf[self.read_start] == '\r' and self.read_buf[self.read_start + 1] == '\n') {
-                self.read_start += 2;
-                return;
-            }
-            if (available >= 1 and self.read_buf[self.read_start] == '\n') {
-                self.read_start += 1;
-                return;
-            }
+        const reader = self.r();
+        const first = reader.peekByte() catch return;
+        if (first == '\r') {
+            reader.toss(1);
+            const second = reader.peekByte() catch return;
+            if (second == '\n') reader.toss(1);
+        } else if (first == '\n') {
+            reader.toss(1);
         }
-        var discard: [2]u8 = undefined;
-        _ = self.stream.read(&discard) catch return;
     }
 
     pub fn get(self: *Memcached, key: []const u8) !?[]u8 {
-        try self.flushWrite();
         var cmd_buf: [512]u8 = undefined;
         const cmd = try std.fmt.bufPrint(&cmd_buf, "mg {s} v\r\n", .{key});
-        try self.stream.writeAll(cmd);
+        try self.bufferedWrite(cmd);
+        try self.flushWrite();
 
         var line_buf: [512]u8 = undefined;
         const line = (try self.readLine(&line_buf)) orelse return null;
@@ -203,7 +174,8 @@ pub const Memcached = struct {
 
             var cmd_buf: [512]u8 = undefined;
             const cmd = try std.fmt.bufPrint(&cmd_buf, "mg {s} v\r\n", .{chunk_key});
-            try self.stream.writeAll(cmd);
+            try self.bufferedWrite(cmd);
+            try self.flushWrite();
 
             var line_buf: [512]u8 = undefined;
             const line = (try self.readLine(&line_buf)) orelse return error.CacheChunkMissing;