diff --git a/.envrc b/.envrc index 680227f..9a7514f 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,4 @@ use flake -export PATH=".:/home/spencer/github.com/envr-zig/deps/zig:/home/spencer/github.com/envr-zig/deps/zls:$PATH" +ROOT="/home/spencer/Desktop/envr" +export PATH=".:${ROOT}/deps/zig:${ROOT}/deps/zls:$PATH" diff --git a/.gitignore b/.gitignore index 8461b76..8c93171 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # dev env .direnv +/.env # dependencies deps diff --git a/src/Db.zig b/src/Db.zig index c297deb..1b831a0 100644 --- a/src/Db.zig +++ b/src/Db.zig @@ -46,11 +46,27 @@ pub fn open( defer private_keys.deinit(gpa); for (opts.config.keys) |key| { - private_keys.appendAssumeCapacity(key.private); + // FIXME: cheating here + if (std.mem.startsWith(u8, key.private, "~/")) { + const key_path = try std.fs.path.join(gpa, &.{ + opts.home, + key.private[2..], + }); + private_keys.appendAssumeCapacity(key_path); + // defer gpa.free(key_path); + } else { + private_keys.appendAssumeCapacity(key.private); + } } // TODO: Pass key(s) from Config try age.decrypt(io, gpa, private_keys.items, db_path, tmp_db_path); + + for (opts.config.keys, 0..) |key, i| { + if (std.mem.startsWith(u8, key.private, "~/")) { + gpa.free(private_keys.items[i]); + } + } } } @@ -173,7 +189,7 @@ const EnvFile = struct { sha256: []const u8, contents: []const u8, - fn deinit(self: *EnvFile, alloc: std.mem.Allocator) void { + pub fn deinit(self: *EnvFile, alloc: std.mem.Allocator) void { alloc.free(self.path); alloc.free(self.remotes); alloc.free(self.sha256); diff --git a/src/root.zig b/src/root.zig index 9a196c4..fd94fff 100644 --- a/src/root.zig +++ b/src/root.zig @@ -2,11 +2,11 @@ const std = @import("std"); const Io = std.Io; -const comma = @import("comma"); -const Command = comma.Command; +const Command = @import("comma").Command; const Config = @import("Config.zig"); const Db = @import("Db.zig"); +const tabula = @import("./tabula.zig"); pub const root: Command = .new(.{ .name = "envr", @@ -80,15 +80,23 @@ pub fn list( .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}); - } + defer arena.free(files); + + try tabula.structs( + @TypeOf(files[0]), + files, + .{ .out = out, .fields = .initOne(.path) }, + // .{ .out = out }, + ); try out.flush(); - return db.close(io, arena); // TODO: Defer this + try db.close(io, arena); // TODO: Defer this + + // TODO: Is this bad? + for (files) |file| { + @constCast(&file).deinit(arena); + } } test { @@ -129,3 +137,78 @@ test "parse unknown" { try std.testing.expectEqual(.unknown, cmd); } + +test "list returns a table" { + const io = std.testing.io; + const gpa = std.testing.allocator; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + try tmp_dir.dir.createDir(io, "home", .default_dir); + try tmp_dir.dir.createDir(io, "home/.envr", .default_dir); + try tmp_dir.dir.createDir(io, "home/.ssh", .default_dir); + try tmp_dir.dir.createDir(io, "tmp", .default_dir); + + const tmp_dir_path = try tmp_dir.dir.realPathFileAlloc(io, ".", gpa); + defer gpa.free(tmp_dir_path); + + const home = try std.fs.path.join(gpa, &.{ tmp_dir_path, "home" }); + defer gpa.free(home); + const tmp = try std.fs.path.join(gpa, &.{ tmp_dir_path, "tmp" }); + defer gpa.free(tmp); + + try std.Io.Dir.cwd().copyFile( + "fixtures/encrypted-single-file.db.age", + tmp_dir.dir, + "home/.envr/data.age", + io, + .{}, + ); + + try std.Io.Dir.cwd().copyFile( + "fixtures/default_config.json", + tmp_dir.dir, + "home/.envr/config.json", + io, + .{}, + ); + + try std.Io.Dir.cwd().copyFile( + "fixtures/insecure-test-key", + tmp_dir.dir, + "home/.ssh/id_ed25519", + io, + .{}, + ); + + try std.Io.Dir.cwd().copyFile( + "fixtures/insecure-test-key.pub", + tmp_dir.dir, + "home/.ssh/id_ed25519.pub", + io, + .{}, + ); + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + // Run Test + + try list( + io, + std.testing.allocator, + &out.writer, + home, + tmp, + ); + + try std.testing.expectEqualStrings( + \\┌────────────────────────┐ + \\│ path │ + \\├────────────────────────┤ + \\│ ~/project/.env.example │ + \\└────────────────────────┘ + \\ + , try out.toOwnedSlice()); +} diff --git a/src/tabula.zig b/src/tabula.zig new file mode 100644 index 0000000..981592f --- /dev/null +++ b/src/tabula.zig @@ -0,0 +1,254 @@ +const std = @import("std"); + +const hor = "─"; +const tl = "┌"; +const tm = "┬"; +const tr = "┐"; +const sep = "│"; +const ml = "├"; +const mm = "┼"; +const mr = "┤"; +const bl = "└"; +const bm = "┴"; +const br = "┘"; + +// Print a list of structs as a table to opts.out. +pub fn structs( + comptime T: type, + items: []T, + opts: struct { + out: *std.Io.Writer, // TODO: Default stdout. + fields: std.EnumSet(std.meta.FieldEnum(T)) = .full, + }, +) !void { + const writer = opts.out; + const all_fields = @typeInfo(T).@"struct".fields; + + // Calculate column widths + var max_column_widths: [all_fields.len]usize = @splat(0); + { + for (items) |item| { + inline for (all_fields, 0..) |field, i| { + // TODO: Get str len of item + max_column_widths[i] = @max(max_column_widths[i], field.name.len, @field(item, field.name).len); + } + } + } + + try header(T, opts.fields, &max_column_widths, opts.out); + + // Print body + for (items) |item| { + _ = try writer.write(sep); + + // TODO: Not the most efficient + inline for (all_fields) |field| { + var itr = opts.fields.iterator(); + while (itr.next()) |c| { + if (std.mem.eql(u8, @tagName(c), field.name)) { + try writer.print(" {s} {s}", .{ + @field(item, field.name), + sep, + }); + break; + } + } + } + + _ = try writer.write("\n"); + } + + // Print post-body + { + _ = try writer.write(bl); + + var itr = opts.fields.iterator(); + var i: usize = 0; + while (itr.next()) |_| : (i += 1) { + if (i > 0) { + _ = try writer.write(bm); + } + + const padding = max_column_widths[i] + 2; + for (0..padding) |_| { + _ = try writer.write(hor); + } + } + + _ = try writer.write(br); + _ = try writer.write("\n"); + } +} + +// Print the header of a table +fn header( + T: type, + fields: std.EnumSet(std.meta.FieldEnum(T)), + max_column_widths: []usize, + writer: *std.Io.Writer, +) !void { + + // Print Pre-Header + { + _ = try writer.write(tl); + + var itr = fields.iterator(); + var i: usize = 0; + while (itr.next()) |_| : (i += 1) { + if (i > 0) { + _ = try writer.write(tm); + } + const padding = max_column_widths[i] + 2; + for (0..padding) |_| { + _ = try writer.write(hor); + } + } + + _ = try writer.write(tr); + _ = try writer.write("\n"); + } + + // Main Header + { + _ = try writer.write(sep); + + var itr = fields.iterator(); + var i: usize = 0; + while (itr.next()) |field| : (i += 1) { + try writer.print(" {s}", .{@tagName(field)}); + + const padding = max_column_widths[i] - @tagName(field).len + 1; + for (0..padding) |_| { + _ = try writer.write(" "); + } + _ = try writer.write(sep); + } + + try writer.print("\n", .{}); + } + + // Print post-header + { + _ = try writer.write(ml); + + var itr = fields.iterator(); + var i: usize = 0; + while (itr.next()) |_| : (i += 1) { + if (i > 0) { + _ = try writer.write(mm); + } + const padding = max_column_widths[i] + 2; + for (0..padding) |_| { + _ = try writer.write(hor); + } + } + + _ = try writer.write(mr ++ "\n"); + } +} + +pub fn Table(T: type) type { + return struct { + rows: []const T, // TODO: Do better + // cols: []const std.builtin.Type.StructField = @typeInfo(T).@"struct".fields, + // cols: []const [:0]const u8, + cols: std.EnumSet(std.meta.FieldEnum(T)) = .full, + + fn initMany(rows: []const T, cols: []const [:0]const u8) Table(T) { + return .{ .rows = rows, .cols = cols }; + } + + pub fn format( + self: @This(), + writer: *std.Io.Writer, + ) !void { + const cols = @typeInfo(T).@"struct".fields; + // const cols = self.cols; + + // Print Header + { + _ = try writer.write(sep); + + inline for (cols) |col| { + var itr = self.cols.iterator(); + while (itr.next()) |c| { + if (std.mem.eql(u8, @tagName(c), col.name)) { + try writer.print(" {s} {s}", .{ col.name, sep }); + break; + } + } + } + + try writer.print("\n", .{}); + } + + // Print Body { + { + for (self.rows) |row| { + _ = try writer.write(sep); + + inline for (cols) |col| { + var itr = self.cols.iterator(); + while (itr.next()) |c| { + if (std.mem.eql(u8, @tagName(c), col.name)) { + try writer.print(" {s} {s}", .{ + @field(row, col.name), + sep, + }); + } + } + } + _ = try writer.write("\n"); + } + } + } + }; +} + +test "can print a simple table" { + const F = struct { foo: []const u8, bar: []const u8 }; + // const table: Table(F) = .{ .rows = &.{.{ .foo = "bat", .bar = "baz" }} }; + + var out: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer out.deinit(); + + const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }}; + try structs(F, @constCast(&rows), .{ .out = &out.writer }); + + // try out.writer.print("{f}", .{table}); + + try std.testing.expectEqualStrings( + \\┌─────┬─────┐ + \\│ foo │ bar │ + \\├─────┼─────┤ + \\│ bat │ baz │ + \\└─────┴─────┘ + \\ + , try out.toOwnedSlice()); +} + +test "can print a table with limited columns" { + const F = struct { foo: []const u8, bar: []const u8 }; + const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }}; + // const table: Table(F) = .{ .rows = &.{.{ .foo = "bat", .bar = "baz" }}, .cols = .initOne(.foo) }; + + var out: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer out.deinit(); + + // try out.writer.print("{f}", .{table}); + + try structs(F, @constCast(&rows), .{ .out = &out.writer, .fields = .initOne(.foo) }); + + try std.testing.expectEqualStrings( + \\┌─────┐ + \\│ foo │ + \\├─────┤ + \\│ bat │ + \\└─────┘ + \\ + , try out.toOwnedSlice()); +} + +// TODO: fn determine_line_lengths(T: type, items: []T) + +// TODO: test "returns a table" {}