5 Commits

Author SHA1 Message Date
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
spencer 86a4da6cba feat: Brought back the docs generator. 2026-07-27 13:12:31 -04:00
spencer 222a0a3452 feat: Added uninstall command. 2026-07-27 13:12:31 -04:00
27 changed files with 692 additions and 287 deletions
+1
View File
@@ -15,5 +15,6 @@ envr-prof
findr/findr
findr/findr-prof
findr/bench-*.md
gendocs
result
version.odin
+33
View File
@@ -1,5 +1,38 @@
# 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)
+1 -5
View File
@@ -8,13 +8,9 @@
4. Test all command branches.
5. Generate md and man pages again.
6. Add tests for untested commands.
7. Add uninstall command.
8. Add purge command?
7. Add purge command?
## Double-check AI output
+91 -74
View File
@@ -52,6 +52,7 @@ Positional_Arg :: struct {
ntype: string, // nushell type: "path", "string". "" defaults to "path"
completion: string,
optional: bool,
desc: string,
}
CommandInfo :: struct {
@@ -96,14 +97,18 @@ key somewhere, otherwise your data could be lost forever.`,
short = "Import a .env file into envr",
aliases = {"add"},
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "untracked-paths"}},
args = {
{name = "path", completion = "untracked-paths", desc = "Path to .env file to backup"},
},
},
{
name = "restore",
usage = "envr restore <path>",
short = "Restore a .env file from the database",
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths"}},
args = {
{name = "path", completion = "tracked-paths", desc = "Path to .env file to restore"},
},
},
{
name = "list",
@@ -116,14 +121,22 @@ key somewhere, otherwise your data could be lost forever.`,
usage = "envr remove <path>",
short = "Remove a .env file from your database",
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths"}},
args = {
{name = "path", completion = "tracked-paths", desc = "Path to .env file to remove"},
},
},
{
name = "check",
usage = "envr check [path]",
short = "Check if files are backed up",
flags = GLOBAL_FLAGS,
args = {{name = "path", optional = true}},
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}},
{
@@ -141,7 +154,14 @@ key somewhere, otherwise your data could be lost forever.`,
nushell
bash`,
flags = {.Help},
args = {{name = "shell", ntype = "string", completion = "shells"}},
args = {
{
name = "shell",
ntype = "string",
completion = "shells",
desc = "Shell to generate completions for",
},
},
},
{
name = "uninstall",
@@ -270,6 +290,64 @@ Flag_Field :: struct {
completion: string,
}
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
table.caption(tbl, "Flags:")
for ft in Flag_Type {
if ft not_in flags do continue
names, hint, desc := flag_field_info(ft)
if len(hint) > 0 {
display := table.format(
tbl,
"%s%s",
colorize(.Flag, names, tbl.format_allocator),
hint,
)
table.row(tbl, display, desc)
} else {
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
}
}
}
flag_field_info :: proc(
ft: Flag_Type,
) -> (
names: string,
value_hint: string,
description: string,
) {
f := flag_field(ft)
if len(f.short_name) > 0 {
names = fmt.tprintf("-%s, --%s", f.short_name, f.long_name)
} else {
names = fmt.tprintf("--%s", f.long_name)
}
switch f.kind {
case .Bool:
value_hint = ""
case .String:
value_hint = " <value>"
case .Enum:
value_hint = fmt.tprintf(" %s", f.enum_values)
}
description = f.usage
if len(f.default_val) > 0 {
switch f.kind {
case .Bool:
// do nothing
case .String:
description = fmt.tprintf(`%s (default "%s")`, f.usage, f.default_val)
case .Enum:
description = fmt.tprintf("%s (default '%s')", f.usage, f.default_val)
}
}
return
}
flag_field :: proc(ft: Flag_Type) -> Flag_Field {
field := reflect.struct_field_at(Flags, int(ft))
@@ -315,64 +393,6 @@ flag_field :: proc(ft: Flag_Type) -> Flag_Field {
}
}
flag_field_info :: proc(
ft: Flag_Type,
) -> (
names: string,
value_hint: string,
description: string,
) {
f := flag_field(ft)
if len(f.short_name) > 0 {
names = fmt.tprintf("-%s, --%s", f.short_name, f.long_name)
} else {
names = fmt.tprintf("--%s", f.long_name)
}
switch f.kind {
case .Bool:
value_hint = ""
case .String:
value_hint = " <value>"
case .Enum:
value_hint = fmt.tprintf(" %s", f.enum_values)
}
description = f.usage
if len(f.default_val) > 0 {
switch f.kind {
case .Bool:
// do nothing
case .String:
description = fmt.tprintf(`%s (default "%s")`, f.usage, f.default_val)
case .Enum:
description = fmt.tprintf("%s (default '%s')", f.usage, f.default_val)
}
}
return
}
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) {
table.caption(tbl, "Flags:")
for ft in Flag_Type {
if ft not_in flags do continue
names, hint, desc := flag_field_info(ft)
if len(hint) > 0 {
display := table.format(
tbl,
"%s%s",
colorize(.Flag, names, tbl.format_allocator),
hint,
)
table.row(tbl, display, desc)
} else {
table.row(tbl, colorize(.Flag, names, tbl.format_allocator), desc)
}
}
}
find_command :: proc(name: string) -> (CommandInfo, bool) {
for c in COMMANDS {
if c.name == name {
@@ -387,11 +407,7 @@ find_command :: proc(name: string) -> (CommandInfo, bool) {
return CommandInfo{}, false
}
// TODO: command args should be shown in usage.
write_usage :: proc(w: io.Writer) {
fmt.wprintf(
w,
`envr keeps your .env synced to a local, encrypted database.
ENVR_DESCRIPTION :: `envr keeps your .env synced to a local, encrypted database.
Is a safe and easy way to gather all your .env files in one place where they can
easily be backed by another tool such as restic or git.
@@ -420,13 +436,14 @@ Select the files you want to back up from the interactive list.
5. If you lose a repository, after re-cloning the repo into the same path it was
at before, restore your backup with:
> envr restore ~/<path to repository>/.env
> envr restore ~/<path to repository>/.env`
%s
%s [command]
`,
// TODO: command args should be shown in usage.
write_usage :: proc(w: io.Writer) {
fmt.wprintf(
w,
"%s\n\n%s\n\n %s [command]\n\n",
ENVR_DESCRIPTION,
colorize(.Heading, "Usage:"),
colorize(.Flag, "envr"),
flush = false,
-1
View File
@@ -1,7 +1,6 @@
package main
import "core:fmt"
import "core:terminal/ansi"
cmd_init :: proc(cmd: ^Command) {
force := cmd.flags.force
+6 -5
View File
@@ -221,7 +221,8 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
include = expanded
} else {
// TODO: show errors?
resolved, err := filepath.abs(expanded, allocator)
resolved: string
resolved, err = filepath.abs(expanded, allocator)
if err == nil {
include = resolved
}
@@ -231,10 +232,6 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
return result
}
envr_dir :: proc(config_path: string) -> string {
return filepath.dir(config_path)
}
// User is responsible for freeing the path
data_path :: proc(
config_path: string,
@@ -246,3 +243,7 @@ data_path :: proc(
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
import "base:runtime"
import "core:crypto/hash"
import "core:encoding/hex"
import "core:encoding/ini"
+11 -10
View File
@@ -5,7 +5,7 @@ Manage your .env files.
### Synopsis
envr keeps your .env synced to a local, encrypted database.
Is a safe and eay way to gather all your .env files in one place where they can
Is a safe and easy way to gather all your .env files in one place where they can
easily be backed by another tool such as restic or git.
All your data is stored in ~/.envr/data.envr
@@ -33,25 +33,26 @@ Select the files you want to back up from the interactive list.
5. If you lose a repository, after re-cloning the repo into the same path it was
at before, restore your backup with:
> envr restore ~/&lt;path to repository&gt;/.env
> envr restore ~/<path to repository>/.env
### Options
```
-h, --help help for envr
-h, --help show this documentation
```
### SEE ALSO
* [envr backup](envr_backup.md) - Import a .env file into envr
* [envr check](envr_check.md) - check if files in the current directory are backed up
* [envr edit-config](envr_edit-config.md) - Edit your config with your default editor
* [envr init](envr_init.md) - Set up envr
* [envr list](envr_list.md) - View your tracked files
* [envr nushell-completion](envr_nushell-completion.md) - Generate custom completions for nushell
* [envr remove](envr_remove.md) - Remove a .env file from your database
* [envr restore](envr_restore.md) - Install a .env file from the database into your file system
* [envr scan](envr_scan.md) - Find and select .env files for backup
* [envr sync](envr_sync.md) - Update or restore your env backups
* [envr backup](envr_backup.md) - Import a .env file into envr
* [envr restore](envr_restore.md) - Restore a .env file from the database
* [envr list](envr_list.md) - View your tracked files
* [envr remove](envr_remove.md) - Remove a .env file from your database
* [envr check](envr_check.md) - Check if files are backed up
* [envr version](envr_version.md) - Show envr's version
* [envr edit-config](envr_edit-config.md) - Edit your config with your default editor
* [envr completion](envr_completion.md) - Generate shell completion scripts
* [envr uninstall](envr_uninstall.md) - Remove envr configuration and database
+9 -2
View File
@@ -6,13 +6,20 @@ Import a .env file into envr
envr backup <path> [flags]
```
**Arguments:**
* `path` — Path to .env file to backup
### Options
```
-h, --help help for backup
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
*Alias: `add`*
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+8 -3
View File
@@ -1,18 +1,23 @@
## envr check
check if files in the current directory are backed up
Check if files are backed up
```
envr check [path] [flags]
```
**Arguments:**
* `path` — Path to check (defaults to current directory) (optional)
### Options
```
-h, --help help for check
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+28
View File
@@ -0,0 +1,28 @@
## envr completion
Generate shell completion scripts
### Synopsis
Supported shells:
nushell
bash
```
envr completion <shell> [flags]
```
**Arguments:**
* `shell` — Shell to generate completions for
### Options
```
-h, --help show this documentation
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+3 -2
View File
@@ -9,10 +9,11 @@ envr edit-config [flags]
### Options
```
-h, --help help for edit-config
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+5 -6
View File
@@ -5,9 +5,7 @@ Set up envr
### Synopsis
The init command generates your initial config and saves it to
~/.envr/config in JSON format.
During setup, you will be prompted to select one or more ssh keys with which to
~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
key somewhere, otherwise your data could be lost forever.
@@ -18,11 +16,12 @@ envr init [flags]
### Options
```
-f, --force Overwrite an existing config
-h, --help help for init
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
-f, --force Overwrite existing config
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+4 -2
View File
@@ -9,10 +9,12 @@ envr list [flags]
### Options
```
-h, --help help for list
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
-o, --output the format of output data (default 'table')
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
-23
View File
@@ -1,23 +0,0 @@
## envr nushell-completion
Generate custom completions for nushell
### Synopsis
At time of writing, cobra does not natively support nushell,
so a custom command had to be written
```
envr nushell-completion [flags]
```
### Options
```
-h, --help help for nushell-completion
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+8 -3
View File
@@ -3,16 +3,21 @@
Remove a .env file from your database
```
envr remove [flags]
envr remove <path> [flags]
```
**Arguments:**
* `path` — Path to .env file to remove
### Options
```
-h, --help help for remove
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+9 -4
View File
@@ -1,18 +1,23 @@
## envr restore
Install a .env file from the database into your file system
Restore a .env file from the database
```
envr restore [flags]
envr restore <path> [flags]
```
**Arguments:**
* `path` — Path to .env file to restore
### Options
```
-h, --help help for restore
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+3 -2
View File
@@ -9,10 +9,11 @@ envr scan [flags]
### Options
```
-h, --help help for scan
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+4 -2
View File
@@ -9,10 +9,12 @@ envr sync [flags]
### Options
```
-h, --help help for sync
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
-o, --output the format of output data (default 'table')
--color Whether or not to colorize output (default 'auto')
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+24
View File
@@ -0,0 +1,24 @@
## envr uninstall
Remove envr configuration and database
### Synopsis
This will remove all files generated by envr from your system.
```
envr uninstall [flags]
```
### Options
```
-h, --help show this documentation
-c, --config-file config file (default "~/.envr/config.json")
--color Whether or not to colorize output (default 'auto')
-f, --force Overwrite existing config
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+1 -3
View File
@@ -9,11 +9,9 @@ envr version [flags]
### Options
```
-h, --help help for version
-l, --long Show all version information
-h, --help show this documentation
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.
+58 -53
View File
@@ -4,64 +4,12 @@ import "base:runtime"
import "core:reflect"
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) {
field_count := reflect.struct_field_count(T)
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)
for i in 0..<field_count {
for i in 0 ..< field_count {
field := reflect.struct_field_at(T, i)
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[:]
}
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
}
+8
View File
@@ -91,6 +91,11 @@
buildPhase = ''
runHook preBuild
echo '${version}' > version.txt
# Generate man pages before main build (binary gets clobbered)
odin build . -define:GENDOCS=true -out:gendocs
./gendocs --man
odin build . -o:speed -out:${pname}
runHook postBuild
'';
@@ -98,6 +103,9 @@
installPhase = ''
runHook preInstall
install -Dm755 ${pname} $out/bin/${pname}
mkdir -p $out/share/man/man1
cp docs/man/*.1 $out/share/man/man1/
runHook postInstall
'';
};
+289
View File
@@ -0,0 +1,289 @@
package main
import "core:fmt"
import "core:os"
import "core:strings"
import "core:time"
when #config(GENDOCS, false) {
main :: proc() {
args := os.args
if len(args) < 2 {
fmt.eprintln("Usage: gendocs --man | --md")
os.exit(1)
}
switch args[1] {
case "--man":
generate_man_pages()
case "--md":
generate_markdown()
case:
fmt.eprintf("Unknown format: %s\n", args[1])
os.exit(1)
}
}
}
generate_man_pages :: proc() {
os.mkdir_all("docs/man")
now := time.now()
month_year := fmt.tprintf("%s %d", time.month(now), time.year(now))
generate_man_main(month_year)
for &c in COMMANDS {
generate_man_command(&c, month_year)
}
fmt.println("Generated man pages in docs/man/")
}
generate_man_main :: proc(month_year: string) {
sb: strings.Builder
strings.builder_init(&sb, context.temp_allocator)
defer strings.builder_destroy(&sb)
fmt.sbprintf(&sb, ".TH ENVR 1 \"%s\" \"envr\" \"envr Manual\"\n", month_year)
fmt.sbprintf(&sb, ".SH NAME\nenvr \\- keep your .env files synced to an encrypted database\n")
fmt.sbprintf(&sb, ".SH SYNOPSIS\n\\fBenvr\\fR [\\fIcommand\\fR] [\\fIflags\\fR]\n")
fmt.sbprintf(
&sb,
".SH DESCRIPTION\nenvr keeps your .env synced to a local, encrypted database.\n",
)
fmt.sbprintf(
&sb,
"Is a safe and easy way to gather all your .env files in one place where they can\n",
)
fmt.sbprintf(&sb, "easily be backed by another tool such as restic or git.\n")
fmt.sbprintf(&sb, ".PP\nAll your data is stored in ~/.envr/data.envr\n")
fmt.sbprintf(&sb, ".SH COMMANDS\n")
for c in COMMANDS {
fmt.sbprintf(&sb, ".TP\n\\fB%s\\fR\n%s\n", c.name, c.short)
for a in c.aliases {
fmt.sbprintf(&sb, ".TP\n\\fB%s\\fR (alias for %s)\n%s\n", a, c.name, c.short)
}
}
fmt.sbprintf(&sb, ".SH GLOBAL OPTIONS\n")
for ft in Flag_Type {
if ft not_in GLOBAL_FLAGS do continue
write_man_flag(&sb, ft)
}
fmt.sbprintf(&sb, ".SH SEE ALSO\n")
for c, i in COMMANDS {
if i > 0 do fmt.sbprintf(&sb, ",\n")
fmt.sbprintf(&sb, ".BR envr-%s (1)", c.name)
}
fmt.sbprintf(&sb, "\n")
path := "docs/man/envr.1"
err := os.write_entire_file(path, transmute([]u8)strings.to_string(sb))
if err != nil {
fmt.eprintf("Error writing %s: %v\n", path, err)
}
}
generate_man_command :: proc(c: ^CommandInfo, month_year: string) {
sb: strings.Builder
strings.builder_init(&sb, context.temp_allocator)
defer strings.builder_destroy(&sb)
upper_name := strings.to_upper(fmt.tprintf("envr-%s", c.name))
fmt.sbprintf(&sb, ".TH %s 1 \"%s\" \"envr\" \"envr Manual\"\n", upper_name, month_year)
fmt.sbprintf(&sb, ".SH NAME\nenvr-%s \\- %s\n", c.name, c.short)
fmt.sbprintf(&sb, ".SH SYNOPSIS\n\\fB%s\\fR", c.usage)
has_positionals := false
for arg in c.args {
has_positionals = true
if arg.optional {
fmt.sbprintf(&sb, " [\\fI%s\\fR]", arg.name)
} else {
fmt.sbprintf(&sb, " \\fI%s\\fR", arg.name)
}
}
fmt.sbprintf(&sb, "\n")
if len(c.long) > 0 {
fmt.sbprintf(&sb, ".SH DESCRIPTION\n%s\n", c.long)
}
if has_positionals {
fmt.sbprintf(&sb, ".SH ARGUMENTS\n")
for arg in c.args {
fmt.sbprintf(&sb, ".TP\n\\fB%s\\fR\n%s\n", arg.name, arg.desc)
}
}
fmt.sbprintf(&sb, ".SH OPTIONS\n")
for ft in Flag_Type {
if ft not_in c.flags do continue
write_man_flag(&sb, ft)
}
path := fmt.tprintf("docs/man/envr-%s.1", c.name)
err := os.write_entire_file(path, transmute([]u8)strings.to_string(sb))
if err != nil {
fmt.eprintf("Error writing %s: %v\n", path, err)
}
}
write_man_flag :: proc(sb: ^strings.Builder, ft: Flag_Type) {
f := flag_field(ft)
fmt.sbprintf(sb, ".TP\n")
if len(f.short_name) > 0 {
fmt.sbprintf(sb, "\\fB-%s\\fR, \\fB--%s\\fR", f.short_name, f.long_name)
} else {
fmt.sbprintf(sb, "\\fB--%s\\fR", f.long_name)
}
#partial switch f.kind {
case .String:
fmt.sbprintf(sb, " \\fIvalue\\fR")
case .Enum:
values, _ := strings.replace(f.enum_values, "'", "", -1)
fmt.sbprintf(sb, " \\fI%s\\fR", values)
}
fmt.sbprintf(sb, "\n%s\n", f.usage)
if len(f.default_val) > 0 {
#partial switch f.kind {
case .String:
fmt.sbprintf(sb, "(default \"%s\")\n", f.default_val)
case .Enum:
fmt.sbprintf(sb, "(default '%s')\n", f.default_val)
}
}
}
generate_markdown :: proc() {
os.mkdir_all("docs/cli")
generate_md_main()
for &c in COMMANDS {
generate_md_command(&c)
}
fmt.println("Generated markdown in docs/cli/")
}
generate_md_main :: proc() {
sb: strings.Builder
strings.builder_init(&sb, context.temp_allocator)
defer strings.builder_destroy(&sb)
fmt.sbprintf(&sb, "## envr\n\n")
fmt.sbprintf(&sb, "Manage your .env files.\n\n")
fmt.sbprintf(&sb, "### Synopsis\n\n")
fmt.sbprint(&sb, ENVR_DESCRIPTION)
fmt.sbprintf(&sb, "\n\n### Options\n\n")
write_md_options(&sb, {.Help})
fmt.sbprintf(&sb, "\n### SEE ALSO\n\n")
for c in COMMANDS {
fmt.sbprintf(&sb, "* [envr %s](envr_%s.md)\t - %s\n", c.name, c.name, c.short)
}
fmt.sbprintf(&sb, "\n")
write_file("docs/cli/envr.md", strings.to_string(sb))
}
generate_md_command :: proc(c: ^CommandInfo) {
sb: strings.Builder
strings.builder_init(&sb, context.temp_allocator)
defer strings.builder_destroy(&sb)
fmt.sbprintf(&sb, "## envr %s\n\n", c.name)
fmt.sbprintf(&sb, "%s\n\n", c.short)
if len(c.long) > 0 {
fmt.sbprintf(&sb, "### Synopsis\n\n")
fmt.sbprint(&sb, c.long)
fmt.sbprintf(&sb, "\n\n")
}
fmt.sbprintf(&sb, "```\n%s [flags]\n```\n\n", c.usage)
if len(c.args) > 0 {
fmt.sbprintf(&sb, "**Arguments:**\n\n")
for arg in c.args {
optional := arg.optional ? " (optional)" : ""
fmt.sbprintf(&sb, "* `%s` — %s%s\n", arg.name, arg.desc, optional)
}
fmt.sbprintf(&sb, "\n")
}
fmt.sbprintf(&sb, "### Options\n\n")
write_md_options(&sb, c.flags)
for a in c.aliases {
fmt.sbprintf(&sb, "\n*Alias: `%s`*\n", a)
}
fmt.sbprintf(&sb, "\n### SEE ALSO\n\n")
fmt.sbprintf(&sb, "* [envr](envr.md)\t - Manage your .env files.\n")
path := fmt.tprintf("docs/cli/envr_%s.md", c.name)
write_file(path, strings.to_string(sb))
}
write_md_options :: proc(sb: ^strings.Builder, flags: bit_set[Flag_Type]) {
displays := make([dynamic]string, 0, 5, context.temp_allocator)
defer delete(displays)
descs := make([dynamic]string, 0, 5, context.temp_allocator)
defer delete(descs)
max_width := 0
for ft in Flag_Type {
if ft not_in flags do continue
f := flag_field(ft)
display: string
if len(f.short_name) > 0 {
display = fmt.tprintf("-%s, --%s", f.short_name, f.long_name)
} else {
display = fmt.tprintf(" --%s", f.long_name)
}
desc := f.usage
if len(f.default_val) > 0 {
#partial switch f.kind {
case .String:
desc = fmt.tprintf(`%s (default "%s")`, f.usage, f.default_val)
case .Enum:
desc = fmt.tprintf("%s (default '%s')", f.usage, f.default_val)
}
}
append(&displays, display)
append(&descs, desc)
if len(display) > max_width {
max_width = len(display)
}
}
fmt.sbprintf(sb, "```\n")
pad_buf: [64]u8
for i in 0..<len(displays) {
padding := max_width - len(displays[i]) + 3
for j in 0..<padding {
pad_buf[j] = ' '
}
fmt.sbprintf(sb, " %s%s%s\n", displays[i], string(pad_buf[:padding]), descs[i])
}
fmt.sbprintf(sb, "```\n")
}
write_file :: proc(path: string, content: string) {
err := os.write_entire_file(path, transmute([]u8)content)
if err != nil {
fmt.eprintf("Error writing %s: %v\n", path, err)
}
}
+4 -2
View File
@@ -14,7 +14,8 @@ when SPALL {
spall_buffer: spall.Buffer
}
main :: proc() {
when !#config(GENDOCS, false) {
main :: proc() {
when SPALL {
ctx, spall_ok := spall.context_create_with_scale("envr.spall", false, 1.0)
if !spall_ok {
@@ -86,7 +87,8 @@ main :: proc() {
write_usage(cmd.out)
os.exit(1)
}
}
}
} // !GENDOCS
when SPALL {
@(instrumentation_enter)
+15 -15
View File
@@ -1,6 +1,5 @@
package main
import "core:fmt"
import "core:io"
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) {
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)
}
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) {
clear(&t.cells)
clear(&t.colw)
+1 -1
View File
@@ -1 +1 @@
0.4.0
1.0.0