4 Commits

Author SHA1 Message Date
581967a58d nix: Fixed the nix build. 2026-06-25 18:08:08 -04:00
6ec09309dd fix: -h short flag now works on subcommands. 2026-06-25 18:07:31 -04:00
c5020bd6a6 chore: Re-numbered todos. 2026-06-25 18:00:54 -04:00
d5981d7b88 feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-25 17:53:44 -04:00
10 changed files with 192 additions and 26 deletions

View File

@@ -1,32 +1,32 @@
# TODOs # TODOs
1. Commands are still leaking. (Write tests for everything first) 1. Bring back windows support / cross-compilation.
2. Add color flag and support non colored output. 2. Commands are still leaking. (Write tests for everything first)
3. Rewrite `write_command_help` to use text/tables 3. procedures should be ordered by use, main at the top, then in the order they are called from main.
4. Generate md and man pages again. 4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
5. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`. 5. Test all cmds / terminal branches.
6. Add a text filter to the multi_select. 6. Generate md and man pages again.
7. Add tests for untested commands. 7. Shell completion
8. add --format -f flag to commands that draw tables. 8. Add tests for untested commands.
9. procedures should be ordered by use, main at the top, then in the order they are called from main. 9. Update `read_wire_string` to use a slice.
10. Shell completion 10. Pass allocator to findr?
11. Bring back windows support / cross-compilation. 11. Smarter flag parsing?
12. Test all cmds / terminal branches. 12. Rewrite `write_command_help` to use text/tables
13. Pass allocator to findr? 13. Add color flag and support non colored output.
14. Update `read_wire_string` to use a slice. 14. Add a text filter to the multi_select.
## Double-check AI output ## Double-check AI output

View File

@@ -5,6 +5,7 @@ import "core:fmt"
import "core:io" import "core:io"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
import "core:terminal"
import "core:text/table" import "core:text/table"
Command :: struct { Command :: struct {
@@ -18,6 +19,11 @@ Command :: struct {
err: io.Writer, err: io.Writer,
} }
Output_Format :: enum {
Table,
JSON,
}
CommandInfo :: struct { CommandInfo :: struct {
name: string, name: string,
usage: string, usage: string,
@@ -117,7 +123,7 @@ parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Comm
cmd.config_path = default_config_path(home, context.temp_allocator) cmd.config_path = default_config_path(home, context.temp_allocator)
} }
if has_flag(&cmd, "help") { if has_flag(&cmd, "help") || has_flag(&cmd, "h") {
print_command_help(&cmd) print_command_help(&cmd)
return cmd, false return cmd, false
} }
@@ -288,6 +294,11 @@ at before, restore your backup with:
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>", COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`config file (default "~/.envr/config.json")`, `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)
fmt.wprintf( fmt.wprintf(
@@ -303,6 +314,22 @@ has_flag :: proc(cmd: ^Command, name: string) -> bool {
return name in cmd.flags || name in cmd.bool_set 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)

View File

@@ -361,3 +361,43 @@ test_parse_args_config_file_defaults :: proc(t: ^testing.T) {
} }
@(test) @(test)
test_get_format_long_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
}
@(test)
test_get_format_short_json :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "json"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, get_format(&cmd), Output_Format.JSON)
}
@(test)
test_get_format_long_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "table"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, get_format(&cmd), Output_Format.Table)
}
@(test)
test_get_format_short_table :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "table"})
testing.expect(t, ok, "should succeed")
if !ok do return
defer delete_command(&cmd)
testing.expect_value(t, get_format(&cmd), Output_Format.Table)
}

View File

