From 84764d03a6a66fe3dac273749729d04fff843dd2 Mon Sep 17 00:00:00 2001 From: Spencer Brower Date: Thu, 18 Jun 2026 17:42:14 -0400 Subject: [PATCH] refactor: Cleaned up the sync and scan commands. --- cmd_scan.odin | 2 +- cmd_sync.odin | 83 ++++++++--------- config.odin | 30 +++--- config_test.odin | 8 +- db.odin | 180 +++++++++++++++++------------------ db_test.odin | 237 ++++++++++++++++++++++++++++++----------------- table.odin | 13 ++- 7 files changed, 316 insertions(+), 237 deletions(-) diff --git a/cmd_scan.odin b/cmd_scan.odin index 921de8c..b0f03f1 100644 --- a/cmd_scan.odin +++ b/cmd_scan.odin @@ -12,7 +12,7 @@ cmd_scan :: proc(cmd: ^Command) { } defer db_close(&db) - search_dirs := search_paths(db.cfg) + search_dirs := search_paths(db.cfg, context.temp_allocator) if len(search_dirs) == 0 { fmt.wprintln( cmd.err, diff --git a/cmd_sync.odin b/cmd_sync.odin index f388055..c3838b0 100644 --- a/cmd_sync.odin +++ b/cmd_sync.odin @@ -24,63 +24,42 @@ cmd_sync :: proc(cmd: ^Command) { if !list_ok { return } - defer delete(files) - // TODO: Set sane default size - results: [dynamic]SyncEntry - defer delete(results) + results := make([]SyncEntry, len(files), context.temp_allocator) - for &file in files { - old_path: string - old_path, _ = strings.clone(file.Path, context.temp_allocator) - - result, err_msg := db_sync(&db, &file) + for &file, i in files { + result, err := db_sync(&db, &file) status: string - is_dir_updated := .DirUpdated in result - - switch { - case .Error in result: - if len(err_msg) > 0 { - status = err_msg - } else { - status = "error" - } - case .BackedUp in result: - status = "Backed Up" - case .Restored in result: - status = "Restored" - case .DirUpdated in result: + if err != .None { + status = sync_error_message(err) + } else if .BackedUp in result { + status = .DirUpdated in result ? "Moved & Backed Up" : "Backed Up" + } else if .Restored in result { + status = .DirUpdated in result ? "Moved & Restored" : "Restored" + } else if .DirUpdated in result { status = "Moved" - case: + } else { status = "OK" } - if is_dir_updated { - if !db_delete(&db, old_path) { - return - } + // TODO: Handle errors + path_str, _ := strings.clone(file.Path, context.temp_allocator) + status_str, _ := strings.clone(status, context.temp_allocator) + results[i] = SyncEntry { + Path = path_str, + Status = status_str, } - if db_update_required(result) { - if !db_insert(&db, file) { - return - } - } - - path_str, _ := strings.clone(file.Path) - status_str, _ := strings.clone(status) - append(&results, SyncEntry{Path = path_str, Status = status_str}) } if terminal.is_terminal(os.stdout) { headers := []string{"File", "Status"} - table_rows := make([dynamic][]string, 0, len(results)) + // TODO: Use [2]string instead of slice here + table_rows := make([dynamic][]string, 0, len(results), context.temp_allocator) for res in results { - row_slice := make([]string, 2) - row_slice[0] = res.Path - row_slice[1] = res.Status - append(&table_rows, row_slice) + row_slice := [2]string{res.Path, res.Status} + append(&table_rows, row_slice[:]) } render_table(cmd.out, headers, table_rows[:]) @@ -94,3 +73,23 @@ cmd_sync :: proc(cmd: ^Command) { } } +sync_error_message :: proc(e: SyncError) -> string { + switch e { + case .None: + return "" + case .DirMissing: + return "directory missing" + case .MultipleDirs: + return "multiple directories found" + case .GitRootFailed: + return "failed to find git roots" + case .WriteFailed: + return "failed to write file" + case .ReadFailed: + return "failed to read file" + case .DbFailed: + return "failed to update database" + } + return "unknown error" +} + diff --git a/config.odin b/config.odin index 9373d83..238e340 100644 --- a/config.odin +++ b/config.odin @@ -187,32 +187,40 @@ find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) { return } -find_git_roots :: proc(cfg: Config) -> (roots: [dynamic]string, ok: bool) { - paths := search_paths(cfg) +find_git_roots :: proc( + cfg: Config, + allocator := context.temp_allocator, +) -> ( + roots: [dynamic]string, + ok: bool, +) { + 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 return } -search_paths :: proc(cfg: Config) -> (paths: [dynamic]string) { - // TODO: Is this okay? +search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string { // TODO: handle error home, _ := os.user_home_dir(context.temp_allocator) - for include in cfg.ScanConfig.Include { + paths, _ := new_clone(cfg.ScanConfig.Include, allocator) + + for &include in paths { // TODO: Do we need to manually expand ~/ in odin? - expanded, _ := strings.replace(include, "~", home, 1) + expanded, _ := strings.replace(include, "~", home, 1, allocator) if filepath.is_abs(expanded) { - append(&paths, expanded) + include = expanded } else { - defer delete(expanded) - resolved, err := filepath.abs(expanded) + resolved, err := filepath.abs(expanded, allocator) if err == nil { - append(&paths, resolved) + include = resolved } } } - return + return paths^ } envr_dir :: proc(config_path: string) -> string { diff --git a/config_test.odin b/config_test.odin index e3df0fe..ef002f4 100644 --- a/config_test.odin +++ b/config_test.odin @@ -187,14 +187,10 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) { cfg := Config { ScanConfig = ScanConfig{Include = make([dynamic]string, 0, 1)}, } - defer delete(cfg.ScanConfig.Include) append(&cfg.ScanConfig.Include, "~") + defer delete(cfg.ScanConfig.Include) - paths := search_paths(cfg) - defer delete(paths) - for path in paths { - defer delete(path) - } + paths := search_paths(cfg, context.temp_allocator) testing.expect(t, len(paths) == 1, "should have 1 path") if len(paths) > 0 { diff --git a/db.odin b/db.odin index fd3579f..1c6d68c 100644 --- a/db.odin +++ b/db.odin @@ -13,15 +13,23 @@ import "core:strings" import "sqlite" SyncFlagEnum :: enum { - Noop, DirUpdated, Restored, BackedUp, - Error, } SyncFlag :: bit_set[SyncFlagEnum] +SyncError :: enum { + None, + DirMissing, + MultipleDirs, + GitRootFailed, + WriteFailed, + ReadFailed, + DbFailed, +} + Db :: struct { // Pointer to the sqlite db db: ^rawptr, @@ -387,7 +395,8 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) { dir := filepath.dir(abs_path) - remotes := get_git_remotes(dir) + // TODO: Should we use the db allocator here? + remotes := get_git_remotes(dir, context.allocator) data, read_err := os.read_entire_file_from_path(abs_path, context.allocator) defer delete(data) @@ -410,109 +419,106 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) { true } -// If SyncFlag is .BackedUp, Caller is responsible for calling delete on f.contents and f.Sha256 -db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) { +// Reconciles `f` with the filesystem and persists changes to the database. +db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) { + allocator := db_allocator(d) result: SyncFlag = {} + old_path := f.Path if !os.exists(f.Dir) { - assert(d != nil) - moved_dirs, dirs_ok := find_moved_dirs(d, f) - if !dirs_ok { - return {.Error}, "failed to find moved dirs" - } - - switch len(moved_dirs) { - case 0: - return {.Error}, "directory missing" - case 1: - update_dir(f, moved_dirs[0]) - result = {.DirUpdated} - case: - return {.Error}, "multiple directories found" + moved, err := try_move_dir(d, f, allocator) + if !moved { + return {}, err } + result += {.DirUpdated} } if !os.exists(f.Path) { write_err := os.write_entire_file(f.Path, f.contents) if write_err != nil { - msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)}) - return {.Error}, msg + fmt.eprintf("db_sync: failed to write %s: %v\n", f.Path, write_err) + return result, .WriteFailed } - return result + {.Restored}, "" + if !db_persist(d, f, old_path) { + return result, .DbFailed + } + return result + {.Restored}, .None } - // TODO: Use temp allocator? - data, read_err := os.read_entire_file_from_path(f.Path, context.allocator) - defer delete(data) + data, read_err := os.read_entire_file_from_path(f.Path, allocator) if read_err != nil { - msg, _ := strings.concatenate( - {"failed to read file for SHA comparison: ", fmt.tprintf("%v", read_err)}, - ) - return {.Error}, msg + fmt.eprintf("db_sync: failed to read %s: %v\n", f.Path, read_err) + return result, .ReadFailed } - digest := hash.hash_bytes(hash.Algorithm.SHA256, data) - // TODO: Handle error - hex_bytes, _ := hex.encode(digest) + digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator) + hex_bytes, hex_err := hex.encode(digest, allocator) + if hex_err != nil { + fmt.eprintf("db_sync: failed to encode hash for %s: %v\n", f.Path, hex_err) + return result, .ReadFailed + } current_sha := string(hex_bytes) if current_sha == f.Sha256 { - return result, "" - } - - if env_file_backup(f) { - return result + {.BackedUp}, "" - } else { - return {.Error}, "failed to backup file" - } - -} - -find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) { - roots, roots_ok := find_git_roots(d.cfg) - if !roots_ok { - return {}, false - } - - moved: [dynamic]string - for root in roots { - remotes := get_git_remotes(root) - if shares_remote(f, remotes[:]) { - cloned, _ := strings.clone(root) - append(&moved, cloned) + if !db_persist(d, f, old_path) { + return result, .DbFailed } - } - return moved, true -} - -update_dir :: proc(f: ^EnvFile, new_dir: string) { - f.Dir = new_dir - base := filepath.base(f.Path) - new_path, _ := filepath.join({new_dir, base}) - f.Path = new_path - f.Remotes = get_git_remotes(new_dir) -} - -// Loads the contents of the the file at f.Path into f.contents -// -// Caller is responsible for calling delete on f.contents and f.Sha256 -env_file_backup :: proc(f: ^EnvFile) -> bool { - data, read_err := os.read_entire_file_from_path(f.Path, context.allocator) - if read_err != nil { - fmt.printf("Error reading file %s: %v\n", f.Path, read_err) - return false + return result, .None } f.contents = string(data) - digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator) - hex_bytes, alloc_err := hex.encode(digest) - if alloc_err != nil { - fmt.printf("Error generating hash for file %s: %v\n", f.Path, alloc_err) - return false + f.Sha256 = current_sha + if !db_persist(d, f, old_path) { + return result, .DbFailed + } + return result + {.BackedUp}, .None +} + +db_persist :: proc(d: ^Db, f: ^EnvFile, old_path: string) -> bool { + if f.Path != old_path { + if !db_delete(d, old_path) { + return false + } + } + return db_insert(d, f^) +} + +try_move_dir :: proc(d: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) { + roots, ok := find_git_roots(d.cfg) + if !ok { + return false, .GitRootFailed + } + defer { + for root in roots { + delete(root) + } + delete(roots) + } + + match_count := 0 + matched_dir: string + for root in roots { + remotes := get_git_remotes(root, context.temp_allocator) + if shares_remote(f, remotes[:]) { + match_count += 1 + matched_dir = root + } + } + + switch match_count { + case 0: + return false, .DirMissing + case 1: + f.Dir, _ = strings.clone(matched_dir, allocator) + base := filepath.base(f.Path) + new_path, _ := filepath.join({f.Dir, base}, allocator) + f.Path = new_path + f.Remotes = get_git_remotes(f.Dir, allocator) + return true, .None + case: + return false, .MultipleDirs } - f.Sha256 = string(hex_bytes) - return true } shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool { @@ -526,7 +532,7 @@ shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool { return false } -get_git_remotes :: proc(dir: string) -> [dynamic]string { +get_git_remotes :: proc(dir: string, allocator: mem.Allocator) -> [dynamic]string { config_path, _ := filepath.join({dir, ".git", "config"}, context.temp_allocator) // TODO: Handle error m, _, ok := ini.load_map_from_path(config_path, context.temp_allocator) @@ -534,7 +540,7 @@ get_git_remotes :: proc(dir: string) -> [dynamic]string { return nil } - remotes := make([dynamic]string, 0, 1) + remotes := make([dynamic]string, 0, 1, allocator) for section_name, section in m { if strings.has_prefix(section_name, "remote ") { @@ -544,7 +550,7 @@ get_git_remotes :: proc(dir: string) -> [dynamic]string { if r == url {found = true; break} } if !found { - cloned, _ := strings.clone(url) + cloned, _ := strings.clone(url, allocator) append(&remotes, cloned) } } @@ -554,10 +560,6 @@ get_git_remotes :: proc(dir: string) -> [dynamic]string { return remotes } -db_update_required :: proc(status: SyncFlag) -> bool { - return .BackedUp in status || .DirUpdated in status -} - to_cstring :: proc { string_to_cstring, strings.to_cstring, diff --git a/db_test.odin b/db_test.odin index 27b32c5..0671e3e 100644 --- a/db_test.odin +++ b/db_test.odin @@ -1,5 +1,7 @@ package main +import "core:crypto/hash" +import "core:encoding/hex" import "core:fmt" import "core:os" import "core:path/filepath" @@ -14,7 +16,7 @@ make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) Dir = "", Sha256 = sha, contents = contents, - Remotes = make([dynamic]string, 0, len(remotes)), + Remotes = make([dynamic]string, 0, len(remotes), context.temp_allocator), } for r in remotes { append(&f.Remotes, r) @@ -201,37 +203,6 @@ test_db_serialize :: proc(t: ^testing.T) { testing.expect(t, sz > 0, "serialized size should be > 0") } -@(test) -test_db_update_required_noop :: proc(t: ^testing.T) { - testing.expect(t, !db_update_required({}), "Noop should not require update") -} - -@(test) -test_db_update_required_backed_up :: proc(t: ^testing.T) { - testing.expect(t, db_update_required({.BackedUp}), "BackedUp should require update") -} - -@(test) -test_db_update_required_dir_updated :: proc(t: ^testing.T) { - testing.expect(t, db_update_required({.DirUpdated}), "DirUpdated should require update") -} - -@(test) -test_db_update_required_restored :: proc(t: ^testing.T) { - testing.expect(t, !db_update_required({.Restored}), "Restored alone should not require update") -} - -@(test) -test_db_update_required_error :: proc(t: ^testing.T) { - testing.expect(t, !db_update_required({.Error}), "Error alone should not require update") -} - -@(test) -test_db_update_required_combined :: proc(t: ^testing.T) { - combined := SyncFlag{.DirUpdated, .Restored} - testing.expect(t, db_update_required(combined), "DirUpdated|Restored should require update") -} - @(test) test_shares_remote_overlap :: proc(t: ^testing.T) { f := EnvFile { @@ -307,8 +278,7 @@ test_get_git_remotes_single :: proc(t: ^testing.T) { err := os.write_entire_file(config_path, transmute([]u8)config_content) testing.expect(t, err == nil, "should write .git/config") - remotes := get_git_remotes(base) - defer delete_remotes(remotes) + remotes := get_git_remotes(base, context.temp_allocator) testing.expect(t, len(remotes) == 1, "should find 1 remote") if len(remotes) != 1 do return @@ -329,8 +299,7 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) { err := os.write_entire_file(config_path, transmute([]u8)config_content) testing.expect(t, err == nil, "should write .git/config") - remotes := get_git_remotes(base) - defer delete_remotes(remotes) + remotes := get_git_remotes(base, context.temp_allocator) testing.expect(t, len(remotes) == 2, "should find 2 remotes") } @@ -341,8 +310,7 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) { os.mkdir_all(base) defer os.remove_all(base) - remotes := get_git_remotes(base) - defer delete_remotes(remotes) + remotes := get_git_remotes(base, context.temp_allocator) testing.expect(t, len(remotes) == 0, "should return empty when no .git/config") } @@ -361,8 +329,7 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) { err := os.write_entire_file(config_path, transmute([]u8)config_content) testing.expect(t, err == nil, "should write .git/config") - remotes := get_git_remotes(base) - defer delete_remotes(remotes) + remotes := get_git_remotes(base, context.temp_allocator) testing.expect(t, len(remotes) == 0, "should return empty when no remote sections") } @@ -396,49 +363,6 @@ test_new_env_file_missing :: proc(t: ^testing.T) { testing.expect(t, !ok, "missing file should return false") } -@(test) -test_env_file_backup :: proc(t: ^testing.T) { - base := fmt.tprintf("/tmp/envr-test-backup-%d", os.get_pid()) - os.mkdir_all(base) - defer os.remove_all(base) - - env_path := fmt.tprintf("%s/.env", base) - err := os.write_entire_file(env_path, "KEY=12345\n") - testing.expect(t, err == nil, ".env file should exist") - - f := EnvFile { - Path = env_path, - } - defer delete(f.contents) - defer delete(f.Sha256) - testing.expect(t, env_file_backup(&f), "backup should succeed") - testing.expect_value(t, f.contents, "KEY=12345\n") - testing.expect_value(t, len(f.Sha256), 64) -} - -@(test) -test_env_file_backup_missing :: proc(t: ^testing.T) { - f := EnvFile { - Path = "/tmp/envr-nonexistent-backup/.env", - } - testing.expect(t, !env_file_backup(&f), "missing file should return false") -} - -@(test) -test_update_dir :: proc(t: ^testing.T) { - f := EnvFile { - Path = "/old/project/.env", - Dir = "/old/project", - Remotes = make([dynamic]string, 0, context.temp_allocator), - } - defer delete_envfile(&f) - - update_dir(&f, "/new/location") - - testing.expect_value(t, f.Dir, "/new/location") - testing.expect_value(t, f.Path, "/new/location/.env") -} - @(test) test_closing_db_has_no_leaks :: proc(t: ^testing.T) { base := fmt.tprintf("/tmp/envr-test-leak-%d", os.get_pid()) @@ -495,3 +419,150 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) { db_close(&db2) } +@(test) +test_db_sync_noop :: proc(t: ^testing.T) { + base := fmt.tprintf("/tmp/envr-test-sync-noop-%d", os.get_pid()) + os.mkdir_all(base) + defer os.remove_all(base) + + env_path := fmt.tprintf("%s/.env", base) + content := "KEY=value\n" + write_err := os.write_entire_file(env_path, transmute([]u8)content) + testing.expect(t, write_err == nil, "should write .env file") + + digest := hash.hash_bytes( + hash.Algorithm.SHA256, + transmute([]u8)content, + context.temp_allocator, + ) + hex_bytes, _ := hex.encode(digest, context.temp_allocator) + sha := string(hex_bytes) + + d, ok := db_init() + testing.expect(t, ok, "failed to create test db") + defer db_close(&d) + + f := make_test_env_file(env_path, sha, content) + f.Dir = base + db_insert(&d, f) + + result, sync_err := db_sync(&d, &f) + testing.expect(t, sync_err == .None, "sync should not error") + testing.expect(t, result == {}, "should be noop") +} + +@(test) +test_db_sync_backed_up :: proc(t: ^testing.T) { + base := fmt.tprintf("/tmp/envr-test-sync-backup-%d", os.get_pid()) + os.mkdir_all(base) + defer os.remove_all(base) + + env_path := fmt.tprintf("%s/.env", base) + changed_content := "KEY=changed\n" + write_err := os.write_entire_file(env_path, transmute([]u8)changed_content) + testing.expect(t, write_err == nil, "should write .env file") + + d, ok := db_init() + testing.expect(t, ok, "failed to create test db") + defer db_close(&d) + + f := make_test_env_file(env_path, "old_sha", "KEY=original") + f.Dir = base + db_insert(&d, f) + + result, sync_err := db_sync(&d, &f) + testing.expect(t, sync_err == .None, "sync should not error") + testing.expect(t, .BackedUp in result, "should be backed up") +} + +@(test) +test_db_sync_restored :: proc(t: ^testing.T) { + base := fmt.tprintf("/tmp/envr-test-sync-restore-%d", os.get_pid()) + os.mkdir_all(base) + defer os.remove_all(base) + + env_path := fmt.tprintf("%s/.env", base) + + d, ok := db_init() + testing.expect(t, ok, "failed to create test db") + defer db_close(&d) + + f := make_test_env_file(env_path, "some_sha", "SECRET=value") + f.Dir = base + defer delete(f.Remotes) + db_insert(&d, f) + + result, err := db_sync(&d, &f) + testing.expect(t, err == .None, "sync should not error") + testing.expect(t, .Restored in result, "should be restored") + + data, read_err := os.read_entire_file_from_path(env_path, context.temp_allocator) + testing.expect(t, read_err == nil, "file should exist after restore") + if read_err == nil { + testing.expect_value(t, string(data), "SECRET=value") + } +} + +@(test) +test_db_sync_dir_missing :: proc(t: ^testing.T) { + d, ok := db_init() + testing.expect(t, ok, "failed to create test db") + defer db_close(&d) + + f := make_test_env_file("/nonexistent/path/.env", "sha", "KEY=val") + db_insert(&d, f) + + result, err := db_sync(&d, &f) + testing.expect(t, err == .DirMissing, "should return DirMissing error") +} + +@(test) +test_db_sync_moved :: proc(t: ^testing.T) { + base := fmt.tprintf("/tmp/envr-test-sync-moved-%d", os.get_pid()) + search_root := fmt.tprintf("%s/search", base) + repo_dir := fmt.tprintf("%s/myproject", search_root) + git_dir := fmt.tprintf("%s/.git", repo_dir) + defer os.remove_all(base) + + os.mkdir_all(git_dir) + + config_content := "[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n" + config_path := fmt.tprintf("%s/config", git_dir) + write_err := os.write_entire_file(config_path, transmute([]u8)config_content) + testing.expect(t, write_err == nil, "should write .git/config") + + d, ok := db_init() + testing.expect(t, ok, "failed to create test db") + defer db_close(&d) + + d.cfg.ScanConfig.Include = make([dynamic]string, 0, 1, context.temp_allocator) + append(&d.cfg.ScanConfig.Include, search_root) + + f := make_test_env_file( + "/old/nonexistent/path/.env", + "some_sha", + "SECRET=value", + []string{"git@github.com:user/repo.git"}, + ) + testing.expect(t, db_insert(&d, f), "insert should succeed") + + result, err := db_sync(&d, &f) + testing.expect(t, err == .None, "sync should not error") + if err != .None do return + testing.expect(t, .DirUpdated in result, "should have DirUpdated flag") + testing.expect(t, .Restored in result, "should have Restored flag") + + expected_path := fmt.tprintf("%s/.env", repo_dir) + testing.expect_value(t, f.Path, expected_path) + testing.expect_value(t, f.Dir, repo_dir) + + _, old_exists := db_fetch(&d, "/old/nonexistent/path/.env") + testing.expect(t, !old_exists, "old path should be deleted from db") + + new_fetched, new_ok := db_fetch(&d, expected_path) + testing.expect(t, new_ok, "new path should exist in db") + if new_ok { + testing.expect_value(t, new_fetched.contents, "SECRET=value") + } +} + diff --git a/table.odin b/table.odin index 0350ee0..d661618 100644 --- a/table.odin +++ b/table.odin @@ -6,7 +6,7 @@ import "core:io" import "core:strings" render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) { - col_widths := make([dynamic]int, 0, len(headers)) + col_widths := make([dynamic]int, 0, len(headers), context.temp_allocator) for i in 0 ..< len(headers) { append(&col_widths, strings.rune_count(headers[i])) } @@ -20,11 +20,14 @@ render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) { } b: strings.Builder - strings.builder_init(&b) - defer strings.builder_destroy(&b) - defer delete(col_widths) + strings.builder_init(&b, context.temp_allocator) - hline :: proc(w: io.Writer, b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) { + hline :: proc( + w: io.Writer, + b: ^strings.Builder, + left, mid, right: string, + widths: [dynamic]int, + ) { strings.write_string(b, left) for i in 0 ..< len(widths) { for _ in 0 ..< widths[i] + 2 {