4 Commits

4 changed files with 123 additions and 14 deletions

View File

@@ -3,6 +3,10 @@
Have you ever wanted to back up all your .env files in case your hard drive gets
nuked? `envr` makes it easier.
> [!CAUTION]
> The Zig community is quite anti-AI. Please read the [AI Disclaimer](#ai-disclaimer)
> before wasting your time.
`envr` is a binary application that tracks your `.env` files
in an encyrpted sqlite database. Changes can be effortlessly synced with
`envr sync`, and restored with `envr restore`.
@@ -132,3 +136,13 @@ This project is licensed under the [MIT License](./LICENSE).
For issues, feature requests, or questions, please
[open an issue](https://github.com/sbrow/envr/issues).
## AI Disclaimer
Unless noted here, you can be assured that I have personally written and reviewed
every line of code in this software.
- Many compiler errors that couldn't be solved with a quick google search were
solved by passing errors to AI and transcribing the suggestions.
- The "Pre-Zig" version of this readme was written by AI and then edited by me.
- The Go code was mostly written using opencode, and manually tested by me.

View File

@@ -6,7 +6,8 @@ pub const Command = struct {
name: []const u8,
short: ?[]const u8 = null,
long: ?[]const u8 = null,
subcommands: []const Command = &[0]Command{},
subcommands: []const Command = &.{},
examples: [][]const u8 = &.{},
Type: type,
pub fn new(cmd: CommandOptions) Command {
@@ -27,10 +28,11 @@ pub const Command = struct {
};
}
pub fn parse(comptime self: @This(), args: []const []const u8) ParseError!self.Type {
pub fn parse(comptime self: @This(), args: []const []const u8) self.Type {
if (args.len == 0) {
return ParseError.InvalidType;
return @enumFromInt(0);
}
const target = args[0];
inline for (self.subcommands, 1..) |cmd, idx| {
@@ -39,7 +41,7 @@ pub const Command = struct {
}
}
return @enumFromInt(0);
return @enumFromInt(self.subcommands.len + 1);
}
};
@@ -54,8 +56,8 @@ const CommandOptions = struct {
subcommands: []const CommandOptions = &[0]CommandOptions{},
fn as_enum(self: @This()) type {
var field_names: [self.subcommands.len + 1][]const u8 = undefined;
var field_values: [self.subcommands.len + 1]u32 = undefined;
var field_names: [self.subcommands.len + 2][]const u8 = undefined;
var field_values: [self.subcommands.len + 2]u32 = undefined;
field_names[0] = self.name;
field_values[0] = 0;
@@ -65,6 +67,9 @@ const CommandOptions = struct {
field_values[idx] = idx;
}
field_names[self.subcommands.len + 1] = "unknown";
field_values[self.subcommands.len + 1] = self.subcommands.len + 1;
return @Enum(
u32,
.exhaustive,

View File

@@ -14,16 +14,20 @@ pub fn main(init: std.process.Init) !void {
const args = try init.minimal.args.toSlice(arena);
run(init.io, args) catch return fallback_to_go(init.io, arena, args);
try run(init.environ_map, init.io, arena, args);
}
/// Attempt to run the requested command.
fn run(io: Io, args: []const [:0]const u8) !void {
const cmd = try envr.root.parse(args[1..]);
fn run(
environ_map: *std.process.Environ.Map,
io: Io,
arena: std.mem.Allocator,
args: []const [:0]const u8,
) !void {
const cmd = envr.root.parse(args[1..]);
switch (cmd) {
.envr => {
// TODO: Print help
return comma.ParseError.InvalidType;
.envr, .unknown => {
return fallback_to_go(io, arena, args);
},
.version => {
var stdout_buffer: [1024]u8 = undefined;
@@ -32,6 +36,17 @@ fn run(io: Io, args: []const [:0]const u8) !void {
return version(stdout_writer);
},
.deps => {
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 deps(
io,
stdout_writer,
environ_map.get("PATH").?,
);
},
}
}
@@ -40,6 +55,63 @@ fn version(writer: *Io.Writer) !void {
try writer.flush();
}
// Display dependency statuses
fn deps(
io: Io,
writer: *Io.Writer,
path: []const u8,
) !void {
const feats: Features = try .scan(io, path);
// FIXME: Draw as a table
try writer.print("features: {}", .{feats});
try writer.flush();
}
const Features = packed struct {
git: bool = false,
fd: bool = false,
const all_features: Features = .{
.git = true,
.fd = true,
};
/// Scans your PATH variable for programs.
pub fn scan(io: Io, path: []const u8) !@This() {
var feats: Features = .{};
var dirs = std.mem.splitScalar(u8, path, std.fs.path.delimiter);
loop: while (dirs.next()) |dir| {
const dirt = Io.Dir.openDir(Io.Dir.cwd(), io, dir, .{ .follow_symlinks = true, .iterate = true }) catch continue;
defer dirt.close(io);
var dir_paths = dirt.iterate();
while (try dir_paths.next(io)) |file| {
// FIXME: Check if executable
if (std.mem.eql(u8, std.fs.path.basename(file.name), "git")) {
feats.git = true;
if (feats == Features.all_features) {
break :loop;
}
}
if (std.mem.eql(u8, std.fs.path.basename(file.name), "fd")) {
feats.fd = true;
if (feats == Features.all_features) {
break :loop;
}
}
}
}
return feats;
}
};
fn fallback_to_go(
io: Io,
arena: std.mem.Allocator,

View File

@@ -7,11 +7,22 @@ const Command = comma.Command;
pub const root: Command = .new(.{
.name = "envr",
.subcommands = &.{.{ .name = "version" }},
.subcommands = &.{
.{
.name = "deps",
.short = "Check for missing binaries",
.long =
\\envr relies on external binaries for certain functionality.
\\
\\ The deps command reports which binaries are available and which are not."
,
},
.{ .name = "version" },
},
});
test "enum type" {
const got: root.Type = @enumFromInt(1);
const got: root.Type = @enumFromInt(2);
try std.testing.expectEqual(.version, got);
}
@@ -22,3 +33,10 @@ test "parse version" {
try std.testing.expectEqual(.version, cmd);
}
test "parse unknown" {
const args = &[_][]const u8{"bad", "value"};
const cmd = root.parse(args);
try std.testing.expectEqual(.unknown, cmd);
}