chore: Completed todos.

This commit is contained in:
2026-06-16 09:27:49 -04:00
parent 73a41830d1
commit 397f45d4d0
5 changed files with 127 additions and 111 deletions

View File

@@ -1,12 +1,7 @@
# TODO # TODOs
Note: These todos can wait until all the subcommands have been ported.
## HIGH 1. Encrypt/decrypt the database in memory.
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
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. 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. 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. 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. 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. 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. 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"`. 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)`. 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. 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 ## Double-check AI output
- [ ] cli.odin - [ ] cli.odin

View File

@@ -126,6 +126,7 @@ find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) {
return return
} }
// Caller is responsible for calling delete_config()
new_config :: proc( new_config :: proc(
private_key_paths: []string, private_key_paths: []string,
cfg_path: string = "~/.envr/config.json", cfg_path: string = "~/.envr/config.json",

24
db.odin
View File

@@ -382,18 +382,34 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
cpath := to_cstring(file.Path) cpath := to_cstring(file.Path)
defer delete(cpath) defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) 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)) cremotes := to_cstring(string(remotes_json))
defer delete(cremotes) defer delete(cremotes)
rc = sqlite.bind_text(stmt, 2, cremotes, -1, nil) 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) csha := to_cstring(file.Sha256)
defer delete(csha) defer delete(csha)
rc = sqlite.bind_text(stmt, 3, csha, -1, nil) 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) ccontents := to_cstring(file.contents)
defer delete(ccontents) defer delete(ccontents)
rc = sqlite.bind_text(stmt, 4, ccontents, -1, nil) 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) rc = sqlite.step(stmt)
if rc != sqlite.DONE { if rc != sqlite.DONE {
@@ -418,6 +434,10 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi
cpath := to_cstring(path, allocator) cpath := to_cstring(path, allocator)
defer delete(cpath, allocator) defer delete(cpath, allocator)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) 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) rc = sqlite.step(stmt)
if rc == sqlite.DONE { if rc == sqlite.DONE {
fmt.printf("No file found with path: %s\n", path) 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) cpath := to_cstring(path)
defer delete(cpath) defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil) 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) rc = sqlite.step(stmt)
if rc != sqlite.DONE { if rc != sqlite.DONE {
fmt.printf("Error deleting: %s\n", sqlite.db_errmsg(d.db)) fmt.printf("Error deleting: %s\n", sqlite.db_errmsg(d.db))

View File

@@ -5,6 +5,8 @@ import "core:fmt"
import "core:os" import "core:os"
main :: proc() { main :: proc() {
defer free_all(context.temp_allocator)
cmd, ok := parse_args(os.args, os.to_writer(os.stdout), os.to_writer(os.stderr)) cmd, ok := parse_args(os.args, os.to_writer(os.stdout), os.to_writer(os.stderr))
defer bufio.writer_flush(cmd.out_buf) defer bufio.writer_flush(cmd.out_buf)
if !ok { if !ok {

View File

@@ -3,36 +3,9 @@ package main
import "core:fmt" import "core:fmt"
import "core:sys/posix" import "core:sys/posix"
Raw_State :: struct { MultiSelect_Result :: enum {
original: posix.termios, Confirm,
fd: posix.FD, Cancel,
}
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)
} }
Key :: enum { Key :: enum {
@@ -44,71 +17,9 @@ Key :: enum {
Unknown, Unknown,
} }
read_key :: proc() -> Key { Raw_State :: struct {
buf: [3]u8 original: posix.termios,
fd: posix.FD,
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,
} }
MAX_VISIBLE :: 7 MAX_VISIBLE :: 7
@@ -125,7 +36,7 @@ multi_select :: proc(
return return
} }
selected = make([dynamic]bool, len(options)) selected = make([dynamic]bool, 0, len(options))
cursor: int = 0 cursor: int = 0
scroll_offset: int = 0 scroll_offset: int = 0
@@ -199,3 +110,92 @@ render_options :: proc(
return end - scroll_offset 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
}
}