5 Commits

Author SHA1 Message Date
spencer 69d93797ce chore: Added TODO. 2026-07-27 14:19:57 -04:00
spencer a4c96265f8 chore: Added TODOs. 2026-07-27 13:53:20 -04:00
Spencer Brower 3813719c84 chore(prerelease): release 1.0.0 2026-07-27 13:43:48 -04:00
spencer 18e6b0b5b1 style: Cleaned up vet errors. 2026-07-27 13:35:09 -04:00
spencer 149902cf91 refactor: re-ordered procs by calling order. 2026-07-27 13:31:35 -04:00
9 changed files with 199 additions and 137 deletions
+33
View File
@@ -1,5 +1,38 @@
# Changelog # Changelog
## [1.0.0](https://github.com/sbrow/envr/compare/v0.4.0...v1.0.0) (2026-07-27)
### ⚠ BREAKING CHANGES
* Renamed `nushell-completion` command to `completion nushell`.
* Databases with a JSON formatted remotes column will no longer parse correctly. If you have an old database, make sure you upgrade to envr 0.4.0 and run `envr list` before upgrading to this version.
### Features
* Added `uninstall` command. ([222a0a3](https://github.com/sbrow/envr/commit/222a0a3452b978d9bd14d6d2b91e40cd8f4959e3))
* Added bash completion support. ([e0a339e](https://github.com/sbrow/envr/commit/e0a339e8ec996c0e91e27357854abd9a78bd695a))
* Brought back the docs generator. ([86a4da6](https://github.com/sbrow/envr/commit/86a4da6cbaae698379c73b8345e6d238b2a11a46))
* Improved nushell wrapper output. ([209ec89](https://github.com/sbrow/envr/commit/209ec89addbed4718c5aed845ab20b386d1e425d))
* Scan results can now be filtered during selection. ([ae42d53](https://github.com/sbrow/envr/commit/ae42d53f029dd185143a645fb3f3a787051c1bb4))
### Bug Fixes
* Removed irrelevent flags from `nushell-completion` command help text. ([af1860c](https://github.com/sbrow/envr/commit/af1860c970604470992182bbfe9ecb15609a0acf))
### Performance Improvements
* Added `#no_bounds_check` to ssh parsers. ([f5624e9](https://github.com/sbrow/envr/commit/f5624e91f1236eecb263f28600ce7f4b2fda149f))
* Added a pre-alloc opportunity. ([997f11b](https://github.com/sbrow/envr/commit/997f11b257f1c981f73c6ae97d6e821147b5e2fc))
* Removed support for JSON formatted remotes. ([5282a44](https://github.com/sbrow/envr/commit/5282a4414b9a0211604553f2cb9434025aa5296b))
### Code Refactoring
* Renamed `nushell-completion` command to `completion nushell`. ([b0635e0](https://github.com/sbrow/envr/commit/b0635e035c44fb20f9b02265cff1ed8db30aa9d0))
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29) ## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29)
+6
View File
@@ -12,6 +12,12 @@
7. Add purge command? 7. Add purge command?
8. Add docs for "building from source"
9. Add AI Usage Disclaimer to README.md
10. Check up on GitHub "SEO". i.e. Description, tags, etc.
## Double-check AI output ## Double-check AI output
+80 -61
View File
@@ -97,14 +97,18 @@ key somewhere, otherwise your data could be lost forever.`,
short = "Import a .env file into envr", short = "Import a .env file into envr",
aliases = {"add"}, aliases = {"add"},
flags = GLOBAL_FLAGS, flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "untracked-paths", desc = "Path to .env file to backup"}}, args = {
{name = "path", completion = "untracked-paths", desc = "Path to .env file to backup"},
},
}, },
{ {
name = "restore", name = "restore",
usage = "envr restore <path>", usage = "envr restore <path>",
short = "Restore a .env file from the database", short = "Restore a .env file from the database",
flags = GLOBAL_FLAGS, flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths", desc = "Path to .env file to restore"}}, args = {
{name = "path", completion = "tracked-paths", desc = "Path to .env file to restore"},
},
}, },
{ {
name = "list", name = "list",
@@ -117,14 +121,22 @@ key somewhere, otherwise your data could be lost forever.`,
usage = "envr remove <path>", usage = "envr remove <path>",
short = "Remove a .env file from your database", short = "Remove a .env file from your database",
flags = GLOBAL_FLAGS, flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths", desc = "Path to .env file to remove"}}, args = {
{name = "path", completion = "tracked-paths", desc = "Path to .env file to remove"},
},
}, },
{ {
name = "check", name = "check",
usage = "envr check [path]", usage = "envr check [path]",
short = "Check if files are backed up", short = "Check if files are backed up",
flags = GLOBAL_FLAGS, flags = GLOBAL_FLAGS,
args = {{name = "path", optional = true, desc = "Path to check (defaults to current directory)"}}, args = {
{
name = "path",
optional = true,
desc = "Path to check (defaults to current directory)",
},
},
}, },
{name = "version", usage = "envr version", short = "Show envr's version", flags = {.Help}}, {name = "version", usage = "envr version", short = "Show envr's version", flags = {.Help}},
{ {
@@ -142,7 +154,14 @@ key somewhere, otherwise your data could be lost forever.`,
nushell nushell
bash`, bash`,
flags = {.Help}, flags = {.Help},
args = {{name = "shell", ntype = "string", completion = "shells", desc = "Shell to generate completions for"}}, args = {
{
name = "shell",
ntype = "string",
completion = "shells",
desc = "Shell to generate completions for",
},
},
}, },
{ {
name = "uninstall", name = "uninstall",
@@ -271,48 +290,22 @@ Flag_Field :: struct {
completion: string, completion: string,
} }
flag_field :: proc(ft: Flag_Type) -> Flag_Field { write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
field := reflect.struct_field_at(Flags, int(ft)) table.caption(tbl, "Flags:")
for ft in Flag_Type {
args_tag := reflect.struct_tag_get(field.tag, "args") if ft not_in flags do continue
long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator) names, hint, desc := flag_field_info(ft)
if n, ok := get_subtag(args_tag, "name"); ok { if len(hint) > 0 {
long_name = n display := table.format(
} tbl,
"%s%s",
short, has_short := get_subtag(args_tag, "short") colorize(.Flag, names, tbl.format_allocator),
hint,
base_ti := runtime.type_info_base(field.type) )
kind: Flag_Kind table.row(tbl, display, desc)
enum_values: string } else {
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
kind = .Bool
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
kind = .String
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
kind = .Enum
parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
for name in enum_ti.names {
lower := strings.to_lower(name, context.temp_allocator)
append(&parts, fmt.tprintf("'%s'", lower))
} }
enum_values = strings.join(parts[:], "|", context.temp_allocator)
delete(parts)
}
usage := reflect.struct_tag_get(field.tag, "usage")
default_val := reflect.struct_tag_get(field.tag, "default")
completion := reflect.struct_tag_get(field.tag, "completion")
return {
long_name = long_name,
short_name = has_short ? short : "",
kind = kind,
usage = usage,
default_val = default_val,
enum_values = enum_values,
completion = completion,
} }
} }
@@ -355,22 +348,48 @@ flag_field_info :: proc(
return return
} }
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) { flag_field :: proc(ft: Flag_Type) -> Flag_Field {
table.caption(tbl, "Flags:") field := reflect.struct_field_at(Flags, int(ft))
for ft in Flag_Type {
if ft not_in flags do continue args_tag := reflect.struct_tag_get(field.tag, "args")
names, hint, desc := flag_field_info(ft) long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
if len(hint) > 0 { if n, ok := get_subtag(args_tag, "name"); ok {
display := table.format( long_name = n
tbl, }
"%s%s",
colorize(.Flag, names, tbl.format_allocator), short, has_short := get_subtag(args_tag, "short")
hint,
) base_ti := runtime.type_info_base(field.type)
table.row(tbl, display, desc) kind: Flag_Kind
} else { enum_values: string
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
kind = .Bool
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
kind = .String
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
kind = .Enum
parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
for name in enum_ti.names {
lower := strings.to_lower(name, context.temp_allocator)
append(&parts, fmt.tprintf("'%s'", lower))
} }
enum_values = strings.join(parts[:], "|", context.temp_allocator)
delete(parts)
}
usage := reflect.struct_tag_get(field.tag, "usage")
default_val := reflect.struct_tag_get(field.tag, "default")
completion := reflect.struct_tag_get(field.tag, "completion")
return {
long_name = long_name,
short_name = has_short ? short : "",
kind = kind,
usage = usage,
default_val = default_val,
enum_values = enum_values,
completion = completion,
} }
} }
-1
View File
@@ -1,7 +1,6 @@
package main package main
import "core:fmt" import "core:fmt"
import "core:terminal/ansi"
cmd_init :: proc(cmd: ^Command) { cmd_init :: proc(cmd: ^Command) {
force := cmd.flags.force force := cmd.flags.force
+6 -5
View File
@@ -221,7 +221,8 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
include = expanded include = expanded
} else { } else {
// TODO: show errors? // TODO: show errors?
resolved, err := filepath.abs(expanded, allocator) resolved: string
resolved, err = filepath.abs(expanded, allocator)
if err == nil { if err == nil {
include = resolved include = resolved
} }
@@ -231,10 +232,6 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
return result return result
} }
envr_dir :: proc(config_path: string) -> string {
return filepath.dir(config_path)
}
// User is responsible for freeing the path // User is responsible for freeing the path
data_path :: proc( data_path :: proc(
config_path: string, config_path: string,
@@ -246,3 +243,7 @@ data_path :: proc(
return filepath.join([]string{envr_dir(config_path), "data.envr"}, allocator) return filepath.join([]string{envr_dir(config_path), "data.envr"}, allocator)
} }
envr_dir :: proc(config_path: string) -> string {
return filepath.dir(config_path)
}
-1
View File
@@ -1,6 +1,5 @@
package main package main
import "base:runtime"
import "core:crypto/hash" import "core:crypto/hash"
import "core:encoding/hex" import "core:encoding/hex"
import "core:encoding/ini" import "core:encoding/ini"
+58 -53
View File
@@ -4,64 +4,12 @@ import "base:runtime"
import "core:reflect" import "core:reflect"
import "core:strings" import "core:strings"
get_subtag :: proc(tag: string, id: string) -> (value: string, ok: bool) {
parts := strings.split(tag, ",", context.temp_allocator)
for part in parts {
trimmed := strings.trim_space(part)
if strings.has_prefix(trimmed, id) && len(trimmed) > len(id) && trimmed[len(id)] == '=' {
return trimmed[len(id) + 1:], true
}
if trimmed == id {
return "", true
}
}
return "", false
}
is_bool_type :: proc(field: reflect.Struct_Field) -> bool {
base_ti := runtime.type_info_base(field.type)
_, is_bool := base_ti.variant.(runtime.Type_Info_Boolean)
return is_bool
}
set_field :: proc(model: rawptr, field: reflect.Struct_Field, value: string) -> bool {
ptr := rawptr(uintptr(model) + field.offset)
base_ti := runtime.type_info_base(field.type)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
(cast(^bool)ptr)^ = true
return true
}
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
(cast(^string)ptr)^ = value
return true
}
if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
for name, i in enum_ti.names {
if strings.equal_fold(value, name) {
v := enum_ti.values[i]
switch base_ti.size {
case 1: (cast(^u8)ptr)^ = cast(u8)v
case 2: (cast(^u16)ptr)^ = cast(u16)v
case 4: (cast(^u32)ptr)^ = cast(u32)v
case 8: (cast(^u64)ptr)^ = cast(u64)v
}
return true
}
}
}
return false
}
parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) { parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) {
field_count := reflect.struct_field_count(T) field_count := reflect.struct_field_count(T)
long_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator) long_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator)
short_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator) short_map := make(map[string]reflect.Struct_Field, field_count, context.temp_allocator)
for i in 0..<field_count { for i in 0 ..< field_count {
field := reflect.struct_field_at(T, i) field := reflect.struct_field_at(T, i)
name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator) name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
@@ -132,3 +80,60 @@ parse_flags :: proc(model: ^$T, args: []string) -> (overflow: []string) {
return overflow_dyn[:] return overflow_dyn[:]
} }
get_subtag :: proc(tag: string, id: string) -> (value: string, ok: bool) {
parts := strings.split(tag, ",", context.temp_allocator)
for part in parts {
trimmed := strings.trim_space(part)
if strings.has_prefix(trimmed, id) && len(trimmed) > len(id) && trimmed[len(id)] == '=' {
return trimmed[len(id) + 1:], true
}
if trimmed == id {
return "", true
}
}
return "", false
}
is_bool_type :: proc(field: reflect.Struct_Field) -> bool {
base_ti := runtime.type_info_base(field.type)
_, is_bool := base_ti.variant.(runtime.Type_Info_Boolean)
return is_bool
}
set_field :: proc(model: rawptr, field: reflect.Struct_Field, value: string) -> bool {
ptr := rawptr(uintptr(model) + field.offset)
base_ti := runtime.type_info_base(field.type)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
(cast(^bool)ptr)^ = true
return true
}
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
(cast(^string)ptr)^ = value
return true
}
if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
for name, i in enum_ti.names {
if strings.equal_fold(value, name) {
v := enum_ti.values[i]
switch base_ti.size {
case 1:
(cast(^u8)ptr)^ = cast(u8)v
case 2:
(cast(^u16)ptr)^ = cast(u16)v
case 4:
(cast(^u32)ptr)^ = cast(u32)v
case 8:
(cast(^u64)ptr)^ = cast(u64)v
}
return true
}
}
}
return false
}
+15 -15
View File
@@ -1,6 +1,5 @@
package main package main
import "core:fmt"
import "core:io" import "core:io"
import "core:text/table" import "core:text/table"
@@ -18,20 +17,6 @@ decorations := table.Decorations {
"─", "─",
} }
ansi_aware_width :: proc(str: string) -> int #no_bounds_check {
width := 0
for i := 0; i < len(str); {
if i + 1 < len(str) && str[i] == 0x1b && str[i + 1] == '[' {
i += 2
for i < len(str) {c := str[i]; i += 1; if c >= 0x40 && c <= 0x7E {break}}
} else {
width += 1
i += 1
}
}
return width
}
write_borderless_table :: proc(w: io.Writer, t: ^table.Table) { write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
table.build(t, ansi_aware_width) table.build(t, ansi_aware_width)
@@ -64,6 +49,21 @@ write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
write_table_separator(w, t) write_table_separator(w, t)
} }
ansi_aware_width :: proc(str: string) -> int #no_bounds_check {
width := 0
for i := 0; i < len(str); {
if i + 1 < len(str) && str[i] == 0x1b && str[i + 1] == '[' {
i += 2
for i < len(str) {c := str[i]; i += 1; if c >= 0x40 && c <= 0x7E {break}}
} else {
width += 1
i += 1
}
}
return width
}
table_reset :: proc(t: ^table.Table) { table_reset :: proc(t: ^table.Table) {
clear(&t.cells) clear(&t.cells)
clear(&t.colw) clear(&t.colw)
+1 -1
View File
@@ -1 +1 @@
0.4.0 1.0.0