refactor: scaffolded odin project with CLI parser, version command, Go fallback

This commit is contained in:
2026-06-11 20:02:12 -04:00
parent c6d0308842
commit e989b88303
11 changed files with 385 additions and 12 deletions

1
.gitignore vendored
View File

@@ -7,4 +7,5 @@ man
# build artifacts # build artifacts
builds builds
envr envr
envr-go
result result

95
cli.odin Normal file
View File

@@ -0,0 +1,95 @@
package main
import "core:fmt"
import "core:os"
import "core:strings"
Command :: struct {
name: string,
args: [dynamic]string,
flags: map[string]string,
bool_set: map[string]bool,
}
IMPLEMENTED_COMMANDS := []string{
"version",
}
parse_args :: proc() -> (cmd: Command, ok: bool) {
args := os.args
if len(args) < 2 {
print_usage()
return Command{}, false
}
cmd.name = args[1]
cmd.args = make([dynamic]string)
cmd.flags = make(map[string]string)
cmd.bool_set = make(map[string]bool)
i := 2
for i < len(args) {
arg := args[i]
if strings.starts_with(arg, "--") {
key := arg[2:]
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
cmd.flags[key] = args[i+1]
i += 2
} else {
cmd.bool_set[key] = true
i += 1
}
} else if strings.starts_with(arg, "-") && len(arg) == 2 {
key_slice := arg[1:2]
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
cmd.flags[key_slice] = args[i+1]
i += 2
} else {
cmd.bool_set[key_slice] = true
i += 1
}
} else {
append(&cmd.args, arg)
i += 1
}
}
return cmd, true
}
is_implemented :: proc(name: string) -> bool {
for c in IMPLEMENTED_COMMANDS {
if c == name {
return true
}
}
return false
}
has_flag :: proc(cmd: ^Command, name: string) -> bool {
_, ok := cmd.flags[name]
if ok {
return true
}
_, ok2 := cmd.bool_set[name]
return ok2
}
print_usage :: proc() {
fmt.println("envr - Manage your .env files.")
fmt.println("")
fmt.println("Usage: envr <command> [args]")
fmt.println("")
fmt.println("Commands:")
fmt.println(" init Set up envr")
fmt.println(" scan Find and select .env files for backup")
fmt.println(" sync Update or restore your env backups")
fmt.println(" backup <path> Import a .env file into envr")
fmt.println(" restore <path> Restore a .env file from the database")
fmt.println(" list View your tracked files")
fmt.println(" remove <path> Remove a .env file from your database")
fmt.println(" check [path] Check if files are backed up")
fmt.println(" deps Check for missing binaries")
fmt.println(" version Show envr's version")
fmt.println(" edit-config Edit your config with your default editor")
}

30
cmd_deps.odin Normal file
View File

@@ -0,0 +1,30 @@
package main
import "core:fmt"
cmd_deps :: proc(cmd: ^Command) {
feats := check_features()
headers := []string{"Feature", "Status"}
rows: [dynamic][]string
if .Git in feats {
append(&rows, []string{"Git", "\u2713 Available"})
} else {
append(&rows, []string{"Git", "\u2717 Missing"})
}
if .Fd in feats {
append(&rows, []string{"fd", "\u2713 Available"})
} else {
append(&rows, []string{"fd", "\u2717 Missing"})
}
if .Age in feats {
append(&rows, []string{"age", "\u2713 Available"})
} else {
append(&rows, []string{"age", "\u2717 Missing"})
}
render_table(headers, rows[:])
}

45
features.odin Normal file
View File

@@ -0,0 +1,45 @@
package main
import "core:os"
import "core:strings"
Feature :: enum {
Git,
Fd,
Age,
}
AvailableFeatures :: bit_set[Feature]
check_features :: proc() -> AvailableFeatures {
feats: AvailableFeatures
if find_binary("git") != "" {
feats += {.Git}
}
if find_binary("fd") != "" {
feats += {.Fd}
}
if find_binary("age") != "" {
feats += {.Age}
}
return feats
}
find_binary :: proc(name: string) -> string {
path_env := os.get_env("PATH", context.allocator)
paths := strings.split(path_env, ":")
for p in paths {
candidate := strings.join({strings.trim_right(p, "/"), name}, "/")
_, err := os.stat(candidate, context.allocator)
if err == nil {
return candidate
}
}
return ""
}
has_feature :: proc(feats: AvailableFeatures, f: Feature) -> bool {
return f in feats
}

24
flake.lock generated
View File

