From aedbeb28bdc80f677f60a77860c64262c23b68c2 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Wed, 27 May 2026 18:41:03 -0400 Subject: [PATCH] feat: list cmd. --- src/Db.zig | 23 +++++++++++------------ src/main.zig | 43 ++++++++++++++++++++++++++++--------------- src/root.zig | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/Db.zig b/src/Db.zig index 9344eef..c297deb 100644 --- a/src/Db.zig +++ b/src/Db.zig @@ -7,7 +7,7 @@ const age = @import("age.zig"); const Config = @import("Config.zig"); /// controls the keys and filepaths used for saving -config: Config, +opts: OpenOptions, /// The underlying data store. sql_db: sqlite.Db, @@ -54,7 +54,7 @@ pub fn open( } } - return open_decrypted(opts.config, tmp_db_path); + return open_decrypted(opts, tmp_db_path); } const OpenOptions = struct { @@ -68,7 +68,7 @@ const OpenOptions = struct { }; /// Create a new instance of the database -fn open_decrypted(config: Config, tmp_db_path: [:0]const u8) !@This() { +fn open_decrypted(opts: OpenOptions, tmp_db_path: [:0]const u8) !@This() { var db = try sqlite.Db.init(.{ .mode = .{ .File = tmp_db_path }, .open_flags = .{ @@ -89,7 +89,7 @@ fn open_decrypted(config: Config, tmp_db_path: [:0]const u8) !@This() { return .{ .sql_db = db, - .config = config, + .opts = opts, }; } @@ -119,28 +119,27 @@ pub fn close( self: *@This(), io: std.Io, gpa: std.mem.Allocator, - opts: OpenOptions, ) !void { defer self.sql_db.deinit(); if (self.changed) { - const tmp_db_path = try std.fs.path.join(gpa, &.{ opts.tmp, "envr.db" }); + const tmp_db_path = try std.fs.path.join(gpa, &.{ self.opts.tmp, "envr.db" }); defer gpa.free(tmp_db_path); try self.sql_db.exec("VACUUM INTO ?", .{}, .{tmp_db_path}); - const db_path = try std.fs.path.join(gpa, &.{ opts.home, ".envr", "data.age" }); + const db_path = try std.fs.path.join(gpa, &.{ self.opts.home, ".envr", "data.age" }); defer gpa.free(db_path); { // TODO: Use std.MultiArrayList? Had json issues var public_keys: std.ArrayList([]const u8) = try .initCapacity( gpa, - opts.config.keys.len, + self.opts.config.keys.len, ); defer public_keys.deinit(gpa); - for (opts.config.keys) |key| { + for (self.opts.config.keys) |key| { public_keys.appendAssumeCapacity(key.private); } @@ -153,7 +152,7 @@ pub fn close( /// Returns a list of all the .env files present in the database. /// The caller is responsible for freeing memory -fn list(self: *@This(), gpa: std.mem.Allocator) ![]EnvFile { +pub fn list(self: *@This(), gpa: std.mem.Allocator) ![]EnvFile { var stmt = try self.sql_db.prepare( "select path, remotes, sha256, contents from envr_env_files", ); @@ -284,7 +283,7 @@ test "Closing a fresh database does not create a file" { tmp_dir.dir.access(io, db_path, .{ .read = true }), ); - try db.close(io, gpa, .{ .home = home, .tmp = tmp }); + try db.close(io, gpa); try std.testing.expectError( error.FileNotFound, @@ -469,5 +468,5 @@ test "list displays the database's keys" { try std.testing.expectEqualStrings(&std.fmt.bytesToHex(&hash, .lower), file.sha256); } - try db.close(io, gpa, .{ .home = home, .tmp = tmp }); + try db.close(io, gpa); } diff --git a/src/main.zig b/src/main.zig index 3da4333..df7c36e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,7 +2,6 @@ const std = @import("std"); const Io = std.Io; const config = @import("config"); - const comma = @import("comma"); const envr = @import("envr"); @@ -51,6 +50,20 @@ fn run( environ_map.get("PATH").?, ); }, + .list => { + var stdout_buffer: [1024]u8 = undefined; + var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); + const stdout_writer = &stdout_file_writer.interface; + + return envr.list( + io, + arena, + stdout_writer, + environ_map.get("HOME").?, + // TODO: Don't hardcode this? + "/tmp", + ); + }, .unknown => { return fallback_to_go(io, arena, args); }, @@ -137,10 +150,10 @@ fn fallback_to_go( test "simple test" { const gpa = std.testing.allocator; - var list: std.ArrayList(i32) = .empty; - defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak! - try list.append(gpa, 42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); + var alist: std.ArrayList(i32) = .empty; + defer alist.deinit(gpa); // Try commenting this out and see if zig detects the memory leak! + try alist.append(gpa, 42); + try std.testing.expectEqual(@as(i32, 42), alist.pop()); } test "fuzz example" { @@ -152,23 +165,23 @@ fn testOne(context: void, smith: *std.testing.Smith) !void { // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! const gpa = std.testing.allocator; - var list: std.ArrayList(u8) = .empty; - defer list.deinit(gpa); + var alist: std.ArrayList(u8) = .empty; + defer alist.deinit(gpa); while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) { .add_data => { - const slice = try list.addManyAsSlice(gpa, smith.value(u4)); + const slice = try alist.addManyAsSlice(gpa, smith.value(u4)); smith.bytes(slice); }, .dup_data => { - if (list.items.len == 0) continue; - if (list.items.len > std.math.maxInt(u32)) return error.SkipZigTest; - const len = smith.valueRangeAtMost(u32, 1, @min(32, list.items.len)); - const off = smith.valueRangeAtMost(u32, 0, @intCast(list.items.len - len)); - try list.appendSlice(gpa, list.items[off..][0..len]); + if (alist.items.len == 0) continue; + if (alist.items.len > std.math.maxInt(u32)) return error.SkipZigTest; + const len = smith.valueRangeAtMost(u32, 1, @min(32, alist.items.len)); + const off = smith.valueRangeAtMost(u32, 0, @intCast(alist.items.len - len)); + try alist.appendSlice(gpa, alist.items[off..][0..len]); try std.testing.expectEqualSlices( u8, - list.items[off..][0..len], - list.items[list.items.len - len ..], + alist.items[off..][0..len], + alist.items[alist.items.len - len ..], ); }, }; diff --git a/src/root.zig b/src/root.zig index 7ed93c4..9a196c4 100644 --- a/src/root.zig +++ b/src/root.zig @@ -5,6 +5,9 @@ const Io = std.Io; const comma = @import("comma"); const Command = comma.Command; +const Config = @import("Config.zig"); +const Db = @import("Db.zig"); + pub const root: Command = .new(.{ .name = "envr", .short = "Manage your .env files.", @@ -49,6 +52,10 @@ pub const root: Command = .new(.{ \\ The deps command reports which binaries are available and which are not." , }, + .{ + .name = "list", + .short = "View your tracked files", + }, .{ .name = "version", .short = "Show envr's version", @@ -56,17 +63,59 @@ pub const root: Command = .new(.{ }, }); +pub fn list( + io: Io, + arena: std.mem.Allocator, + out: *std.Io.Writer, + home: []const u8, + tmp: []const u8, +) !void { + // TODO: Don't hardcode + const cfgPath = try std.fs.path.join(arena, &.{ home, ".envr", "config.json" }); + const cfg: Config = (try Config.load(io, arena, cfgPath)).value; + + var db: Db = try .open(io, arena, .{ + .config = cfg, + .home = home, + .tmp = tmp, + }); + + _ = try out.write("Path\n"); + const files = try db.list(arena); + for (files) |file| { + // TODO: Table printer + try out.print("{s}\n", .{file.path}); + } + try out.flush(); + + return db.close(io, arena); // TODO: Defer this +} + test { std.testing.refAllDecls(@import("Config.zig")); std.testing.refAllDecls(@import("Db.zig")); } test "enum type" { - const got: root.Type = @enumFromInt(2); + const got: root.Type = @enumFromInt(3); try std.testing.expectEqual(.version, got); } +test "parse deps" { + const args = &[_][]const u8{"deps"}; + const cmd = root.parse(args); + + try std.testing.expectEqual(.deps, cmd); +} + +test "parse list" { + const args = &[_][]const u8{"list"}; + const cmd = root.parse(args); + + try std.testing.expectEqual(.list, cmd); +} + test "parse version" { const args = &[_][]const u8{"version"}; const cmd = root.parse(args);