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.orig.zig b/src/tabula.orig.zig new file mode 100644 index 0000000..8847d21 --- /dev/null +++ b/src/tabula.orig.zig @@ -0,0 +1,314 @@ +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 max_column_widths = determine_col_widths(T, items); + + try header(T, opts.fields, &max_column_widths, opts.out); + + // Print body + for (items) |item| { + _ = try writer.write(sep); + + // TODO: Not the most efficient + const all_fields = @typeInfo(T).@"struct".fields; + inline for (all_fields) |field| { + var itr = opts.fields.iterator(); + var i: usize = 0; + while (itr.next()) |c| : (i += 1) { + if (std.mem.eql(u8, @tagName(c), field.name)) { + _ = try writer.write(" "); + try write_aligned(writer, @field(item, field.name), max_column_widths[i], .left); + try writer.print(" {s}", .{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"); + } +} + +fn determine_col_widths( + T: type, + items: []T, +) [@typeInfo(T).@"struct".fields.len]usize { + const all_fields = @typeInfo(T).@"struct".fields; + + 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 + const value_len = @field(item, field.name).len; + max_column_widths[i] = @max( + max_column_widths[i], + field.name.len, + value_len, + ); + } + } + + return max_column_widths; +} + +// Print the header of a table +fn header( + T: type, + fields: std.EnumSet(std.meta.FieldEnum(T)), + max_column_widths: []const 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 ++ "\n"); + } + + // Main Header + { + _ = try writer.write(sep); + + var itr = fields.iterator(); + var i: usize = 0; + while (itr.next()) |field| : (i += 1) { + _ = try writer.write(" "); + try write_aligned( + writer, + @tagName(field), + max_column_widths[i], + .center, + ); + try writer.print(" {s}", .{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"); + } +} + +fn write_aligned( + writer: *std.Io.Writer, + data: []const u8, + max_width: usize, + alignment: Alignment, +) !void { + const padding: [2]usize = switch (alignment) { + .left => .{ 0, max_width - data.len }, + .right => .{ max_width - data.len, 0 }, + .center => blk: { + // Faster to inline the divFloor? + const half = @divFloor(max_width - data.len, 2); + break :blk .{ half, max_width - data.len - half }; + }, + }; + + for (0..padding[0]) |_| { + _ = try writer.write(" "); + } + + _ = try writer.write(data); + + for (0..padding[1]) |_| { + _ = try writer.write(" "); + } +} + +const Alignment = enum { left, center, right }; + +test "can print a simple table" { + const gpa = std.testing.allocator; + 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(gpa); + 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}); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬─────┐ + \\│ foo │ bar │ + \\├─────┼─────┤ + \\│ bat │ baz │ + \\└─────┴─────┘ + \\ + , got); +} + +test "can print a table with varying header widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, abart: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: [1]F = .{.{ .foo = "bat", .abart = "baz" }}; + try structs(F, @constCast(&rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬───────┐ + \\│ foo │ abart │ + \\├─────┼───────┤ + \\│ bat │ baz │ + \\└─────┴───────┘ + \\ + , got); +} + +test "can print a table with varying column widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: [1]F = .{.{ .foo = "bat", .bar = "bazzar" }}; + try structs(F, @constCast(&rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬────────┐ + \\│ foo │ bar │ + \\├─────┼────────┤ + \\│ bat │ bazzar │ + \\└─────┴────────┘ + \\ + , got); +} + +test "can print a multi row table with varying column widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: []const F = &.{ + .{ .foo = "baz", .bar = "quz" }, + .{ .foo = "bat", .bar = "bazzar" }, + }; + try structs(F, @constCast(rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬────────┐ + \\│ foo │ bar │ + \\├─────┼────────┤ + \\│ baz │ quz │ + \\│ bat │ bazzar │ + \\└─────┴────────┘ + \\ + , got); +} + +test "can print a table with limited columns" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }}; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + try structs(F, @constCast(&rows), .{ .out = &out.writer, .fields = .initOne(.foo) }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┐ + \\│ foo │ + \\├─────┤ + \\│ bat │ + \\└─────┘ + \\ + , got); +} + +// TODO: test "returns a table" {} diff --git a/src/tabula.zig b/src/tabula.zig new file mode 100644 index 0000000..93f76e5 --- /dev/null +++ b/src/tabula.zig @@ -0,0 +1,331 @@ +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, + }, +) error{WriteFailed}!void { + const writer = opts.out; + const max_column_widths = determine_col_widths(T, items); + var buf: [255]u8 = undefined; + + try header(T, opts.fields, &max_column_widths, opts.out); + + // Print body + for (items) |item| { + _ = try writer.write(sep); + + // TODO: Not the most efficient + const all_fields = @typeInfo(T).@"struct".fields; + inline for (all_fields) |field| { + var itr = opts.fields.iterator(); + var i: usize = 0; + while (itr.next()) |c| : (i += 1) { + if (std.mem.eql(u8, @tagName(c), field.name)) { + const col = buf[0..max_column_widths[i]]; + @memset(col, ' '); + + align_text(@field(item, field.name), col, .left); + + try writer.print(" {s} {s}", .{ col, 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 len = hor.len; + const padding = buf[0 .. (max_column_widths[i] + 2) * len]; + + for (0..max_column_widths[i] + 2) |j| { + @memcpy(padding[len * j .. len * j + len], hor); + } + + _ = try writer.write(padding); + } + + _ = try writer.write(br ++ "\n"); + } +} + +fn determine_col_widths( + T: type, + items: []T, +) [@typeInfo(T).@"struct".fields.len]usize { + const all_fields = @typeInfo(T).@"struct".fields; + + 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 + const value_len = @field(item, field.name).len; + max_column_widths[i] = @max( + max_column_widths[i], + field.name.len, + value_len, + ); + } + } + + return max_column_widths; +} + +// Print the header of a table +fn header( + T: type, + fields: std.EnumSet(std.meta.FieldEnum(T)), + max_column_widths: []const usize, + writer: *std.Io.Writer, +) error{WriteFailed}!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 ++ "\n"); + } + + // Main Header + { + var buf: [255]u8 = undefined; + + _ = try writer.write(sep); + + var itr = fields.iterator(); + var i: usize = 0; + while (itr.next()) |field| : (i += 1) { + const col = buf[0..max_column_widths[i]]; + @memset(col, ' '); + align_text(@tagName(field), col, .center); + + try writer.print(" {s} {s}", .{ col, 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"); + } +} + +fn align_text(data: []const u8, buf: []u8, alignment: Alignment) void { + const dest = switch (alignment) { + .left => buf[0..data.len], + .right => buf[buf.len - data.len .. buf.len], + .center => blk: { + const start = (buf.len - data.len) / 2; + + break :blk buf[start .. start + data.len]; + }, + }; + + @memcpy(dest, data); +} + +const Alignment = enum { left, center, right }; + +test "align text left" { + var buf: [12]u8 = @splat(' '); + + align_text("foo", &buf, .left); + + try std.testing.expectEqualStrings("foo ", &buf); +} + +test "align text right" { + var buf: [12]u8 = @splat(' '); + + align_text("foo", &buf, .right); + + try std.testing.expectEqualStrings(" foo", &buf); +} + +test "align text center" { + var buf: [12]u8 = @splat(' '); + + align_text("foo", &buf, .center); + + try std.testing.expectEqualStrings(" foo ", &buf); +} + +test "can print a simple table" { + const gpa = std.testing.allocator; + 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(gpa); + 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}); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬─────┐ + \\│ foo │ bar │ + \\├─────┼─────┤ + \\│ bat │ baz │ + \\└─────┴─────┘ + \\ + , got); +} + +test "can print a table with varying header widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, abart: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: [1]F = .{.{ .foo = "bat", .abart = "baz" }}; + try structs(F, @constCast(&rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬───────┐ + \\│ foo │ abart │ + \\├─────┼───────┤ + \\│ bat │ baz │ + \\└─────┴───────┘ + \\ + , got); +} + +test "can print a table with varying column widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: [1]F = .{.{ .foo = "bat", .bar = "bazzar" }}; + try structs(F, @constCast(&rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬────────┐ + \\│ foo │ bar │ + \\├─────┼────────┤ + \\│ bat │ bazzar │ + \\└─────┴────────┘ + \\ + , got); +} + +test "can print a multi row table with varying column widths" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + const rows: []const F = &.{ + .{ .foo = "baz", .bar = "quz" }, + .{ .foo = "bat", .bar = "bazzar" }, + }; + try structs(F, @constCast(rows), .{ .out = &out.writer }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┬────────┐ + \\│ foo │ bar │ + \\├─────┼────────┤ + \\│ baz │ quz │ + \\│ bat │ bazzar │ + \\└─────┴────────┘ + \\ + , got); +} + +test "can print a table with limited columns" { + const gpa = std.testing.allocator; + const F = struct { foo: []const u8, bar: []const u8 }; + const rows: [1]F = .{.{ .foo = "bat", .bar = "baz" }}; + + var out: std.Io.Writer.Allocating = .init(gpa); + defer out.deinit(); + + try structs(F, @constCast(&rows), .{ .out = &out.writer, .fields = .initOne(.foo) }); + + const got = try out.toOwnedSlice(); + defer gpa.free(got); + + try std.testing.expectEqualStrings( + \\┌─────┐ + \\│ foo │ + \\├─────┤ + \\│ bat │ + \\└─────┘ + \\ + , got); +} + +// TODO: test "returns a table" {}