╭ changed ────────────╮ │src/ui/Home/Repos.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │ 9 const inp = xitui.input; │ │10 const Grid = xitui.grid.Grid; │ │11 const Focus = xitui.focus.Focus; │ │12 + │ │13 +// how many repos a page shows before the "load more" row appears.│ │14 +pub const page_size = 20; │ │15 │ │16 repos: []const evt.Repo, │ │17 owner_names: []const []const u8, │ ╰──────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │18 +// the window start this page was built with, mirrored into the url. │ │19 +after: usize, │ │20 +// the `after` for the "load more" link, or null when this is the last page.│ │21 +next_after: ?usize, │ │22 │ │23 const Self = @This(); │ │24 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────╮ │25 pub fn init( │ │26 arena: *std.heap.ArenaAllocator, │ │27 haxy_moment: evt.AdminDB.HashMap(.read_only),│ │28 + after: usize, │ │29 ) !Self { │ │30 const DB = evt.AdminDB; │ │31 const hash_kind = evt.admin_repo_opts.hash; │ ╰─────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │32 │ │33 var repos: std.ArrayList(evt.Repo) = .empty; │ │34 var owner_names: std.ArrayList([]const u8) = .empty; │ │35 + │ │36 + const empty: Self = .{ .repos = &.{}, .owner_names = &.{}, .after = after, .next_after = null }; │ │37 │ │28 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return error.NotFound;│ │38 + // the ordered repo-list (oldest first); absent until the first repo exists. │ │39 + const repo_list_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "repo-list")) orelse return empty; │ │40 + const repo_list = try DB.ArrayList(.read_only).init(repo_list_cursor); │ │41 + const count = try repo_list.count(); │ │42 + │ │43 + const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return empty; │ │44 const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │45 │ │31 - var repos_iter = try event_id_to_repo.iterator(); │ │32 - while (try repos_iter.next()) |kv_cursor| { │ │33 - const kv = try kv_cursor.readKeyValuePair(); │ │34 - const repo_map = try DB.HashMap(.read_only).init(kv.value_cursor); │ │46 + // read the window [after, after+page_size) by index — a direct O(log n) │ │47 + // seek per entry, never scanning the whole table. ids whose repo has been │ │48 + // deleted (tombstones) are skipped. │ │49 + const end = @min(after + page_size, count); │ │50 + var i = after; │ │51 + while (i < end) : (i += 1) { │ │52 + const id_cursor = try repo_list.getCursor(@intCast(i)) orelse continue; │ │53 + var event_id: [evt.event_id_size]u8 = undefined; │ │54 + _ = try id_cursor.readBytes(&event_id); │ │55 + const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, &event_id)) orelse continue; │ │56 + const repo_map = try DB.HashMap(.read_only).init(repo_cursor); │ │57 const repo_event = try evt.read(evt.Repo, DB, hash_kind, arena, repo_map); │ │58 try repos.append(arena.allocator(), repo_event); │ │59 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │64 return .{ │ │65 .repos = repos.items, │ │66 .owner_names = owner_names.items, │ │67 + .after = after, │ │68 + .next_after = if (end < count) end else null,│ │69 }; │ │70 } │ │71 │ ╰─────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │89 defer arena.deinit(); │ │90 const aa = arena.allocator(); │ │91 │ │68 - const lines = try aa.alloc([]const u8, data.repos.len); │ │69 - const links = try aa.alloc([]const u8, data.repos.len); │ │92 + // one row per repo, plus a trailing "load more" row when more remain. │ │93 + const extra: usize = if (data.next_after != null) 1 else 0; │ │94 + const lines = try aa.alloc([]const u8, data.repos.len + extra); │ │95 + const links = try aa.alloc([]const u8, data.repos.len + extra); │ │96 for (data.repos, 0..) |repo, i| { │ │97 lines[i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description });│ │98 // clicking a repo opens its page; the "a:" prefix makes the web │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │101 const owner = data.owner_names[i]; │ │102 links[i] = if (owner.len > 0) try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ owner, repo.name }) else "";│ │103 } │ │104 + if (data.next_after) |next_after| { │ │105 + // the load-more row navigates to the next window (full reload on web, │ │106 + // Nav rebuild on the TUI), exactly like the commits "load more". │ │107 + lines[data.repos.len] = "load more"; │ │108 + links[data.repos.len] = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{next_after}); │ │109 + } │ │110 try self.list.setItems(allocator, lines, links); │ │111 │ │112 return self; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