mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
refactor: scaffolded odin project with CLI parser, version command, Go fallback
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ man
|
|||||||
# build artifacts
|
# build artifacts
|
||||||
builds
|
builds
|
||||||
envr
|
envr
|
||||||
|
envr-go
|
||||||
result
|
result
|
||||||
|
|||||||
95
cli.odin
Normal file
95
cli.odin
Normal 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
30
cmd_deps.odin
Normal 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
45
features.odin
Normal 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
24
flake.lock
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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
56
main.odin
Normal 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
39
stubs.odin
Normal 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
84
table.odin
Normal 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
7
tty.odin
Normal 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
13
version.odin
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user