magdalena

Log | Files | Refs | README | LICENSE

commit 6d4e9c975b027c7846a7133974c2ad8e8ccb4c00
parent f1e6f51bc60368c7032a44f5d58f451c17179178
Author: mtmn <miro@haravara.org>
Date:   Tue,  9 Jun 2026 23:57:51 +0200

refactor: update to 0.16

Diffstat:
M.gitignore | 5+++--
Mbuild.zig | 2+-
Mbuild.zig.zon | 6+++---
Mjustfile | 7+++++--
Msrc/cfg.zig | 8+++-----
Msrc/cli.zig | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/db.zig | 83+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/fzf.zig | 323+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/io.zig | 22++++++++++++++++------
Msrc/main.zig | 19+++++++++++--------
Msrc/memcached.zig | 148++++++++++++++++++++++++++++++++-----------------------------------------------
11 files changed, 402 insertions(+), 334 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ -/.zig-cache -/zig-out +.zig-cache +zig-pkg +zig-out diff --git a/build.zig b/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); diff --git a/build.zig.zon b/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 = .{ diff --git a/justfile b/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 diff --git a/src/cfg.zig b/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 u8) Config { } 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, .{ diff --git a/src/cli.zig b/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,15 +25,121 @@ 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, diff --git a/src/db.zig b/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; diff --git a/src/fzf.zig b/src/fzf.zig @@ -26,36 +26,37 @@ fn gotoFile(allocator: std.mem.Allocator, db: ?*hist.Db, file_path: []const u8, 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: []const []const u8) bool { 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 []const u8, item 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: *hist.Db, config: *const cfg. 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: *hist.Db, config: *const cfg. 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: *hist.Db, config: *const cfg. 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: *hist.Db, config: *const cfg. 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: *hist.Db, config: *const cfg. } 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: *hist.Db, config: *const cfg 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: *hist.Db, config: *const cfg 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: *hist.Db, config: *const cfg 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, config: *const cfg.Confi "right,60%,border-left,+{2}+3/3,~3", }); defer fzf.deinit(); - const child = &fzf.child; - child.stdin_behavior = .Inherit; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; + fzf.stdin = .inherit; + fzf.stdout = .pipe; + fzf.stderr = .inherit; - try child.spawn(); + try fzf.spawn(); + const child = &fzf.child; - 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; 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 }); + return max_mtime; +} - while (stack.items.len > 0) { - var current = stack.pop() orelse unreachable; - defer current.dir.close(); +// 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; - 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; + 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 }); } - } - if (skip) continue; + continue; + }; + if (stat.kind == .directory) { + if (stat.mtime.nanoseconds > max_mtime.*) max_mtime.* = stat.mtime.nanoseconds; - 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 }); - } + 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); } } } } +} - return 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.fs.File.Kind, config: *const cfg.Config, depth: ?usize) !void { +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: *hist.Db, kind: std.fs.Fil 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: *hist.Db, kind: std.fs.Fil 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,66 +495,9 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.fs.Fil } 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); - } - - 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); - } - } - } + 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| { if (cache_key) |key| { @@ -533,7 +514,7 @@ fn runFzfWithWalker(allocator: std.mem.Allocator, db: *hist.Db, kind: std.fs.Fil } }.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: *hist.Db, kind: std.fs.Fil 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; }; diff --git a/src/io.zig b/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}); }; } diff --git a/src/main.zig b/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); diff --git a/src/memcached.zig b/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;