4 Commits

10 changed files with 161 additions and 102 deletions
+3 -9
View File
@@ -16,15 +16,9 @@
8. Add tests for untested commands. 8. Add tests for untested commands.
9. Update `read_wire_string` to use a slice. 9. Consider getting rid of color global.
10. Pass allocator to findr? 10. `write_flags_table` should never return false.
11. Consider getting rid of color global.
12. `write_flags_table` should never return false.
13. Add a text filter to the multi_select.
## Double-check AI output ## Double-check AI output
@@ -54,7 +48,7 @@
- [ ] db_test.odin - [ ] db_test.odin
- [ ] flags.odin - [ ] flags.odin
- [x] main.odin - [x] main.odin
- [x] prompt.odin - [ ] prompt.odin
- [x] scan.odin - [x] scan.odin
- [ ] scan_test.odin - [ ] scan_test.odin
- [ ] sodium.odin - [ ] sodium.odin
+4 -3
View File
@@ -201,8 +201,7 @@ find_git_roots :: proc(
ok: bool, ok: bool,
) { ) {
paths := search_paths(cfg, allocator) paths := search_paths(cfg, allocator)
// TODO: Pass allocator to findr findr.find_repos(paths[:], &roots, os.get_processor_core_count(), allocator)
findr.find_repos(paths[:], &roots, os.get_processor_core_count())
ok = true ok = true
return return
} }
@@ -214,6 +213,7 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
} }
paths := new_clone(cfg.scan_config.include, allocator) paths := new_clone(cfg.scan_config.include, allocator)
defer free(paths, allocator)
for &include in paths { for &include in paths {
expanded, _ := strings.replace(include, "~", home, 1, allocator) expanded, _ := strings.replace(include, "~", home, 1, allocator)
@@ -227,7 +227,8 @@ search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]st
} }
} }
} }
return paths^ result := paths^
return result
} }
envr_dir :: proc(config_path: string) -> string { envr_dir :: proc(config_path: string) -> string {
+12
View File
@@ -2,6 +2,7 @@
package main package main
import "core:fmt" import "core:fmt"
import "core:mem"
import "core:os" import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
@@ -194,3 +195,14 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) {
} }
} }
@(test)
test_search_paths_no_leak :: proc(t: ^testing.T) {
cfg := Config {
scan_config = ScanConfig{include = make([dynamic]string, 0, 1)},
}
defer delete(cfg.scan_config.include)
append(&cfg.scan_config.include, "/tmp")
_ = search_paths(cfg, context.allocator)
}
+1 -1
View File
@@ -491,7 +491,7 @@ db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
} }
try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) { try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
roots, ok := find_git_roots(db.cfg) roots, ok := find_git_roots(db.cfg, context.allocator)
if !ok { if !ok {
return false, .GitRootFailed return false, .GitRootFailed
} }
+9 -1
View File
@@ -16,7 +16,14 @@ RepoPool :: struct {
threads: []^thread.Thread, threads: []^thread.Thread,
} }
find_repos :: proc(roots: []string, results: ^[dynamic]string, thread_count: int) { find_repos :: proc(
roots: []string,
results: ^[dynamic]string,
thread_count: int,
allocator := context.allocator,
) {
// TODO: This may be a code smell
context.allocator = allocator
if len(roots) == 0 do return if len(roots) == 0 do return
pool := new(RepoPool) pool := new(RepoPool)
@@ -126,3 +133,4 @@ process_repo_dir :: proc(pool: ^RepoPool, dir_path: string) {
} }
} }
} }
+9 -1
View File
@@ -78,7 +78,15 @@ collect_worker :: proc(t: ^thread.Thread) {
} }
} }
walk :: proc(roots: []string, results: ^[dynamic]string, opts: WalkOptions, thread_count: int) { walk :: proc(
roots: []string,
results: ^[dynamic]string,
opts: WalkOptions,
thread_count: int,
allocator := context.allocator,
) {
// TODO: This may be a code smell
context.allocator = allocator
if len(roots) == 0 do return if len(roots) == 0 do return
ch, _ := chan.create(chan.Chan([]u8), max(2 * thread_count, 2), context.allocator) ch, _ := chan.create(chan.Chan([]u8), max(2 * thread_count, 2), context.allocator)
+76 -26
View File
@@ -1,6 +1,7 @@
package main package main
import "core:fmt" import "core:fmt"
import "core:strings"
import "core:sys/posix" import "core:sys/posix"
import "core:terminal/ansi" import "core:terminal/ansi"
@@ -15,6 +16,8 @@ Key :: enum {
Space, Space,
Enter, Enter,
Escape, Escape,
Backspace,
Char,
Unknown, Unknown,
} }
@@ -38,11 +41,17 @@ multi_select :: proc(
} }
selected = make([dynamic]bool, len(options)) selected = make([dynamic]bool, len(options))
filter: [dynamic]u8
defer delete(filter)
filtered: [dynamic]int
defer delete(filtered)
rebuild_filtered(options, filter[:], &filtered)
cursor: int = 0 cursor: int = 0
scroll_offset: int = 0 scroll_offset: int = 0
fmt.printf(ansi.CSI + ansi.DECTCEM_HIDE) fmt.printf(ansi.CSI + ansi.DECTCEM_HIDE)
visible := render_options(prompt, options, selected[:], cursor, scroll_offset) visible := render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
raw, ok := enable_raw_mode(posix.STDIN_FILENO) raw, ok := enable_raw_mode(posix.STDIN_FILENO)
if !ok { if !ok {
@@ -52,19 +61,31 @@ multi_select :: proc(
defer disable_raw_mode(&raw) defer disable_raw_mode(&raw)
for { for {
key := read_key() key, ch := read_key()
switch key { switch key {
case .Char:
append(&filter, u8(ch))
rebuild_filtered(options, filter[:], &filtered)
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
case .Backspace:
if len(filter) > 0 {
pop(&filter)
rebuild_filtered(options, filter[:], &filtered)
cursor = clamp(cursor, 0, max(0, len(filtered) - 1))
}
case .Up: case .Up:
if cursor > 0 { if cursor > 0 {
cursor -= 1 cursor -= 1
} }
case .Down: case .Down:
if cursor < len(options) - 1 { if cursor < len(filtered) - 1 {
cursor += 1 cursor += 1
} }
case .Space: case .Space:
selected[cursor] = !selected[cursor] if len(filtered) > 0 {
selected[filtered[cursor]] = !selected[filtered[cursor]]
}
case .Enter: case .Enter:
fmt.printf( fmt.printf(
ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW, ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.ED + ansi.CSI + ansi.DECTCEM_SHOW,
@@ -82,9 +103,21 @@ multi_select :: proc(
case .Unknown: case .Unknown:
} }
scroll_offset = max(0, min(cursor - MAX_VISIBLE / 2, len(options) - MAX_VISIBLE)) scroll_offset = max(0, min(cursor - MAX_VISIBLE / 2, len(filtered) - MAX_VISIBLE))
fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.RESET + ansi.ED, visible + 1) fmt.printf(ansi.CSI + "%d" + ansi.CUU + ansi.CSI + ansi.RESET + ansi.ED, visible + 1)
visible = render_options(prompt, options, selected[:], cursor, scroll_offset) visible = render_options(prompt, options, selected[:], filtered[:], string(filter[:]), cursor, scroll_offset)
}
}
rebuild_filtered :: proc(options: []string, filter: []u8, filtered: ^[dynamic]int) {
clear(filtered)
filter_str := string(filter)
filter_lower := strings.to_lower(filter_str, context.temp_allocator)
for opt, i in options {
opt_lower := strings.to_lower(opt, context.temp_allocator)
if strings.contains(opt_lower, filter_lower) {
append(filtered, i)
}
} }
} }
@@ -92,22 +125,34 @@ render_options :: proc(
prompt: string, prompt: string,
options: []string, options: []string,
selected: []bool, selected: []bool,
filtered: []int,
filter_text: string,
cursor: int, cursor: int,
scroll_offset: int, scroll_offset: int,
) -> int { ) -> int {
fmt.printf( fmt.printf(
"%s (↑/↓ move, space select, enter confirm)\r\n", "%s (type to filter, ↑/↓ move, space select, enter confirm)\r\n",
colorize(.Option_Label, prompt), colorize(.Option_Label, prompt),
) )
fmt.printf("filter: %s\r\n", filter_text)
line_count := 1
if len(filtered) == 0 {
fmt.printf(" No matches\r\n")
return line_count + 1
}
end := scroll_offset + MAX_VISIBLE end := scroll_offset + MAX_VISIBLE
if end > len(options) { if end > len(filtered) {
end = len(options) end = len(filtered)
} }
for i in scroll_offset ..< end { for i in scroll_offset ..< end {
original := filtered[i]
checkbox := " " checkbox := " "
if selected[i] { if selected[original] {
checkbox = "x" checkbox = "x"
} }
if i == cursor { if i == cursor {
@@ -115,14 +160,14 @@ render_options :: proc(
"%s [%s] %s\r\n", "%s [%s] %s\r\n",
colorize(.Caret, ">"), colorize(.Caret, ">"),
colorize(.Sucess, checkbox), colorize(.Sucess, checkbox),
options[i], options[original],
) )
} else { } else {
fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[i]) fmt.printf(" [%s] %s\r\n", colorize(.Sucess, checkbox), options[original])
} }
} }
return end - scroll_offset return line_count + (end - scroll_offset)
} }
enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) { enable_raw_mode :: proc(fd: posix.FD) -> (Raw_State, bool) {
@@ -152,21 +197,23 @@ disable_raw_mode :: proc(state: ^Raw_State) {
posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original) posix.tcsetattr(state.fd, .TCSAFLUSH, &state.original)
} }
read_key :: proc() -> Key { read_key :: proc() -> (key: Key, ch: rune) {
buf: [3]u8 buf: [3]u8
n := posix.read(posix.STDIN_FILENO, &buf[0], 1) n := posix.read(posix.STDIN_FILENO, &buf[0], 1)
if n <= 0 { if n <= 0 {
return .Unknown return .Unknown, 0
} }
switch buf[0] { switch buf[0] {
case ' ': case ' ':
return .Space return .Space, 0
case '\n', '\r': case '\n', '\r':
return .Enter return .Enter, 0
case 0x03: case 0x03:
return .Escape return .Escape, 0
case 0x08, 0x7F:
return .Backspace, 0
case 0x1b: case 0x1b:
tv: posix.timeval tv: posix.timeval
tv.tv_sec = 0 tv.tv_sec = 0
@@ -178,12 +225,12 @@ read_key :: proc() -> Key {
ready := posix.select(1, &set, nil, nil, &tv) ready := posix.select(1, &set, nil, nil, &tv)
if ready <= 0 { if ready <= 0 {
return .Escape return .Escape, 0
} }
n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1) n2 := posix.read(posix.STDIN_FILENO, &buf[1], 1)
if n2 <= 0 || buf[1] != '[' { if n2 <= 0 || buf[1] != '[' {
return .Escape return .Escape, 0
} }
posix.FD_ZERO(&set) posix.FD_ZERO(&set)
@@ -193,24 +240,27 @@ read_key :: proc() -> Key {
ready = posix.select(1, &set, nil, nil, &tv) ready = posix.select(1, &set, nil, nil, &tv)
if ready <= 0 { if ready <= 0 {
return .Escape return .Escape, 0
} }
n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1) n3 := posix.read(posix.STDIN_FILENO, &buf[2], 1)
if n3 <= 0 { if n3 <= 0 {
return .Escape return .Escape, 0
} }
switch buf[2] { switch buf[2] {
case 'A': case 'A':
return .Up return .Up, 0
case 'B': case 'B':
return .Down return .Down, 0
case: case:
return .Escape return .Escape, 0
} }
case: case:
return .Unknown if buf[0] >= 0x20 && buf[0] <= 0x7E {
return .Char, rune(buf[0])
}
return .Unknown, 0
} }
} }
+40 -55
View File
@@ -35,18 +35,18 @@ parse_ssh_public_key :: proc(pub_path: string) -> (pub: [32]u8, ok: bool) {
return return
} }
offset := 0 buf := decoded
key_type, type_ok := read_wire_string(decoded, &offset) key_type, type_ok := read_wire_string(&buf)
if !type_ok || key_type != SSH_ED25519 { if !type_ok || string(key_type) != SSH_ED25519 {
return return
} }
pk_data, pk_ok := read_wire_string(decoded, &offset) pk_data, pk_ok := read_wire_string(&buf)
if !pk_ok || len(pk_data) != 32 { if !pk_ok || len(pk_data) != 32 {
return return
} }
mem.copy_non_overlapping(&pub[0], raw_data(pk_data), 32) mem.copy_non_overlapping(&pub[0], &pk_data[0], 32)
ok = true ok = true
return return
@@ -91,81 +91,63 @@ parse_ssh_private_key :: proc(priv_path: string) -> (kp: Ed25519Keypair, ok: boo
return return
} }
offset := len(magic) buf := decoded[len(magic):]
ciphername, cipher_ok := read_wire_string(decoded, &offset) ciphername, cipher_ok := read_wire_string(&buf)
if !cipher_ok || ciphername != "none" { if !cipher_ok || string(ciphername) != "none" {
return return
} }
kdfname, kdf_ok := read_wire_string(decoded, &offset) kdfname, kdf_ok := read_wire_string(&buf)
if !kdf_ok || kdfname != "none" { if !kdf_ok || string(kdfname) != "none" {
return return
} }
_, opts_ok := read_wire_string(decoded, &offset) _, opts_ok := read_wire_string(&buf)
if !opts_ok { if !opts_ok {
return return
} }
if offset + 4 > len(decoded) { num_keys, nkeys_ok := read_wire_u32(&buf)
if !nkeys_ok || num_keys != 1 {
return return
} }
num_keys := endian.get_u32(decoded[offset:offset + 4], .Big) or_return _, pub_blob_ok := read_wire_string(&buf)
offset += 4
if num_keys != 1 {
return
}
_, pub_blob_ok := read_wire_string(decoded, &offset)
if !pub_blob_ok { if !pub_blob_ok {
return return
} }
priv_blob, priv_blob_ok := read_wire_string(decoded, &offset) priv_blob, priv_blob_ok := read_wire_string(&buf)
if !priv_blob_ok { if !priv_blob_ok {
return return
} }
inner_offset := 0 inner := priv_blob
if inner_offset + 8 > len(priv_blob) {
check1, c1_ok := read_wire_u32(&inner)
check2, c2_ok := read_wire_u32(&inner)
if !c1_ok || !c2_ok || check1 != check2 {
return return
} }
check1 := endian.get_u32( priv_type, type_ok := read_wire_string(&inner)
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4], if !type_ok || string(priv_type) != SSH_ED25519 {
.Big,
) or_return
inner_offset += 4
check2 := endian.get_u32(
transmute([]u8)(priv_blob)[inner_offset:inner_offset + 4],
.Big,
) or_return
inner_offset += 4
if check1 != check2 {
return return
} }
priv_type, type_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset) pub_wire, pub_ok := read_wire_string(&inner)
if !type_ok || priv_type != SSH_ED25519 {
return
}
pub_wire, pub_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset)
if !pub_ok || len(pub_wire) != 32 { if !pub_ok || len(pub_wire) != 32 {
return return
} }
mem.copy_non_overlapping(&kp.Public[0], raw_data(pub_wire), 32) mem.copy_non_overlapping(&kp.Public[0], &pub_wire[0], 32)
priv_wire, priv_ok := read_wire_string(transmute([]u8)priv_blob, &inner_offset) priv_wire, priv_ok := read_wire_string(&inner)
if !priv_ok || len(priv_wire) != 64 { if !priv_ok || len(priv_wire) != 64 {
return return
} }
mem.copy_non_overlapping(&kp.Private[0], raw_data(priv_wire), 32) mem.copy_non_overlapping(&kp.Private[0], &priv_wire[0], 32)
ok = true ok = true
return return
@@ -182,19 +164,22 @@ is_ed25519_key :: proc(
return ok, nil return ok, nil
} }
read_wire_string :: proc(data: []u8, offset: ^int) -> (s: string, ok: bool) { read_wire_string :: proc(data: ^[]u8) -> (s: []u8, ok: bool) {
if offset^ + 4 > len(data) { if len(data^) < 4 do return
return length := endian.get_u32(data^[:4], .Big) or_return
} data^ = data^[4:]
length := endian.get_u32(data[offset^:offset^ + 4], .Big) or_return
offset^ += 4
if offset^ + int(length) > len(data) { if len(data^) < int(length) do return
return s = data^[:int(length)]
} data^ = data^[int(length):]
ok = true
return
}
s = string(data[offset^:offset^ + int(length)]) read_wire_u32 :: proc(data: ^[]u8) -> (v: u32, ok: bool) {
offset^ += int(length) if len(data^) < 4 do return
v = endian.get_u32(data^[:4], .Big) or_return
data^ = data^[4:]
ok = true ok = true
return return
} }
+6 -6
View File
@@ -52,15 +52,15 @@ test_private_key_pub_matches_public_key :: proc(t: ^testing.T) {
@(test) @(test)
test_read_wire_string :: proc(t: ^testing.T) { test_read_wire_string :: proc(t: ^testing.T) {
data := []u8{0, 0, 0, 5, u8('h'), u8('e'), u8('l'), u8('l'), u8('o'), 0, 0, 0, 0} data := []u8{0, 0, 0, 5, u8('h'), u8('e'), u8('l'), u8('l'), u8('o'), 0, 0, 0, 0}
offset := 0
s, ok := read_wire_string(data, &offset) buf := data
s, ok := read_wire_string(&buf)
testing.expect(t, ok, "expected read_wire_string to succeed") testing.expect(t, ok, "expected read_wire_string to succeed")
testing.expect_value(t, s, "hello") testing.expect_value(t, string(s), "hello")
testing.expect_value(t, offset, 9) testing.expect_value(t, len(data) - len(buf), 9)
s2, ok2 := read_wire_string(data, &offset) s2, ok2 := read_wire_string(&buf)
testing.expect(t, ok2, "expected second read to succeed") testing.expect(t, ok2, "expected second read to succeed")
testing.expect_value(t, s2, "") testing.expect_value(t, len(s2), 0)
} }
+1
View File
@@ -35,6 +35,7 @@ test_ansi_aware_width_multiple_escape_sequences :: proc(t: ^testing.T) {
colorize(.Heading, "b", disable = false), colorize(.Heading, "b", disable = false),
colorize(.Heading, "c", disable = false), colorize(.Heading, "c", disable = false),
}, },
context.temp_allocator,
) )
testing.expect_value(t, ansi_aware_width(colored), 3) testing.expect_value(t, ansi_aware_width(colored), 3)
} }