@@ -5,11 +5,11 @@
"nixpkgs-lib": "nixpkgs-lib" "nixpkgs-lib": "nixpkgs-lib"
}, },
"locked": { "locked": {
"lastModified": 1768135262, "lastModified": 1778716662,
"narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=", "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci", "owner": "hercules-ci",
"repo": "flake-parts", "repo": "flake-parts",
"rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac", "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -36,11 +36,11 @@
}, },
"nixpkgs-lib": { "nixpkgs-lib": {
"locked": { "locked": {
"lastModified": 1765674936, "lastModified": 1777168982,
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=", "narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixpkgs.lib", "repo": "nixpkgs.lib",
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85", "rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -51,11 +51,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1768178648, "lastModified": 1781173989,
"narHash": "sha256-kz/F6mhESPvU1diB7tOM3nLcBfQe7GU7GQCymRlTi/s=", "narHash": "sha256-fnzKKPvS+oieI/pTzotA5tkoM47EB1NpaBcgk4R97hE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3fbab70c6e69c87ea2b6e48aa6629da2aa6a23b0", "rev": "8c91a71d13451abc40eb9dae8910f972f979852f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -80,11 +80,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1768158989, "lastModified": 1780220602,
"narHash": "sha256-67vyT1+xClLldnumAzCTBvU0jLZ1YBcf4vANRWP3+Ak=", "narHash": "sha256-eynAfOmbmxJnkp7YewvCEbShNnnYJ9gLLqkzsYtBPeM=",
"owner": "numtide", "owner": "numtide",
"repo": "treefmt-nix", "repo": "treefmt-nix",
"rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca", "rev": "db947814a175b7ca6ded66e21383d938df01c227",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -97,6 +97,9 @@
gotools gotools
cobra-cli cobra-cli
unstable.odin
unstable.ols
# Build tools # Build tools
zip zip

56
main.odin Normal file
View File

@@ -0,0 +1,56 @@
package main
import "core:fmt"
import "core:os"
GO_BINARY :: "./envr-go"
main :: proc() {
cmd, ok := parse_args()
if !ok {
return
}
if !is_implemented(cmd.name) {
fallback_to_go()
return
}
switch cmd.name {
case "version":
cmd_version(&cmd)
case:
fmt.printf("Unknown command: %s\n", cmd.name)
print_usage()
os.exit(1)
}
}
fallback_to_go :: proc() {
args := make([dynamic]string)
append(&args, "./envr-go")
for i in 1..<len(os.args) {
append(&args, os.args[i])
}
desc := os.Process_Desc{
command = args[:],
stdin = os.stdin,
stdout = os.stdout,
stderr = os.stderr,
}
p, err1 := os.process_start(desc)
if err1 != nil {
fmt.printf("Error: failed to run envr-go: %v\n", err1)
os.exit(1)
}
state, err2 := os.process_wait(p)
if err2 != nil {
fmt.printf("Error waiting for envr-go: %v\n", err2)
os.exit(1)
}
os.exit(int(state.exit_code))
}

39
stubs.odin Normal file
View File

@@ -0,0 +1,39 @@
package main
import "core:fmt"
cmd_init :: proc(cmd: ^Command) {
fmt.println("TODO: init")
}
cmd_list :: proc(cmd: ^Command) {
fmt.println("TODO: list")
}
cmd_scan :: proc(cmd: ^Command) {
fmt.println("TODO: scan")
}
cmd_sync :: proc(cmd: ^Command) {
fmt.println("TODO: sync")
}
cmd_backup :: proc(cmd: ^Command) {
fmt.println("TODO: backup")
}
cmd_restore :: proc(cmd: ^Command) {
fmt.println("TODO: restore")
}
cmd_remove :: proc(cmd: ^Command) {
fmt.println("TODO: remove")
}
cmd_check :: proc(cmd: ^Command) {
fmt.println("TODO: check")
}
cmd_edit_config :: proc(cmd: ^Command) {
fmt.println("TODO: edit-config")
}

84
table.odin Normal file
View File

@@ -0,0 +1,84 @@
package main
import "core:fmt"
import "core:strings"
render_table :: proc(headers: []string, rows: [][]string) {
if !is_tty() {
render_json_rows(headers, rows)
return
}
col_widths := make([dynamic]int, len(headers))
for i in 0..<len(headers) {
append(&col_widths, len(headers[i]))
}
for r in rows {
for i in 0..<len(r) {
if i < len(col_widths) && len(r[i]) > col_widths[i] {
col_widths[i] = len(r[i])
}
}
}
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
defer delete(col_widths)
hline :: proc(b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
strings.write_string(b, left)
for i in 0..<len(widths) {
for _ in 0..<widths[i]+2 {
strings.write_string(b, "\u2500")
}
if i < len(widths)-1 {
strings.write_string(b, mid)
} else {
strings.write_string(b, right)
}
}
fmt.println(strings.to_string(b^))
strings.builder_reset(b)
}
hline(&b, "\u250c", "\u252c", "\u2510", col_widths)
strings.write_string(&b, "\u2502")
for i in 0..<len(headers) {
fmt.sbprintf(&b, " %-*s \u2502", col_widths[i], headers[i])
}
fmt.println(strings.to_string(b))
strings.builder_reset(&b)
hline(&b, "\u251c", "\u253c", "\u2524", col_widths)
for r in rows {
strings.write_string(&b, "\u2502")
for i in 0..<len(r) {
fmt.sbprintf(&b, " %-*s \u2502", col_widths[i], r[i])
}
fmt.println(strings.to_string(b))
strings.builder_reset(&b)
}
hline(&b, "\u2514", "\u2534", "\u2518", col_widths)
}
render_json_rows :: proc(headers: []string, rows: [][]string) {
fmt.print("[")
for i in 0..<len(rows) {
if i > 0 {
fmt.print(",")
}
fmt.print("{")
for j in 0..<len(headers) {
if j > 0 {
fmt.print(",")
}
fmt.printf("\"%s\":\"%s\"", headers[j], rows[i][j])
}
fmt.print("}")
}
fmt.println("]")
}

7
tty.odin Normal file
View File

@@ -0,0 +1,7 @@
package main
import "core:sys/posix"
is_tty :: proc() -> bool {
return bool(posix.isatty(1))
}

13
version.odin Normal file
View File

@@ -0,0 +1,13 @@
package main
import "core:fmt"
VERSION :: "0.2.0"
cmd_version :: proc(cmd: ^Command) {
if has_flag(cmd, "long") || has_flag(cmd, "l") {
fmt.printf("envr version %s\n", VERSION)
} else {
fmt.println(VERSION)
}
}