From 397f45d4d0b52f6802b79dd8aa631cd55020b834 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Tue, 16 Jun 2026 09:27:49 -0400 Subject: [PATCH] chore: Completed todos. --- TODOS.md | 19 ++---- config.odin | 1 + db.odin | 24 +++++++ main.odin | 2 + prompt.odin | 192 ++++++++++++++++++++++++++-------------------------- 5 files changed, 127 insertions(+), 111 deletions(-) diff --git a/TODOS.md b/TODOS.md index cec737d..2127bd3 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,12 +1,7 @@ -# TODO +# TODOs -Note: These todos can wait until all the subcommands have been ported. -## HIGH - -1. **db.odin:380-383, 405, 446** — `sqlite.bind_text` return values overwritten but never checked. A failed bind means `sqlite.step` operates on unbound params. - -## MEDIUM +1. Encrypt/decrypt the database in memory. 2. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing. @@ -18,22 +13,14 @@ Note: These todos can wait until all the subcommands have been ported. 6. **cmd_restore.odin:44** — `os.mkdir_all` error silently discarded. Subsequent write failure will be confusing. -7. **cmd_edit_config.odin:27** — `$EDITOR` used as single binary name. Breaks for multi-word values like `"code -w"`. Needs `strings.fields()`. - 8. **config.odin:178** — `search_paths` silently ignores `os.user_home_dir` error. If home is empty, `~` isn't expanded. Same class of bug as issue 3. -9. **prompt.odin:124** — `make([dynamic]bool, len(options))` creates N zero-initialized elements. Works because `false` is the default, but same footgun as original issue 1. Should be `make([dynamic]bool, 0, len(options))`. - -## LOW - 10. **db.odin:115** — `json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data. 11. **db.odin:352-353** — `hex.encode` error ignored. `string(hex_bytes)` aliases the byte slice. 12. **cmd_sync.odin:80, cmd_list.odin:33, cmd_deps.odin:9** — `make([]string, 2)` for table rows never freed. Leaks per row. Defer to memory pass. -## REFACTOR - 13. **cmd_list.odin** — Non-TTY branch builds `ListEntry` structs and marshals JSON separately. Now that `render_json_rows` (issue 1) accepts an `io.Writer` and uses `json.marshal`, unify both branches to use it. Note: will change JSON keys from `"directory"/"path"` to `"Directory"/"Path"`. 14. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`. @@ -54,6 +41,8 @@ Note: These todos can wait until all the subcommands have been ported. 22. Change struct field names from PascalCase to snake_case. +23. procedures should be ordered by use, main at the top, then in the order they are called from main. + ## Double-check AI output - [ ] cli.odin diff --git a/config.odin b/config.odin index 67f6f73..6d61ef1 100644 --- a/config.odin +++ b/config.odin @@ -126,6 +126,7 @@ find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) { return } +// Caller is responsible for calling delete_config() new_config :: proc( private_key_paths: []string, cfg_path: string = "~/.envr/config.json", diff --git a/db.odin b/db.odin index abe743c..ad6c9f3 100644 --- a/db.odin +++ b/db.odin @@ -382,18 +382,34 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool { cpath := to_cstring(file.Path) defer delete(cpath) rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db)) + return false + } cremotes := to_cstring(string(remotes_json)) defer delete(cremotes) rc = sqlite.bind_text(stmt, 2, cremotes, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding remotes: %s\n", sqlite.db_errmsg(d.db)) + return false + } csha := to_cstring(file.Sha256) defer delete(csha) rc = sqlite.bind_text(stmt, 3, csha, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding sha256: %s\n", sqlite.db_errmsg(d.db)) + return false + } ccontents := to_cstring(file.contents) defer delete(ccontents) rc = sqlite.bind_text(stmt, 4, ccontents, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding contents: %s\n", sqlite.db_errmsg(d.db)) + return false + } rc = sqlite.step(stmt) if rc != sqlite.DONE { @@ -418,6 +434,10 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi cpath := to_cstring(path, allocator) defer delete(cpath, allocator) rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db)) + return EnvFile{}, false + } rc = sqlite.step(stmt) if rc == sqlite.DONE { fmt.printf("No file found with path: %s\n", path) @@ -459,6 +479,10 @@ db_delete :: proc(d: ^Db, path: string) -> bool { cpath := to_cstring(path) defer delete(cpath) rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) + if rc != sqlite.OK { + fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db)) + return false + } rc = sqlite.step(stmt) if rc != sqlite.DONE { fmt.printf("Error deleting: %s\n", sqlite.db_errmsg(d.db)) diff --git a/main.odin b/main.odin index d2709e4..6274545 100644 --- a/main.odin +++ b/main.odin @@ -5,6 +5,8 @@ import "core:fmt" import "core:os" main :: proc() { + defer free_all(context.temp_allocator) + cmd, ok := parse_args(os.args, os.to_writer(os.stdout), os.to_writer(os.stderr)) defer bufio.writer_flush(cmd.out_buf) if !ok { diff --git a/prompt.odin b/prompt.odin index 9c6cb4d..e39c33a 100644 --- a/prompt.odin +++ b/prompt.odin @@ -3,36 +3,9 @@ package main import "core:fmt" import "core:sys/posix" -Raw_State :: struct { - original: posix.termios, - fd: posix.FD, -} - -enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) { - state: Raw_State - state.fd = fd - - if posix.tcgetattr(fd, &state.original) != .OK { - return state, false - } - - attr: posix.termios = state.original - attr.c_lflag -= {.ICANON, .ECHO, .ISIG, .IEXTEN} - attr.c_iflag -= {.IXON, .ICRNL, .BRKINT, .INPCK, .ISTRIP} - attr.c_oflag -= {.OPOST} - attr.c_cflag += {.CS8} - attr.c_cc[.VMIN] = 1 - attr.c_cc[.VTIME] = 0 - - if posix.tcsetattr(fd, .TCSAFLUSH, &attr) != .OK { - return state, false - } - - return state, true -} - -disable_raw_mode :: proc(state: ^Raw_State) { - posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original) +MultiSelect_Result :: enum { + Confirm, + Cancel, } Key :: enum { @@ -44,71 +17,9 @@ Key :: enum { Unknown, } -read_key :: proc() -> Key { - buf: [3]u8 - - n := posix.read(posix.STDIN_FILENO, &buf[0], 1) - if n <= 0 { - return .Unknown - } - - switch buf[0] { - case ' ': - return .Space - case '\n', '\r': - return .Enter - case 0x03: - return .Escape - case 0x1b: - tv: posix.timeval - tv.tv_sec = 0 - tv.tv_usec = posix.suseconds_t(100000) - - set: posix.fd_set - posix.FD_ZERO(&set) - posix.FD_SET(posix.STDIN_FILENO, &set) - - ready := posix.select(1, &set, nil, nil, &tv) - if ready <= 0 { - return .Escape - } - - n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1) - if n2 <= 0 || buf[1] != '[' { - return .Escape - } - - posix.FD_ZERO(&set) - posix.FD_SET(posix.STDIN_FILENO, &set) - tv.tv_sec = 0 - tv.tv_usec = posix.suseconds_t(100000) - - ready = posix.select(1, &set, nil, nil, &tv) - if ready <= 0 { - return .Escape - } - - n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1) - if n3 <= 0 { - return .Escape - } - - switch buf[2] { - case 'A': - return .Up - case 'B': - return .Down - case: - return .Escape - } - case: - return .Unknown - } -} - -MultiSelect_Result :: enum { - Confirm, - Cancel, +Raw_State :: struct { + original: posix.termios, + fd: posix.FD, } MAX_VISIBLE :: 7 @@ -125,7 +36,7 @@ multi_select :: proc( return } - selected = make([dynamic]bool, len(options)) + selected = make([dynamic]bool, 0, len(options)) cursor: int = 0 scroll_offset: int = 0 @@ -199,3 +110,92 @@ render_options :: proc( return end - scroll_offset } +enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) { + state: Raw_State + state.fd = fd + + if posix.tcgetattr(fd, &state.original) != .OK { + return state, false + } + + attr: posix.termios = state.original + attr.c_lflag -= {.ICANON, .ECHO, .ISIG, .IEXTEN} + attr.c_iflag -= {.IXON, .ICRNL, .BRKINT, .INPCK, .ISTRIP} + attr.c_oflag -= {.OPOST} + attr.c_cflag += {.CS8} + attr.c_cc[.VMIN] = 1 + attr.c_cc[.VTIME] = 0 + + if posix.tcsetattr(fd, .TCSAFLUSH, &attr) != .OK { + return state, false + } + + return state, true +} + +disable_raw_mode :: proc(state: ^Raw_State) { + posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original) +} + +read_key :: proc() -> Key { + buf: [3]u8 + + n := posix.read(posix.STDIN_FILENO, &buf[0], 1) + if n <= 0 { + return .Unknown + } + + switch buf[0] { + case ' ': + return .Space + case '\n', '\r': + return .Enter + case 0x03: + return .Escape + case 0x1b: + tv: posix.timeval + tv.tv_sec = 0 + tv.tv_usec = posix.suseconds_t(100000) + + set: posix.fd_set + posix.FD_ZERO(&set) + posix.FD_SET(posix.STDIN_FILENO, &set) + + ready := posix.select(1, &set, nil, nil, &tv) + if ready <= 0 { + return .Escape + } + + n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1) + if n2 <= 0 || buf[1] != '[' { + return .Escape + } + + posix.FD_ZERO(&set) + posix.FD_SET(posix.STDIN_FILENO, &set) + tv.tv_sec = 0 + tv.tv_usec = posix.suseconds_t(100000) + + ready = posix.select(1, &set, nil, nil, &tv) + if ready <= 0 { + return .Escape + } + + n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1) + if n3 <= 0 { + return .Escape + } + + switch buf[2] { + case 'A': + return .Up + case 'B': + return .Down + case: + return .Escape + } + case: + return .Unknown + } +} +