1 Commits

Author SHA1 Message Date
spencer e74fc4f35a feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-25 17:51:31 -04:00
24 changed files with 324 additions and 539 deletions
-1
View File
@@ -2,7 +2,6 @@ on:
push: push:
branches: branches:
- main - main
- prerelease
permissions: permissions:
contents: write contents: write
-31
View File
@@ -1,36 +1,5 @@
# 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.
+17 -14
View File
@@ -1,30 +1,34 @@
# TODOs # TODOs
1. Bring back windows support / cross-compilation. 1. Commands are still leaking. (Write tests for everything first)
2. Commands are still leaking. (Write tests for everything first) 2. Add color flag and support non colored output.
3. procedures should be ordered by use, main at the top, then in the order they are called from main. 3. Rewrite `write_command_help` to use text/tables
4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`. 4. Generate md and man pages again.
5. Test all cmds / terminal branches. 5. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
6. Generate md and man pages again. 6. Add a text filter to the multi_select.
7. Shell completion 7. Add tests for untested commands.
8. Add tests for untested commands. 8. add --format -f flag to commands that draw tables.
9. Update `read_wire_string` to use a slice. 9. procedures should be ordered by use, main at the top, then in the order they are called from main.
10. Pass allocator to findr? 10. Shell completion
11. Consider getting rid of color global. 11. Bring back windows support / cross-compilation.
12. `write_flags_table` should never return false. 12. Test all cmds / terminal branches.
13. Add a text filter to the multi_select. 13. Pass allocator to findr?
14. Update `read_wire_string` to use a slice.
15. `-h` short flag seems to fail, at least with `envr list`
## Double-check AI output ## Double-check AI output
@@ -52,7 +56,6 @@
- [ ] 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
+131 -177
View File
@@ -1,11 +1,9 @@
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:terminal"
import "core:text/table" import "core:text/table"
@@ -13,52 +11,27 @@ import "core:text/table"
Command :: struct { Command :: struct {
name: string, name: string,
args: [dynamic]string, args: [dynamic]string,
flags: Flags, flags: map[string]string,
bool_set: map[string]bool,
config_path: string,
out_buf: ^bufio.Writer, out_buf: ^bufio.Writer,
out: io.Writer, out: io.Writer,
err: 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 { Output_Format :: enum {
Auto,
Table, Table,
JSON, JSON,
} }
Color_Mode :: enum {
Auto,
Always,
Never,
}
CommandInfo :: struct { CommandInfo :: struct {
name: string, name: string,
usage: string, usage: string,
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",
@@ -69,45 +42,22 @@ 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},
}, },
} }
@@ -127,33 +77,53 @@ 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)
overflow := parse_flags(&cmd.flags, args[2:]) // TODO: Optimize loop?
for arg in overflow { i := 2
for i < len(args) {
arg := args[i]
if strings.starts_with(arg, "--") {
key := arg[2:]
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
cmd.flags[key] = args[i + 1]
i += 2
} else {
cmd.bool_set[key] = true
i += 1
}
} else if strings.starts_with(arg, "-") && len(arg) == 2 {
key_slice := arg[1:2]
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
cmd.flags[key_slice] = args[i + 1]
i += 2
} else {
cmd.bool_set[key_slice] = true
i += 1
}
} else {
append(&cmd.args, arg) append(&cmd.args, arg)
i += 1
}
} }
if cmd.flags.output == .Auto { val: string = ---
cmd.flags.output = terminal.is_terminal(os.stdout) ? .Table : .JSON if val, ok = cmd.flags["config-file"]; ok {
} cmd.config_path = val
} else if val, ok = cmd.flags["c"]; ok {
if cmd.flags.color == .Auto { cmd.config_path = val
cmd.flags.color = terminal.is_terminal(os.stdout) ? .Always : .Never } else {
}
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.flags.config_file = default_config_path(home, context.temp_allocator) cmd.config_path = default_config_path(home, context.temp_allocator)
} }
if cmd.flags.help { if has_flag(&cmd, "help") {
print_command_help(&cmd) print_command_help(&cmd)
return cmd, false return cmd, false
} }
@@ -170,7 +140,6 @@ 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
@@ -178,23 +147,36 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
fmt.wprintf( fmt.wprintf(
w, w,
"%s\n\n\n%s\n\n %s [flags]\n\n", "%s\n\n\n" +
COLOR_HEADINGS +
"Usage:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"%s" +
ANSI_RESET +
" [flags]\n\n",
info.short, info.short,
colorize(.Heading, "Usage:"), info.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%s\n\n %s", "\n" +
colorize(.Heading, "Aliases:"), COLOR_HEADINGS +
colorize(.Command, info.name), "Aliases:" +
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, ", %s", colorize(.Command, a), flush = false) fmt.wprintf(w, ", " + COLOR_COMMANDS + "%s" + ANSI_RESET, a, flush = false)
} }
fmt.wprintf(w, "\n", flush = false) fmt.wprintf(w, "\n", flush = false)
} }
@@ -203,88 +185,25 @@ 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)
} }
tbl: table.Table fmt.wprintf(
table.init(&tbl, context.temp_allocator, context.temp_allocator) w,
table.padding(&tbl, 2, 0) "\n" +
if write_flags_table(&tbl, info.flags) { COLOR_HEADINGS +
fmt.wprintf(w, "\n", flush = false) "Flags:" +
write_borderless_table(w, &tbl) ANSI_RESET +
} "\n\n " +
table_reset(&tbl) COLOR_FLAGS +
return true "-h, --help" +
} ANSI_RESET +
" help for %s\n " +
flag_field_info :: proc( COLOR_FLAGS +
ft: Flag_Type, "-c, --config-file" +
) -> ( ANSI_RESET +
names: string, ` <path> config file (default "~/.envr/config.json")
value_hint: string, `,
description: string, info.name,
) { 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
} }
@@ -337,13 +256,15 @@ at before, restore your backup with:
> envr restore ~/<path to repository>/.env > envr restore ~/<path to repository>/.env
%s %sUsage:%s
%s [command] %senvr%s [command]
`, `,
colorize(.Heading, "Usage:"), COLOR_HEADINGS,
colorize(.Flag, "envr"), ANSI_RESET,
COLOR_FLAGS,
ANSI_RESET,
flush = false, flush = false,
) )
@@ -359,28 +280,61 @@ 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, colorize(.Command, name, tbl.format_allocator), c.short) table.row(&tbl, table.format(&tbl, "%s%s%s", COLOR_COMMANDS, name, ANSI_RESET), c.short)
} }
write_borderless_table(w, &tbl) write_borderless_table(w, &tbl)
table_reset(&tbl) table_reset(&tbl)
if write_flags_table(&tbl, GLOBAL_FLAGS) { table.caption(&tbl, "Flags:")
table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`)
table.row(
&tbl,
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`config file (default "~/.envr/config.json")`,
)
table.row(
&tbl,
COLOR_FLAGS + "-f, --format" + ANSI_RESET + " 'json'|'table'",
`the format of output data. (default 'table', unless piping)`,
)
write_borderless_table(w, &tbl) write_borderless_table(w, &tbl)
}
table_reset(&tbl)
fmt.wprintf( fmt.wprintf(
w, w,
`Use "%s [command] --help" for more information about a command.`, `Use "%senvr%s [command] --help" for more information about a command.`,
colorize(.Flag, "envr", tbl.format_allocator), COLOR_FLAGS,
ANSI_RESET,
flush = false, flush = false,
) )
} }
has_flag :: proc(cmd: ^Command, name: string) -> bool {
return name in cmd.flags || name in cmd.bool_set
}
get_format :: proc(cmd: ^Command) -> Output_Format {
flags :: []string{"format", "f"}
for name in flags {
if val, ok := cmd.flags[name]; ok {
switch val {
case "json":
return .JSON
case "table":
return .Table
}
}
}
return terminal.is_terminal(os.stdout) ? .Table : .JSON
}
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)
} }
+102 -43
View File
@@ -110,8 +110,7 @@ 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, "help for init"), "missing 'help for init'") testing.expect(t, strings.contains(text, "help for init"), "missing 'help for init'")
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) {
@@ -145,6 +144,53 @@ test_command_help_version :: proc(t: ^testing.T) {
} }
@(test) @(test)
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,
@@ -180,6 +226,8 @@ 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.flags), 0) testing.expect_value(t, len(cmd.flags), 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) {
@@ -194,45 +242,43 @@ test_parse_args_positional :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_long_flag_with_value :: proc(t: ^testing.T) { test_parse_args_long_flag_with_value :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args( cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "--config", "x.json"})
[]string{"envr", "sync", "--config-file", "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"], "x.json") testing.expect_value(t, cmd.flags["config"], "x.json")
} }
@(test) @(test)
test_parse_args_short_flag_with_value :: proc(t: ^testing.T) { test_parse_args_short_flag_with_value :: 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["c"], "x.json") testing.expect_value(t, cmd.flags["c"], "x.json")
} }
@(test) @(test)
test_parse_args_long_bool_flag :: proc(t: ^testing.T) { test_parse_args_long_bool_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.bool_set["force"], true) testing.expect_value(t, cmd.bool_set["force"], true)
} }
@(test) @(test)
test_parse_args_short_bool_flag :: proc(t: ^testing.T) { test_parse_args_short_bool_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "-f"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "version", "-l"})
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.bool_set["l"], true) testing.expect_value(t, cmd.bool_set["l"], true)
} }
@(test) @(test)
test_parse_args_multiple_positionals :: proc(t: ^testing.T) { test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
@@ -254,7 +300,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.bool_set["force"], true) testing.expect_value(t, cmd.bool_set["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")
} }
@@ -268,77 +314,90 @@ 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", "a.env", "--force", "--verbose"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "a.env", "--force", "--verbose"})
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.bool_set["force"], true) testing.expect_value(t, cmd.bool_set["force"], true)
testing.expect_value(t, cmd.flags.output, Output_Format.JSON) testing.expect_value(t, cmd.bool_set["verbose"], true)
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_long_flag :: proc(t: ^testing.T) { test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list"}) cmd, ok, _, _ := test_parse_args(
[]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.config_path) > 0, "config_path should default to non-empty path") testing.expect(t, len(cmd.config_path) > 0, "config_path should default to non-empty path")
testing.expect( testing.expect(
t, t,
strings.contains(cmd.config_path, ".envr"), strings.contains(cmd.config_path, ".envr"),
"default config_file should contain .envr dir, got %s", "default config_path should contain .envr dir, got %s",
) )
} }
@(test) @(test)
test_get_format_long_json :: proc(t: ^testing.T) { test_get_format_long_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--output", "json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "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, get_format(&cmd), Output_Format.JSON) testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
} }
@(test) @(test)
test_get_format_short_json :: proc(t: ^testing.T) { test_get_format_short_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-o", "json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "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, get_format(&cmd), Output_Format.JSON) testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
} }
@(test) @(test)
test_get_format_long_table :: proc(t: ^testing.T) { test_get_format_long_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--output", "table"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "table"})
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, get_format(&cmd), Output_Format.Table) testing.expect_value(t, get_format(&cmd), Output_Format.Table)
} }
@(test) @(test)
test_get_format_short_table :: proc(t: ^testing.T) { test_get_format_short_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-o", "table"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "table"})
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, get_format(&cmd), Output_Format.Table) testing.expect_value(t, get_format(&cmd), Output_Format.Table)
} }
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.flags.config_file) db, db_ok := db_open(cmd.config_path)
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.flags.config_file) db, db_ok := db_open(cmd.config_path)
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.flags.config_file config_path := cmd.config_path
if !os.exists(config_path) { if !os.exists(config_path) {
fmt.wprintf( fmt.wprintf(
+7 -13
View File
@@ -4,12 +4,11 @@ import "core:fmt"
import "core:terminal/ansi" import "core:terminal/ansi"
cmd_init :: proc(cmd: ^Command) { cmd_init :: proc(cmd: ^Command) {
force := cmd.flags.force force := has_flag(cmd, "force") || has_flag(cmd, "f")
config_file := cmd.flags.config_file
fmt.wprintln(cmd.out, cmd.flags.config_file, flush = false) fmt.wprintln(cmd.out, cmd.config_path, flush = false)
_, cfg_exists := load_config(config_file) _, cfg_exists := load_config(cmd.config_path)
if cfg_exists && !force { if cfg_exists && !force {
fmt.wprintln( fmt.wprintln(
cmd.out, cmd.out,
@@ -26,19 +25,15 @@ Run again with the --force flag if you want to reinitialize.`,
} }
if len(keys) == 0 { if len(keys) == 0 {
fmt.wprintln( fmt.wprintln(cmd.err, `No ssh-ed25519 keys found in ~/.ssh
cmd.err, Generate one with: ssh-keygen -t ed25519`, flush = false)
`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, colorize(.Message, "Cancelled."), flush = false) fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
return return
} }
@@ -54,7 +49,7 @@ Generate one with: ssh-keygen -t ed25519`,
return return
} }
cfg := new_config(selected_paths[:], config_file) cfg := new_config(selected_paths[:], cmd.config_path)
if !save_config(cfg, force = force) { if !save_config(cfg, force = force) {
return return
} }
@@ -66,4 +61,3 @@ Generate one with: ssh-keygen -t ed25519`,
flush = false, flush = false,
) )
} }
+4 -4
View File
@@ -14,7 +14,7 @@ ListEntry :: struct {
// TODO: Improve table rendering // TODO: Improve table rendering
cmd_list :: proc(cmd: ^Command) { cmd_list :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.flags.config_file) db, db_ok := db_open(cmd.config_path)
if !db_ok { if !db_ok {
return return
} }
@@ -25,15 +25,15 @@ cmd_list :: proc(cmd: ^Command) {
return return
} }
if cmd.flags.output == .Table { if get_format(cmd) == .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,
colorize(.Table_Heading, "Directory"), COLOR_TABLE_HEADING + "Directory" + ANSI_RESET,
colorize(.Table_Heading, "Path"), COLOR_TABLE_HEADING + "Path" + ANSI_RESET,
) )
for row in rows { for row in rows {
+4 -4
View File
@@ -22,7 +22,7 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) {
} }
@(test) @(test)
test_cmd_list_output_json :: proc(t: ^testing.T) { test_cmd_list_format_json :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-json-*") base := test_temp_dir(t, "envr-test-list-json-*")
defer os.remove_all(base) defer os.remove_all(base)
@@ -47,7 +47,7 @@ test_cmd_list_output_json :: proc(t: ^testing.T) {
defer strings.builder_destroy(&err_b) defer strings.builder_destroy(&err_b)
cmd, ok := parse_args( cmd, ok := parse_args(
[]string{"envr", "list", "--output", "json", "--config-file", cfg_path}, []string{"envr", "list", "--format", "json", "--config-file", cfg_path},
strings.to_stream(&out_b), strings.to_stream(&out_b),
strings.to_stream(&err_b), strings.to_stream(&err_b),
) )
@@ -68,7 +68,7 @@ test_cmd_list_output_json :: proc(t: ^testing.T) {
} }
@(test) @(test)
test_cmd_list_output_table :: proc(t: ^testing.T) { test_cmd_list_format_table :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-table-*") base := test_temp_dir(t, "envr-test-list-table-*")
defer os.remove_all(base) defer os.remove_all(base)
@@ -93,7 +93,7 @@ test_cmd_list_output_table :: proc(t: ^testing.T) {
defer strings.builder_destroy(&err_b) defer strings.builder_destroy(&err_b)
cmd, ok := parse_args( cmd, ok := parse_args(
[]string{"envr", "list", "--output", "table", "--config-file", cfg_path}, []string{"envr", "list", "--format", "table", "--config-file", cfg_path},
strings.to_stream(&out_b), strings.to_stream(&out_b),
strings.to_stream(&err_b), strings.to_stream(&err_b),
) )
+1 -1
View File
@@ -22,7 +22,7 @@ cmd_remove :: proc(cmd: ^Command) {
return return
} }
db, db_ok := db_open(cmd.flags.config_file) db, db_ok := db_open(cmd.config_path)
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.flags.config_file) db, db_ok := db_open(cmd.config_path)
if !db_ok { if !db_ok {
return return
} }
+22 -8
View File
@@ -4,9 +4,10 @@ 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.flags.config_file) db, db_ok := db_open(cmd.config_path)
if !db_ok { if !db_ok {
return return
} }
@@ -71,7 +72,11 @@ 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(cmd.out, colorize(.Message, "Cancelled."), flush = false) fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
flush = false,
)
return return
} }
@@ -95,16 +100,25 @@ cmd_scan :: proc(cmd: ^Command) {
} }
if added_count > 0 { if added_count > 0 {
fmt.wprintln( fmt.wprintf(
cmd.out, cmd.out,
colorize( ansi.CSI +
.Sucess, ansi.BOLD +
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(cmd.out, colorize(.Message, "No files were added."), flush = false) fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET,
flush = false,
)
} }
} }
+4 -4
View File
@@ -11,7 +11,7 @@ SyncEntry :: struct {
// TODO: Check for quiet failures. // TODO: Check for quiet failures.
cmd_sync :: proc(cmd: ^Command) { cmd_sync :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.flags.config_file) db, db_ok := db_open(cmd.config_path)
if !db_ok { if !db_ok {
return return
} }
@@ -46,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) {
} }
} }
if cmd.flags.output == .Table { if get_format(cmd) == .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)
@@ -54,8 +54,8 @@ cmd_sync :: proc(cmd: ^Command) {
table.aligned_header_of_values( table.aligned_header_of_values(
&t, &t,
.Center, .Center,
colorize(.Table_Heading, "File"), COLOR_TABLE_HEADING + "File" + ANSI_RESET,
colorize(.Table_Heading, "Status"), COLOR_TABLE_HEADING + "Status" + ANSI_RESET,
) )
for res in results { for res in results {
+9 -58
View File
@@ -1,66 +1,17 @@
package main package main
import "base:runtime"
import "core:strings"
import "core:terminal/ansi" import "core:terminal/ansi"
Color_Code :: enum { COLOR_HEADINGS ::
Heading, ansi.CSI + ansi.FG_BRIGHT_GREEN + ";" + ansi.BOLD + ";" + ansi.UNDERLINE + ansi.SGR
Command,
// Example
Flag,
Table_Heading,
Message,
Sucess,
Caret,
Option_Label,
}
// COLOR_EXAMPLE :: ansi.CSI + ansi.ITALIC + ansi.SGR COLOR_COMMANDS :: ansi.CSI + ansi.FG_BRIGHT_CYAN + ";" + ansi.BOLD + 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, {}) data := sqlite.serialize(db.conn, "main", &sz, 0)
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, {}) data := sqlite.serialize(db.conn, "main", &sz, 0)
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
@@ -1,134 +0,0 @@
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,8 +75,6 @@
]; ];
buildInputs = [ buildInputs = [
pkgs.git
pkgs.libsodium pkgs.libsodium
mysqlite mysqlite
]; ];
+5 -19
View File
@@ -66,17 +66,11 @@ multi_select :: proc(
case .Space: case .Space:
selected[cursor] = !selected[cursor] selected[cursor] = !selected[cursor]
case .Enter: case .Enter:
fmt.printf( fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1)
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( fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, visible + 1)
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:
@@ -95,10 +89,7 @@ render_options :: proc(
cursor: int, cursor: int,
scroll_offset: int, scroll_offset: int,
) -> int { ) -> int {
fmt.printf( fmt.printf(ansi.CSI + ansi.BOLD + ";" + ansi.FG_CYAN + ansi.SGR + "%s" + ANSI_RESET + " (↑/↓ move, space select, enter confirm)\r\n", prompt)
"%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) {
@@ -111,14 +102,9 @@ render_options :: proc(
checkbox = "x" checkbox = "x"
} }
if i == cursor { if i == cursor {
fmt.printf( 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])
"%s [%s] %s\r\n",
colorize(.Caret, ">"),
colorize(.Sucess, checkbox),
options[i],
)
} else { } else {
fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[i]) fmt.printf(" [" + ansi.CSI + ansi.FAINT + ansi.SGR + "%s" + ANSI_RESET + "] %s\r\n", checkbox, options[i])
} }
} }
+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,
colorize(.Heading, t.caption), fmt.tprintf("%s%s%s", COLOR_HEADINGS, t.caption, ANSI_RESET),
.Left, .Left,
0, //t.lpad, 0, //t.lpad,
0, //t.rpad, 0, //t.rpad,
+2 -10
View File
@@ -2,7 +2,6 @@
package main package main
import "core:strings"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -17,7 +16,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 := colorize(.Heading, "Directory", disable = false) colored := COLOR_TABLE_HEADING + "Directory" + ANSI_RESET
testing.expect_value(t, ansi_aware_width(colored), 9) testing.expect_value(t, ansi_aware_width(colored), 9)
} }
@@ -29,13 +28,6 @@ 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 := strings.concatenate( colored := COLOR_TABLE_HEADING + "a" + ANSI_RESET + "b" + COLOR_TABLE_HEADING + "c" + ANSI_RESET
{
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.4.0 0.3.0