4 Commits

Author SHA1 Message Date
spencer 581967a58d nix: Fixed the nix build. 2026-06-25 18:08:08 -04:00
spencer 6ec09309dd fix: -h short flag now works on subcommands. 2026-06-25 18:07:31 -04:00
spencer c5020bd6a6 chore: Re-numbered todos. 2026-06-25 18:00:54 -04:00
spencer d5981d7b88 feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-25 17:53:44 -04:00
21 changed files with 310 additions and 525 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.
+5 -4
View File
@@ -20,11 +20,13 @@
10. Pass allocator to findr? 10. Pass allocator to findr?
11. Consider getting rid of color global. 11. Smarter flag parsing?
12. `write_flags_table` should never return false. 12. Rewrite `write_command_help` to use text/tables
13. Add a text filter to the multi_select. 13. Add color flag and support non colored output.
14. Add a text filter to the multi_select.
## Double-check AI output ## Double-check AI output
@@ -52,7 +54,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") || has_flag(&cmd, "h") {
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")
}
}
-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[:]
}
+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