1 Commits

Author SHA1 Message Date
e74fc4f35a feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-25 17:51:31 -04:00
18 changed files with 208 additions and 341 deletions

View File

@@ -1,34 +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. Smarter flag parsing? 11. Bring back windows support / cross-compilation.
12. Rewrite `write_command_help` to use text/tables 12. Test all cmds / terminal branches.
13. Instead of using a writer to strip colors, just don't print the colors. 13. Pass allocator to findr?
14. Add a text filter to the multi_select. 14. Update `read_wire_string` to use a slice.
15. init -h doesn't show --force flag. Separate into multiple structs: Global_FLags, and Init_Flags? 15. `-h` short flag seems to fail, at least with `envr list`
## Double-check AI output ## Double-check AI output
@@ -56,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

103
cli.odin
View File

@@ -11,33 +11,19 @@ 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,
} }
// 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 { 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,
@@ -91,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 {
cmd.out = make_ansi_strip_writer(cmd.out)
}
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
} }
@@ -290,13 +296,8 @@ at before, restore your backup with:
) )
table.row( table.row(
&tbl, &tbl,
COLOR_FLAGS + "-o, --output" + ANSI_RESET + " 'table'|'json'", COLOR_FLAGS + "-f, --format" + ANSI_RESET + " 'json'|'table'",
`the format of output data. (default 'table')`, `the format of output data. (default 'table', unless piping)`,
)
table.row(
&tbl,
COLOR_FLAGS + "--color" + ANSI_RESET + " 'auto'|'always'|'never'",
`Whether or not to colorize output. (default 'auto')`,
) )
write_borderless_table(w, &tbl) write_borderless_table(w, &tbl)
@@ -309,9 +310,31 @@ 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) { 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)
} }

View File

@@ -144,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,
@@ -179,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) {
@@ -193,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) {
@@ -253,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")
} }
@@ -267,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)
}

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
} }

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
} }

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(

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,23 +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( fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
flush = false,
)
return return
} }
@@ -58,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
} }
@@ -70,4 +61,3 @@ Generate one with: ssh-keygen -t ed25519`,
flush = false, flush = false,
) )
} }

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,7 +25,7 @@ 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)

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),
) )

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
} }

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
} }

View File

@@ -7,7 +7,7 @@ import "core:terminal"
import "core:terminal/ansi" 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
} }

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)

View File

@@ -1,6 +1,5 @@
package main package main
import "core:io"
import "core:terminal/ansi" import "core:terminal/ansi"
COLOR_HEADINGS :: COLOR_HEADINGS ::
@@ -16,71 +15,3 @@ COLOR_TABLE_HEADING :: ansi.CSI + ansi.FG_BRIGHT_GREEN + ansi.SGR
ANSI_RESET :: ansi.CSI + ansi.RESET + 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)}
}

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

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)

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[:]
}

View File

@@ -75,8 +75,6 @@
]; ];
buildInputs = [ buildInputs = [
pkgs.git
pkgs.libsodium pkgs.libsodium
mysqlite mysqlite
]; ];