10 Commits

Author SHA1 Message Date
Spencer Brower d978cb6869 chore(main): release 0.4.0 2026-06-29 17:38:20 -04:00
spencer 70b5c0e184 ci: Updated release-please. 2026-06-29 17:32:55 -04:00
spencer fb90342126 fix: Flags in help text are now customized per command. 2026-06-29 17:32:55 -04:00
spencer 17d7fb5eb6 refactor: Removed hardcoded colors. 2026-06-29 17:32:55 -04:00
spencer c3e667e7bc feat: Added --color flag. 2026-06-29 17:32:55 -04:00
spencer 16819f65cb refactor: Used RTTI for more sophisticated flag parsing. 2026-06-29 17:32:55 -04:00
spencer 4d3da01f65 nix: Fixed the nix build. 2026-06-29 17:32:54 -04:00
spencer 2b68617b5d fix: -h short flag now works on subcommands. 2026-06-29 17:32:54 -04:00
spencer 36f124a4fd chore: Re-numbered todos. 2026-06-29 17:32:54 -04:00
spencer bb84c56c98 feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-29 17:32:54 -04:00
25 changed files with 673 additions and 292 deletions
+1
View File
@@ -2,6 +2,7 @@ on:
push: push:
branches: branches:
- main - main
- prerelease
permissions: permissions:
contents: write contents: write
+31
View File
@@ -1,5 +1,36 @@
# Changelog # Changelog
## [0.4.0](https://github.com/sbrow/envr/compare/v0.3.0...v0.4.0) (2026-06-29)
### Features
* Added `--color` flag. ([c3e667e](https://github.com/sbrow/envr/commit/c3e667e7bca660b21b5851460691ec8bd5026f80))
* Added `--format`, `-f` flag. ([bb84c56](https://github.com/sbrow/envr/commit/bb84c56c98bead9ce9bad3f89fb3b61b53d05a10))
* Colorized console output. ([33cd7c4](https://github.com/sbrow/envr/commit/33cd7c4eda43287fe3dc2d2289a4e1531b524ef6))
* Removed runtime git dependency. ([12574e1](https://github.com/sbrow/envr/commit/12574e123bdedba3aca813143e906ec5e0b95719))
### Bug Fixes
* -h short flag now works on subcommands. ([2b68617](https://github.com/sbrow/envr/commit/2b68617b5d6c17444b04307d7b4b7f4a3aefb978))
* Databases errors are less likely to go unnoticed. ([f825bc2](https://github.com/sbrow/envr/commit/f825bc2b096632bb258d5681b1072941e9d4233d))
* Fixed leaks. ([c7c254f](https://github.com/sbrow/envr/commit/c7c254f6f2ac871182f3fcc233766884753e2049))
* Fixed memory leaks in the db. ([5059572](https://github.com/sbrow/envr/commit/5059572951b3ec20b3d2027032a9c3be5cb14dba))
* Fixed some leaks in `backup` and `scan`. ([dc72ff5](https://github.com/sbrow/envr/commit/dc72ff56fd0e165930771682b2fd266eef3e7e16))
* Fixed vet errors. ([1562fb3](https://github.com/sbrow/envr/commit/1562fb3665b8704dcffd4944047f7984b308e52e))
* Flags in help text are now customized per command. ([fb90342](https://github.com/sbrow/envr/commit/fb903421265baa65adbbe092f6bf797639f1fb1f))
* Handled mk_dir error. ([de1594d](https://github.com/sbrow/envr/commit/de1594d9d1fe46dd9ebc3be01fc5a5ebaf4064e5))
* **scan:** Fixed a bug preventing TUI from working. ([0083e4e](https://github.com/sbrow/envr/commit/0083e4e0dbc5b949a3cf4cd8b64af4f698ed9d33))
* Used os path separator rather than '/' where appropriate. ([5cc7973](https://github.com/sbrow/envr/commit/5cc79737753f0f19db2bc55304ea5f70237cbffd))
### Performance Improvements
* Improved the performance of table rendering. ([0b5bf4d](https://github.com/sbrow/envr/commit/0b5bf4db73113bdd43d8a225ec0ae53fedd25918))
* remotes are now stored as a newline delimited list. ([96b3d63](https://github.com/sbrow/envr/commit/96b3d6340a95b67c884ba940b46461e51989fcbb))
* Replaced `fd` with custom internals. ([2ef733f](https://github.com/sbrow/envr/commit/2ef733fe58594b0a0b6e3ef85142b74af445ccb8))
## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16) ## [0.3.0](https://github.com/sbrow/envr/compare/v0.2.1...v0.3.0) (2026-06-16)
Version 0.3.0 represents a significant departure (and improvement) for envr. Version 0.3.0 represents a significant departure (and improvement) for envr.
+14 -15
View File
@@ -1,32 +1,30 @@
# TODOs # TODOs
1. Commands are still leaking. (Write tests for everything first) 1. Bring back windows support / cross-compilation.
2. Add color flag and support non colored output. 2. Commands are still leaking. (Write tests for everything first)
3. Rewrite `write_command_help` to use text/tables 3. procedures should be ordered by use, main at the top, then in the order they are called from main.
4. Generate md and man pages again. 4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
5. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`. 5. Test all cmds / terminal branches.
6. Add a text filter to the multi_select. 6. Generate md and man pages again.
7. Add tests for untested commands. 7. Shell completion
8. add --format -f flag to commands that draw tables. 8. Add tests for untested commands.
9. procedures should be ordered by use, main at the top, then in the order they are called from main. 9. Update `read_wire_string` to use a slice.
10. Shell completion 10. Pass allocator to findr?
11. Bring back windows support / cross-compilation. 11. Consider getting rid of color global.
12. Test all cmds / terminal branches. 12. `write_flags_table` should never return false.
13. Pass allocator to findr? 13. Add a text filter to the multi_select.
14. Update `read_wire_string` to use a slice.
## Double-check AI output ## Double-check AI output
@@ -54,6 +52,7 @@
- [ ] db.odin - [ ] db.odin
- [ ] db_integration_test.odin - [ ] db_integration_test.odin
- [ ] db_test.odin - [ ] db_test.odin
- [ ] flags.odin
- [x] main.odin - [x] main.odin
- [x] prompt.odin - [x] prompt.odin
- [x] scan.odin - [x] scan.odin
+191 -118
View File
@@ -1,21 +1,50 @@
package main package main
import "base:runtime"
import "core:bufio" import "core:bufio"
import "core:fmt" import "core:fmt"
import "core:io" import "core:io"
import "core:os" import "core:os"
import "core:reflect"
import "core:strings" import "core:strings"
import "core:terminal"
import "core:text/table" import "core:text/table"
Command :: struct { Command :: struct {
name: string, name: string,
args: [dynamic]string, args: [dynamic]string,
flags: map[string]string, flags: Flags,
bool_set: map[string]bool, out_buf: ^bufio.Writer,
config_path: string, out: io.Writer,
out_buf: ^bufio.Writer, err: io.Writer,
out: io.Writer, }
err: io.Writer,
Flags :: struct {
help: bool `args:"short=h" usage:"show this documentation"`,
config_file: string `args:"name=config-file,short=c" usage:"config file" default:"~/.envr/config.json"`,
output: Output_Format `args:"short=o" usage:"the format of output data" default:"table"`,
color: Color_Mode `usage:"Whether or not to colorize output" default:"auto"`,
force: bool `args:"short=f" usage:"Overwrite existing config"`,
}
Flag_Type :: enum {
Help,
Config_File,
Output,
Color,
Force,
}
Output_Format :: enum {
Auto,
Table,
JSON,
}
Color_Mode :: enum {
Auto,
Always,
Never,
} }
CommandInfo :: struct { CommandInfo :: struct {
@@ -24,8 +53,12 @@ CommandInfo :: struct {
short: string, short: string,
long: string, long: string,
aliases: []string, aliases: []string,
// Flags supported by the command
flags: bit_set[Flag_Type],
} }
GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color}
COMMANDS := []CommandInfo { COMMANDS := []CommandInfo {
{ {
"init", "init",
@@ -36,22 +69,45 @@ COMMANDS := []CommandInfo {
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
key somewhere, otherwise your data could be lost forever.`, key somewhere, otherwise your data could be lost forever.`,
{}, {},
GLOBAL_FLAGS + {.Force},
},
{"scan", "envr scan", "Find and select .env files for backup", "", {}, GLOBAL_FLAGS},
{"sync", "envr sync", "Update or restore your env backups", "", {}, GLOBAL_FLAGS + {.Output}},
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}, GLOBAL_FLAGS},
{
"restore",
"envr restore <path>",
"Restore a .env file from the database",
"",
{},
GLOBAL_FLAGS,
},
{"list", "envr list", "View your tracked files", "", {}, GLOBAL_FLAGS + {.Output}},
{
"remove",
"envr remove <path>",
"Remove a .env file from your database",
"",
{},
GLOBAL_FLAGS,
},
{"check", "envr check [path]", "Check if files are backed up", "", {}, GLOBAL_FLAGS},
{"version", "envr version", "Show envr's version", "", {}, {.Help}},
{
"edit-config",
"envr edit-config",
"Edit your config with your default editor",
"",
{},
GLOBAL_FLAGS,
}, },
{"scan", "envr scan", "Find and select .env files for backup", "", {}},
{"sync", "envr sync", "Update or restore your env backups", "", {}},
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}},
{"restore", "envr restore <path>", "Restore a .env file from the database", "", {}},
{"list", "envr list", "View your tracked files", "", {}},
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
{"check", "envr check [path]", "Check if files are backed up", "", {}},
{"version", "envr version", "Show envr's version", "", {}},
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
{ {
"nushell-completion", "nushell-completion",
"envr nushell-completion", "envr nushell-completion",
"Generate custom completions for nushell", "Generate custom completions for nushell",
"", "",
{}, {},
GLOBAL_FLAGS - {.Color},
}, },
} }
@@ -71,53 +127,33 @@ parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Comm
} }
cmd.name = args[1] cmd.name = args[1]
cmd.args = make([dynamic]string) cmd.args = make([dynamic]string)
cmd.flags = make(map[string]string)
cmd.bool_set = make(map[string]bool)
// TODO: Optimize loop? overflow := parse_flags(&cmd.flags, args[2:])
i := 2 for arg in overflow {
for i < len(args) { append(&cmd.args, arg)
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
}
} }
val: string = --- if cmd.flags.output == .Auto {
if val, ok = cmd.flags["config-file"]; ok { cmd.flags.output = terminal.is_terminal(os.stdout) ? .Table : .JSON
cmd.config_path = val }
} else if val, ok = cmd.flags["c"]; ok {
cmd.config_path = val if cmd.flags.color == .Auto {
} else { cmd.flags.color = terminal.is_terminal(os.stdout) ? .Always : .Never
}
if cmd.flags.color == .Never {
disable_color = true
}
if cmd.flags.config_file == "" {
// FIXME: Handle err // FIXME: Handle err
// TODO: Is this right? // TODO: Is this right?
home, _ := os.user_home_dir(context.temp_allocator) home, _ := os.user_home_dir(context.temp_allocator)
// TODO: should we copy out of the temp_allocator? // TODO: should we copy out of the temp_allocator?
cmd.config_path = default_config_path(home, context.temp_allocator) cmd.flags.config_file = default_config_path(home, context.temp_allocator)
} }
if has_flag(&cmd, "help") { if cmd.flags.help {
print_command_help(&cmd) print_command_help(&cmd)
return cmd, false return cmd, false
} }
@@ -134,6 +170,7 @@ print_command_help :: proc(cmd: ^Command) {
} }
write_command_help :: proc(name: string, w: io.Writer) -> bool { write_command_help :: proc(name: string, w: io.Writer) -> bool {
// TODO: rename info to cmd?
info, found := find_command(name) info, found := find_command(name)
if !found { if !found {
return false return false
@@ -141,36 +178,23 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
fmt.wprintf( fmt.wprintf(
w, w,
"%s\n\n\n" + "%s\n\n\n%s\n\n %s [flags]\n\n",
COLOR_HEADINGS +
"Usage:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"%s" +
ANSI_RESET +
" [flags]\n\n",
info.short, info.short,
info.usage, colorize(.Heading, "Usage:"),
colorize(.Flag, info.usage),
flush = false, flush = false,
) )
if len(info.aliases) > 0 { if len(info.aliases) > 0 {
fmt.wprintf( fmt.wprintf(
w, w,
"\n" + "\n%s\n\n %s",
COLOR_HEADINGS + colorize(.Heading, "Aliases:"),
"Aliases:" + colorize(.Command, info.name),
ANSI_RESET +
"\n\n " +
COLOR_COMMANDS +
"%s" +
ANSI_RESET,
info.name,
flush = false, flush = false,
) )
for a in info.aliases { for a in info.aliases {
fmt.wprintf(w, ", " + COLOR_COMMANDS + "%s" + ANSI_RESET, a, flush = false) fmt.wprintf(w, ", %s", colorize(.Command, a), flush = false)
} }
fmt.wprintf(w, "\n", flush = false) fmt.wprintf(w, "\n", flush = false)
} }
@@ -179,25 +203,88 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
fmt.wprintf(w, "\n%s\n", info.long, flush = false) fmt.wprintf(w, "\n%s\n", info.long, flush = false)
} }
fmt.wprintf( tbl: table.Table
w, table.init(&tbl, context.temp_allocator, context.temp_allocator)
"\n" + table.padding(&tbl, 2, 0)
COLOR_HEADINGS + if write_flags_table(&tbl, info.flags) {
"Flags:" + fmt.wprintf(w, "\n", flush = false)
ANSI_RESET + write_borderless_table(w, &tbl)
"\n\n " + }
COLOR_FLAGS + table_reset(&tbl)
"-h, --help" + return true
ANSI_RESET + }
" help for %s\n " +
COLOR_FLAGS + flag_field_info :: proc(
"-c, --config-file" + ft: Flag_Type,
ANSI_RESET + ) -> (
` <path> config file (default "~/.envr/config.json") names: string,
`, value_hint: string,
info.name, description: string,
flush = false, ) {
) field := reflect.struct_field_at(Flags, int(ft))
args_tag := reflect.struct_tag_get(field.tag, "args")
long_name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
if n, ok := get_subtag(args_tag, "name"); ok {
long_name = n
}
short, has_short := get_subtag(args_tag, "short")
if has_short {
names = fmt.tprintf("-%s, --%s", short, long_name)
} else {
names = fmt.tprintf("--%s", long_name)
}
base_ti := runtime.type_info_base(field.type)
if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool {
value_hint = ""
} else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
value_hint = " <value>"
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_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))
}
value_hint = fmt.tprintf(" %s", 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")
description = usage
if len(default_val) > 0 {
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
description = fmt.tprintf(`%s (default "%s")`, usage, default_val)
} else if _, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum {
description = fmt.tprintf("%s (default '%s')", usage, default_val)
}
}
return
}
write_flags_table :: proc(tbl: ^table.Table, flags: bit_set[Flag_Type]) -> (has_rows: bool) {
if flags == {} do return false
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)
}
}
return true return true
} }
@@ -250,15 +337,13 @@ at before, restore your backup with:
> envr restore ~/<path to repository>/.env > envr restore ~/<path to repository>/.env
%sUsage:%s %s
%senvr%s [command] %s [command]
`, `,
COLOR_HEADINGS, colorize(.Heading, "Usage:"),
ANSI_RESET, colorize(.Flag, "envr"),
COLOR_FLAGS,
ANSI_RESET,
flush = false, flush = false,
) )
@@ -274,40 +359,28 @@ at before, restore your backup with:
for a in c.aliases { for a in c.aliases {
name = strings.join([]string{name, a}, ", ", tbl.format_allocator) name = strings.join([]string{name, a}, ", ", tbl.format_allocator)
} }
table.row(&tbl, table.format(&tbl, "%s%s%s", COLOR_COMMANDS, name, ANSI_RESET), c.short) table.row(&tbl, colorize(.Command, name, tbl.format_allocator), c.short)
} }
write_borderless_table(w, &tbl) write_borderless_table(w, &tbl)
table_reset(&tbl) table_reset(&tbl)
table.caption(&tbl, "Flags:") if write_flags_table(&tbl, GLOBAL_FLAGS) {
write_borderless_table(w, &tbl)
table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`) }
table.row( table_reset(&tbl)
&tbl,
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`config file (default "~/.envr/config.json")`,
)
write_borderless_table(w, &tbl)
fmt.wprintf( fmt.wprintf(
w, w,
`Use "%senvr%s [command] --help" for more information about a command.`, `Use "%s [command] --help" for more information about a command.`,
COLOR_FLAGS, colorize(.Flag, "envr", tbl.format_allocator),
ANSI_RESET,
flush = false, flush = false,
) )
} }
has_flag :: proc(cmd: ^Command, name: string) -> bool {
return name in cmd.flags || name in cmd.bool_set
}
delete_command :: proc(cmd: ^Command) { delete_command :: proc(cmd: ^Command) {
bufio.writer_flush(cmd.out_buf) bufio.writer_flush(cmd.out_buf)
delete(cmd.args) delete(cmd.args)
delete(cmd.flags)
delete(cmd.bool_set)
bufio.writer_destroy(cmd.out_buf) bufio.writer_destroy(cmd.out_buf)
free(cmd.out_buf) free(cmd.out_buf)
} }
+71 -90
View File
@@ -110,7 +110,8 @@ test_command_help_init_no_aliases :: proc(t: ^testing.T) {
testing.expect(t, !strings.contains(text, "Aliases:"), "init should not have Aliases section") testing.expect(t, !strings.contains(text, "Aliases:"), "init should not have Aliases section")
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section") testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
testing.expect(t, strings.contains(text, "show this documentation"), "missing help flag description") testing.expect(t, strings.contains(text, "show this documentation"), "missing help flag description")
} testing.expect(t, strings.contains(text, "--force"), "missing --force flag")
}
@(test) @(test)
test_command_help_unknown :: proc(t: ^testing.T) { test_command_help_unknown :: proc(t: ^testing.T) {
@@ -144,53 +145,6 @@ test_command_help_version :: proc(t: ^testing.T) {
} }
test_parse_args :: proc( test_parse_args :: proc(
test_has_flag_bool_set :: proc(t: ^testing.T) {
cmd := Command {
name = "test",
bool_set = map[string]bool{"force" = true},
}
defer delete(cmd.bool_set)
testing.expect(t, has_flag(&cmd, "force"), "should find flag in bool_set")
testing.expect(t, !has_flag(&cmd, "verbose"), "should not find missing flag")
}
@(test)
test_has_flag_value_map :: proc(t: ^testing.T) {
cmd := Command {
name = "test",
flags = map[string]string{"output" = "/tmp/out"},
}
defer delete(cmd.flags)
testing.expect(t, has_flag(&cmd, "output"), "should find flag in flags map")
testing.expect(t, !has_flag(&cmd, "force"), "should not find missing flag")
}
@(test)
test_has_flag_both_maps :: proc(t: ^testing.T) {
cmd := Command {
name = "test",
flags = map[string]string{"output" = "/tmp/out"},
bool_set = map[string]bool{"force" = true},
}
defer delete(cmd.flags)
defer delete(cmd.bool_set)
testing.expect(t, has_flag(&cmd, "output"), "should find in flags")
testing.expect(t, has_flag(&cmd, "force"), "should find in bool_set")
testing.expect(t, !has_flag(&cmd, "verbose"), "should not find missing flag")
}
@(test)
test_has_flag_empty_command :: proc(t: ^testing.T) {
cmd := Command {
name = "test",
}
testing.expect(t, !has_flag(&cmd, "anything"), "empty command should have no flags")
}
test_parse_args :: proc(
args: []string, args: []string,
) -> ( ) -> (
cmd: Command, cmd: Command,
@@ -226,8 +180,6 @@ test_parse_args_bare_command :: proc(t: ^testing.T) {
testing.expect_value(t, cmd.name, "list") testing.expect_value(t, cmd.name, "list")
testing.expect_value(t, len(cmd.args), 0) testing.expect_value(t, len(cmd.args), 0)
} }
testing.expect_value(t, len(cmd.bool_set), 0)
}
@(test) @(test)
test_parse_args_positional :: proc(t: ^testing.T) { test_parse_args_positional :: proc(t: ^testing.T) {
@@ -242,43 +194,45 @@ test_parse_args_positional :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_config_file_long_flag :: proc(t: ^testing.T) { test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "--config", "x.json"}) cmd, ok, _, _ := test_parse_args(
testing.expect(t, ok, "should succeed") []string{"envr", "sync", "--config-file", "x.json"},
)
testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.config_file, "x.json") testing.expect_value(t, cmd.flags.config_file, "x.json")
} }
@(test) @(test)
test_parse_args_config_file_short_flag :: proc(t: ^testing.T) { test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "-c", "x.json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "-c", "x.json"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.config_file, "x.json") testing.expect_value(t, cmd.flags.config_file, "x.json")
} }
@(test) @(test)
test_parse_args_force_long_flag :: proc(t: ^testing.T) { test_parse_args_force_long_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "--force"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "--force"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.force, true) testing.expect_value(t, cmd.flags.force, true)
} }
@(test) @(test)
test_parse_args_force_short_flag :: proc(t: ^testing.T) { test_parse_args_force_short_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "version", "-l"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "-f"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.force, true) testing.expect_value(t, cmd.flags.force, true)
} }
@(test) @(test)
test_parse_args_multiple_positionals :: proc(t: ^testing.T) { test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
@@ -300,7 +254,7 @@ test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.force, true) testing.expect_value(t, cmd.flags.force, true)
testing.expect_value(t, len(cmd.args), 1) testing.expect_value(t, len(cmd.args), 1)
testing.expect_value(t, cmd.args[0], "/project/.env") testing.expect_value(t, cmd.args[0], "/project/.env")
} }
@@ -314,50 +268,77 @@ test_parse_args_no_args :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) { test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "--force", "a.env", "--output", "json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "--force", "a.env", "--output", "json"})
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
testing.expect_value(t, cmd.flags.force, true) testing.expect_value(t, cmd.flags.force, true)
testing.expect_value(t, cmd.bool_set["verbose"], true) testing.expect_value(t, cmd.flags.output, Output_Format.JSON)
testing.expect_value(t, len(cmd.args), 1) testing.expect_value(t, len(cmd.args), 1)
testing.expect_value(t, cmd.args[0], "a.env") testing.expect_value(t, cmd.args[0], "a.env")
} }
@(test) @(test)
test_parse_args_config_file_default :: proc(t: ^testing.T) { test_parse_args_config_file_default :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args( cmd, ok, _, _ := test_parse_args([]string{"envr", "list"})
[]string{"envr", "list", "--config-file", "/custom/config.json"},
)
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.config_path, "/custom/config.json")
}
@(test)
test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-c", "/custom/config.json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.config_path, "/custom/config.json")
}
@(test)
test_parse_args_config_file_defaults :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
testing.expect(t, len(cmd.flags.config_file) > 0, "config_file should default to non-empty path") testing.expect(t, len(cmd.flags.config_file) > 0, "config_file should default to non-empty path")
testing.expect( testing.expect(
t, t,
strings.contains(cmd.flags.config_file, ".envr"), strings.contains(cmd.flags.config_file, ".envr"),
"default config_path should contain .envr dir, got %s", "default config_file should contain .envr dir, got %s",
) )
} }
@(test) @(test)
test_parse_args_output_long_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--output", "json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.output, Output_Format.JSON)
}
@(test)
test_parse_args_output_short_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-o", "json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.output, Output_Format.JSON)
}
@(test)
test_parse_args_output_long_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--output", "table"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.output, Output_Format.Table)
}
@(test)
test_parse_args_output_short_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-o", "table"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.output, Output_Format.Table)
}
@(test)
test_parse_args_output_equals_syntax :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--output=json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, cmd.flags.output, Output_Format.JSON)
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ cmd_backup :: proc(cmd: ^Command) {
return return
} }
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
+1 -1
View File
@@ -24,7 +24,7 @@ cmd_check :: proc(cmd: ^Command) {
return return
} }
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
+1 -1
View File
@@ -10,7 +10,7 @@ cmd_edit_config :: proc(cmd: ^Command) {
return return
} }
config_path := cmd.config_path config_path := cmd.flags.config_file
if !os.exists(config_path) { if !os.exists(config_path) {
fmt.wprintf( fmt.wprintf(
+13 -7
View File
@@ -4,11 +4,12 @@ import "core:fmt"
import "core:terminal/ansi" import "core:terminal/ansi"
cmd_init :: proc(cmd: ^Command) { cmd_init :: proc(cmd: ^Command) {
force := has_flag(cmd, "force") || has_flag(cmd, "f") force := cmd.flags.force
config_file := cmd.flags.config_file
fmt.wprintln(cmd.out, cmd.config_path, flush = false) fmt.wprintln(cmd.out, cmd.flags.config_file, flush = false)
_, cfg_exists := load_config(cmd.config_path) _, cfg_exists := load_config(config_file)
if cfg_exists && !force { if cfg_exists && !force {
fmt.wprintln( fmt.wprintln(
cmd.out, cmd.out,
@@ -25,15 +26,19 @@ Run again with the --force flag if you want to reinitialize.`,
} }
if len(keys) == 0 { if len(keys) == 0 {
fmt.wprintln(cmd.err, `No ssh-ed25519 keys found in ~/.ssh fmt.wprintln(
Generate one with: ssh-keygen -t ed25519`, flush = false) cmd.err,
`No ssh-ed25519 keys found in ~/.ssh
Generate one with: ssh-keygen -t ed25519`,
flush = false,
)
return return
} }
selected, result := multi_select("Select SSH private keys:", keys[:]) selected, result := multi_select("Select SSH private keys:", keys[:])
defer delete(selected) defer delete(selected)
if result == .Cancel { if result == .Cancel {
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false) fmt.wprintln(cmd.out, colorize(.Message, "Cancelled."), flush = false)
return return
} }
@@ -49,7 +54,7 @@ Generate one with: ssh-keygen -t ed25519`, flush = false)
return return
} }
cfg := new_config(selected_paths[:], cmd.config_path) cfg := new_config(selected_paths[:], config_file)
if !save_config(cfg, force = force) { if !save_config(cfg, force = force) {
return return
} }
@@ -61,3 +66,4 @@ Generate one with: ssh-keygen -t ed25519`, flush = false)
flush = false, flush = false,
) )
} }
+5 -7
View File
@@ -5,7 +5,6 @@ import "core:fmt"
import "core:os" import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
import "core:terminal"
import "core:text/table" import "core:text/table"
ListEntry :: struct { ListEntry :: struct {
@@ -13,10 +12,9 @@ ListEntry :: struct {
path: string `json:"path"`, path: string `json:"path"`,
} }
// TODO: Support --format flag
// TODO: Improve table rendering // TODO: Improve table rendering
cmd_list :: proc(cmd: ^Command) { cmd_list :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
@@ -27,15 +25,15 @@ cmd_list :: proc(cmd: ^Command) {
return return
} }
if terminal.is_terminal(os.stdout) { if cmd.flags.output == .Table {
t: table.Table t: table.Table
table.init(&t, context.temp_allocator, context.temp_allocator) table.init(&t, context.temp_allocator, context.temp_allocator)
table.padding(&t, 1, 1) table.padding(&t, 1, 1)
table.aligned_header_of_values( table.aligned_header_of_values(
&t, &t,
.Center, .Center,
COLOR_TABLE_HEADING + "Directory" + ANSI_RESET, colorize(.Table_Heading, "Directory"),
COLOR_TABLE_HEADING + "Path" + ANSI_RESET, colorize(.Table_Heading, "Path"),
) )
for row in rows { for row in rows {
@@ -51,7 +49,7 @@ cmd_list :: proc(cmd: ^Command) {
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width) table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)
} else { } else {
// TODO: Should we instead print full entries here? // TODO: Should we instead print full entries here?
entries: [dynamic]ListEntry entries := make([dynamic]ListEntry, 0, len(rows), context.temp_allocator)
for row in rows { for row in rows {
filename := filepath.base(row.path) filename := filepath.base(row.path)
append( append(
+96
View File
@@ -1,7 +1,11 @@
#+feature dynamic-literals
#+test #+test
package main package main
import "core:bufio"
import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -17,3 +21,95 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) {
} }
} }
@(test)
test_cmd_list_output_json :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-json-*")
defer os.remove_all(base)
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
db, db_ok := db_open(cfg_path)
testing.expect(t, db_ok, "db should open")
if !db_ok do return
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
out_b: strings.Builder
strings.builder_init(&out_b)
defer strings.builder_destroy(&out_b)
err_b: strings.Builder
strings.builder_init(&err_b)
defer strings.builder_destroy(&err_b)
cmd, ok := parse_args(
[]string{"envr", "list", "--output", "json", "--config-file", cfg_path},
strings.to_stream(&out_b),
strings.to_stream(&err_b),
)
testing.expect(t, ok, "parse_args should succeed")
if !ok do return
defer delete_command(&cmd)
cmd_list(&cmd)
bufio.writer_flush(cmd.out_buf)
output := strings.to_string(out_b)
testing.expect(t, strings.contains(output, "["), "json output should contain '['")
testing.expect(
t,
strings.contains(output, "\"directory\""),
"json output should contain directory key",
)
}
@(test)
test_cmd_list_output_table :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-table-*")
defer os.remove_all(base)
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
db, db_ok := db_open(cfg_path)
testing.expect(t, db_ok, "db should open")
if !db_ok do return
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
out_b: strings.Builder
strings.builder_init(&out_b)
defer strings.builder_destroy(&out_b)
err_b: strings.Builder
strings.builder_init(&err_b)
defer strings.builder_destroy(&err_b)
cmd, ok := parse_args(
[]string{"envr", "list", "--output", "table", "--config-file", cfg_path},
strings.to_stream(&out_b),
strings.to_stream(&err_b),
)
testing.expect(t, ok, "parse_args should succeed")
if !ok do return
defer delete_command(&cmd)
cmd_list(&cmd)
bufio.writer_flush(cmd.out_buf)
output := strings.to_string(out_b)
testing.expect(t, strings.contains(output, "│"), "table output should contain border chars")
testing.expect(
t,
strings.contains(output, "Directory"),
"table output should contain Directory header",
)
}
+1 -1
View File
@@ -22,7 +22,7 @@ cmd_remove :: proc(cmd: ^Command) {
return return
} }
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
+1 -1
View File
@@ -23,7 +23,7 @@ cmd_restore :: proc(cmd: ^Command) {
return return
} }
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
+8 -22
View File
@@ -4,10 +4,9 @@ import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:os" import "core:os"
import "core:terminal" import "core:terminal"
import "core:terminal/ansi"
cmd_scan :: proc(cmd: ^Command) { cmd_scan :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
@@ -72,11 +71,7 @@ cmd_scan :: proc(cmd: ^Command) {
selected, result := multi_select("Select .env files to backup:", files[:]) selected, result := multi_select("Select .env files to backup:", files[:])
defer delete(selected) defer delete(selected)
if result == .Cancel { if result == .Cancel {
fmt.wprintln( fmt.wprintln(cmd.out, colorize(.Message, "Cancelled."), flush = false)
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
flush = false,
)
return return
} }
@@ -100,25 +95,16 @@ cmd_scan :: proc(cmd: ^Command) {
} }
if added_count > 0 { if added_count > 0 {
fmt.wprintf( fmt.wprintln(
cmd.out, cmd.out,
ansi.CSI + colorize(
ansi.BOLD + .Sucess,
";" + fmt.tprintf("Successfully added %d file(s) to backup.", added_count),
ansi.FG_GREEN + ),
ansi.SGR +
"Successfully added %d file(s) to backup." +
ANSI_RESET +
"\n",
added_count,
flush = false, flush = false,
) )
} else { } else {
fmt.wprintln( fmt.wprintln(cmd.out, colorize(.Message, "No files were added."), flush = false)
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET,
flush = false,
)
} }
} }
+4 -7
View File
@@ -2,8 +2,6 @@ package main
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:os"
import "core:terminal"
import "core:text/table" import "core:text/table"
SyncEntry :: struct { SyncEntry :: struct {
@@ -12,9 +10,8 @@ SyncEntry :: struct {
} }
// TODO: Check for quiet failures. // TODO: Check for quiet failures.
// TODO: Support --format -f flags
cmd_sync :: proc(cmd: ^Command) { cmd_sync :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.flags.config_file)
if !db_ok { if !db_ok {
return return
} }
@@ -49,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) {
} }
} }
if terminal.is_terminal(os.stdout) { if cmd.flags.output == .Table {
t: table.Table t: table.Table
table.init(&t, context.temp_allocator, context.temp_allocator) table.init(&t, context.temp_allocator, context.temp_allocator)
table.padding(&t, 1, 1) table.padding(&t, 1, 1)
@@ -57,8 +54,8 @@ cmd_sync :: proc(cmd: ^Command) {
table.aligned_header_of_values( table.aligned_header_of_values(
&t, &t,
.Center, .Center,
COLOR_TABLE_HEADING + "File" + ANSI_RESET, colorize(.Table_Heading, "File"),
COLOR_TABLE_HEADING + "Status" + ANSI_RESET, colorize(.Table_Heading, "Status"),
) )
for res in results { for res in results {
+58 -9
View File
@@ -1,17 +1,66 @@
package main package main
import "base:runtime"
import "core:strings"
import "core:terminal/ansi" import "core:terminal/ansi"
COLOR_HEADINGS :: Color_Code :: enum {
ansi.CSI + ansi.FG_BRIGHT_GREEN + ";" + ansi.BOLD + ";" + ansi.UNDERLINE + ansi.SGR Heading,
Command,
// Example
Flag,
Table_Heading,
Message,
Sucess,
Caret,
Option_Label,
}
COLOR_COMMANDS :: ansi.CSI + ansi.FG_BRIGHT_CYAN + ";" + ansi.BOLD + ansi.SGR // COLOR_EXAMPLE :: ansi.CSI + ansi.ITALIC + ansi.SGR
COLOR_EXAMPLE :: ansi.CSI + ansi.ITALIC + ansi.SGR
COLOR_FLAGS :: ansi.CSI + ansi.BOLD + ";" + ansi.FG_BRIGHT_WHITE + ansi.SGR
COLOR_TABLE_HEADING :: ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
@(private = "file")
ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
disable_color := false
colorize :: proc(
color: Color_Code,
text: string,
allocator := context.temp_allocator,
disable := disable_color,
) -> (
string,
runtime.Allocator_Error,
) #optional_allocator_error {
if disable {
return text, nil
} else {
return strings.concatenate(
{ansi.CSI, color_code(color), ansi.SGR, text, ANSI_RESET},
allocator,
)
}
}
@(private = "file")
color_code :: proc(code: Color_Code) -> string {
switch code {
case .Heading:
return ansi.BOLD + ";" + ansi.UNDERLINE + ";" + ansi.FG_BRIGHT_GREEN
case .Command:
return ansi.BOLD + ";" + ansi.FG_BRIGHT_CYAN
case .Flag:
return ansi.BOLD + ";" + ansi.FG_BRIGHT_WHITE
case .Table_Heading:
return ansi.FG_BRIGHT_GREEN
case .Message:
return ansi.FAINT
case .Sucess, .Caret:
return ansi.BOLD + ";" + ansi.FG_GREEN
case .Option_Label:
return ansi.BOLD + ";" + ansi.FG_CYAN
case:
panic("Unknown case")
}
}
+1 -1
View File
@@ -172,7 +172,7 @@ db_close :: proc(db: ^Db) {
} }
sz: i64 sz: i64
data := sqlite.serialize(db.conn, "main", &sz, 0) data := sqlite.serialize(db.conn, "main", &sz, {})
if data == nil { if data == nil {
fmt.eprintln("Error: failed to serialize database") fmt.eprintln("Error: failed to serialize database")
return return
+1 -1
View File
@@ -196,7 +196,7 @@ test_db_serialize :: proc(t: ^testing.T) {
db_insert(&db, f) db_insert(&db, f)
sz: i64 sz: i64
data := sqlite.serialize(db.conn, "main", &sz, 0) data := sqlite.serialize(db.conn, "main", &sz, {})
testing.expect(t, data != nil, "serialize should return non-nil") testing.expect(t, data != nil, "serialize should return non-nil")
if data == nil do return if data == nil do return
defer sqlite.free(data) defer sqlite.free(data)
+134
View File
@@ -0,0 +1,134 @@
package main
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 {
field := reflect.struct_field_at(T, i)
name, _ := strings.replace(field.name, "_", "-", -1, context.temp_allocator)
args_tag := reflect.struct_tag_get(field.tag, "args")
if n, ok := get_subtag(args_tag, "name"); ok {
name = n
}
long_map[name] = field
if s, ok := get_subtag(args_tag, "short"); ok {
short_map[s] = field
}
}
overflow_dyn := make([dynamic]string, 0, len(args), context.temp_allocator)
i := 0
for i < len(args) {
arg := args[i]
if strings.starts_with(arg, "--") {
key := arg[2:]
value := ""
has_value := false
if eq_idx := strings.index(key, "="); eq_idx >= 0 {
value = key[eq_idx + 1:]
key = key[:eq_idx]
has_value = true
}
if field, ok := long_map[key]; ok {
if is_bool_type(field) {
set_field(model, field, "")
i += 1
} else if has_value {
set_field(model, field, value)
i += 1
} else if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
set_field(model, field, args[i + 1])
i += 2
} else {
i += 1
}
} else {
i += 1
}
} else if strings.starts_with(arg, "-") && len(arg) == 2 {
short := arg[1:2]
if field, ok := short_map[short]; ok {
if is_bool_type(field) {
set_field(model, field, "")
i += 1
} else if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
set_field(model, field, args[i + 1])
i += 2
} else {
i += 1
}
} else {
i += 1
}
} else {
append(&overflow_dyn, arg)
i += 1
}
}
return overflow_dyn[:]
}
+2
View File
@@ -75,6 +75,8 @@
]; ];
buildInputs = [ buildInputs = [
pkgs.git
pkgs.libsodium pkgs.libsodium
mysqlite mysqlite
]; ];
+19 -5
View File
@@ -66,11 +66,17 @@ multi_select :: proc(
case .Space: case .Space:
selected[cursor] = !selected[cursor] selected[cursor] = !selected[cursor]
case .Enter: case .Enter:
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1) fmt.printf(
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
visible + 1,
)
result = .Confirm result = .Confirm
return return
case .Escape: case .Escape:
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1) fmt.printf(
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
visible + 1,
)
result = .Cancel result = .Cancel
return return
case .Unknown: case .Unknown:
@@ -89,7 +95,10 @@ render_options :: proc(
cursor: int, cursor: int,
scroll_offset: int, scroll_offset: int,
) -> int { ) -> int {
fmt.printf(ansi.CSI + ansi.BOLD + ";" + ansi.FG_CYAN + ansi.SGR + "%s" + ANSI_RESET + " (↑/↓ move, space select, enter confirm)\r\n", prompt) fmt.printf(
"%s (↑/↓ move, space select, enter confirm)\r\n",
colorize(.Option_Label, prompt),
)
end := scroll_offset + MAX_VISIBLE end := scroll_offset + MAX_VISIBLE
if end > len(options) { if end > len(options) {
@@ -102,9 +111,14 @@ render_options :: proc(
checkbox = "x" checkbox = "x"
} }
if i == cursor { if i == cursor {
fmt.printf(ansi.CSI + ansi.BOLD + ";" + ansi.FG_GREEN + ansi.SGR + "> " + ANSI_RESET + "[" + ansi.CSI + ansi.FG_GREEN + ansi.SGR + "%s" + ANSI_RESET + "] %s\r\n", checkbox, options[i]) fmt.printf(
"%s [%s] %s\r\n",
colorize(.Caret, ">"),
colorize(.Sucess, checkbox),
options[i],
)
} else { } else {
fmt.printf(" [" + ansi.CSI + ansi.FAINT + ansi.SGR + "%s" + ANSI_RESET + "] %s\r\n", checkbox, options[i]) fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[i])
} }
} }
+7 -1
View File
@@ -17,6 +17,12 @@ DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG]
DESERIALIZE_FLAG :: enum u32 { DESERIALIZE_FLAG :: enum u32 {
FREEONCLOSE = 1, FREEONCLOSE = 1,
RESIZEABLE = 2, RESIZEABLE = 2,
READONLY = 4,
}
SERIALIZE_FLAGS :: bit_set[SERIALIZE_FLAG]
SERIALIZE_FLAG :: enum u32 {
NOCOPY = 1,
} }
foreign lib { foreign lib {
@@ -43,7 +49,7 @@ foreign lib {
@(link_name = "sqlite3_changes") @(link_name = "sqlite3_changes")
changes :: proc(db: Db) -> c.int --- changes :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_serialize") @(link_name = "sqlite3_serialize")
serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 --- serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: SERIALIZE_FLAGS) -> [^]u8 ---
@(link_name = "sqlite3_deserialize") @(link_name = "sqlite3_deserialize")
deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int --- deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int ---
@(link_name = "sqlite3_malloc64") @(link_name = "sqlite3_malloc64")
+1 -1
View File
@@ -42,7 +42,7 @@ write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
if t.caption != "" { if t.caption != "" {
table.write_text_align( table.write_text_align(
w, w,
fmt.tprintf("%s%s%s", COLOR_HEADINGS, t.caption, ANSI_RESET), colorize(.Heading, t.caption),
.Left, .Left,
0, //t.lpad, 0, //t.lpad,
0, //t.rpad, 0, //t.rpad,
+10 -2
View File
@@ -2,6 +2,7 @@
package main package main
import "core:strings"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -16,7 +17,7 @@ test_ansi_aware_width_empty :: proc(t: ^testing.T) {
@(test) @(test)
test_ansi_aware_width_with_color_codes :: proc(t: ^testing.T) { test_ansi_aware_width_with_color_codes :: proc(t: ^testing.T) {
colored := COLOR_TABLE_HEADING + "Directory" + ANSI_RESET colored := colorize(.Heading, "Directory", disable = false)
testing.expect_value(t, ansi_aware_width(colored), 9) testing.expect_value(t, ansi_aware_width(colored), 9)
} }
@@ -28,6 +29,13 @@ test_ansi_aware_width_multibyte :: proc(t: ^testing.T) {
@(test) @(test)
test_ansi_aware_width_multiple_escape_sequences :: proc(t: ^testing.T) { test_ansi_aware_width_multiple_escape_sequences :: proc(t: ^testing.T) {
colored := COLOR_TABLE_HEADING + "a" + ANSI_RESET + "b" + COLOR_TABLE_HEADING + "c" + ANSI_RESET colored := strings.concatenate(
{
colorize(.Heading, "a", disable = false),
colorize(.Heading, "b", disable = false),
colorize(.Heading, "c", disable = false),
},
)
testing.expect_value(t, ansi_aware_width(colored), 3) testing.expect_value(t, ansi_aware_width(colored), 3)
} }
+1 -1
View File
@@ -1 +1 @@
0.3.0 0.4.0