mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
Compare commits
2 Commits
581967a58d
...
ac6c734722
| Author | SHA1 | Date | |
|---|---|---|---|
| ac6c734722 | |||
| a4f4b10a7b |
5
TODOS.md
5
TODOS.md
@@ -24,10 +24,12 @@
|
||||
|
||||
12. Rewrite `write_command_help` to use text/tables
|
||||
|
||||
13. Add color flag and support non colored output.
|
||||
13. Instead of using a writer to strip colors, just don't print the colors.
|
||||
|
||||
14. Add a text filter to the multi_select.
|
||||
|
||||
15. init -h doesn't show --force flag. Separate into multiple structs: Global_FLags, and Init_Flags?
|
||||
|
||||
## Double-check AI output
|
||||
|
||||
- [ ] cli.odin
|
||||
@@ -54,6 +56,7 @@
|
||||
- [ ] db.odin
|
||||
- [ ] db_integration_test.odin
|
||||
- [ ] db_test.odin
|
||||
- [ ] flags.odin
|
||||
- [x] main.odin
|
||||
- [x] prompt.odin
|
||||
- [x] scan.odin
|
||||
|
||||
103
cli.odin
103
cli.odin
@@ -11,19 +11,33 @@ import "core:text/table"
|
||||
Command :: struct {
|
||||
name: string,
|
||||
args: [dynamic]string,
|
||||
flags: map[string]string,
|
||||
bool_set: map[string]bool,
|
||||
config_path: string,
|
||||
flags: Flags,
|
||||
out_buf: ^bufio.Writer,
|
||||
out: io.Writer,
|
||||
err: io.Writer,
|
||||
}
|
||||
|
||||
// TODO: Put help test in usage:"whatever" tag.
|
||||
Flags :: struct {
|
||||
help: bool `args:"short=h"`,
|
||||
config_file: string `args:"name=config-file,short=c"`,
|
||||
output: Output_Format `args:"short=o"`,
|
||||
color: Color_Mode,
|
||||
force: bool `args:"short=f"`,
|
||||
}
|
||||
|
||||
Output_Format :: enum {
|
||||
Auto,
|
||||
Table,
|
||||
JSON,
|
||||
}
|
||||
|
||||
Color_Mode :: enum {
|
||||
Auto,
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
|
||||
CommandInfo :: struct {
|
||||
name: string,
|
||||
usage: string,
|
||||
@@ -77,53 +91,33 @@ parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Comm
|
||||
}
|
||||
|
||||
cmd.name = args[1]
|
||||
|
||||
cmd.args = make([dynamic]string)
|
||||
cmd.flags = make(map[string]string)
|
||||
cmd.bool_set = make(map[string]bool)
|
||||
|
||||
// TODO: Optimize loop?
|
||||
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 {
|
||||
overflow := parse_flags(&cmd.flags, args[2:])
|
||||
for arg in overflow {
|
||||
append(&cmd.args, arg)
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
val: string = ---
|
||||
if val, ok = cmd.flags["config-file"]; ok {
|
||||
cmd.config_path = val
|
||||
} else if val, ok = cmd.flags["c"]; ok {
|
||||
cmd.config_path = val
|
||||
} else {
|
||||
if cmd.flags.output == .Auto {
|
||||
cmd.flags.output = terminal.is_terminal(os.stdout) ? .Table : .JSON
|
||||
}
|
||||
|
||||
if cmd.flags.color == .Auto {
|
||||
cmd.flags.color = terminal.is_terminal(os.stdout) ? .Always : .Never
|
||||
}
|
||||
if cmd.flags.color == .Never {
|
||||
cmd.out = make_ansi_strip_writer(cmd.out)
|
||||
}
|
||||
|
||||
if cmd.flags.config_file == "" {
|
||||
// FIXME: Handle err
|
||||
// TODO: Is this right?
|
||||
home, _ := os.user_home_dir(context.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") || has_flag(&cmd, "h") {
|
||||
if cmd.flags.help {
|
||||
print_command_help(&cmd)
|
||||
return cmd, false
|
||||
}
|
||||
@@ -296,8 +290,13 @@ at before, restore your backup with:
|
||||
)
|
||||
table.row(
|
||||
&tbl,
|
||||
COLOR_FLAGS + "-f, --format" + ANSI_RESET + " 'json'|'table'",
|
||||
`the format of output data. (default 'table', unless piping)`,
|
||||
COLOR_FLAGS + "-o, --output" + ANSI_RESET + " 'table'|'json'",
|
||||
`the format of output data. (default 'table')`,
|
||||
)
|
||||
table.row(
|
||||
&tbl,
|
||||
COLOR_FLAGS + "--color" + ANSI_RESET + " 'auto'|'always'|'never'",
|
||||
`Whether or not to colorize output. (default 'auto')`,
|
||||
)
|
||||
write_borderless_table(w, &tbl)
|
||||
|
||||
@@ -310,31 +309,9 @@ at before, restore your backup with:
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
bufio.writer_flush(cmd.out_buf)
|
||||
delete(cmd.args)
|
||||
delete(cmd.flags)
|
||||
delete(cmd.bool_set)
|
||||
bufio.writer_destroy(cmd.out_buf)
|
||||
free(cmd.out_buf)
|
||||
}
|
||||
|
||||
142
cli_test.odin
142
cli_test.odin
@@ -144,53 +144,6 @@ test_command_help_version :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
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,
|
||||
) -> (
|
||||
cmd: Command,
|
||||
@@ -226,8 +179,6 @@ test_parse_args_bare_command :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, cmd.name, "list")
|
||||
testing.expect_value(t, len(cmd.args), 0)
|
||||
}
|
||||
testing.expect_value(t, len(cmd.bool_set), 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_positional :: proc(t: ^testing.T) {
|
||||
@@ -242,43 +193,45 @@ test_parse_args_positional :: proc(t: ^testing.T) {
|
||||
|
||||
@(test)
|
||||
test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "--config", "x.json"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
cmd, ok, _, _ := test_parse_args(
|
||||
[]string{"envr", "sync", "--config-file", "x.json"},
|
||||
)
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags.config_file, "x.json")
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
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")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags.config_file, "x.json")
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
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")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags.force, true)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_force_short_flag :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "version", "-l"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "-f"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect_value(t, cmd.flags.force, true)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
|
||||
@@ -300,7 +253,7 @@ test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
|
||||
defer delete_command(&cmd)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -314,90 +267,77 @@ test_parse_args_no_args :: proc(t: ^testing.T) {
|
||||
@(test)
|
||||
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"})
|
||||
defer delete_command(&cmd)
|
||||
defer delete_command(&cmd)
|
||||
testing.expect(t, ok, "should succeed")
|
||||
|
||||
testing.expect_value(t, cmd.flags.force, true)
|
||||
testing.expect_value(t, cmd.bool_set["verbose"], true)
|
||||
testing.expect_value(t, len(cmd.args), 1)
|
||||
testing.expect_value(t, cmd.flags.output, Output_Format.JSON)
|
||||
testing.expect_value(t, len(cmd.args), 1)
|
||||
testing.expect_value(t, cmd.args[0], "a.env")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_parse_args_config_file_default :: proc(t: ^testing.T) {
|
||||
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"})
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
if !ok do return
|
||||
defer delete_command(&cmd)
|
||||
|
||||
testing.expect(t, len(cmd.flags.config_file) > 0, "config_file should default to non-empty path")
|
||||
testing.expect(
|
||||
testing.expect(
|
||||
t,
|
||||
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_parse_args_output_long_json :: proc(t: ^testing.T) {
|
||||
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "json"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
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", "-f", "json"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
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", "--format", "table"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
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", "-f", "table"})
|
||||
testing.expect(t, ok, "should succeed")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ cmd_backup :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
db, db_ok := db_open(cmd.flags.config_file)
|
||||
if !db_ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ cmd_check :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
db, db_ok := db_open(cmd.flags.config_file)
|
||||
if !db_ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
config_path := cmd.config_path
|
||||
config_path := cmd.flags.config_file
|
||||
|
||||
if !os.exists(config_path) {
|
||||
fmt.wprintf(
|
||||
|
||||
@@ -4,11 +4,12 @@ import "core:fmt"
|
||||
import "core:terminal/ansi"
|
||||
|
||||
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 {
|
||||
fmt.wprintln(
|
||||
cmd.out,
|
||||
@@ -25,15 +26,23 @@ Run again with the --force flag if you want to reinitialize.`,
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
fmt.wprintln(cmd.err, `No ssh-ed25519 keys found in ~/.ssh
|
||||
Generate one with: ssh-keygen -t ed25519`, flush = false)
|
||||
fmt.wprintln(
|
||||
cmd.err,
|
||||
`No ssh-ed25519 keys found in ~/.ssh
|
||||
Generate one with: ssh-keygen -t ed25519`,
|
||||
flush = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
selected, result := multi_select("Select SSH private keys:", keys[:])
|
||||
defer delete(selected)
|
||||
if result == .Cancel {
|
||||
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
|
||||
fmt.wprintln(
|
||||
cmd.out,
|
||||
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
|
||||
flush = false,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,7 +58,7 @@ Generate one with: ssh-keygen -t ed25519`, flush = false)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := new_config(selected_paths[:], cmd.config_path)
|
||||
cfg := new_config(selected_paths[:], config_file)
|
||||
if !save_config(cfg, force = force) {
|
||||
return
|
||||
}
|
||||
@@ -61,3 +70,4 @@ Generate one with: ssh-keygen -t ed25519`, flush = false)
|
||||
flush = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ ListEntry :: struct {
|
||||
|
||||
// TODO: Improve table rendering
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -25,7 +25,7 @@ cmd_list :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
if get_format(cmd) == .Table {
|
||||
if cmd.flags.output == .Table {
|
||||
t: table.Table
|
||||
table.init(&t, context.temp_allocator, context.temp_allocator)
|
||||
table.padding(&t, 1, 1)
|
||||
|
||||
@@ -22,7 +22,7 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_cmd_list_format_json :: proc(t: ^testing.T) {
|
||||
test_cmd_list_output_json :: proc(t: ^testing.T) {
|
||||
base := test_temp_dir(t, "envr-test-list-json-*")
|
||||
defer os.remove_all(base)
|
||||
|
||||
@@ -47,7 +47,7 @@ test_cmd_list_format_json :: proc(t: ^testing.T) {
|
||||
defer strings.builder_destroy(&err_b)
|
||||
|
||||
cmd, ok := parse_args(
|
||||
[]string{"envr", "list", "--format", "json", "--config-file", cfg_path},
|
||||
[]string{"envr", "list", "--output", "json", "--config-file", cfg_path},
|
||||
strings.to_stream(&out_b),
|
||||
strings.to_stream(&err_b),
|
||||
)
|
||||
@@ -68,7 +68,7 @@ test_cmd_list_format_json :: proc(t: ^testing.T) {
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_cmd_list_format_table :: proc(t: ^testing.T) {
|
||||
test_cmd_list_output_table :: proc(t: ^testing.T) {
|
||||
base := test_temp_dir(t, "envr-test-list-table-*")
|
||||
defer os.remove_all(base)
|
||||
|
||||
@@ -93,7 +93,7 @@ test_cmd_list_format_table :: proc(t: ^testing.T) {
|
||||
defer strings.builder_destroy(&err_b)
|
||||
|
||||
cmd, ok := parse_args(
|
||||
[]string{"envr", "list", "--format", "table", "--config-file", cfg_path},
|
||||
[]string{"envr", "list", "--output", "table", "--config-file", cfg_path},
|
||||
strings.to_stream(&out_b),
|
||||
strings.to_stream(&err_b),
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ cmd_remove :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
db, db_ok := db_open(cmd.flags.config_file)
|
||||
if !db_ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ cmd_restore :: proc(cmd: ^Command) {
|
||||
return
|
||||
}
|
||||
|
||||
db, db_ok := db_open(cmd.config_path)
|
||||
db, db_ok := db_open(cmd.flags.config_file)
|
||||
if !db_ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import "core:terminal"
|
||||
import "core:terminal/ansi"
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ SyncEntry :: struct {
|
||||
|
||||
// TODO: Check for quiet failures.
|
||||
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 {
|
||||
return
|
||||
}
|
||||
@@ -46,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) {
|
||||
}
|
||||
}
|
||||
|
||||
if get_format(cmd) == .Table {
|
||||
if cmd.flags.output == .Table {
|
||||
t: table.Table
|
||||
table.init(&t, context.temp_allocator, context.temp_allocator)
|
||||
table.padding(&t, 1, 1)
|
||||
|
||||
69
colors.odin
69
colors.odin
@@ -1,5 +1,6 @@
|
||||
package main
|
||||
|
||||
import "core:io"
|
||||
import "core:terminal/ansi"
|
||||
|
||||
COLOR_HEADINGS ::
|
||||
@@ -15,3 +16,71 @@ COLOR_TABLE_HEADING :: ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
|
||||
|
||||
ANSI_RESET :: ansi.CSI + ansi.RESET + ansi.SGR
|
||||
|
||||
ANSI_Strip_State :: enum { Normal, GotESC, InCSI }
|
||||
|
||||
ANSI_Strip_Data :: struct {
|
||||
inner: io.Writer,
|
||||
state: ANSI_Strip_State,
|
||||
}
|
||||
|
||||
ansi_strip_proc :: proc(
|
||||
stream_data: rawptr,
|
||||
mode: io.Stream_Mode,
|
||||
p: []byte,
|
||||
offset: i64,
|
||||
whence: io.Seek_From,
|
||||
) -> (n: i64, err: io.Error) {
|
||||
data := cast(^ANSI_Strip_Data) stream_data
|
||||
|
||||
#partial switch mode {
|
||||
case .Write:
|
||||
start := 0
|
||||
for i in 0..<len(p) {
|
||||
b := p[i]
|
||||
|
||||
switch data.state {
|
||||
case .Normal:
|
||||
if b == 0x1b {
|
||||
if i > start {
|
||||
io.write(data.inner, p[start:i])
|
||||
}
|
||||
data.state = .GotESC
|
||||
}
|
||||
|
||||
case .GotESC:
|
||||
if b == '[' {
|
||||
data.state = .InCSI
|
||||
} else {
|
||||
start = i
|
||||
data.state = .Normal
|
||||
}
|
||||
|
||||
case .InCSI:
|
||||
if b >= 0x40 && b <= 0x7E {
|
||||
start = i + 1
|
||||
data.state = .Normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.state == .Normal && len(p) > start {
|
||||
io.write(data.inner, p[start:])
|
||||
}
|
||||
|
||||
n = i64(len(p))
|
||||
return
|
||||
|
||||
case .Flush:
|
||||
return 0, io.flush(data.inner)
|
||||
case .Close:
|
||||
return 0, io.close(data.inner)
|
||||
case:
|
||||
return data.inner.procedure(data.inner.data, mode, p, offset, whence)
|
||||
}
|
||||
}
|
||||
|
||||
make_ansi_strip_writer :: proc(inner: io.Writer) -> io.Writer {
|
||||
data := new(ANSI_Strip_Data, context.temp_allocator)
|
||||
data.inner = inner
|
||||
return io.Writer{procedure = ansi_strip_proc, data = rawptr(data)}
|
||||
}
|
||||
|
||||
134
flags.odin
Normal file
134
flags.odin
Normal 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[:]
|
||||
}
|
||||
Reference in New Issue
Block a user