@@ -5,7 +5,6 @@ import "core:fmt"
import "core:os" import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
import "core:terminal"
import "core:text/table" import "core:text/table"
ListEntry :: struct { ListEntry :: struct {
@@ -13,7 +12,6 @@ ListEntry :: struct {
path: string `json:"path"`, path: string `json:"path"`,
} }
// TODO: Support --format flag
// TODO: Improve table rendering // TODO: Improve table rendering
cmd_list :: proc(cmd: ^Command) { cmd_list :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.config_path)
@@ -27,7 +25,7 @@ cmd_list :: proc(cmd: ^Command) {
return return
} }
if terminal.is_terminal(os.stdout) { 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)
@@ -51,7 +49,7 @@ cmd_list :: proc(cmd: ^Command) {
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width) table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)
} else { } else {
// TODO: Should we instead print full entries here? // TODO: Should we instead print full entries here?
entries: [dynamic]ListEntry entries := make([dynamic]ListEntry, 0, len(rows), context.temp_allocator)
for row in rows { for row in rows {
filename := filepath.base(row.path) filename := filepath.base(row.path)
append( append(

View File

@@ -1,7 +1,11 @@
#+feature dynamic-literals
#+test #+test
package main package main
import "core:bufio"
import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings"
import "core:testing" import "core:testing"
@(test) @(test)
@@ -17,3 +21,95 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) {
} }
} }
@(test)
test_cmd_list_format_json :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-json-*")
defer os.remove_all(base)
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
db, db_ok := db_open(cfg_path)
testing.expect(t, db_ok, "db should open")
if !db_ok do return
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
out_b: strings.Builder
strings.builder_init(&out_b)
defer strings.builder_destroy(&out_b)
err_b: strings.Builder
strings.builder_init(&err_b)
defer strings.builder_destroy(&err_b)
cmd, ok := parse_args(
[]string{"envr", "list", "--format", "json", "--config-file", cfg_path},
strings.to_stream(&out_b),
strings.to_stream(&err_b),
)
testing.expect(t, ok, "parse_args should succeed")
if !ok do return
defer delete_command(&cmd)
cmd_list(&cmd)
bufio.writer_flush(cmd.out_buf)
output := strings.to_string(out_b)
testing.expect(t, strings.contains(output, "["), "json output should contain '['")
testing.expect(
t,
strings.contains(output, "\"directory\""),
"json output should contain directory key",
)
}
@(test)
test_cmd_list_format_table :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-list-table-*")
defer os.remove_all(base)
cfg_path, _ := filepath.join([]string{base, "config.json"}, context.temp_allocator)
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
testing.expect(t, save_config(cfg, force = true), "save should succeed")
delete_config(&cfg)
db, db_ok := db_open(cfg_path)
testing.expect(t, db_ok, "db should open")
if !db_ok do return
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
defer delete(f.remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
out_b: strings.Builder
strings.builder_init(&out_b)
defer strings.builder_destroy(&out_b)
err_b: strings.Builder
strings.builder_init(&err_b)
defer strings.builder_destroy(&err_b)
cmd, ok := parse_args(
[]string{"envr", "list", "--format", "table", "--config-file", cfg_path},
strings.to_stream(&out_b),
strings.to_stream(&err_b),
)
testing.expect(t, ok, "parse_args should succeed")
if !ok do return
defer delete_command(&cmd)
cmd_list(&cmd)
bufio.writer_flush(cmd.out_buf)
output := strings.to_string(out_b)
testing.expect(t, strings.contains(output, "│"), "table output should contain border chars")
testing.expect(
t,
strings.contains(output, "Directory"),
"table output should contain Directory header",
)
}

View File

@@ -2,8 +2,6 @@ package main
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:os"
import "core:terminal"
import "core:text/table" import "core:text/table"
SyncEntry :: struct { SyncEntry :: struct {
@@ -12,7 +10,6 @@ SyncEntry :: struct {
} }
// TODO: Check for quiet failures. // TODO: Check for quiet failures.
// TODO: Support --format -f flags
cmd_sync :: proc(cmd: ^Command) { cmd_sync :: proc(cmd: ^Command) {
db, db_ok := db_open(cmd.config_path) db, db_ok := db_open(cmd.config_path)
if !db_ok { if !db_ok {
@@ -49,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) {
} }
} }
if terminal.is_terminal(os.stdout) { 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

@@ -172,7 +172,7 @@ db_close :: proc(db: ^Db) {
} }
sz: i64 sz: i64
data := sqlite.serialize(db.conn, "main", &sz, 0) data := sqlite.serialize(db.conn, "main", &sz, {})
if data == nil { if data == nil {
fmt.eprintln("Error: failed to serialize database") fmt.eprintln("Error: failed to serialize database")
return return

View File

@@ -196,7 +196,7 @@ test_db_serialize :: proc(t: ^testing.T) {
db_insert(&db, f) db_insert(&db, f)
sz: i64 sz: i64
data := sqlite.serialize(db.conn, "main", &sz, 0) data := sqlite.serialize(db.conn, "main", &sz, {})
testing.expect(t, data != nil, "serialize should return non-nil") testing.expect(t, data != nil, "serialize should return non-nil")
if data == nil do return if data == nil do return
defer sqlite.free(data) defer sqlite.free(data)

View File

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

View File

@@ -17,6 +17,12 @@ DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG]
DESERIALIZE_FLAG :: enum u32 { DESERIALIZE_FLAG :: enum u32 {
FREEONCLOSE = 1, FREEONCLOSE = 1,
RESIZEABLE = 2, RESIZEABLE = 2,
READONLY = 4,
}
SERIALIZE_FLAGS :: bit_set[SERIALIZE_FLAG]
SERIALIZE_FLAG :: enum u32 {
NOCOPY = 1,
} }
foreign lib { foreign lib {
@@ -43,7 +49,7 @@ foreign lib {
@(link_name = "sqlite3_changes") @(link_name = "sqlite3_changes")
changes :: proc(db: Db) -> c.int --- changes :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_serialize") @(link_name = "sqlite3_serialize")
serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 --- serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: SERIALIZE_FLAGS) -> [^]u8 ---
@(link_name = "sqlite3_deserialize") @(link_name = "sqlite3_deserialize")
deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int --- deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int ---
@(link_name = "sqlite3_malloc64") @(link_name = "sqlite3_malloc64")